@avelonjs/supabase
v0.8.1
Published
Supabase drivers for Avelon's database, identity, social, tokens, storage, and queue contracts.
Maintainers
Readme
@avelonjs/supabase
@avelonjs/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 @avelonjs/supabaseexport SUPABASE_REST_URL=http://127.0.0.1:3001
export SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
export SUPABASE_DB_URL=postgresql://postgres:[email protected]:5432/avelon_supabase
export SUPABASE_AUTH_URL=http://127.0.0.1:54321/auth/v1
export SUPABASE_ANON_KEY=local-anon-keyBasic Usage
import { createSupabaseDatabase } from '@avelonjs/supabase'
import type { QueryIR } from '@avelonjs/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 { createSupabaseBunDatabase } from '@avelonjs/supabase/database/bun'
const db = createSupabaseBunDatabase({
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.
Two entries
Query traffic goes over PostgREST, so @avelonjs/supabase/database carries no bun specifier and runs on a Node runtime. Migrations, fixtures and syncWards() issue DDL, which PostgREST cannot, so they need a Postgres socket. @avelonjs/supabase/database/bun supplies one through Bun's SQL client.
| Entry | Request path | Migrations, fixtures, syncWards() |
| --------------------------------- | ------------ | ----------------------------------- |
| @avelonjs/supabase/database | PostgREST | raise Unavailable |
| @avelonjs/supabase/database/bun | PostgREST | run over Bun's SQL client |
Either way the schema cache the request path validates against comes from PostgREST's own OpenAPI document, filtered by the bearer token the queries use.
Migrations
Migration history uses the same driver-owned SQL pairs as @avelonjs/postgres, applied over the admin connection.
import { createSupabaseBunDatabase } from '@avelonjs/supabase/database/bun'
const db = createSupabaseBunDatabase({
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=1Fixture provisioning is owned by this package and reloads the PostgREST schema cache after reset.
Identity
Next on Node must import @avelonjs/supabase/identity. The package root also exports the database driver, which loads Bun SQL.
import { createSupabaseIdentity } from '@avelonjs/supabase/identity'
const auth = createSupabaseIdentity({
authUrl: process.env.SUPABASE_AUTH_URL,
apiKey: process.env.SUPABASE_ANON_KEY,
})
export async function currentUser(cookies: import('@avelonjs/core').RequestCookies) {
return auth(cookies).user()
}Capabilities: passwords: true, magicLinks: true, oauth: false, organizations: false, mfa: ['totp'], emailVerification: false. After password sign-in you challenge the enrolled TOTP factor and verify the authenticator code. challengeMfa() without an argument uses totp. Email confirmation stays on GoTrue's signup flow and is not an Auth method here.
import { createSupabaseIdentity } from '@avelonjs/supabase'
import type { RequestCookies } from '@avelonjs/core'
export async function completeTotpChallenge(cookies: RequestCookies) {
const identity = createSupabaseIdentity({
authUrl: process.env.SUPABASE_AUTH_URL,
apiKey: process.env.SUPABASE_ANON_KEY,
})(cookies)
await identity.signInWithPassword('[email protected]', 'correct-horse-battery')
const challenge = await identity.challengeMfa('totp')
await identity.verifyMfa(challenge.id, '123456')
}Social
import { createSupabaseSocial } from '@avelonjs/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. @avelonjs/supabase stores hashed Signets in Postgres so verify, list, and revoke are durable.
import { createSupabaseTokens } from '@avelonjs/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 '@avelonjs/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. This 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 '@avelonjs/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. |
| createSupabaseBunDatabase | (options?: SupabaseBunDatabaseOptions) => SupabaseDatabase | Same driver, holding a Bun SQL admin connection for DDL. |
| SupabaseBunDatabaseOptions | interface | SupabaseDatabaseOptions without admin, plus databaseUrl. |
| loadSchemaCacheFromRest | (restUrl: string, headers: Readonly<Record<string, string>>) => Promise<SchemaCache> | Reads tables and columns from PostgREST's OpenAPI document. |
| 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.signInWithMagicLink | (token: string) => Promise<SupabaseActor> | Redeems a link token for a session. |
| SupabaseIdentity.challengeMfa | (factor?: 'totp') => Promise<MfaChallenge<'totp'>> | Begins a TOTP challenge for the current session. |
| SupabaseIdentity.verifyMfa | (challengeId: string, code: string) => Promise<void> | Verifies a TOTP challenge response. |
| 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. |
| LocalAuthServer.magicLinkToken | (email: string) => string \| undefined | Returns the magic-link token issued for an email. |
| SUPABASE_IDENTITY_ERROR_MAP | readonly { code: string; framework: string; meaning: string }[] | GoTrue error codes and the taxonomy member each becomes. |
| 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.
