Supabase database adapter for Ankhorage data contracts.
@ankhorage/supabase-db implements Supabase-specific persistence behind the provider-neutral database interfaces from @ankhorage/contracts/db. Higher-level packages can use typed CRUD operations, optional realtime subscriptions, and guarded schema helpers without importing Supabase details into UI, runtime rendering, generated apps, or ZORA components.
bun add @ankhorage/supabase-db @ankhorage/contractsInstall @supabase/supabase-js as well when you want to pass a Supabase realtime client:
bun add @supabase/supabase-jsThis package owns Supabase Database behavior:
- adapter creation
- table select workflows
- insert, update, and delete workflows
- filter, order, and pagination mapping
- provider error normalization
- optional realtime change subscriptions
- guarded schema SQL generation and privileged execution hooks
This package does not own:
- ZORA components or patterns
- Studio UI
- runtime manifest interpretation
- generated routes or layouts
- CLI generation
- deployment orchestration
- auth or storage adapters
Use the runtime adapter with client-safe Supabase credentials. The adapter speaks the canonical DbAdapter shape from @ankhorage/contracts/db.
import { createSupabaseDbAdapter } from '@ankhorage/supabase-db';
const db = createSupabaseDbAdapter({
url: process.env.SUPABASE_URL ?? '',
anonKey: process.env.SUPABASE_ANON_KEY ?? '',
});
const posts = await db.select({
table: 'posts',
columns: ['id', 'title', 'created_at'],
filters: [{ field: 'published', operator: 'eq', value: true }],
sort: [{ field: 'created_at', direction: 'desc' }],
page: { limit: 20 },
});
if (posts.ok) {
console.log(posts.data);
}The runtime adapter uses Supabase PostgREST endpoints and returns normalized DbResult values. Expected provider failures, permission errors, missing tables, and invalid queries are returned as stable adapter errors instead of raw Supabase response objects.
The adapter implements:
selectfindByIdinsertupdatedelete
update and delete require at least one filter to avoid accidental whole-table mutations.
The adapter exposes the canonical DbAdapterCapabilities contract:
const capabilities = db.capabilities;
console.log(capabilities.transactions); // false
console.log(capabilities.returning); // true
console.log(capabilities.realtime); // true only when realtime is enabled and configuredRealtime is optional. It is exposed through the canonical DbRealtimeAdapter contract only when enabled and configured.
import { createClient } from '@supabase/supabase-js';
import { createSupabaseDbAdapter } from '@ankhorage/supabase-db';
const supabase = createClient(process.env.SUPABASE_URL ?? '', process.env.SUPABASE_ANON_KEY ?? '');
const db = createSupabaseDbAdapter({
url: process.env.SUPABASE_URL ?? '',
anonKey: process.env.SUPABASE_ANON_KEY ?? '',
realtime: true,
realtimeClient: supabase,
});
const subscription = db.realtime?.subscribeToCollection({ table: 'posts' }, (event) => {
console.log(event.kind, event.record, event.previousRecord);
});
await subscription?.unsubscribe();Realtime events are normalized to provider-neutral kinds:
insertupdatedelete
Supabase projects must have database change replication configured for realtime table events. If realtime is not enabled or no realtime client is provided, CRUD still works and capabilities.realtime is false.
Schema operations are privileged and separate from runtime CRUD. The admin adapter implements the canonical DbAdminAdapter contract from @ankhorage/contracts/db.
By default, the admin adapter generates SQL only:
import { createSupabaseDbAdminAdapter } from '@ankhorage/supabase-db';
const admin = createSupabaseDbAdminAdapter({
url: process.env.SUPABASE_URL ?? '',
serviceRoleKey: process.env.SUPABASE_SERVICE_ROLE_KEY,
});
const plan = admin.generateCreateCollectionSql({
name: 'posts',
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'body', type: 'text' },
{ name: 'created_at', type: 'datetime' },
],
});
if (plan.ok) {
console.log(plan.sql);
}Direct execution requires all of the following:
execute: true- a
serviceRoleKey - an injected
executeSql(sql)callback from a privileged environment
const admin = createSupabaseDbAdminAdapter({
url: process.env.SUPABASE_URL ?? '',
serviceRoleKey: process.env.SUPABASE_SERVICE_ROLE_KEY,
execute: true,
executeSql: async (sql) => {
// Run SQL through a trusted backend, migration runner, or RPC you control.
return { ok: true };
},
});Do not use service-role credentials in client/runtime code.
The admin adapter is intended for trusted Studio backends, CLI tooling, deployment tooling, or migration workflows. It must not be bundled into generated client screens.
A future app flow should look like this:
Studio creates a collection definition
-> privileged adapter generates or applies schema
Runtime queries rows through DbAdapter
-> runtime maps row fields to component props
ZORA renders presentational patterns onlyFor example, a future ZORA PostCard should receive props. It should not import Supabase or this package.
bun install
bun run build
bun run lint:fix
bun run testTests are mocked and must not call real Supabase services.