# @suaveplan/api-plugin-auth - Better-Auth-first authentication plugin for @suaveplan/api. Wraps Better-Auth as a @suaveplan/api-plugin definePlugin descriptor, applies the canonical 17-table SurrealDB schema at boot via @suaveplan/api-migrations, mounts 9 OpenAPI routes through @suaveplan/api-openapi, registers a typed authService in the host ServiceRegistry, installs a global session-resolver middleware that puts a typed Principal on every Hono context, and exports requireAuth / requireRole / requireEmailVerified middleware factories. Server-tier — runs in Bun and Node. Reference implementation of the 3-way plug-and-play contract (.surql DDL + Zod schemas at boundary + OpenAPI route registration) every @suaveplan/api-plugin-* follows. Better-Auth-first authentication plugin for `@suaveplan/api`. The canonical exemplar of the 3-way plug-and-play contract every `@suaveplan/api-plugin-*` package follows: a plugin owns its `.surql` DDL, its Zod boundary schemas, and its OpenAPI route definitions — all wired into the host's `PluginRegistry`, `MigrationRunner`, and `OpenAPIRegistry` from a single `definePlugin` descriptor. ## Documentation tree - [error-codes](./src/error-codes/error-codes.md) — The error-codes module owns the stable `AUTH.*` code registry and the `AuthError` class used by every production failur… - [error-codes](./src/error-codes/error-codes.md) — The error-codes module owns the stable `AUTH.*` code registry and the `AuthError` class used by every production failur… - [context-augmentation](./src/middleware/context-augmentation.md) — This module installs the canonical Hono `ContextVariableMap` augmentation that lets every consumer call `c.get("user")`… - [require-auth](./src/middleware/require-auth.md) — `requireAuth()` is the canonical Hono middleware factory for gating a route or sub-route on an authenticated principal.… - [require-email-verified](./src/middleware/require-email-verified.md) — `requireEmailVerified()` is the Hono middleware factory that gates a route on the principal's email having been verifie… - [require-role](./src/middleware/require-role.md) — `requireRole(...roles)` builds a Hono middleware that gates a route on the principal carrying at least one of the suppl… - [session](./src/middleware/session.md) — `sessionMiddleware` is the global Hono middleware that resolves every incoming request's Better-Auth session and expose… - [otel](./src/otel/instruments.md) — The instruments module owns the OpenTelemetry counters and histogram emitted by every code path inside `@suaveplan/api-… - [instruments](./src/otel/instruments.md) — The instruments module owns the OpenTelemetry counters and histogram emitted by every code path inside `@suaveplan/api-… - [plugin](./src/plugin/plugin.md) — `createAuthPlugin(options)` is the factory that returns a typed `PluginDescriptor` ready to register… - [plugin](./src/plugin/plugin.md) — `createAuthPlugin(options)` is the factory that returns a typed `PluginDescriptor` ready to register… - [principal](./src/principal/principal.md) — The principal module defines the canonical authenticated-user shape used across the entire `@suaveplan/api-plugin-*` fa… - [principal](./src/principal/principal.md) — The principal module defines the canonical authenticated-user shape used across the entire `@suaveplan/api-plugin-*` fa… - [routes](./src/routes/build-routes.md) — `buildAuthRoutes(auth)` returns the 9 `PluginRouteEntry` values that `createAuthPlugin` mounts on the host's Hono app v… - [build-routes](./src/routes/build-routes.md) — `buildAuthRoutes(auth)` returns the 9 `PluginRouteEntry` values that `createAuthPlugin` mounts on the host's Hono app v… - [schema](./src/schema/schema.md) — This module re-exports the canonical Better-Auth SurrealQL schema shipped by `@suaveplan/better-auth-surrealdb` so cons… - [schema](./src/schema/schema.md) — This module re-exports the canonical Better-Auth SurrealQL schema shipped by `@suaveplan/better-auth-surrealdb` so cons… - [forgot-password](./src/schemas/routes/forgot-password.md) — Schemas for `POST /auth/forgot-password`. The body schema accepts a single email and the response is `{ sent: true }`.… - [oauth](./src/schemas/routes/oauth.md) — Schemas for the OAuth pair `GET /auth/oauth/:provider/start` and `GET /auth/oauth/:provider/callback`. The start endpoi… - [reset-password](./src/schemas/routes/reset-password.md) — Schemas for `POST /auth/reset-password`. The body accepts the single-use token previously emailed to the user plus the… - [session](./src/schemas/routes/session.md) — Response schema for `GET /auth/session`. Returns the current authenticated `Principal` and the matching Better-Auth ses… - [sign-in](./src/schemas/routes/sign-in.md) — Request/response Zod schemas for `POST /auth/sign-in`. The body schema validates email + password credential input; the… - [sign-out](./src/schemas/routes/sign-out.md) — Schemas for `POST /auth/sign-out`. The body is optional (Better-Auth identifies the session from the cookie or `Authori… - [sign-up](./src/schemas/routes/sign-up.md) — Request/response Zod schemas for `POST /auth/sign-up`. The body schema constrains the email + password + optional name… - [verify-email](./src/schemas/routes/verify-email.md) — Schemas for `POST /auth/verify-email`. The body carries the single-use verification token (typically emailed from a sig… - [options](./src/schemas/options.md) — The options module exports `AuthPluginOptionsSchema`, the Zod validator that `createAuthPlugin` runs at the factory bou… - [principal](./src/schemas/principal.md) — This module exports the canonical Zod schemas for the auth boundary — `PrincipalSchema`, `SessionSchema`, `AccountSchem… - [auth-service](./src/services/auth-service.md) — The `auth-service` module owns the `AuthService` interface, its constructor (`createAuthService`), and the side-effect… - [registry-augmentation](./src/services/registry-augmentation.md) — Module augmentation that wires the `authService` slot into `@suaveplan/api-plugin/service-registry`'s `ServiceRegistry`… - [types](./src/types/types.md) — Top-level type definitions for `@suaveplan/api-plugin-auth`. This module exports the `AuthPluginOptions` shape (what ca… - [types](./src/types/types.md) — Top-level type definitions for `@suaveplan/api-plugin-auth`. This module exports the `AuthPluginOptions` shape (what ca… ## Quick start ```ts import { defineApp } from "@suaveplan/api"; import { betterAuthSurrealAdapter } from "@suaveplan/better-auth-surrealdb"; import { createAuthPlugin } from "@suaveplan/api-plugin-auth"; const auth = await defineApp() .name("my-api") .use( createAuthPlugin({ betterAuth: { database: betterAuthSurrealAdapter({ client: surreal }), secret: process.env.BETTER_AUTH_SECRET!, baseURL: "http://localhost:3000", emailAndPassword: { enabled: true }, }, }) ) .start(); ``` `createAuthPlugin` returns a typed `PluginDescriptor`. The host's `PluginRegistry` calls `onInit(ctx)` which: 1. Resolves `app`, `openAPIRegistry`, `migrationRunner`, and `db` from the host `ServiceRegistry`. 2. Applies `BETTER_AUTH_SURREAL_SCHEMA` (the canonical 17-table DDL shipped by `@suaveplan/better-auth-surrealdb`) — uses the `migrationRunner` when registered, otherwise calls `applyBetterAuthSchema(db)`. 3. Constructs the Better-Auth instance. 4. Registers `authService` in the typed `ServiceRegistry`. 5. Mounts 9 OpenAPI routes via `definePluginRoutes` so they appear in the unified `/openapi.json` doc with full Zod schemas. 6. Installs a global session-resolver middleware so every request's `c.get("user")` is a typed `Principal | null`. ## The 3-way plug-and-play contract The auth plugin is the canonical example of the plugin contract. Three artefacts move together; the plugin owns all three. | Surface | Implementation | |---|---| | `.surql` DDL | Re-exports `BETTER_AUTH_SURREAL_SCHEMA` (17 tables) and applies it via the host's `MigrationRunner` | | Zod schemas | Per-route bodies + responses + the canonical `PrincipalSchema` / `SessionSchema` / `AccountSchema` at `./schemas/principal` | | OpenAPI routes | `buildAuthRoutes(auth)` returns 9 `RouteBuilder` entries; `definePluginRoutes` writes both the Hono app AND the `OpenAPIRegistry` | User and vault plugins follow the same shape — read this README to understand the contract, then look at `api-plugin-user` / `api-plugin-vault` for second and third examples. ## Middleware recipes ```ts import { requireAuth, requireRole, requireEmailVerified, } from "@suaveplan/api-plugin-auth"; app.use("/me", requireAuth()); app.use("/admin/*", requireAuth(), requireRole("admin")); app.use("/billing/*", requireAuth(), requireEmailVerified()); ``` Each middleware throws a typed `AuthError(AUTH_*)` carrying `context.statusCode` so the host's error-handler returns the right HTTP status (401 / 403). The error-code registry has 10 entries — see `./src/error-codes/error-codes.md` for the full list. ## OAuth + organisations + 2FA Better-Auth handles every plugin / provider option. Pass them through `options.betterAuth` verbatim: ```ts createAuthPlugin({ betterAuth: { database: betterAuthSurrealAdapter({ client: surreal }), secret: process.env.BETTER_AUTH_SECRET!, baseURL: "https://app.example.test", emailAndPassword: { enabled: true }, socialProviders: { github: { clientId: process.env.GITHUB_CLIENT_ID!, clientSecret: process.env.GITHUB_CLIENT_SECRET!, }, }, }, }); ``` The plugin re-emits Better-Auth's full router under `routePrefix` — 9 OpenAPI-typed routes are advertised; the remaining surface (e.g. the OAuth provider list) is still reachable but not documented in the served OpenAPI doc. ## Schema apply + migrations integration The plugin applies the 17-table DDL during `onInit` by default. Two paths are supported: - **`migrationRunner` (preferred)** — the runner registers the schema as a tracked migration named `0000-better-auth-schema` so subsequent custom migrations can build on it. - **`db` fallback** — when no runner is registered, the plugin calls `applyBetterAuthSchema(db)` directly. The DDL is idempotent (`DEFINE TABLE OVERWRITE`), so re-running is safe. Disable the apply step entirely with `applySchemaOnInit: false` when operators manage migrations externally. ## Customising the principal Override the Better-Auth → `Principal` mapping with `options.principalMapping` to project plugin-extended user fields (organisations, 2FA flags) onto the resolved principal. The default mapping (`mapBetterAuthUserToPrincipal`) covers email, verification state, name/image metadata, and `activeOrganizationId` → `tenantId`. ```ts import { mapBetterAuthUserToPrincipal } from "@suaveplan/api-plugin-auth"; createAuthPlugin({ betterAuth, principalMapping: (view) => { const base = mapBetterAuthUserToPrincipal(view); const session = view.session as { roles?: string[] }; return { ...base, roles: [...base.roles, ...(session.roles ?? [])] }; }, }); ``` ## Subpath exports `.`, `./plugin/plugin`, `./middleware/require-auth`, `./middleware/require-role`, `./middleware/require-email-verified`, `./middleware/session`, `./principal/principal`, `./services/auth-service`, `./schemas/principal`, `./schemas/options`, `./schemas/routes`, `./routes/routes`, `./schema/schema`, `./error-codes/error-codes`, `./otel/instruments`, `./types/types`. ## Peer deps - `better-auth ^1.4` (resolves to 1.6.x via the catalog). - `hono`, `@hono/zod-openapi`, `zod` — supplied by the host application; the plugin imports them directly.