BYOK (bring your own key)
BYOK (bring your own key) lets a tenant supply its own AI provider API key instead of the shared lane, encrypted at rest under a per-tenant field-crypto envelope. Caisson's AI-Production resolves the key at inference time inside a tenant-scoped RLS transaction, debits zero credits only on allowlisted BYOK-covered actions, and never logs or persists the key in the clear.
In code
/**
* Store (or replace) a tenant's encrypted provider key. Runs inside `withTenant(tx, accountId)`; the
* `ctx.tenantId` MUST equal that account (the RLS WITH CHECK and the crypto AAD both enforce it). The
* plaintext key is sealed under the tenant's current envelope version and never persisted in the clear.
*/
export async function putTenantProviderKey(
tx: TenantExecutor,
ctx: FieldCryptoContext,
provider: string,
plaintextKey: string,
): Promise<void> {
assertProvider(provider);
if (plaintextKey.length === 0) {
throw new ValidationError("ai-kit BYOK: provider key is required");
}
const keyVersion = ctx.currentVersion();
const ciphertext = sealField(ctx, BYOK_COLUMN_CONTEXT, plaintextKey);
await tx.query(
`INSERT INTO tenant_ai_credential (id, account_id, provider, key_version, ciphertext, updated_at)
VALUES ($1, $2, $3, $4, $5, now())
ON CONFLICT (account_id, provider)
DO UPDATE SET ciphertext = EXCLUDED.ciphertext, key_version = EXCLUDED.key_version, updated_at = now()`,
[randomUUID(), ctx.tenantId, provider, keyVersion, ciphertext],
);
}How it holds
Reuses field-crypto verbatim, no new crypto
putTenantProviderKey seals a tenant's plaintext provider key with the same sealField/openField pair and per-tenant HKDF-derived AES-256-GCM envelope every other encrypted column in Caisson uses; BYOK adds a table, not a cipher.
One credential, replace not append
tenant_ai_credential holds one current row per (account_id, provider) under a UNIQUE constraint; putTenantProviderKey upserts on conflict, because a rotated API key is a swappable credential, not a data-encryption key with historical ciphertext depending on it.
FORCE RLS scopes every read and write
buildTenantPolicySql wires the same fail-closed FORCE ROW LEVEL SECURITY policy onto tenant_ai_credential that every tenant table gets; a forged cross-tenant write hits the WITH CHECK clause and a read outside withTenant returns nothing.
Owner-gated write, allowlisted zero cost
POST /api/byok requires session.role === "owner" (closing a bypass where any seat could rotate the org's shared key); reads stay seat-visible. resolveActionCost zeroes an inference action's credit cost only when that action is explicitly marked BYOK-covered; an unclassified action still meters, fail-metered by default.