@j0nathan-ll0yd/auth
Better Auth integration for session-based authentication in Lambda handlers, with Drizzle ORM adapter, schema mapping, and permission exports for the CLI.
Setup
createAuth(db, config)
Create and cache a Better Auth instance with a Drizzle database adapter. Cached for Lambda container reuse.
import { createAuth } from "@j0nathan-ll0yd/auth";
import { getRequiredEnv } from "@j0nathan-ll0yd/env";
const auth = createAuth(db, {
secret: getRequiredEnv("AUTH_SECRET"),
baseURL: getRequiredEnv("AUTH_BASE_URL"),
socialProviders: {
apple: {
clientId: getRequiredEnv("APPLE_CLIENT_ID"),
clientSecret: getRequiredEnv("APPLE_CLIENT_SECRET"),
appBundleIdentifier: "com.example.app",
},
google: {
clientId: getRequiredEnv("GOOGLE_CLIENT_ID"),
clientSecret: getRequiredEnv("GOOGLE_CLIENT_SECRET"),
},
},
});getAuth(getDb, config)
Lazily initialize auth with an async database factory function. Returns the cached instance on subsequent calls.
import { getAuth } from "@j0nathan-ll0yd/auth";
const auth = await getAuth(() => getDrizzleClient({ provider: "aurora-dsql", endpoint: "..." }), {
secret: "...",
baseURL: "...",
});resetAuth()
Reset the cached auth instance so the next call to createAuth or getAuth creates a fresh one. Intended for test isolation.
Session Validation
validateSession(auth, token)
Validate a bearer token against Better Auth and return the authenticated user and session.
import { validateSession } from "@j0nathan-ll0yd/auth";
const result = await validateSession(auth, bearerToken);
// result.user.id, result.user.email, result.session.expiresAtThrows UnauthorizedError when the token is missing, invalid, or expired.
validateSessionFromHeader(auth, authorizationHeader)
Extract a bearer token from the Authorization header and validate it in one call.
const result = await validateSessionFromHeader(auth, event.headers.Authorization);Throws UnauthorizedError when the header is missing, malformed, or the token is invalid.
extractBearerToken(header)
Parse a bearer token from an Authorization header value. Returns null if the header is missing or malformed.
const token = extractBearerToken("Bearer abc123");
// 'abc123'expireSession(auth, token, db)
Expire a session by setting its expiresAt to now, preserving the row for audit and cleanup. Unlike Better Auth's revokeSession() which deletes the session row, this keeps it visible for scheduled cleanup.
import { expireSession } from "@j0nathan-ll0yd/auth";
await expireSession(auth, bearerToken, db);Throws UnauthorizedError when the token is invalid or already expired.
getSessionExpirationISO(session)
Return the session expiration as an ISO 8601 string.
const iso = getSessionExpirationISO(sessionResult);
// '2026-04-28T12:00:00.000Z'refreshSession(auth, token) :badge[deprecated]
Validate and refresh a session. Deprecated -- use validateSession instead. BetterAuth's getSession() auto-extends sessions when updateAge has elapsed, making a separate refresh function unnecessary.
Session Auth Configuration
One-time setup for defineApiHandler with auth: 'session'.
configureSessionAuth(getDb, config)
Register a session auth factory for use by defineApiHandler with auth: 'session'. Call once at module scope in your Lambda entry file, before any handler that uses session auth.
import { configureSessionAuth } from "@j0nathan-ll0yd/auth";
configureSessionAuth(() => getDrizzleClient({ provider: "aurora-dsql", endpoint: "..." }), {
secret: getRequiredEnv("AUTH_SECRET"),
baseURL: getRequiredEnv("AUTH_BASE_URL"),
});getConfiguredSessionAuth()
Return the configured Better Auth instance, resolving it via the registered factory. Throws if configureSessionAuth has not been called.
resetSessionAuth()
Reset the session auth factory. Intended for test isolation.
Webhook Signature Validation
Timing-safe HMAC signature validation for machine-to-machine webhook callers. Where the session helpers above authenticate an end user by bearer token, these authenticate a sending service by shared secret. All functions throw UnauthorizedError from @j0nathan-ll0yd/errors on failure.
validateWebhookSignature(options)
Generic HMAC webhook signature validation with configurable algorithm and prefix.
import { validateWebhookSignature } from "@j0nathan-ll0yd/auth";
validateWebhookSignature({
payload: rawBody,
signature: signatureHeader,
secret: webhookSecret,
algorithm: "sha256",
prefix: "sha256=",
headerName: "X-Signature",
});WebhookValidationOptions
interface WebhookValidationOptions {
payload: string | Buffer; // Raw request body
signature: string; // Signature header value
secret: string; // HMAC secret
algorithm?: string; // HMAC algorithm (default: 'sha256')
prefix?: string; // Prefix stripped before comparison (default: '')
headerName?: string; // Header name for error messages (default: 'X-Signature')
}validateGitHubWebhook(payload, signatureHeader, secret)
Validate a GitHub webhook signature (X-Hub-Signature-256). GitHub signs payloads using HMAC-SHA256 with a sha256= prefix.
import { validateGitHubWebhook } from "@j0nathan-ll0yd/auth";
validateGitHubWebhook(rawBody, headers["x-hub-signature-256"], secret);Pass the raw request body, not a re-serialized parsed object -- HMAC is computed over exact bytes.
validateStripeWebhook(payload, signatureHeader, secret, toleranceSeconds?)
Validate a Stripe webhook signature (Stripe-Signature). Parses the t=<timestamp>,v1=<hex> header format and includes replay protection via timestamp age validation.
import { validateStripeWebhook } from "@j0nathan-ll0yd/auth";
validateStripeWebhook(rawBody, headers["stripe-signature"], endpointSecret);
// Custom tolerance (default: 300 seconds)
validateStripeWebhook(rawBody, headers["stripe-signature"], endpointSecret, 600);Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
payload | string | Buffer | - | Raw request body |
signatureHeader | string | - | Value of the Stripe-Signature header |
secret | string | - | Stripe webhook endpoint secret |
toleranceSeconds | number | 300 | Maximum age of the timestamp in seconds |
Permissions
AUTH_TABLE_PERMISSIONS
Table permissions required by Better Auth for session management. Consumed by mantle generate permissions to auto-generate DSQL role grants for Lambdas that import @j0nathan-ll0yd/auth.
const AUTH_TABLE_PERMISSIONS: TablePermission[] = [
{ table: "users", operations: [Select, Insert, Update] },
{ table: "sessions", operations: [Select, Insert, Update, Delete] },
{ table: "accounts", operations: [Select, Insert, Delete] },
{ table: "verification", operations: [Select, Insert, Delete] },
];AUTH_MIGRATION_SQL
SQL string to create all Better Auth tables and indexes. Idempotent (uses IF NOT EXISTS). Creates: users, sessions, accounts, verification tables with appropriate indexes.
Schema
Reusable Drizzle ORM column definitions and table schemas for Better Auth. Instances can spread these into their own pgTable() calls to extend auth tables with additional columns.
Column Definitions
import {
AUTH_USER_COLUMNS,
AUTH_SESSION_COLUMNS,
AUTH_ACCOUNT_COLUMNS,
AUTH_VERIFICATION_COLUMNS,
} from "@j0nathan-ll0yd/auth";Each is a Record<string, AnyPgColumn> that can be spread into pgTable() calls:
import { pgTable, text } from "drizzle-orm/pg-core";
import { AUTH_USER_COLUMNS } from "@j0nathan-ll0yd/auth";
export const users = pgTable("users", {
...AUTH_USER_COLUMNS,
customField: text("custom_field"),
});Default Tables
Pre-built Drizzle tables using the standard column definitions:
import { authUsers, authSessions, authAccounts, authVerification } from "@j0nathan-ll0yd/auth";AUTH_SCHEMA_MAPPING
Schema mapping in the format expected by Better Auth's drizzleAdapter. Maps Better Auth model names to Drizzle tables:
const AUTH_SCHEMA_MAPPING = {
user: authUsers,
session: authSessions,
account: authAccounts,
verification: authVerification,
};Pass to drizzleAdapter({ schema: AUTH_SCHEMA_MAPPING }) or let createAuth() apply it automatically.
Types
AuthConfig
interface AuthConfig {
secret: string;
baseURL: string;
trustedOrigins?: string[];
sessionExpiresIn?: number; // seconds, default: 30 days
sessionUpdateAge?: number; // seconds, default: 24 hours
socialProviders?: {
apple?: SocialProviderConfig;
google?: SocialProviderConfig;
github?: SocialProviderConfig;
custom?: CustomOAuthProvider[];
};
schema?: Record<string, unknown>;
useSecureCookies?: boolean; // default: true
databaseGenerateId?: boolean;
encryptOAuthTokens?: boolean; // default: false
experimentalJoins?: boolean; // default: false (BetterAuth v1.5+)
}SocialProviderConfig
interface SocialProviderConfig {
clientId: string;
clientSecret: string;
enabled?: boolean;
appBundleIdentifier?: string;
}CustomOAuthProvider
interface CustomOAuthProvider {
providerId: string;
clientId: string;
clientSecret: string;
enabled?: boolean;
options?: Record<string, unknown>;
}SessionResult
interface SessionResult {
user: UserDetails;
session: { id: string; token: string; expiresAt: Date };
}UserDetails
interface UserDetails {
id: string;
email: string;
name?: string;
image?: string;
emailVerified: boolean;
}BetterAuthInstance
Opaque return type of betterAuth() used throughout the auth package.