AI providers
Each tenant's own provider keys as credential references, and a registry of provider batch jobs with a poll schedule.
The ai-providers block holds what an app tracks about the model providers
it calls on a tenant's behalf: the tenant's own API keys (bring your own
key) and batch jobs that run at the provider for hours. Code sandboxes live
in the ai-chat block. It stores
no secret and imports no SDK; the
AI SDK helpers and batches
connect it to the AI SDK.
better-supabase sql add ai-providers # adds tenant and access as well| Table | Holds |
|---|---|
ai_provider_keys | One key per tenant, provider and name: a credential_ref, settings and enabled |
ai_batches | A provider batch: its reference, status, counts, expiry and when to poll it next |
ai_batch_items | One result per request of a finished batch: status, output, usage and error |
Organization exports leave out credential_ref, and the organization
purge in data lifecycle revokes each key's credential before it deletes the row.
| Permission | Lets a member | Default roles |
|---|---|---|
ai.create | start batches, and read their own batches | owner, admin, member |
ai.admin | list, save and remove the keys, and read every batch | owner, admin |
| Option | Default | Sets |
|---|---|---|
pollEvery | 60 | Seconds between two polls of a batch; the first poll waits this long |
Creating the client
import "server-only";
import {
createAiProviders,
rpcTransport,
} from "better-supabase/blocks/ai-providers";
import { vaultCredentials } from "better-supabase/credentials";
const service = rpcTransport(bs.admin().$client, { schema: "api" });
export const providersFor = (supabase: SupabaseClient) =>
createAiProviders({
transport: rpcTransport(supabase, { schema: "api" }),
service,
credentials: vaultCredentials({ transport: service, schema: "api" }),
schema: "api",
});Keys
A key row names a credential, never the key itself. Store the key with the credential provider first, then save the reference:
import { tenantCredentialRef } from "better-supabase/credentials";
const ref = tenantCredentialRef(organizationId, {
provider: "vault",
secret: "anthropic",
});
await credentials.set(ref, apiKey).orThrow();
await providers.keys
.save(organizationId, { provider: "anthropic", credentialRef: ref })
.orThrow();The ref must carry the key's tenant
(tenant refs); any other ref fails
with CREDENTIAL_REF_FOREIGN. save replaces the key with the same provider and name, and revokes the
credential the old row named. remove(keyId) revokes the credential and
deletes the row. resolve(organizationId) reads the enabled keys with their
tokens for one request, through the service role; pass the result to
byokOptions. Members never see a token: list
returns the rows without one.
When a tenant is deleted, run keys.removeAll(organizationId) before the
tenant's rows go, so every credential is revoked at the provider.
Batches
batches.record stores a batch the app started, with the SDK's reference.
The poll job claims due batches with due({ batch, leaseSeconds }), so two
workers never poll the same one, and writes the outcome with update and
saveItems. items(batchId, { cursor, limit }) pages the results by request
id. aiBatches does all of this for AI SDK batches.
Last updated on
AI cache
Model responses cached by a hash of the request, with a TTL, a size cap, per-tenant clearing and an hourly purge job.
Chat SDK
Run Chat SDK bots on Supabase with a Postgres state adapter, an inbox adapter for the in-app widget, and helpers that record Slack, WhatsApp and SMS messages in the inbox.