# Add @suaveplan/better-auth-surrealdb — Better-Auth adapter + bundled SurrealQL schema **Package**: `@suaveplan/better-auth-surrealdb` (NEW, server-tier) **Status**: PROPOSED **Type**: new-package **Position**: foundation of the better-auth integration chain — gates `add-api-plugin-auth` - **Spec delta**: ./specs/better-auth-surrealdb/spec.md ## Why Better-Auth supports any backing store via its `Adapter` interface (CRUD + count + Where-filter compilation). Its first-party adapters target Drizzle, Prisma, Kysely, in-memory. SurrealDB has no first-party adapter, but genesis already ships `@suaveplan/surrealdb` (universal `SurrealAdapter` over a real SurrealDB v3 connection), `@suaveplan/api-migrations` (the migration runner with `createSurrealqlAdapter`), and `@suaveplan/testing-integration-surrealdb` (testcontainers fixture). Writing a thin Better-Auth → SurrealDB adapter is the cleanest path to running Better-Auth on Suaveplan's database of record. This package is deliberately **separate from `@suaveplan/api-plugin-auth`** because Better-Auth adapters are useful outside the plugin framework — any Bun/Node app that wants Better-Auth + SurrealDB can pick this up without dragging in the `@suaveplan/api` orchestrator. This package ALSO bundles the canonical 17-table Better-Auth SurrealQL schema as a static asset, so operators get one-stop integration: adapter + schema + conformance harness ship together and stay in lockstep across releases. ## Plug-and-play contract (cross-cutting principle for every api-plugin-* package) This adapter is the first concrete instance of the **3-way plug-and-play contract** every `@suaveplan/api-plugin-*` package follows from here on: 1. **`.surql` schema** — every plugin (or supporting adapter) that owns SurrealDB tables ships its DDL as a bundled string asset at the `./schema` subpath. Operators apply it via `@suaveplan/api-migrations` `createSurrealqlAdapter` — never hand-coded against a remote source-of-truth file. Schema versions move in lockstep with the package version. 2. **Zod schemas** — every public input boundary (adapter options, route bodies, query params, headers) is Zod-validated, exported under `./schemas/` so downstream callers consume the same types the producer asserts. Boundary failures raise typed `SuaveplanError` codes with the ZodError as `cause`. 3. **OpenAPI integration** — every route a plugin owns is built with `@suaveplan/api-openapi`'s `RouteBuilder` (preserves Zod types end-to-end) and registered via `definePluginRoutes(routes, { app, registry, pluginName })`. The host emits one unified OpenAPI 3.1 document covering every plugin atomically. For this adapter (which has no HTTP surface), points 1 and 2 apply: schema bundled, options Zod-validated. Point 3 lands in `@suaveplan/api-plugin-auth` (the first plugin that consumes this adapter and ships routes). ## What changes Net-new package at `0.1.0`. ADDED: ### Adapter factory - `createBetterAuthSurrealAdapter(options): Adapter` — factory returning a Better-Auth `Adapter` object satisfying the published v1.4 interface. `options` is Zod-validated via `BetterAuthSurrealAdapterOptionsSchema`. - `BetterAuthSurrealAdapterOptions`: `{ client: SurrealAdapter | SurrealConnection; modelToTable?: (model) => string; idStrategy?: IdStrategy; linkFields?: LinkFieldMap; logger?: LoggerLike }`. `client` accepts either the high-level `SurrealAdapter` (preferred — connection-managed) or a raw `SurrealConnection`. - `IdStrategy` — controls SurrealDB record-ID format: `"record-id"` (default — `id` becomes record component, storage uses `table:id`), `"plain"` (`id` stored as a plain field on an auto-named record), or `"ulid"` (auto-generated when missing). - `modelToTable(model)` — default identity. Validated against `/^[a-zA-Z_][a-zA-Z0-9_]*$/` on first use per model. ### linkFields — the mixed record-link / string-userId fix The genesis target Better-Auth schema (see "Bundled schema" below) uses **mixed user-reference conventions**: some tables type `userId` as `record` (typed link, stored as `user:abc`), others as plain `string`. Better-Auth gives the adapter `userId: "abc"` regardless of which table; without per-field record-link awareness, writes to `record` columns fail SurrealDB's type assertion and reads return prefixed strings that break Better-Auth's identity comparisons. - `LinkFieldMap` — `Record>`. For each entry, on write the adapter prefixes plain string values with `:` AND on read the inverse — strips the prefix from those same fields before returning. - `DEFAULT_BETTER_AUTH_LINK_FIELDS` — baked-in default matching the bundled 17-table schema: ```ts export const DEFAULT_BETTER_AUTH_LINK_FIELDS: LinkFieldMap = { session: { userId: "user" }, passkey: { userId: "user" }, member: { userId: "user", organizationId: "organization" }, invitation: { inviterId: "user", organizationId: "organization", teamId: "team" }, team: { organizationId: "organization" }, team_member: { userId: "user", teamId: "team" }, two_factor: { userId: "user" }, api_key: { userId: "user" }, sso_provider:{ userId: "user" }, }; ``` - Operators can override per-model when their schema differs (e.g. flat-string userId everywhere). Passing `linkFields: {}` disables prefix manipulation entirely (for "plain" schemas). - Where-filter values for link fields are auto-prefixed too: `{ field: "userId", value: "abc" }` on `session` compiles to `WHERE userId = user:abc`, not `WHERE userId = "abc"`. ### Where-clause compiler - `compileWhere(where, options)` — pure helper mapping Better-Auth's `Where[]` filter onto a SurrealQL `WHERE` fragment + parameterised vars. Operators: `eq` (default), `ne`, `gt`, `gte`, `lt`, `lte`, `in`, `not_in`, `contains`, `starts_with`, `ends_with`. Connectors `AND` (default), `OR`. Placeholders `$w0`, `$w1`, … prevent injection. Link-field values pre-resolved per `options.linkFields[model]` before binding. - `compileSortBy({ field, direction })` → `ORDER BY `. - `compileSelect(model, select?)` → projection builder. ### 8 Better-Auth Adapter methods All implementations dispatch through the configured `SurrealAdapter`'s `query` path: - `create({ model, data })` → `CREATE CONTENT $data RETURN AFTER` or `CREATE
:` (idStrategy="record-id"). Link fields in `data` auto-prefixed; returned row's `id` and link fields stripped of `
:`. - `findOne({ model, where, select? })` → `SELECT FROM
WHERE LIMIT 1`. Returns the first row (with id+link-field prefixes stripped) or `null`. - `findMany({ model, where?, limit?, offset?, sortBy?, select? })` → full `SELECT … LIMIT … START … ORDER BY …` pipeline. - `update({ model, where, update })` → `UPDATE
MERGE $update WHERE RETURN AFTER` → first updated row or `null`. - `updateMany({ model, where, update })` → same as `update`, returns numeric count. - `delete({ model, where })` → `DELETE
WHERE ` → void. - `deleteMany({ model, where })` → same, returns count. - `count({ model, where? })` → `SELECT count() FROM
[WHERE ] GROUP ALL` → numeric. ### Bundled SurrealQL schema (point 1 of the plug-and-play contract) - `./schema` subpath exports the canonical 17-table Better-Auth SurrealQL schema as a frozen string constant `BETTER_AUTH_SURREAL_SCHEMA`. Tables: `user` (field extensions only — the base table is created by the host's user schema), `session`, `account`, `verification`, `passkey`, `jwks`, `sso_provider`, `oauth_application`, `oauth_access_token`, `oauth_consent`, `organization`, `member`, `invitation`, `team`, `team_member`, `two_factor`, `api_key`. Plus 13 indexes (covering email uniqueness, phone uniqueness, provider+account combos, etc.). - DDL uses `DEFINE TABLE OVERWRITE` and `DEFINE FIELD OVERWRITE` so re-application is idempotent (matches the genesis target's authoring convention). - `applyBetterAuthSchema(adapter: SurrealAdapter): Promise` — convenience helper that runs the schema string via `adapter.execute`. Operators using `@suaveplan/api-migrations` apply it as a migration step instead; this helper is for callers who haven't adopted that runner. - The schema content matches the canonical Suaveplan target schema at `/mnt/f/surrealdb/surql/features/02-auth/02-better-auth.surql`. Schema version moves in lockstep with this package's version; consumers pin both together. ### Zod boundary schemas (point 2 of the plug-and-play contract) - `./schemas/options` exports `BetterAuthSurrealAdapterOptionsSchema` (Zod) — runtime-validated at `createBetterAuthSurrealAdapter` boundary; invalid input raises `BetterAuthSurrealError(BETTER_AUTH_SURREALDB_INVALID_INPUT)` carrying the ZodError as `cause`. - `./schemas/link-fields` exports `LinkFieldMapSchema` — Zod-validated structure check on the optional `linkFields` argument. ### Errors + observability - `BetterAuthSurrealError extends SuaveplanError` via `defineCodes("BETTER_AUTH_SURREALDB", [...])` — codes: `INVALID_INPUT`, `INVALID_MODEL`, `INVALID_OPERATOR`, `QUERY_FAILED`, `INVALID_ID_FORMAT`, `INVALID_LINK_FIELD_VALUE`, `CONNECTION_NOT_READY`. - OTel: counter `better_auth.surrealdb.operations_total{model, operation, outcome}`, histogram `better_auth.surrealdb.operation_duration_ms{operation}`. ### Subpath exports `.`, `./adapter`, `./compile`, `./id-strategy`, `./link-fields`, `./schema`, `./schemas/options`, `./schemas/link-fields`, `./error-codes`, `./otel`, `./types`, `./testing`. The `./testing` subpath re-exports `runAdapterConformance(factory)` — runs Better-Auth's full adapter contract against the factory backed by a real SurrealDB testcontainer (applies `BETTER_AUTH_SURREAL_SCHEMA` first). ## Out of scope - **Schema migration framework integration.** This package ships the schema as a string; running it via `@suaveplan/api-migrations` is the operator's choice (and the recommended path — `applyBetterAuthSchema` is a convenience escape hatch). - **Multi-tenant tenant-id auto-injection.** If Better-Auth needs tenant scoping, the operator passes a tenant-scoped `SurrealAdapter` (constructed against a tenant-specific namespace). The adapter does not interpolate tenant context. - **Transactions across Better-Auth operations.** Better-Auth's `Adapter` API doesn't expose transactions to callers; SurrealDB's `runSurrealTransaction` is reachable to operators who need cross-adapter atomicity directly. - **Other Better-Auth-supported databases.** This package is SurrealDB only. - **Schema diffing / generation.** Operators read `BETTER_AUTH_SURREAL_SCHEMA` for the latest, or pin to a package version. ## Source mapping This package is **genesis-original** (no upstream port). Design intent draws from: - Better-Auth's published `Adapter` interface (https://better-auth.com/docs/concepts/database#custom-adapter). - `@suaveplan/surrealdb` shipped surface (`SurrealAdapter`, `surrealCreate`/`surrealSelect`/`surrealUpdate`/`surrealDelete`, `runSurrealTransaction`). - SurrealQL `WHERE`/`ORDER BY`/`LIMIT`/`START`/`SELECT count() GROUP ALL` syntax. - The reference Drizzle adapter (https://github.com/better-auth/better-auth/blob/main/packages/better-auth/src/adapters/drizzle-adapter/drizzle-adapter.ts) — used as the canonical example of expected adapter behaviour for `idStrategy` semantics and `Where[]` compilation. - The canonical 17-table SurrealQL schema bundled at `/mnt/f/surrealdb/surql/features/02-auth/02-better-auth.surql` (the user's source-of-truth schema repo). The genesis package bundles this verbatim as `BETTER_AUTH_SURREAL_SCHEMA`; future schema changes update both in lockstep. | Requirement | Source | |---|---| | `Adapter` interface (8 methods + `id`) | Better-Auth docs | | Where[] compilation with operator + connector | Better-Auth `Where` type | | Record-ID prefix normalisation | SurrealDB `table:id` convention | | `linkFields` per-field record-link awareness | Genesis target schema's mixed `record` vs `string` conventions | | 17-table bundled schema | `/mnt/f/surrealdb/surql/features/02-auth/02-better-auth.surql` verbatim | | Plug-and-play contract (`.surql` + Zod + OpenAPI) | Genesis architectural principle established in this proposal | | Zod boundary validation + structured errors + OTel | `CLAUDE.md` enterprise policy | ## Impact ### Affected dependents None today — net-new. First consumer: `@suaveplan/api-plugin-auth` (sibling change `add-api-plugin-auth`). Reusable by any Bun/Node app wanting Better-Auth + SurrealDB. ### Catalog additions - `better-auth` ^1.4.0 added to a new `catalog:auth` block in root `package.json` (peer dep here; runtime dep in `api-plugin-auth`).