Docs

@avelonjs

neon

Neon driver

@avelonjs/neon

@avelonjs/neon is the Neon stack for Avelon: a transaction-capable Postgres database, Neon Auth identity and social login, S3 object storage, and Postgres-backed Signets and queues. Reach for this package when you want the same six contracts @avelonjs/supabase covers, without PostgREST or GoTrue.

Installation

bun add @avelonjs/neon
export NEON_DATABASE_URL=postgresql://user:pass@ep-xxx.us-east-1.aws.neon.tech/neondb?sslmode=require
export NEON_AUTH_URL=https://ep-xxx.neonauth.net
export NEON_S3_BUCKET=avelon-uploads
export AWS_REGION=us-east-1
export AWS_ACCESS_KEY_ID=AKIA...
export AWS_SECRET_ACCESS_KEY=...

Wire drivers only in avelon.config.ts. Application folders never import @neondatabase/serverless or @aws-sdk/client-s3.

Basic Usage

import { createNeonDatabase } from '@avelonjs/neon'
import type { QueryIR } from '@avelonjs/core'

const db = createNeonDatabase({
  url: process.env.NEON_DATABASE_URL,
  instance: 'primary',
})

const latest: 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(latest)

Capabilities

SurfaceNotable capabilities
databasetransactions: true, upsert: true, returning: true, maxRelationDepth: 8
identitypasswords: true, magicLinks: true, mfa: ['totp'], emailVerification: true
socialproviders github, google
tokensabilities: true, expiration: true
storagesignedUrls: true, transforms empty
queuedelayed: true, retries: true, deadLetter: true

rowSecurity stays false. Ward predicates still inject into Query IR. Neon is Postgres, so you can compile RLS yourself through raw(); this package does not ship a PostgREST-shaped syncWards() surface.

Database

Query IR, migrations, and schema checks compile through @avelonjs/postgres/sql, the subpath of that package whose import graph carries no bun specifier, so @avelonjs/neon/database runs on Node as well as Bun. raw() returns the @neondatabase/serverless query function.

Reads, writes, rpc(), and migrations go over Neon's HTTP endpoint, which holds no connection between calls. transaction() is the exception: the callback reads rows before deciding what to issue next, and Neon's HTTP transactions are non-interactive, so the first call opens a WebSocket Pool connection and close() ends it. That path needs a global WebSocket, which Node has from 22 on; on older runtimes set neonConfig.webSocketConstructor before the first transaction.

Import from @avelonjs/neon/database rather than the package root when the runtime is Node. The root barrel also exports the queue and token drivers, which are built on Bun's SQL client.

import { createNeonDatabase } from '@avelonjs/neon'

const db = createNeonDatabase({
  migrations: [
    {
      id: '20260828_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.transaction(async (tx) => {
  await tx.execute({
    table: 'posts',
    mode: 'insert',
    select: [],
    where: [],
    relations: [],
    order: [],
    values: { id: 'post-1', title: 'Hello' },
  })
})

Identity

Neon Auth speaks the Better Auth HTTP surface. The config-time factory receives request-scoped cookies.

import { createNeonIdentity } from '@avelonjs/neon'

const auth = createNeonIdentity({
  authUrl: process.env.NEON_AUTH_URL,
})

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: true. After password sign-in you challenge the enrolled TOTP factor and verify the authenticator code. challengeMfa() without an argument uses totp. Email confirmation uses Better Auth's send-and-verify token pair.

import { createNeonIdentity } from '@avelonjs/neon'
import type { RequestCookies } from '@avelonjs/core'

export async function completeTotpAndEmail(cookies: RequestCookies) {
  const identity = createNeonIdentity({
    authUrl: process.env.NEON_AUTH_URL,
  })(cookies)

  await identity.signInWithPassword('jordan@example.com', 'correct-horse-battery')
  const challenge = await identity.challengeMfa('totp')
  await identity.verifyMfa(challenge.id, '123456')
  await identity.sendEmailVerification()
  await identity.verifyEmail('confirmation-token')
}

OAuth linking stays undeclared until a live Neon Auth project exercises that surface in CI.

Social

import { createNeonSocial } from '@avelonjs/neon'

const social = createNeonSocial({
  authUrl: process.env.NEON_AUTH_URL,
})

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

Neon Auth does not issue named API tokens. Hashed Signets live in Postgres on the Neon database.

import { createNeonTokens } from '@avelonjs/neon'

const tokens = createNeonTokens({
  url: process.env.NEON_DATABASE_URL,
  subject: 'user-1',
})

const issued = await tokens.issue('deployment', { abilities: ['records:read'] })
await tokens.verify(issued.plainText)

Storage

Object bytes persist in S3. Point endpoint at AWS, Cloudflare R2, MinIO, or the in-process LocalS3Server used by conformance. Signed reads are SigV4 query URLs.

import { createNeonStorage } from '@avelonjs/neon'

const disk = createNeonStorage({
  bucket: process.env.NEON_S3_BUCKET,
  instance: 'uploads',
})
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 on the Neon database.

import { createNeonQueue } from '@avelonjs/neon'

const queue = createNeonQueue({
  url: process.env.NEON_DATABASE_URL,
})
const id = await queue.enqueue({ name: 'GenerateReport', payload: { reportId: 'report-1' } })
await queue.drain(async (receipt) => {
  if (receipt.id !== id) return
})

Method Reference

Method / exportSignatureDescription
createNeonDatabase(options?: NeonDatabaseOptions) => NeonDatabaseConstructs the Neon database driver.
NeonDatabaseclass NeonDatabaseNeon database implementation wrapping the Postgres Query IR compiler.
NeonDatabase.execute(query: QueryIR) => Promise<QueryResult>Compiles and executes one query IR operation.
NeonDatabase.rpc(routine: string, args: Readonly<Record<string, unknown>>) => Promise<T>Invokes a Postgres routine; missing routines raise Invalid.
NeonDatabase.plan() => Promise<MigrationPlan>Returns pending migration identifiers and SQL steps.
NeonDatabase.apply() => Promise<readonly MigrationStatus[]>Applies pending migrations.
NeonDatabase.rollback(steps?: number) => Promise<readonly MigrationStatus[]>Rolls back the newest applied migration batches.
NeonDatabase.status() => Promise<readonly MigrationStatus[]>Lists applied and pending migration states.
NeonDatabase.transaction(callback) => Promise<T>Runs a callback inside a Postgres transaction.
NeonDatabase.resetFixtures() => Promise<void>Recreates assay fixtures used by live conformance.
NeonDatabase.raw() => NeonSqlReturns the @neondatabase/serverless query function.
NeonDatabase.close() => Promise<void>Closes the underlying SQL client pool.
neonDatabaseCapabilitiesconstLiteral database capability declaration.
NeonDatabaseOptionsinterfaceConstruction options for the database driver.
NeonSqltypeVendor client returned by NeonDatabase.raw().
PostgresMigrationinterfaceDriver-owned id, up, and down SQL pair.
createNeonIdentity(options?: NeonIdentityOptions) => (cookies: RequestCookies) => NeonIdentityReturns the pinned config-time identity factory.
NeonIdentityclass NeonIdentityNeon Auth identity implementation.
NeonIdentity.user() => Promise<NeonActor | null>Returns the current actor from the request cookie session.
NeonIdentity.session() => Promise<NeonSession | null>Returns the current session or null.
NeonIdentity.register(email: string, password: string) => Promise<NeonActor>Registers and establishes a session cookie.
NeonIdentity.signInWithPassword(email: string, password: string) => Promise<NeonActor>Signs in and writes the session cookie.
NeonIdentity.signOut() => Promise<void>Ends the Auth session and clears the cookie.
NeonIdentity.sendPasswordReset(email: string) => Promise<void>Sends a recovery request without revealing account existence.
NeonIdentity.resetPassword(token: string, password: string) => Promise<void>Replaces a password after validating a recovery token.
NeonIdentity.updatePassword(password: string) => Promise<void>Changes the current actor's password.
NeonIdentity.sendMagicLink(email: string, redirectTo?: string) => Promise<void>Sends a passwordless sign-in link.
NeonIdentity.signInWithMagicLink(token: string) => Promise<NeonActor>Redeems a link token for a session.
NeonIdentity.challengeMfa(factor?: 'totp') => Promise<MfaChallenge<'totp'>>Begins a TOTP challenge for the current session.
NeonIdentity.verifyMfa(challengeId: string, code: string) => Promise<void>Verifies a TOTP challenge response.
NeonIdentity.sendEmailVerification(email?: string) => Promise<void>Sends a confirmation message without revealing account existence.
NeonIdentity.verifyEmail(token: string) => Promise<void>Confirms an email address after validating a vendor token.
NeonIdentity.raw() => { authUrl: string }Returns the Auth HTTP root at the vendor boundary.
neonIdentityCapabilitiesconstLiteral identity capability declaration.
NeonIdentityOptionsinterfaceConstruction options for the identity factory.
NeonActorinterfaceActor payload with id and email.
NeonSessioninterfaceCookie session with access and refresh tokens.
readSessionCookie(cookies: RequestCookies, cookieName: string) => NeonSession | nullParses the request-scoped session cookie.
writeSessionCookie(cookies: RequestCookies, cookieName: string, session: NeonSession) => voidWrites the session cookie for the current request.
clearSessionCookie(cookies: RequestCookies, cookieName: string) => voidDeletes the session cookie for the current request.
DEFAULT_SESSION_COOKIEstringDefault session cookie name.
LocalAuthServerclass LocalAuthServerBetter Auth-shaped fixture for identity conformance.
LocalAuthServer.start() => Promise<string>Starts the 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 | undefinedReturns the recovery token issued for an email.
LocalAuthServer.magicLinkToken(email: string) => string | undefinedReturns the magic-link token issued for an email.
NEON_IDENTITY_ERROR_MAPreadonly { code: string; framework: string; meaning: string }[]Better Auth error codes and the taxonomy member each becomes.
LocalAuthServer.verificationToken(email: string) => string | undefinedReturns the email confirmation token issued for an email.
createNeonSocial(options?: NeonSocialOptions) => NeonSocialConstructs the Neon Auth social driver.
NeonSocialclass NeonSocialNeon Auth social implementation.
NeonSocial.redirect(provider: string, callbackUrl: string, state?: string) => Promise<string>Builds a Better Auth authorize URL and records CSRF state.
NeonSocial.callback(provider: string, params: Readonly<Record<string, string>>, callbackUrl: string) => Promise<SocialIdentity>Verifies state and exchanges the authorization code.
NeonSocial.raw() => { authUrl: string }Returns the Auth HTTP root at the vendor boundary.
neonSocialCapabilitiesconstLiteral social provider declaration.
NeonSocialOptionsinterfaceConstruction options for the social driver.
NeonSocialProfileinterfaceNormalized provider profile.
LocalSocialServerclass LocalSocialServerBetter Auth-shaped token fixture for social conformance.
LocalSocialServer.start() => Promise<string>Starts the social fixture on an ephemeral port.
LocalSocialServer.stop() => Promise<void>Stops the social fixture listener.
createNeonTokens(options?: NeonTokenOptions) => NeonTokensConstructs the Postgres-backed Signet driver.
NeonTokensclass NeonTokensNeon Signet implementation.
NeonTokens.issue(name: string, options?: TokenIssueOptions) => Promise<IssuedToken>Issues a named token and returns plaintext once.
NeonTokens.verify(plainText: string) => Promise<TokenRecord>Authenticates a plaintext token or raises Unauthenticated.
NeonTokens.list() => Promise<readonly TokenRecord[]>Lists metadata for the current subject without plaintext.
NeonTokens.revoke(id: string) => Promise<void>Revokes one token by stable identifier.
NeonTokens.reset() => Promise<void>Recreates the empty signet table.
NeonTokens.raw() => SQLReturns the Bun SQL client at the vendor boundary.
NeonTokens.close() => Promise<void>Closes the SQL client pool.
neonTokenCapabilitiesconstLiteral token capability declaration.
NeonTokenOptionsinterfaceConstruction options for the token driver.
createNeonStorage(options?: NeonStorageOptions) => NeonStorageConstructs the S3 storage driver.
NeonStorageclass NeonStorageS3 storage implementation.
NeonStorage.put(path: string, contents: Uint8Array | AsyncIterable<Uint8Array>, options?: { contentType?: string }) => Promise<StorageObject>Stores bytes and returns metadata.
NeonStorage.get(path: string) => Promise<Uint8Array>Reads object bytes or raises NotFound.
NeonStorage.delete(path: string) => Promise<void>Deletes an object if it exists.
NeonStorage.exists(path: string) => Promise<boolean>Reports whether an object exists.
NeonStorage.signedUrl(path: string, expiresInSeconds: number) => Promise<string>Creates a SigV4 signed read URL.
NeonStorage.raw() => S3ClientReturns the AWS S3 client at the vendor boundary.
neonStorageCapabilitiesconstLiteral storage capability declaration.
NeonStorageOptionsinterfaceConstruction options for the storage driver.
LocalS3Serverclass LocalS3ServerPath-style S3 fixture for storage conformance.
LocalS3Server.start() => Promise<string>Starts the S3 fixture on an ephemeral port.
LocalS3Server.reset() => voidClears stored objects.
LocalS3Server.stop() => Promise<void>Stops the S3 fixture listener.
signedUrlExpiryMs(url: URL) => number | undefinedParses SigV4 query expiry to epoch milliseconds.
createNeonQueue(options?: NeonQueueOptions) => NeonQueueConstructs the skip-locked Postgres queue driver.
NeonQueueclass NeonQueueNeon queue implementation.
NeonQueue.enqueue(job: QueueJob) => Promise<string>Enqueues a job for immediate delivery.
NeonQueue.enqueueAt(job: QueueJob, availableAt: Date) => Promise<string>Enqueues a job no earlier than the supplied time.
NeonQueue.drain(handler: (receipt: QueueReceipt) => Promise<void>, options?: { queue?: string; limit?: number }) => Promise<number>Delivers available jobs and counts attempts.
NeonQueue.retry(id: string, delaySeconds?: number) => Promise<void>Releases a failed job, optionally after a delay.
NeonQueue.failed(queue?: string) => Promise<readonly FailedQueueJob[]>Lists retained terminal failures.
NeonQueue.replay(id: string) => Promise<void>Replays a terminal failure with a fresh attempt budget.
NeonQueue.forget(id: string) => Promise<void>Permanently removes a terminal failure.
NeonQueue.reset() => Promise<void>Recreates empty job tables.
NeonQueue.raw() => SQLReturns the Bun SQL client at the vendor boundary.
NeonQueue.close() => Promise<void>Closes the SQL client pool.
neonQueueCapabilitiesconstLiteral queue capability declaration.
NeonQueueOptionsinterfaceConstruction options for the queue driver.

Testing

Run the shared suites. Identity and social use Better Auth-shaped local servers. Storage uses LocalS3Server so SigV4 signed reads stay fetchable without AWS. Database, tokens, and queue require a reachable Neon or Postgres URL and fail closed when it is missing.

The database driver talks to Neon's HTTP and WebSocket endpoints, which a plain Postgres does not serve. When the configured URL is not a neon.tech host, tests/neon-local.ts starts both endpoints locally in front of that Postgres, so live conformance exercises the real vendor client end to end.

import { identitySuite, storageSuite } from '@avelonjs/conformance/suites'
import {
  createNeonIdentity,
  createNeonStorage,
  LocalAuthServer,
  LocalS3Server,
} from '@avelonjs/neon'

const auth = new LocalAuthServer()
const authUrl = await auth.start()
identitySuite({
  name: 'neon identity',
  create: () => {
    auth.reset()
    return createNeonIdentity({ authUrl })
  },
  recoveryToken: async (email) => {
    const token = auth.recoveryToken(email)
    if (token === undefined) throw new Error(`No recovery token for ${email}`)
    return token
  },
  emailVerificationToken: async (email) => {
    const token = auth.verificationToken(email)
    if (token === undefined) throw new Error(`No verification token for ${email}`)
    return token
  },
})

const s3 = new LocalS3Server()
const endpoint = await s3.start()
storageSuite({
  name: 'neon storage',
  create: () => createNeonStorage({ endpoint, bucket: 'assay' }),
})
bun test
bun run typecheck
reeve docs:check