@avelonjs/neon
v0.8.1
Published
Neon drivers for Avelon's database, identity, social, tokens, storage, and queue contracts.
Maintainers
Readme
@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/neonexport NEON_DATABASE_URL=postgresql://user:[email protected]/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
| Surface | Notable capabilities |
| -------- | --------------------------------------------------------------------------------- |
| database | transactions: true, upsert: true, returning: true, maxRelationDepth: 8 |
| identity | passwords: true, magicLinks: true, mfa: ['totp'], emailVerification: true |
| social | providers github, google |
| tokens | abilities: true, expiration: true |
| storage | signedUrls: true, transforms empty |
| queue | delayed: 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('[email protected]', '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 / export | Signature | Description |
| ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| createNeonDatabase | (options?: NeonDatabaseOptions) => NeonDatabase | Constructs the Neon database driver. |
| NeonDatabase | class NeonDatabase | Neon 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 | () => NeonSql | Returns the @neondatabase/serverless query function. |
| NeonDatabase.close | () => Promise<void> | Closes the underlying SQL client pool. |
| neonDatabaseCapabilities | const | Literal database capability declaration. |
| NeonDatabaseOptions | interface | Construction options for the database driver. |
| NeonSql | type | Vendor client returned by NeonDatabase.raw(). |
| PostgresMigration | interface | Driver-owned id, up, and down SQL pair. |
| createNeonIdentity | (options?: NeonIdentityOptions) => (cookies: RequestCookies) => NeonIdentity | Returns the pinned config-time identity factory. |
| NeonIdentity | class NeonIdentity | Neon 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. |
| neonIdentityCapabilities | const | Literal identity capability declaration. |
| NeonIdentityOptions | interface | Construction options for the identity factory. |
| NeonActor | interface | Actor payload with id and email. |
| NeonSession | interface | Cookie session with access and refresh tokens. |
| readSessionCookie | (cookies: RequestCookies, cookieName: string) => NeonSession \| null | Parses the request-scoped session cookie. |
| writeSessionCookie | (cookies: RequestCookies, cookieName: string, session: NeonSession) => void | Writes the session cookie for the current request. |
| clearSessionCookie | (cookies: RequestCookies, cookieName: string) => void | Deletes the session cookie for the current request. |
| DEFAULT_SESSION_COOKIE | string | Default session cookie name. |
| LocalAuthServer | class LocalAuthServer | Better Auth-shaped fixture for identity conformance. |
| LocalAuthServer.start | () => Promise<string> | Starts the 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. |
| NEON_IDENTITY_ERROR_MAP | readonly { code: string; framework: string; meaning: string }[] | Better Auth error codes and the taxonomy member each becomes. |
| LocalAuthServer.verificationToken | (email: string) => string \| undefined | Returns the email confirmation token issued for an email. |
| createNeonSocial | (options?: NeonSocialOptions) => NeonSocial | Constructs the Neon Auth social driver. |
| NeonSocial | class NeonSocial | Neon 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. |
| neonSocialCapabilities | const | Literal social provider declaration. |
| NeonSocialOptions | interface | Construction options for the social driver. |
| NeonSocialProfile | interface | Normalized provider profile. |
| LocalSocialServer | class LocalSocialServer | Better 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) => NeonTokens | Constructs the Postgres-backed Signet driver. |
| NeonTokens | class NeonTokens | Neon 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 | () => SQL | Returns the Bun SQL client at the vendor boundary. |
| NeonTokens.close | () => Promise<void> | Closes the SQL client pool. |
| neonTokenCapabilities | const | Literal token capability declaration. |
| NeonTokenOptions | interface | Construction options for the token driver. |
| createNeonStorage | (options?: NeonStorageOptions) => NeonStorage | Constructs the S3 storage driver. |
| NeonStorage | class NeonStorage | S3 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 | () => S3Client | Returns the AWS S3 client at the vendor boundary. |
| neonStorageCapabilities | const | Literal storage capability declaration. |
| NeonStorageOptions | interface | Construction options for the storage driver. |
| LocalS3Server | class LocalS3Server | Path-style S3 fixture for storage conformance. |
| LocalS3Server.start | () => Promise<string> | Starts the S3 fixture on an ephemeral port. |
| LocalS3Server.reset | () => void | Clears stored objects. |
| LocalS3Server.stop | () => Promise<void> | Stops the S3 fixture listener. |
| signedUrlExpiryMs | (url: URL) => number \| undefined | Parses SigV4 query expiry to epoch milliseconds. |
| createNeonQueue | (options?: NeonQueueOptions) => NeonQueue | Constructs the skip-locked Postgres queue driver. |
| NeonQueue | class NeonQueue | Neon 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 | () => SQL | Returns the Bun SQL client at the vendor boundary. |
| NeonQueue.close | () => Promise<void> | Closes the SQL client pool. |
| neonQueueCapabilities | const | Literal queue capability declaration. |
| NeonQueueOptions | interface | Construction 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