AvelonDocs

@avelon

supabase

@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

CapabilityValueNotes
transactionsfalsePostgREST has no interactive transactions
rowSecuritytruesyncWards() compiles registered wards to RLS
maxRelationDepth2Measured against live PostgREST relation loading
fullTextSearchfalseNo portable search() surface in v1
upserttrueWildcard upserts are native; explicit update lists use avelon_upsert_subset
returningtrueWrite queries may project rows
windowFunctionsfalseNot available over PostgREST
jsonOperatorstrueInformational

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

MethodSignatureDescription
createSupabaseDatabase(options?: SupabaseDatabaseOptions) => SupabaseDatabaseConstructs 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) => SupabaseIdentityReturns 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) => voidWrites the session cookie for the current request.
clearSessionCookie(cookies: RequestCookies, cookieName: string) => voidDeletes the session cookie for the current request.
LocalAuthServer.start() => Promise<string>Starts a GoTrue-shaped Auth fixture on an ephemeral port.
LocalAuthServer.reset() => voidClears 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) => SupabaseSocialConstructs 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) => SupabaseTokensConstructs 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() => SQLReturns the Bun SQL client at the vendor boundary.
SupabaseTokens.close() => Promise<void>Closes the SQL client pool.
createSupabaseStorage(options?: SupabaseStorageOptions) => SupabaseStorageConstructs 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() => SQLReturns the Bun SQL client at the vendor boundary.
SupabaseStorage.close() => Promise<void>Stops the signed-URL listener and closes SQL.
createSupabaseQueue(options?: SupabaseQueueOptions) => SupabaseQueueConstructs 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() => SQLReturns the Bun SQL client at the vendor boundary.
SupabaseQueue.close() => Promise<void>Closes the SQL client pool.
compilePostgrest(ir: QueryIR, predicate?: Predicate) => CompiledPostgrestRequestCompiles IR to an HTTP request.
applyPostgrestPredicate(parameters: URLSearchParams, predicate: Predicate) => voidWrites 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) => neverMaps PostgREST failures into framework errors.
resetSupabaseAssayFixtures(sql: SQL) => Promise<void>Provisions empty assay fixtures on a SQL client.
normalizePredicate(predicate: Predicate) => PredicateApplies 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.