Files
2026-05-27 23:29:53 +01:00

27 KiB

Capability Spec: better-auth-surrealdb

Package: @suaveplan/better-auth-surrealdb Status: ACTIVE

Requirements

Requirement: createBetterAuthSurrealAdapter returns a valid Better-Auth Adapter with Zod-validated options

The package SHALL export createBetterAuthSurrealAdapter(options): Adapter returning an object that satisfies Better-Auth's published Adapter interface: { id: "@suaveplan/better-auth-surrealdb"; create; findOne; findMany; update; updateMany; delete; deleteMany; count }. Options SHALL be Zod-validated via BetterAuthSurrealAdapterOptionsSchema (exported from ./schemas/options) at the factory boundary; failures SHALL raise BetterAuthSurrealError(BETTER_AUTH_SURREALDB_INVALID_INPUT) carrying the ZodError as cause.

Scenario: factory produces all required methods

  • WHEN createBetterAuthSurrealAdapter({ client }) is called with a valid SurrealAdapter
  • THEN the returned object has id === "@suaveplan/better-auth-surrealdb" and every Better-Auth Adapter method is a function

Scenario: invalid options raise INVALID_INPUT

  • WHEN createBetterAuthSurrealAdapter({} as never) is called (missing client)
  • THEN BetterAuthSurrealError(BETTER_AUTH_SURREALDB_INVALID_INPUT) is thrown with cause instanceof ZodError

Verified by: src/adapter/adapter.test.ts::"factory produces every Better-Auth Adapter method" AND src/adapter/adapter.test.ts::"invalid options raise INVALID_INPUT" AND src/adapter/adapter.spec-conformance.test.ts::"satisfies Better-Auth Adapter type via expectTypeOf"


Requirement: linkFields auto-prefix writes and strip prefixes on reads

The package SHALL export LinkFieldMap type and DEFAULT_BETTER_AUTH_LINK_FIELDS constant. The constant SHALL cover every record-link field in the bundled 17-table schema: session.userId, passkey.userId, member.userId/organizationId, invitation.inviterId/organizationId/teamId, team.organizationId, team_member.userId/teamId, two_factor.userId, api_key.userId, sso_provider.userId. On create and update, the adapter SHALL prefix string values for declared link fields with <target_table>:. On findOne, findMany, the adapter SHALL strip the matching prefix from those fields before returning rows. On Where[] values for declared link fields, the adapter SHALL pre-resolve the prefix before binding.

  • WHEN adapter.create({ model: "session", data: { userId: "u1", token: "t" } }) is awaited against a real SurrealDB testcontainer (schema applied) with default linkFields
  • THEN the stored row's userId field equals the SurrealDB record-link user:u1 AND the returned object has userId === "u1" (prefix stripped)
  • WHEN adapter.findMany({ model: "session", where: [{ field: "userId", value: "u1" }] }) is awaited
  • THEN the generated SurrealQL WHERE clause binds userId = user:u1 (NOT userId = "u1")

Scenario: empty linkFields disables prefix manipulation

  • WHEN createBetterAuthSurrealAdapter({ client, linkFields: {} }) runs create/findOne on session
  • THEN values pass through verbatim — no prefix added on write, none stripped on read

Scenario: account.userId (string-typed in schema) is NOT prefixed

  • WHEN adapter.create({ model: "account", data: { userId: "u1", providerId: "p", accountId: "a", password: "" } }) runs with default linkFields
  • THEN the stored userId field equals the plain string "u1" (NOT user:u1) — account is absent from DEFAULT_BETTER_AUTH_LINK_FIELDS

Verified by: src/link-fields/link-fields.test.ts::"DEFAULT_BETTER_AUTH_LINK_FIELDS covers every record-link field in the bundled schema" AND src/adapter/create.test.ts::"session.userId write wraps into record-link form" AND src/adapter/find.test.ts::"where-filter pre-resolves link prefix" AND src/link-fields/link-fields.test.ts::"empty linkFields disables prefix manipulation" AND src/link-fields/link-fields.test.ts::"account.userId is not prefixed under default config"


Requirement: create maps to SurrealQL CREATE … CONTENT … RETURN AFTER

adapter.create<T>({ model, data }) SHALL issue CREATE <modelToTable(model)> CONTENT $data RETURN AFTER (or CREATE <table>:<id> CONTENT $data when data.id is supplied or idStrategy === "record-id") via the configured SurrealAdapter.query path, return the first returned row with the <table>: prefix stripped from id AND from every declared link field. When idStrategy: "ulid", an auto-generated ULID SHALL be injected into data.id when Better-Auth does not supply one.

Scenario: create injects record-id prefix in storage

  • WHEN adapter.create({ model: "user", data: { id: "abc", email: "x@y" } }) is awaited
  • THEN a row exists at user:abc with email === "x@y" AND the returned object has id === "abc" (no user: prefix)

Scenario: ulid strategy fills missing id

  • WHEN adapter.create({ model: "session", data: { userId: "u1" } }) runs with idStrategy: "ulid" and data.id absent
  • THEN the returned object has a non-empty id matching /^[0-9A-Z]{26}$/

Verified by: src/adapter/create.test.ts::"create stores under table:id and strips prefix on return" AND src/adapter/create.test.ts::"ulid strategy fills missing id" AND tests/integration/conformance.test.ts::"create roundtrips against real SurrealDB"


Requirement: findOne and findMany compile Where[] to parameterised SurrealQL

adapter.findOne<T>({ model, where, select? }) and adapter.findMany<T>({ model, where?, limit?, offset?, sortBy?, select? }) SHALL build a parameterised SELECT <projection> FROM <table> [WHERE …] [ORDER BY <field> <dir>] [LIMIT …] [START …] query. findOne appends LIMIT 1 and returns the first row or null. Every returned row's id AND declared link fields SHALL have prefixes stripped.

Scenario: findOne returns null when no match

  • WHEN adapter.findOne({ model: "user", where: [{ field: "email", value: "missing@x" }] }) is awaited
  • THEN the result is null

Scenario: findMany honours limit / offset / sortBy

  • WHEN 5 user rows exist and adapter.findMany({ model: "user", limit: 2, offset: 1, sortBy: { field: "createdAt", direction: "asc" } }) is awaited
  • THEN the result has length 2 starting at the second-oldest row

Verified by: src/adapter/find.test.ts::"findOne returns null on no match" AND src/adapter/find.test.ts::"findMany honours limit/offset/sortBy" AND tests/integration/conformance.test.ts::"findOne and findMany conformance against real SurrealDB"


Requirement: compileWhere supports the full Better-Auth operator set

compileWhere(where: Where[], options) SHALL support operators eq (default), ne, gt, gte, lt, lte, in, not_in, contains, starts_with, ends_with AND connectors AND (default), OR. The output SHALL be { clause: string; vars: Record<string, unknown> } with placeholders $w0, $w1, …. Link-field values are pre-resolved per options.linkFields[model] before binding. Unknown operators SHALL raise BetterAuthSurrealError(BETTER_AUTH_SURREALDB_INVALID_OPERATOR).

Scenario: operator maps to SurrealQL

  • WHEN compileWhere([{ field: "age", value: 18, operator: "gte" }], { model: "user", linkFields: {} }) is called
  • THEN the result clause contains age >= $w0 AND vars.w0 === 18

Scenario: connector composes clauses

  • WHEN compileWhere([{ field: "a", value: 1 }, { field: "b", value: 2, connector: "OR" }], { model, linkFields: {} }) is called
  • THEN the clause is a = $w0 OR b = $w1

Scenario: unknown operator raises INVALID_OPERATOR

  • WHEN compileWhere([{ field: "x", value: 1, operator: "weird" as never }], { model, linkFields: {} }) is called
  • THEN BetterAuthSurrealError(BETTER_AUTH_SURREALDB_INVALID_OPERATOR) is thrown

Verified by: src/compile/compile-where.test.ts::"maps every supported operator" AND src/compile/compile-where.test.ts::"AND/OR connectors compose" AND src/compile/compile-where.test.ts::"unknown operator raises INVALID_OPERATOR" AND src/compile/compile-where.test.ts::"placeholder names never collide" AND src/compile/compile-where.test.ts::"link-field where values pre-resolved to record-link form"


Requirement: update / updateMany merge fields and return the affected count or row

adapter.update<T>({ model, where, update }) SHALL issue UPDATE <table> MERGE $update WHERE <compiled> RETURN AFTER and return the first updated row (with prefixes stripped) or null. adapter.updateMany(args) SHALL return the number of rows updated.

Scenario: update returns the merged row

  • WHEN a user exists with email: "old" and adapter.update({ model: "user", where: [{ field: "id", value: "u1" }], update: { email: "new" } }) is awaited
  • THEN the result has email === "new" AND id === "u1" (prefix stripped)

Scenario: updateMany returns count

  • WHEN 3 users have role: "free" and adapter.updateMany({ model: "user", where: [{ field: "role", value: "free" }], update: { role: "pro" } }) is awaited
  • THEN the result equals 3

Verified by: src/adapter/update.test.ts::"update returns merged row" AND src/adapter/update.test.ts::"updateMany returns affected count" AND tests/integration/conformance.test.ts::"update conformance against real SurrealDB"


Requirement: delete / deleteMany remove rows by Where filter

adapter.delete({ model, where }) SHALL issue DELETE <table> WHERE <compiled> and return void. adapter.deleteMany(args) SHALL return the number of rows deleted.

Scenario: delete removes the row

  • WHEN a session exists at session:s1 and adapter.delete({ model: "session", where: [{ field: "id", value: "s1" }] }) is awaited
  • THEN a subsequent findOne for that id returns null

Scenario: deleteMany returns count

  • WHEN 4 expired sessions exist and adapter.deleteMany({ model: "session", where: [{ field: "expiresAt", value: now, operator: "lt" }] }) is awaited
  • THEN the result equals 4

Verified by: src/adapter/delete.test.ts::"delete removes the row" AND src/adapter/delete.test.ts::"deleteMany returns affected count"


Requirement: count uses SELECT count() GROUP ALL

adapter.count({ model, where? }) SHALL issue SELECT count() FROM <table> [WHERE <compiled>] GROUP ALL and return the numeric count.

Scenario: count without where returns total

  • WHEN 10 sessions exist and adapter.count({ model: "session" }) is awaited
  • THEN the result equals 10

Scenario: count with where returns filtered

  • WHEN 10 sessions exist and 3 have userId: "u1", and adapter.count({ model: "session", where: [{ field: "userId", value: "u1" }] }) is awaited
  • THEN the result equals 3 (where-clause pre-resolves userId to record-link form)

Verified by: src/adapter/count.test.ts::"count without where returns total" AND src/adapter/count.test.ts::"count with where returns filtered"


Requirement: idStrategy controls record-ID format

The package SHALL support 3 idStrategy values: "record-id" (default), "plain", "ulid". idStrategy: "record-id" with an id containing a : character SHALL raise BetterAuthSurrealError(BETTER_AUTH_SURREALDB_INVALID_ID_FORMAT).

Scenario: plain strategy stores id as a field

  • WHEN createBetterAuthSurrealAdapter({ client, idStrategy: "plain" }) runs adapter.create({ model: "user", data: { id: "u1", email: "x" } })
  • THEN the stored record's SurrealDB-native id is auto-generated AND the row has fields id: "u1" and email: "x"

Scenario: invalid record-id format raises

  • WHEN the default strategy receives data.id = "abc:def" on create
  • THEN BetterAuthSurrealError(BETTER_AUTH_SURREALDB_INVALID_ID_FORMAT) is thrown

Verified by: src/id-strategy/id-strategy.test.ts::"plain strategy stores id as a field" AND src/id-strategy/id-strategy.test.ts::"record-id strategy strips and reapplies prefix" AND src/id-strategy/id-strategy.test.ts::"ulid strategy fills missing id" AND src/id-strategy/id-strategy.test.ts::"colon in record-id raises INVALID_ID_FORMAT"


Requirement: modelToTable mapping is overridable and validated

options.modelToTable?: (model: string) => string SHALL default to identity. When supplied, the function output SHALL be cached per model. Returned table names SHALL match /^[a-zA-Z_][a-zA-Z0-9_]*$/; mismatches raise BetterAuthSurrealError(BETTER_AUTH_SURREALDB_INVALID_MODEL) on first use.

Scenario: custom mapping prefixes every table

  • WHEN createBetterAuthSurrealAdapter({ client, modelToTable: (m) => "bauth_" + m }) runs adapter.create({ model: "user", data: { … } })
  • THEN the row is stored under bauth_user:<id>

Scenario: invalid table name raises

  • WHEN modelToTable returns "weird-name" for some model and create is called
  • THEN BetterAuthSurrealError(BETTER_AUTH_SURREALDB_INVALID_MODEL) is thrown

Verified by: src/adapter/model-to-table.test.ts::"custom mapping prefixes tables" AND src/adapter/model-to-table.test.ts::"invalid table name raises INVALID_MODEL" AND src/adapter/model-to-table.test.ts::"mapping cached after first call"


Requirement: Bundled SurrealQL schema covers all 17 Better-Auth tables

The package SHALL export BETTER_AUTH_SURREAL_SCHEMA (frozen string) from ./schema containing the DDL for all 17 Better-Auth tables: user (field extensions: emailVerified, name, image), 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 SHALL use DEFINE TABLE OVERWRITE and DEFINE FIELD OVERWRITE so re-application is idempotent. applyBetterAuthSchema(adapter): Promise<void> SHALL execute the schema string via adapter.execute. Schema content matches the canonical Suaveplan target at /mnt/f/surrealdb/surql/features/02-auth/02-better-auth.surql.

Scenario: schema string contains every required table

  • WHEN the conformance test parses BETTER_AUTH_SURREAL_SCHEMA for DEFINE TABLE OVERWRITE <name> occurrences
  • THEN the set includes session, account, verification, passkey, jwks, sso_provider, oauth_application, oauth_access_token, oauth_consent, organization, member, invitation, team, team_member, two_factor, api_key (16 explicit DEFINE TABLE — user table is created elsewhere, only fields are extended here)

Scenario: applyBetterAuthSchema is idempotent

  • WHEN applyBetterAuthSchema(adapter) is called twice in succession against a real SurrealDB testcontainer
  • THEN the second call resolves without error AND the table set is unchanged

Scenario: applied schema accepts canonical Better-Auth row writes

  • WHEN applyBetterAuthSchema(adapter) is called, then adapter.create is invoked for a session with userId: "user:u1" and an account with userId: "u1"
  • THEN both writes succeed (session enforces record<user>; account accepts string)

Verified by: src/schema/schema.test.ts::"BETTER_AUTH_SURREAL_SCHEMA contains every required table" AND src/schema/schema.test.ts::"applyBetterAuthSchema is idempotent against real SurrealDB" AND tests/integration/schema-write-compatibility.test.ts::"applied schema accepts canonical Better-Auth row writes"


Requirement: Zod schemas exported at ./schemas/* subpaths for cross-package consumption

The package SHALL export Zod schemas at dedicated subpaths so downstream callers consume the same types the producer asserts at the boundary: ./schemas/options exports BetterAuthSurrealAdapterOptionsSchema; ./schemas/link-fields exports LinkFieldMapSchema. Boundary failures SHALL raise BetterAuthSurrealError(BETTER_AUTH_SURREALDB_INVALID_INPUT) carrying the ZodError as cause.

  • WHEN LinkFieldMapSchema.safeParse({ session: { userId: 5 } }) is called (non-string target table)
  • THEN safeParse.success === false

Scenario: createBetterAuthSurrealAdapter rejects malformed linkFields

  • WHEN createBetterAuthSurrealAdapter({ client, linkFields: { session: { userId: 5 as never } } }) is called
  • THEN BetterAuthSurrealError(BETTER_AUTH_SURREALDB_INVALID_INPUT) is thrown with cause instanceof ZodError

Verified by: src/schemas/options.test.ts::"BetterAuthSurrealAdapterOptionsSchema rejects missing client" AND src/schemas/link-fields.test.ts::"LinkFieldMapSchema rejects non-string target table" AND src/adapter/adapter.test.ts::"createBetterAuthSurrealAdapter rejects malformed linkFields"


Requirement: Every BETTER_AUTH_SURREALDB_* code is thrown from a production path

BETTER_AUTH_SURREALDB_CODES SHALL define exactly 7 codes (INVALID_INPUT, INVALID_MODEL, INVALID_OPERATOR, QUERY_FAILED, INVALID_ID_FORMAT, INVALID_LINK_FIELD_VALUE, CONNECTION_NOT_READY). Every code SHALL be raised by ≥1 production call site.

Scenario: QUERY_FAILED wraps underlying SurrealError

  • WHEN the underlying SurrealAdapter.query throws a SurrealError and an adapter operation is in-flight
  • THEN the operation rejects with BetterAuthSurrealError(BETTER_AUTH_SURREALDB_QUERY_FAILED) carrying the SurrealError as cause
  • WHEN adapter.create({ model: "session", data: { userId: "abc:def" } }) is called with default linkFields (record-link target receives a value containing :)
  • THEN BetterAuthSurrealError(BETTER_AUTH_SURREALDB_INVALID_LINK_FIELD_VALUE) is thrown

Verified by: src/better-auth-surrealdb.spec-conformance.test.ts::"every BETTER_AUTH_SURREALDB_* code is thrown from a production path"


Requirement: OTel counter + histogram emit per operation

The package SHALL register counter better_auth.surrealdb.operations_total (attributes: model, operation, outcome: "success" | "error") and histogram better_auth.surrealdb.operation_duration_ms (attribute: operation). Every adapter method emits both.

Scenario: counter emits success + error

  • WHEN a successful create and a failed operation run against a real in-memory MeterProvider
  • THEN operations_total{outcome: "success"} AND operations_total{outcome: "error"} both have observations; the histogram records both durations

Verified by: src/otel/otel.test.ts::"counter emits success" AND src/otel/otel.test.ts::"counter emits error" AND src/otel/otel.test.ts::"histogram records per operation"


Requirement: Conformance harness exercises the full Better-Auth Adapter contract

The package SHALL export runAdapterConformance(factory) from ./testing. The harness SHALL apply BETTER_AUTH_SURREAL_SCHEMA to the testcontainer-backed connection before running scenarios, then exercise every contract scenario Better-Auth's first-party adapters pass (CRUD + count + Where-filter combinations).

Scenario: harness registers every Better-Auth scenario

  • WHEN runAdapterConformance(() => createBetterAuthSurrealAdapter({ client })) is invoked inside a test file backed by a real SurrealDB testcontainer
  • THEN the resulting test run includes a "Better-Auth adapter conformance" describe block covering create / findOne / findMany / update / updateMany / delete / deleteMany / count, plus per-operator Where-filter coverage and link-field roundtrip checks

Verified by: tests/integration/conformance.test.ts::"runAdapterConformance pins every Better-Auth scenario against real SurrealDB"


Requirement: Server-tier package shape

The package SHALL run in Bun + Node. package.json SHALL declare subpath exports per feature module (., ./adapter, ./compile, ./id-strategy, ./link-fields, ./schema, ./schemas/options, ./schemas/link-fields, ./testing, ./error-codes, ./otel, ./types). Every implementation .ts SHALL have a sibling .md ≥200 words. No export * in feature barrels. 100/100/100/100 coverage via Istanbul.

Scenario: every subpath is reachable

  • WHEN tests/dep-graph.test.ts inspects package.json.exports and imports each subpath
  • THEN each resolves to a real module

Verified by: tests/dep-graph.test.ts::"every subpath export resolves" AND tests/dep-graph.test.ts::"every impl .ts has a sibling .md" AND tests/dep-graph.test.ts::"dependencies match the allowlist"


Requirement: betterAuthSurrealAdapter returns a Better-Auth v1.6 DBAdapterInstance

The package SHALL export betterAuthSurrealAdapter(options): DBAdapterInstance from the root barrel AND from a new ./adapter/v1.6 subpath. The return value SHALL be a function (betterAuthOptions: BetterAuthOptions) => DBAdapter that Better-Auth v1.6's createBetterAuth accepts as its database field. The factory SHALL Zod-validate options via BetterAuthSurrealAdapterOptionsSchema (same schema the v1.4 factory uses); failures raise BetterAuthSurrealError(BETTER_AUTH_SURREALDB_INVALID_INPUT).

Scenario: factory consumed by createBetterAuth

  • WHEN createBetterAuth({ database: betterAuthSurrealAdapter({ client }), secret: "x", baseUrl: "http://localhost" }) is called against a real SurrealDB testcontainer with the schema applied
  • THEN the call returns a BetterAuth instance whose auth.api.signUpEmail({ body: { email, password, name } }) round-trips successfully (creates user, account, session)

Verified by: src/adapter/v1.6.test.ts::"factory consumed by createBetterAuth signs up successfully" AND src/adapter/v1.6.spec-conformance.test.ts::"return type satisfies DBAdapterInstance via expectTypeOf"


Requirement: v1.6 factory wraps the shipped 10-method adapter via createAdapterFactory

The v1.6 factory SHALL internally call createBetterAuthSurrealAdapter(options) to build the underlying 10-method adapter and SHALL wrap it via createAdapterFactory from @better-auth/core/db. The wrapper SHALL forward every Better-Auth v1.6 method onto the underlying adapter's existing methods: findOne/findMany/create/update/updateMany/delete/deleteMany/count map 1:1; transaction forwards to runSurrealTransaction from @suaveplan/surrealdb; getDefaultIdField returns "id"; createSchema returns BETTER_AUTH_SURREAL_SCHEMA. Underlying error classes (BetterAuthSurrealError) propagate through the wrapper unchanged.

Scenario: error propagation through the wrapper

  • WHEN an invalid operator surfaces in a wrapped v1.6 call (e.g. a findMany whose Where uses an unsupported op)
  • THEN the rejection is a BetterAuthSurrealError(BETTER_AUTH_SURREALDB_INVALID_OPERATOR) (not a generic wrapper error)

Scenario: transaction forwards to SurrealDB

  • WHEN Better-Auth invokes adapter.transaction(async (txAdapter) => txAdapter.create({...})) on the wrapped factory
  • THEN runSurrealTransaction is invoked AND the inner create runs inside the transaction (rolled back on throw, committed on resolve)

Verified by: src/adapter/v1.6.test.ts::"errors propagate from the underlying adapter" AND src/adapter/v1.6.test.ts::"transaction forwards to runSurrealTransaction" AND src/adapter/v1.6.test.ts::"getDefaultIdField returns 'id'"


Requirement: createSchema returns BETTER_AUTH_SURREAL_SCHEMA verbatim

The wrapped adapter's createSchema method (called by Better-Auth's CLI generator) SHALL return the exact string content of BETTER_AUTH_SURREAL_SCHEMA. No interpolation, no per-config customisation in v1 — operators who need a modified schema run @better-auth/cli generate --output ./schema.surql then edit the file.

Scenario: createSchema output matches BETTER_AUTH_SURREAL_SCHEMA

  • WHEN the wrapped adapter's createSchema() is invoked
  • THEN the result equals BETTER_AUTH_SURREAL_SCHEMA (character-for-character)

Verified by: src/adapter/v1.6.test.ts::"createSchema returns BETTER_AUTH_SURREAL_SCHEMA verbatim"


Requirement: v1.6 wrapper is exercised end-to-end against real Better-Auth + SurrealDB

The v1.6 wrapper SHALL be exercised by a full Better-Auth end-to-end integration test (tests/integration/better-auth-end-to-end.test.ts) that constructs betterAuth({ database: betterAuthSurrealAdapter({ client }) }) against a real SurrealDB testcontainer and rounds-trips auth.api.signUpEmail. The existing v1.4-shape runAdapterConformance harness MAY NOT run verbatim against the v1.6 DBAdapter because Better-Auth's transformInput whitelists fields against the in-memory Better-Auth schema, which uses column names that differ from the bundled SurrealQL schema (e.g. Better-Auth's verification.value vs. the package's verification.val). The wrapper is still smoke-tested against the testcontainer (factory builds, DBAdapter shape correct) and unit-tested at 100% line/branch/function coverage in src/adapter/v1.6.test.ts.

Scenario: v1.4-shape harness runs against the v1.4 factory

  • WHEN the integration test runs runAdapterConformance(() => createBetterAuthSurrealAdapter({ client })) against the testcontainer
  • THEN every scenario passes (no regression in the v1.4 surface)

Scenario: v1.6 wrapper round-trips a full Better-Auth signUpEmail flow

  • WHEN the e2e test constructs betterAuth({ database: betterAuthSurrealAdapter({ client }), … }) and calls auth.api.signUpEmail({ body: { email, password, name } }) against the testcontainer
  • THEN the call resolves with a user whose email matches the input, AND the SurrealDB user table contains a row with that email, AND the account table contains a credential-provider row

Verified by: tests/integration/conformance.test.ts::"v1.4-shape adapter passes conformance" AND tests/integration/conformance.test.ts::"factory build + DBAdapter shape smoke check (full flow lives in better-auth-end-to-end.test.ts)" AND tests/integration/better-auth-end-to-end.test.ts::"createBetterAuth + signUpEmail round-trips against real SurrealDB"


Requirement: README v1.6 quick-start is the default; v1.4 documented as advanced

The package README SHALL show betterAuthSurrealAdapter (v1.6) usage as the default quick-start. The v1.4-shape createBetterAuthSurrealAdapter SHALL be documented under an "Advanced — direct conformance harness usage" section. New subpath ./adapter/v1.6 is listed alongside the existing ./adapter.

Scenario: README quick-start uses betterAuthSurrealAdapter

  • WHEN the docs:check / readme-lint pass scans the README
  • THEN the first non-install code block uses betterAuthSurrealAdapter(...) (not the v1.4 surface)

Verified by: tests/dep-graph.test.ts::"README quick-start uses betterAuthSurrealAdapter v1.6 factory" AND tests/dep-graph.test.ts::"./adapter/v1.6 subpath export resolves"