@avelon/supabase
@avelon/supabase implements Supabase-backed drivers for Avelon. The database surface compiles QueryIR to PostgREST, declares transactions: false, and synchronizes ward predicates into Postgres row-level security. The identity surface talks to GoTrue over HTTP and binds sessions to request-scoped cookies. Reach for this package when your application targets Supabase and needs portable queries, auth, and database-enforced wards.
Installation
bun add @avelon/supabase
export SUPABASE_REST_URL=http://127.0.0.1:3001
export SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
export SUPABASE_DB_URL=postgresql://postgres:avelon@127.0.0.1:5432/avelon_supabase
export SUPABASE_AUTH_URL=http://127.0.0.1:54321/auth/v1
export SUPABASE_ANON_KEY=local-anon-key
Basic Usage
import { createSupabaseDatabase } from '@avelon/supabase'
import type { QueryIR } from '@avelon/core'
const db = createSupabaseDatabase()
const published: QueryIR = {
table: 'posts',
mode: 'select',
select: ['id', 'title'],
where: [{ kind: 'null', column: 'published_at', negated: true }],
relations: [],
order: [{ column: 'published_at', direction: 'desc' }],
limit: 10,
}
await db.execute(published)
Capabilities
| Capability | Value | Notes |
|---|---|---|
transactions | false | PostgREST has no interactive transactions |
rowSecurity | true | syncWards() compiles registered wards to RLS |
maxRelationDepth | 2 | Measured against live PostgREST relation loading |
fullTextSearch | false | No portable search() surface in v1 |
upsert | true | Wildcard upserts are native; explicit update lists use avelon_upsert_subset |
returning | true | Write queries may project rows |
windowFunctions | false | Not available over PostgREST |
jsonOperators | true | Informational |
Ward Synchronization
import { createSupabaseDatabase } from '@avelon/supabase'
const db = createSupabaseDatabase({
wards: [
{
name: 'posts_owner_read',
table: 'posts',
command: 'select',
using: {
kind: 'compare',
column: 'user_id',
op: '=',
value: { claim: 'uid' },
},
},
],
})
await db.syncWards()
{ claim: 'uid' } compiles to auth.uid()::text. A live two-identity denial test in this package proves a cross-tenant read is rejected after sync.
Migrations
Query traffic stays on PostgREST. Migration history uses the same driver-owned SQL pairs as @avelon/postgres, applied over the admin connection.
import { createSupabaseDatabase } from '@avelon/supabase'
const db = createSupabaseDatabase({
migrations: [
{
id: '20260827_create_posts',
up: ['CREATE TABLE posts (id text PRIMARY KEY, title text NOT NULL)'],
down: ['DROP TABLE posts'],
},
],
})
await db.plan()
await db.apply()
await db.status()
Live Conformance
postgrest packages/supabase/postgrest.conf
bun test packages/supabase --max-concurrency=1
Fixture provisioning is owned by this package and reloads the PostgREST schema cache after reset.
Identity
import { createSupabaseIdentity } from '@avelon/supabase'
const auth = createSupabaseIdentity({
authUrl: process.env.SUPABASE_AUTH_URL,
apiKey: process.env.SUPABASE_ANON_KEY,
})
export async function currentUser(cookies: import('@avelon/core').RequestCookies) {
return auth(cookies).user()
}
Capabilities: passwords: true, magicLinks: true, oauth: false, organizations: false, mfa: []. OAuth linking and MFA are deferred until a live GoTrue stack is available in CI; declaring them early would hide missing surfaces.
Social
import { createSupabaseSocial } from '@avelon/supabase'
const social = createSupabaseSocial({
authUrl: process.env.SUPABASE_AUTH_URL,
apiKey: process.env.SUPABASE_ANON_KEY,
})
export async function githubRedirect(callbackUrl: string): Promise<string> {
return social.redirect('github', callbackUrl)
}
Providers are the literal list github and google. Undeclared providers raise Invalid. A mismatched or missing OAuth state raises Unauthenticated.
Tokens
Supabase Auth does not issue named API tokens. @avelon/supabase stores hashed Signets in Postgres so verify, list, and revoke are durable.
import { createSupabaseTokens } from '@avelon/supabase'
const tokens = createSupabaseTokens({
url: process.env.SUPABASE_DB_URL,
subject: 'user-1',
})
const issued = await tokens.issue('deployment', { abilities: ['records:read'] })
await tokens.verify(issued.plainText)
Storage
Object bytes persist in Postgres. Signed read URLs are HMAC-scoped HTTP URLs served by the driver process so expiry is real and fetchable.
import { createSupabaseStorage } from '@avelon/supabase'
const disk = createSupabaseStorage({ instance: 'default' })
await disk.put('avatars/me.bin', new Uint8Array([1, 2, 3]), {
contentType: 'application/octet-stream',
})
const url = await disk.signedUrl('avatars/me.bin', 60)
Transforms are undeclared in v1 (D28). Signed uploads and listing are deferred.
Queue
Durable jobs use Postgres FOR UPDATE SKIP LOCKED. The Wave A package names pgmq; this environment does not ship that extension, so skip-locked tables carry the same retry, delay, and dead-letter semantics.
import { createSupabaseQueue } from '@avelon/supabase'
const queue = createSupabaseQueue()
const id = await queue.enqueue({ name: 'GenerateReport', payload: { reportId: 'report-1' } })
await queue.drain(async (receipt) => {
if (receipt.id !== id) return
})
Method Reference
| Method | Signature | Description | |
|---|---|---|---|
createSupabaseDatabase | (options?: SupabaseDatabaseOptions) => SupabaseDatabase | Constructs the PostgREST database driver. | |
SupabaseDatabase.execute | (query: QueryIR) => Promise<QueryResult> | Executes IR as the service role. | |
SupabaseDatabase.executeAs | (token: string, query: QueryIR) => Promise<QueryResult> | Executes IR as an arbitrary bearer token. | |
SupabaseDatabase.rpc | (routine: string, args: Readonly<Record<string, unknown>>) => Promise<T> | Invokes a PostgREST RPC; missing routines raise Invalid. | |
SupabaseDatabase.plan | () => Promise<MigrationPlan> | Returns pending migration identifiers and SQL steps. | |
SupabaseDatabase.apply | () => Promise<readonly MigrationStatus[]> | Applies pending migrations over the admin connection. | |
SupabaseDatabase.rollback | (steps?: number) => Promise<readonly MigrationStatus[]> | Rolls back the newest applied migration batches. | |
SupabaseDatabase.status | () => Promise<readonly MigrationStatus[]> | Lists applied and pending migration states. | |
SupabaseDatabase.syncWards | () => Promise<void> | Applies registered ward policies as Postgres RLS. | |
SupabaseDatabase.resetFixtures | () => Promise<void> | Recreates assay fixtures, roles, and helper RPCs. | |
SupabaseDatabase.raw | () => { restUrl: string } | Returns the REST root at the vendor boundary. | |
SupabaseDatabase.close | () => Promise<void> | Closes the direct Postgres admin client. | |
createSupabaseIdentity | (options?: SupabaseIdentityOptions) => (cookies: RequestCookies) => SupabaseIdentity | Returns the pinned config-time identity factory. | |
SupabaseIdentity.user | `() => Promise<SupabaseActor \ | null>` | Returns the current actor from the request cookie session. |
SupabaseIdentity.session | `() => Promise<SupabaseSession \ | null>` | Returns the current session or null. |
SupabaseIdentity.register | (email: string, password: string) => Promise<SupabaseActor> | Registers and establishes a session cookie. | |
SupabaseIdentity.signInWithPassword | (email: string, password: string) => Promise<SupabaseActor> | Signs in and writes the session cookie. | |
SupabaseIdentity.signOut | () => Promise<void> | Ends the Auth session and clears the cookie. | |
SupabaseIdentity.sendPasswordReset | (email: string) => Promise<void> | Sends a recovery request without revealing account existence. | |
SupabaseIdentity.resetPassword | (token: string, password: string) => Promise<void> | Replaces a password after validating a recovery token. | |
SupabaseIdentity.updatePassword | (password: string) => Promise<void> | Changes the current actor's password. | |
SupabaseIdentity.sendMagicLink | (email: string, redirectTo?: string) => Promise<void> | Sends a passwordless sign-in link. | |
SupabaseIdentity.raw | () => { authUrl: string } | Returns the Auth HTTP root at the vendor boundary. | |
readSessionCookie | `(cookies: RequestCookies, cookieName: string) => SupabaseSession \ | null` | Parses the request-scoped session cookie. |
writeSessionCookie | (cookies: RequestCookies, cookieName: string, session: SupabaseSession) => void | Writes the session cookie for the current request. | |
clearSessionCookie | (cookies: RequestCookies, cookieName: string) => void | Deletes the session cookie for the current request. | |
LocalAuthServer.start | () => Promise<string> | Starts a GoTrue-shaped Auth fixture on an ephemeral port. | |
LocalAuthServer.reset | () => void | Clears users, sessions, and recovery tokens. | |
LocalAuthServer.stop | () => Promise<void> | Stops the fixture listener. | |
LocalAuthServer.recoveryToken | `(email: string) => string \ | undefined` | Returns the recovery token issued for an email. |
createSupabaseSocial | (options?: SupabaseSocialOptions) => SupabaseSocial | Constructs the GoTrue social driver. | |
SupabaseSocial.redirect | (provider: string, callbackUrl: string, state?: string) => Promise<string> | Builds a GoTrue authorize URL and records CSRF state. | |
SupabaseSocial.callback | (provider: string, params: Readonly<Record<string, string>>, callbackUrl: string) => Promise<SocialIdentity> | Verifies state and exchanges the authorization code. | |
SupabaseSocial.raw | () => { authUrl: string } | Returns the Auth HTTP root at the vendor boundary. | |
LocalSocialServer.start | () => Promise<string> | Starts a GoTrue-shaped token fixture on an ephemeral port. | |
LocalSocialServer.stop | () => Promise<void> | Stops the social fixture listener. | |
createSupabaseTokens | (options?: SupabaseTokenOptions) => SupabaseTokens | Constructs the Postgres-backed Signet driver. | |
SupabaseTokens.issue | (name: string, options?: TokenIssueOptions) => Promise<IssuedToken> | Issues a named token and returns plaintext once. | |
SupabaseTokens.verify | (plainText: string) => Promise<TokenRecord> | Authenticates a plaintext token or raises Unauthenticated. | |
SupabaseTokens.list | () => Promise<readonly TokenRecord[]> | Lists metadata for the current subject without plaintext. | |
SupabaseTokens.revoke | (id: string) => Promise<void> | Revokes one token by stable identifier. | |
SupabaseTokens.reset | () => Promise<void> | Recreates the empty signet table. | |
SupabaseTokens.raw | () => SQL | Returns the Bun SQL client at the vendor boundary. | |
SupabaseTokens.close | () => Promise<void> | Closes the SQL client pool. | |
createSupabaseStorage | (options?: SupabaseStorageOptions) => SupabaseStorage | Constructs the Postgres-backed storage driver. | |
SupabaseStorage.put | `(path: string, contents: Uint8Array \ | AsyncIterable<Uint8Array>, options?: { contentType?: string }) => Promise<StorageObject>` | Stores bytes and returns metadata. |
SupabaseStorage.get | (path: string) => Promise<Uint8Array> | Reads object bytes or raises NotFound. | |
SupabaseStorage.delete | (path: string) => Promise<void> | Deletes an object if it exists. | |
SupabaseStorage.exists | (path: string) => Promise<boolean> | Reports whether an object exists. | |
SupabaseStorage.signedUrl | (path: string, expiresInSeconds: number) => Promise<string> | Creates a fetchable HMAC-signed read URL. | |
SupabaseStorage.reset | () => Promise<void> | Recreates the empty object table. | |
SupabaseStorage.raw | () => SQL | Returns the Bun SQL client at the vendor boundary. | |
SupabaseStorage.close | () => Promise<void> | Stops the signed-URL listener and closes SQL. | |
createSupabaseQueue | (options?: SupabaseQueueOptions) => SupabaseQueue | Constructs the skip-locked Postgres queue driver. | |
SupabaseQueue.enqueue | (job: QueueJob) => Promise<string> | Enqueues a job for immediate delivery. | |
SupabaseQueue.enqueueAt | (job: QueueJob, availableAt: Date) => Promise<string> | Enqueues a job no earlier than the supplied time. | |
SupabaseQueue.drain | (handler: (receipt: QueueReceipt) => Promise<void>, options?: { queue?: string; limit?: number }) => Promise<number> | Delivers available jobs and counts attempts. | |
SupabaseQueue.retry | (id: string, delaySeconds?: number) => Promise<void> | Releases a failed job, optionally after a delay. | |
SupabaseQueue.failed | (queue?: string) => Promise<readonly FailedQueueJob[]> | Lists retained terminal failures. | |
SupabaseQueue.replay | (id: string) => Promise<void> | Replays a terminal failure with a fresh attempt budget. | |
SupabaseQueue.forget | (id: string) => Promise<void> | Permanently removes a terminal failure. | |
SupabaseQueue.reset | () => Promise<void> | Recreates empty job tables. | |
SupabaseQueue.raw | () => SQL | Returns the Bun SQL client at the vendor boundary. | |
SupabaseQueue.close | () => Promise<void> | Closes the SQL client pool. | |
compilePostgrest | (ir: QueryIR, predicate?: Predicate) => CompiledPostgrestRequest | Compiles IR to an HTTP request. | |
applyPostgrestPredicate | (parameters: URLSearchParams, predicate: Predicate) => void | Writes a normalized predicate into PostgREST and= filters. | |
compileWardPredicate | `(predicate: Predicate \ | boolean) => string` | Compiles a ward predicate to RLS SQL. |
compileWardPolicySql | (policy: WardPolicy) => string[] | Builds DROP/CREATE POLICY statements for one ward. | |
compileAllWardPolicies | (policies: readonly WardPolicy[]) => string[] | Compiles every registered ward into ordered SQL. | |
mapPostgrestError | (status: number, bodyText: string, operation: string) => never | Maps PostgREST failures into framework errors. | |
resetSupabaseAssayFixtures | (sql: SQL) => Promise<void> | Provisions empty assay fixtures on a SQL client. | |
normalizePredicate | (predicate: Predicate) => Predicate | Applies empty-list and constant identities. | |
combinedPredicate | `(ir: Pick<QueryIR, 'where' \ | 'ward'>) => Predicate` | ANDs where and ward, then normalizes. |
Testing
Run the shared database conformance suite against live PostgREST and the package-owned RLS denial test. Run identity and social conformance against a GoTrue-shaped Auth server; when the full Docker stack is unavailable, LocalAuthServer and LocalSocialServer stand in so cookie, password, and OAuth-code flows still exercise the HTTP drivers. Token conformance runs against live Postgres (avelon_signets). Storage conformance runs against live Postgres object bytes plus fetchable signed read URLs. Queue conformance runs against live Postgres skip-locked tables because pgmq is not installed in this environment. Unit tests cover PostgREST compilation without a network dependency.