Files
skills/docs/api-stack/api/api-plugin-auth/src/plugin/plugin.md
2026-05-29 08:20:56 +01:00

4.5 KiB

plugin/plugin

(no submodules)

Purpose

createAuthPlugin(options) is the factory that returns a typed PluginDescriptor<AuthPluginConfig> ready to register with the host's PluginRegistry. It is the canonical exemplar of the 3-way plug-and-play contract every @suaveplan/api-plugin-* package follows: the plugin owns its .surql DDL, its Zod schemas, and its OpenAPI routes, and applies all three during onInit.

Features

  • Zod-validated options via AuthPluginOptionsSchema — invalid input raises AuthError(AUTH_INVALID_INPUT) carrying the ZodError.
  • 6-step onInit lifecycle: resolve services → apply schema → construct Better-Auth → register authService → register routes → install session middleware.
  • routePrefix-aware route mounting — paths are rebuilt at the prefix the operator chose, both in the served Hono app and in the OpenAPIRegistry snapshot.
  • Graceful degradation — when openAPIRegistry is absent, routes mount on the Hono app directly and a warning is logged. When neither migrationRunner nor db is available with applySchemaOnInit: true, init fails fast with AUTH_SERVICE_NOT_AVAILABLE.
  • Collision detection — pre-existing routes under the configured prefix raise AUTH_ROUTE_PREFIX_COLLISION.

Usage: Basic

import { defineApp } from "@suaveplan/api";
import { createAuthPlugin } from "@suaveplan/api-plugin-auth";

const app = await defineApp()
    .name("my-api")
    .use(
        createAuthPlugin({
            betterAuth: {
                secret: process.env.BETTER_AUTH_SECRET!,
                baseURL: "http://localhost:3000",
                emailAndPassword: { enabled: true },
                database: surrealAdapter,
            },
        })
    )
    .start();

Usage: Advanced

Mount the plugin under a versioned prefix, disable the in-plugin schema apply step (an external migration tool handles DDL), and inject a custom principal mapping that decorates the Better-Auth user with roles derived from the active organisation:

import { defineApp } from "@suaveplan/api";
import {
    createAuthPlugin,
    mapBetterAuthUserToPrincipal,
} from "@suaveplan/api-plugin-auth";

const app = await defineApp()
    .name("my-api")
    .use(
        createAuthPlugin({
            betterAuth: { /* … */ },
            routePrefix: "/api/v1/auth",
            applySchemaOnInit: false,
            principalMapping: (view) => {
                const base = mapBetterAuthUserToPrincipal(view);
                const session = view.session as { roles?: string[] };
                return {
                    ...base,
                    roles: [...base.roles, ...(session.roles ?? [])],
                };
            },
        })
    )
    .start();

API Reference

function createAuthPlugin(options: AuthPluginOptions): PluginDescriptor<AuthPluginConfig>

  • Validates options via AuthPluginOptionsSchema.
  • Returns a PluginDescriptor with name: "auth", version: "0.1.0", tags: ["auth", "security"], and the 4 lifecycle hooks (only onInit + onClose are wired; onRegister and onReady are default no-ops).
  • onInit runs the canonical 6-step lifecycle described above. It raises AUTH_* codes on the explicit failure modes — see ../error-codes/error-codes.md.
  • onClose releases the cached Better-Auth reference so the registry can re-init the plugin on subsequent boots.

Implementation Notes

  • The descriptor's configSchema is the AuthPluginOptionsSchema so the host registry re-validates the merged config at registration time. The defaults field carries the resolved options so plugins registered without an explicit config still see the parsed shape.
  • The session middleware is installed AFTER routes are registered so the OpenAPI doc snapshot is taken before any per-request work begins. Order does not affect runtime behaviour but keeps the registry's events ordered consistently across reboots.
  • The runtime check against openAPIRegistry === undefined exists even though the registry's TS-level slot is non-optional — ctx.getService adds | undefined to every slot's return type, so the absent-runtime case still type-checks.
  • applySchema prefers the migration runner over the raw db adapter so operators get migration-record tracking when the runner is wired. Falling back to applyBetterAuthSchema(db) ensures the plugin still boots in lightweight deployments that ship without @suaveplan/api-migrations.