From fd7a949ce7293e24bd7ce9eebeab9a5f0e5dc54b Mon Sep 17 00:00:00 2001
From: olliethedev <5933733+olliethedev@users.noreply.github.com>
Date: Mon, 31 Aug 2026 19:40:36 -0400
Subject: [PATCH] docs: add stable v3 migration playbook (#175)
---
docs/content/docs/breaking-changes.mdx | 530 ++++++++++++++++++++++-
docs/content/docs/databases/adapters.mdx | 61 ++-
2 files changed, 585 insertions(+), 6 deletions(-)
diff --git a/docs/content/docs/breaking-changes.mdx b/docs/content/docs/breaking-changes.mdx
index dff81388..558ce701 100644
--- a/docs/content/docs/breaking-changes.mdx
+++ b/docs/content/docs/breaking-changes.mdx
@@ -10,7 +10,533 @@ This page documents breaking changes between major versions and provides migrati
---
-## v3 RC2 → RC3: Canonical stack and plugin DX
+## v2 or release candidate → stable v3: production playbook
+
+Use this section as the canonical migration order for a production application.
+The RC2→RC3 and v2 framework sections later on this page remain detailed
+references for individual mechanical changes; they are not separate upgrade
+paths.
+
+
+ Do not upgrade one package at a time in a deployed application. Pin the exact
+ verified cohort, migrate on a branch with a database backup, and keep the old
+ application artifact and lockfile available until production verification is
+ complete.
+
+
+### 0. Pin the verified cohort and a rollback point
+
+The final stable package versions will be set in a separate release-preparation
+commit before the release job validates and publishes them. This documentation
+change does not invent those unpublished versions. Until the next core
+candidate is published, use this exact public cohort:
+
+| Role | Exact version |
+| --- | --- |
+| Core | `@btst/stack@3.0.0-rc.3` |
+| Scaffold and command wrapper | `@btst/codegen@0.2.0-rc.3` |
+| Optional Better Auth UI companion | `@btst/better-auth-ui@2.0.0-rc.4` |
+| Better DB and the selected BTST adapter | `2.2.3` |
+| Database CLI for generation/migration | `@btst/cli@2.2.4` directly; public Codegen RC3 still delegates to `2.2.3` |
+| Better Auth and Core | `better-auth@1.6.16`, `@better-auth/core@1.6.16` |
+| Better Auth utilities and transport | `@better-auth/utils@0.4.1`, `@better-fetch/fetch@1.2.2`, `better-call@1.3.6` |
+| API-key and passkey declarations, when the companion is installed | `@better-auth/api-key@1.6.16`, `@better-auth/passkey@1.6.16` |
+
+When stable v3 is published, replace only the three release-candidate entries
+with their exact published stable versions. Retain the Better Auth `1.6.16`
+and Better DB/adapter `2.2.3` cohorts unless a later migration guide explicitly
+changes them. Do not move this release to Better Auth `1.7.x`, and do not use a
+floating `latest`, `next`, caret, or workspace range in a production migration.
+
+For example, a Drizzle application without the optional auth companion can pin
+the current candidate with:
+
+```bash
+pnpm add --save-exact @btst/stack@3.0.0-rc.3 @btst/adapter-drizzle@2.2.3
+pnpm add --save-dev --save-exact @btst/codegen@0.2.0-rc.3
+pnpm install --frozen-lockfile
+```
+
+If the application selects Better Auth UI, add the complete aligned auth
+cohort shown in [Better Auth UI companion](#8-migrate-the-better-auth-ui-companion)
+rather than asking the package manager to repair peers opportunistically.
+
+Before changing code or data:
+
+- record the currently deployed commit, package-manager version, runtime
+ version, build command, start command, and environment names;
+- commit the manifest and lockfile, export the generated schema, and take a
+ restorable database backup or provider snapshot;
+- inventory every BTST plugin, embedded component, direct hook, provider root,
+ custom route, lifecycle hook, auth rule, and trusted/background call site;
+- capture a production-like smoke baseline, including representative existing
+ records and anonymous, regular-user, and privileged-user behavior; and
+- create a migration branch and keep the previous application artifact
+ deployable. Never treat a down migration as the only rollback for data that
+ the new application has already written.
+
+### 1. Apply the ownership changes in this order
+
+{/* canonical-dx-guard: migration:start reason="stable v3 before-and-after inventory" */}
+
+| Concern | Removed or intermediate shape | Stable-v3 shape |
+| --- | --- | --- |
+| Backend constructor | `stack(...)` | `createBackendStack(...)` from `@btst/stack/api` |
+| Client constructor | `createStackClient(...)`, `stackClient(...)` | `createClientStack(...)` from `@btst/stack/client` |
+| Client runtime | API, site, query client, and headers repeated in plugins/provider | one resolved `createClientStack({ api, site, queryClient, plugins })` |
+| Plugin IDs | kebab-case programmatic keys such as `ai-chat` and `form-builder` | canonical camelCase keys such as `aiChat` and `formBuilder`; package paths and URL slugs stay kebab-case |
+| Backend factories | positional arguments or top-level hook callbacks | zero or one options object with callbacks under `hooks` and required domain dependencies explicit |
+| Lifecycle | mixed read/create/error spellings and boolean hook denials | `onBefore` / `onAfter` / `onError` and thrown domain failures |
+| Provider | API/base-path fields and a manual override generic | browser-safe `stack`, framework `router`, optional `auth`, `initialIdentity`, and genuine application services |
+| Overrides | empty blocks used to activate plugins or duplicated runtime/auth fields | optional inferred keys containing only plugin-specific browser or presentation customization |
+| Request calls | ambiguous `api` namespace | `forRequest(request).operations` |
+| Trusted calls | `internal` or a boolean bypass | `trusted`, which skips user authorization but retains validation, trusted facts, domain behavior, transactions, and lifecycle |
+| Low-level calls | ordinary app code reaching exported getters/mutations | narrow `raw` prefetch escape hatches; standalone primitives remain caller-composed and are not the ordinary app API |
+
+{/* canonical-dx-guard: migration:end */}
+
+The canonical browser composition is:
+
+```tsx title="lib/stack-client.tsx"
+const clientStack = createClientStack({
+ api: { baseURL, basePath: "/api/data" },
+ site: { baseURL, basePath: "/pages" },
+ queryClient,
+ plugins: {
+ blog: blogClientPlugin(),
+ comments: commentsClientPlugin(),
+ },
+})
+
+
+ {children}
+
+```
+
+Omit `overrides` when there is no customization. An empty override does not
+register or activate a plugin. The registered resolved definitions infer the
+allowed keys and exact value types; do not restore a provider generic or a
+manual application override map.
+
+Build one request-specific client stack for server loaders and metadata, with
+filtered headers under `api.headers`, and a separate stable browser stack
+without request headers. Only schema-validated identity and trusted API/site
+origins may cross the server/client boundary. Never serialize a backend stack,
+cookies, authorization headers, proxy headers, secrets, or a request-specific
+client stack.
+
+Keep resolved client plugin definitions server-import-safe. Put React state,
+browser auth clients, upload callbacks, navigation, and the provider itself in a
+client-only module. A path-only per-plugin endpoint replacement inherits the
+top-level origin and filtered request headers; a replacement origin is a new
+transport boundary and must provide its own path and deliberately selected
+credentials. Preserve that boundary instead of forwarding server credentials
+to another origin.
+
+### 2. Register plugins, factories, and lifecycle hooks
+
+Register both halves of a full-stack plugin under the same canonical key.
+OpenAPI is backend-only; Route Docs is client-only; UI Builder is client-only
+and composes the registered CMS contract. Do not invent a matching half for a
+one-sided plugin.
+
+Move each backend plugin to one options object and each callback to `hooks`:
+
+```ts
+createBackendStack({
+ basePath: "/api/data",
+ adapter,
+ auth: serverAuth,
+ plugins: {
+ blog: blogBackendPlugin({ hooks: { onAfterCreatePost } }),
+ comments: commentsBackendPlugin({
+ allowEditing: false,
+ resolveUser,
+ hooks: { onBeforeCreateComment },
+ }),
+ },
+})
+```
+
+The stable lifecycle grammar is `onBefore`,
+`onAfter`, and `onError`. The complete rename
+inventory is in [Rename every backend lifecycle callback](#6-rename-every-backend-lifecycle-callback).
+Update every used plugin, including callbacks referenced indirectly from shared
+hook objects. A hook runs only after validation, authoritative fact derivation,
+identity resolution, and authorization have succeeded; it is not a replacement
+for permission enforcement. Hook denials throw—returning `false` is no longer a
+denial.
+
+### 3. Configure atomic writes explicitly
+
+AI Chat, Form Builder, Kanban, and Media contain operations whose authorization
+facts and writes must share one isolated transaction. Configure a supported
+Prisma, Drizzle, or Kysely adapter with `transaction: true`; do not rely on the
+sequential fallback. Form Builder does not support generated memory or MongoDB
+configuration, and Media does not support generated MongoDB configuration,
+because those combinations cannot provide the required isolation.
+
+See [Database adapters](/databases/adapters#isolated-transactions-for-atomic-plugin-writes)
+for copyable adapter examples and the fail-closed behavior.
+
+### 4. Migrate authorization as one application-owned rule
+
+Plugins publish schema-backed descriptors and the minimum facts required for an
+operation. The application owns its identity schema, local rules, and both
+identity resolvers. Authentication discovers identity; authorization decides
+whether that identity may perform a typed operation.
+
+Keep the rule module browser-safe:
+
+```ts title="lib/authorization.ts"
+import { defineAuthorization } from "@btst/stack/authorization"
+import { blogPermissions } from "@btst/stack/plugins/blog/permissions"
+import { z } from "zod"
+
+export const authorization = defineAuthorization({
+ identity: z.object({ id: z.string(), role: z.enum(["user", "admin"]) }),
+ permissions: [blogPermissions] as const,
+ rules: ({ blog }) => [
+ blog.post.delete.when(({ identity, facts }) =>
+ identity !== null &&
+ (identity.role === "admin" || identity.id === facts.authorId),
+ ),
+ ],
+})
+```
+
+Bind it separately on each side. These modules are client-only and server-only,
+respectively:
+
+```tsx title="lib/authorization.client.ts"
+"use client"
+
+export const clientAuth = createClientAuth({
+ authorization,
+ getIdentity: () => session?.user ?? null,
+ loginPath: "/sign-in",
+})
+
+const { CanAccess } = clientAuth
+const control = (
+
+
+
+)
+```
+
+```ts title="lib/authorization.server.ts"
+import "server-only"
+
+export const serverAuth = createServerAuth({
+ authorization,
+ getIdentityFromHeaders: async ({ headers }) => {
+ const session = await auth.api.getSession({ headers })
+ return session?.user ?? null
+ },
+})
+```
+
+The client check is a synchronous presentation decision. It makes no
+permission request and creates no shared authorization-result cache. The
+backend validates input, derives trusted facts from server data, resolves the
+request identity, evaluates the descriptor, and only then enters domain and
+lifecycle execution. A representative plugin operation binds that ordering
+once:
+
+```ts title="plugins/posts/api.ts"
+const deletePost = defineOperation({
+ input: z.object({ id: z.string() }),
+ permission: blogPermissions.post.delete,
+ facts: async ({ input }) => {
+ const post = await adapter.findOne({
+ model: "post",
+ where: [{ field: "id", value: input.id }],
+ })
+ return { id: input.id, ...(post?.authorId ? { authorId: post.authorId } : {}) }
+ },
+ execute: async ({ input }) => {
+ await adapter.delete({
+ model: "post",
+ where: [{ field: "id", value: input.id }],
+ })
+ return { success: true } as const
+ },
+})
+```
+
+Do not accept `authorId`, role, tenant ownership, record visibility, or other
+authoritative facts from the browser merely because the same shapes are used
+for a local UI preview. Row and tenant query scoping is a separate server-only
+data concern, not a boolean authorization check.
+
+Once server authorization is enabled, a missing rule denies. Ordinary anonymous
+and authenticated denials become 401 and 403, respectively. Invalid identity,
+schema, transport, fact derivation, and policy execution remain observable
+errors; never convert them to `false`. Omitting server authorization preserves
+the documented permissive compatibility behavior while the migration is staged,
+but it should be an explicit temporary choice.
+
+Audit every call site against its trust contract:
+
+```ts
+await backend.forRequest(request).operations.blog.deletePost({ id })
+await backend.trusted.blog.deletePost({ id })
+await backend.raw.blog.prefetchForRoute("post", queryClient, { slug })
+```
+
+The first path is request-authorized. The second is for a trusted job or server
+workflow and skips only user authorization. The third is a narrow composition
+escape hatch, not an alternate business API.
+
+For a managed or separately deployed backend, publish only the rule-free,
+versioned contract and descriptors:
+
+```ts title="packages/backend-contract/authorization.ts"
+export const authorizationContract = defineAuthorizationContract({
+ identity: z.object({ id: z.string(), role: z.enum(["user", "admin"]) }),
+ permissions: [blogPermissions] as const,
+})
+```
+
+The browser may bind that contract to `createRemoteAuthorizationEvaluator`,
+but the remote service must parse the contract version and facts, resolve its
+own identity, re-read authoritative records, and evaluate server-owned rules.
+It never trusts browser identity or ownership facts. See
+[Authorization](/auth#use-a-managed-or-separate-backend) for the transport
+example. Core intentionally exports no provider-specific auth adapter and no
+global open-string `useCan` or `CanAccess` API.
+
+### 5. Adopt framework entries and tri-state hydration
+
+Use the framework entry factories instead of copied route resolution, loader,
+metadata, dehydration, or 404 logic:
+
+| Framework | API route | Page route | Provider router | Identity layout |
+| --- | --- | --- | --- | --- |
+| Next.js | `toNextRouteHandlers` | `createNextPage` | `nextRouter()` | `createNextLayout` from `@btst/stack/next/server` |
+| React Router | `toReactRouterHandlers` | `createReactRouterPage` | `reactRouter()` | `createReactRouterLayout` on the parent route |
+| TanStack Start | `toTanStackHandlers` | `createTanStackPageOptions` | `tanstackRouter()` | `createTanStackLayout` plus a server function |
+
+Hydrate identity at the layout or parent-route boundary that owns the complete
+provider subtree:
+
+```tsx
+
+ {children}
+
+```
+
+`initialIdentity` has three deliberate states:
+
+| Value | Meaning | Initial client behavior |
+| --- | --- | --- |
+| `undefined` or omitted | no server snapshot | resolve identity in the browser |
+| `null` | settled anonymous snapshot | do not duplicate the initial request |
+| validated identity | settled authenticated snapshot | use it without a duplicate initial request |
+
+Next.js request-aware pages and layouts must construct their server client from
+the current request and keep static/ISR routes in a separate header-free route
+group. React Router resolves the snapshot in the parent layout loader so it
+covers the full ``. TanStack Start resolves it in a server function
+used by the parent route loader and later client navigations. The complete,
+copyable implementations are in
+[Authorization: hydrate identity at the layout boundary](/auth#hydrate-identity-at-the-layout-boundary).
+
+When an application-owned route renders one plugin page directly instead of
+using the catch-all, keep a dedicated wrapper and pass the page component its
+declarative `{ params }` route context. Supply synthetic params only when that
+wrapper intentionally fixes a resource; do not call route internals or restore
+the removed named-prop adapters. The exact parameter mappings are listed in
+[Update parameterized page-component overrides](#5-update-parameterized-page-component-overrides).
+
+After login, logout, or account switching, refresh at the application/framework
+seam: Next.js `router.refresh()`, React Router `revalidator.revalidate()`,
+TanStack `router.invalidate()`, or `clientAuth.useIdentity().refetch()`.
+
+### 6. Migrate embedded surfaces and every provider root
+
+The catch-all pages layout is not automatically an ancestor of UI embedded in
+the rest of the application. Inventory and migrate `CommentThread`,
+`CommentCount`, `FormRenderer`, direct plugin hooks, and cards such as
+`PostCard` or `TaskCard`. Each rendered surface must be below a `StackProvider`
+whose resolved stack registers that plugin and below the same QueryClient
+provider used to create the stack.
+
+If a modal, parallel route, portal host, microfrontend, or independently mounted
+widget has a separate React root, give that root its own stable browser stack
+and provider using the same trusted origin snapshot and auth contract. Context
+does not cross sibling roots. Hydrate identity per root or intentionally leave
+it `undefined`; never copy a server stack or request headers into the new root.
+
+Remove `apiBaseURL`, `apiBasePath`, `headers`, and current-user props from
+embedded components. They read transport and identity from the nearest
+provider. Keep `CommentThread.loginHref` only when the resource needs a
+specific sign-in return URL; it overrides the provider's general `loginPath`.
+Test embedded mutations and counts as well as their first render—a static card
+that looks correct can still be bound to the wrong endpoint or identity cache.
+
+### 7. Regenerate or merge the framework scaffold
+
+`@btst/codegen` owns application scaffolding. The published
+`@btst/codegen@0.2.0-rc.3` artifact still delegates `generate` and `migrate` to
+`@btst/cli@2.2.3`; do not claim the immutable RC3 artifact has the post-RC3
+fix. For the current public candidate, run `@btst/cli@2.2.4` directly and in
+isolation instead of installing it into the application graph.
+
+The forthcoming Codegen RC4 (versioned by the separate release-preparation
+change) and stable release will delegate to `2.2.4` from the consumer project
+directory. That behavior loads the application's
+TypeScript/JavaScript aliases and standard Next.js environment files, resolves
+its Prisma or Drizzle adapter and ORM peers, and ignores only a bare
+`import "server-only"` marker while evaluating the server config.
+
+Use the scaffold as a reference diff for an existing application rather than
+blindly overwriting owned files:
+
+```bash
+npx @btst/codegen@0.2.0-rc.3 init --framework=nextjs --adapter=drizzle \
+ --plugins=blog,comments --cwd=. --skip-install
+npx --yes @btst/cli@2.2.4 generate \
+ --orm=drizzle --config=lib/stack.ts --output=src/db/schema.ts
+```
+
+Select `react-router` or `tanstack` for those frameworks. Review every planned
+write and TODO, preserve application-owned auth/domain dependencies, then run
+the generated framework's typecheck and production build. See [CLI](/cli) for
+the supported flags and direct failure fallback.
+
+### 8. Migrate the Better Auth UI companion
+
+Better Auth remains application-configured and is a prerequisite. The optional
+companion reads its own Better Auth session and uses native Better Auth account,
+organization, and permission APIs; it exports no Better Auth-to-BTST client or
+server auth factory. Map the session into `createClientAuth` and
+`createServerAuth` yourself when business plugins should authorize the same
+person. Role and tenant fields remain application-owned.
+
+Auth plus account is the minimal runtime integration. Pin the candidate cohort
+exactly when it is selected:
+
+```bash
+pnpm add --save-exact @btst/better-auth-ui@2.0.0-rc.4 \
+ better-auth@1.6.16 @better-auth/core@1.6.16 \
+ @better-auth/api-key@1.6.16 @better-auth/passkey@1.6.16 \
+ @better-auth/utils@0.4.1 @better-fetch/fetch@1.2.2 better-call@1.3.6
+```
+
+Public `@btst/codegen@0.2.0-rc.3` predates the `better-auth-ui` scaffold
+selection. Configure the companion manually for that immutable candidate; the
+forthcoming RC4 and stable Codegen artifacts contain the generated
+auth-and-account path described in the focused guide.
+
+API-key and passkey are required declaration peers in the RC4 package because
+its complete `AuthClient` type exposes those surfaces. Installing them satisfies
+the strict type/dependency graph; it does not enable either feature. Enable
+organization, API-key, passkey, or multi-session only when the matching Better
+Auth server and browser plugins are configured. Optional `tanstack`,
+`instantdb`, and `triplit` companion subpaths have additional peers; the base
+auth/account integration does not require those adapter peers.
+
+Configure `authClient` once under the `auth` override and avatar behavior only
+under `account`. Account and organization overrides intentionally reject
+auth-only fields. Companion route bases derive from the resolved
+`createClientStack({ site })` runtime; do not repeat them in overrides. Use the
+explicit framework session refresh described above. The focused setup and peer
+table live in [Better Auth UI Companion](/plugins/better-auth-ui).
+
+### 9. Verify data, production behavior, and cleanup
+
+Generate the schema from the migrated stack, diff it against the recorded
+baseline, and review the ORM migration before applying it. Test against a
+restored production-like snapshot first. Prefer additive migrations during the
+deployment window, deploy schema changes before code that needs them, and do
+not remove old columns or compatibility reads until the rollback window closes.
+Verify existing rows, relationships, cascades, tenant boundaries, and plugin
+records—not only newly created fixtures.
+
+Run this checklist against the exact packed/published artifacts and the
+optimized production server:
+
+- [ ] Clean install succeeds from the exact selected cohort with no
+ undocumented peer repair, duplicate Better Auth type universe, or application
+ dependency on `@btst/cli`.
+- [ ] Typecheck, lint, unit/integration tests, schema generation, reviewed
+ migration, optimized build, and production start pass.
+- [ ] Server and client bundles remain separated; no backend stack, request
+ headers, cookies, secrets, or server auth are serialized.
+- [ ] Direct navigation, hard refresh, back/forward, 404 and error boundaries,
+ console/server errors, and hydration warnings are clean.
+- [ ] SSR, authenticated SSR, SSG/ISR, metadata, sitemap, and browser refetch
+ use the same resolved endpoints.
+- [ ] Anonymous, regular-user, and privileged-user controls match authoritative
+ backend results.
+- [ ] An allowed operation succeeds; anonymous denial is 401; authenticated
+ denial is 403; a missing rule denies; identity/fact/policy failures remain
+ errors; spoofed browser facts do not grant access; trusted internal execution
+ retains validation, domain behavior, transactions, and lifecycle.
+- [ ] Login, logout, account switching, explicit session refresh, and all three
+ `initialIdentity` states behave without duplicate initial requests.
+- [ ] Embedded components outside the catch-all layout, every independent
+ provider root, resource-specific sign-in return URLs, counts/cards/direct
+ hooks, and representative plugin mutations work.
+- [ ] Better Auth account, profile, avatar, and only the optional features the
+ application configured work on the retained `1.6.16` cohort.
+- [ ] Existing database records remain readable and writable, failed atomic
+ operations roll back, and database plus remote test assets are removed.
+
+Release maintainers prove the clean-room and snippet contracts from the
+repository root before publishing:
+
+```bash
+pnpm test:packed-consumers
+BTST_ARTIFACT_DIR="$(mktemp -d)"
+npm pack @btst/better-auth-ui@2.0.0-rc.4 --pack-destination "$BTST_ARTIFACT_DIR"
+BTST_AUTH_UI_TARBALL="$BTST_ARTIFACT_DIR/btst-better-auth-ui-2.0.0-rc.4.tgz"
+pnpm smoke:packed-consumer -- --fixture core --package-manager npm
+pnpm smoke:packed-consumer -- --fixture core --package-manager pnpm
+pnpm smoke:packed-consumer -- --fixture auth --package-manager npm \
+ --better-auth-ui "$BTST_AUTH_UI_TARBALL"
+pnpm smoke:packed-consumer -- --fixture auth --package-manager pnpm \
+ --better-auth-ui "$BTST_AUTH_UI_TARBALL"
+pnpm --filter @btst/codegen test:better-auth-ui-fixtures
+pnpm typecheck
+```
+
+The first command verifies the harness itself; the four smoke commands then
+install only packed tarballs with npm and pnpm, under strict peers, and exercise
+core plus the auth cohort. The Better Auth UI gate generates untouched Next.js,
+React Router, and TanStack Start applications and builds and typechecks each
+one. The root typecheck includes the constructor, authorization,
+managed-contract, and hydration consumer fixtures used by this guide. A failure
+in any gate blocks publication and must be corrected in the guide or
+implementation rather than repaired by an undocumented fixture edit.
+
+Finally remove migration-only compatibility code: old constructors, positional
+factory arguments, top-level or retired lifecycle names, kebab-case
+programmatic IDs, duplicated API/site/query/header wiring, manual override maps,
+empty activation blocks, render guards, open-string authorization calls,
+provider-specific core auth bridges, ambiguous `api`/`internal` calls, and
+temporary dual-read or dual-write paths after the rollback window. Commit the
+final lockfile and deployment evidence with the migration.
+
+---
+
+## Migration reference: v3 RC2 → RC3 canonical stack and plugin DX
RC3 removes the remaining duplicate runtime configuration and historical naming
seams. The migration is mechanical: rename the constructors, move shared client
@@ -426,7 +952,7 @@ Better Auth UI companion for auth and account pages.
---
-## v2 → v3: Framework entries and resolved client runtime
+## Migration reference: v2 → v3 framework entries and resolved client runtime
BTST v3 has one supported framework-wiring path: framework entry factories
own the catch-all routes, `createClientStack()` owns shared API, site, and
diff --git a/docs/content/docs/databases/adapters.mdx b/docs/content/docs/databases/adapters.mdx
index a07e3e08..854433c6 100644
--- a/docs/content/docs/databases/adapters.mdx
+++ b/docs/content/docs/databases/adapters.mdx
@@ -43,6 +43,57 @@ npm install @btst/adapter-prisma
See the [Installation guide](/installation#install-database-adapter) for detailed adapter setup instructions.
+## Isolated transactions for atomic plugin writes
+
+Some plugin operations authorize against a database snapshot and then update
+that same state. In production, the fact read, domain hooks, compare-and-set,
+and write must commit as one isolated transaction. Configure
+`transaction: true` when the installed plugin set includes **AI Chat, Form
+Builder, Kanban, or Media**.
+
+
+
+ ```ts
+ adapter: (db) => createPrismaAdapter(prisma, db, {
+ provider: "postgresql",
+ transaction: true,
+ })({})
+ ```
+
+
+
+ ```ts
+ adapter: (db) => createDrizzleAdapter(drizzleDb, db, {
+ provider: "pg",
+ transaction: true,
+ })({})
+ ```
+
+
+
+ ```ts
+ adapter: (db) => createKyselyAdapter(kyselyDb, db, {
+ transaction: true,
+ })({})
+ ```
+
+
+
+Choose the provider value that matches the real ORM connection. The flag opts
+the adapter into its native transaction implementation; it is not a promise
+that a sequential callback fallback is safe. Owner-sensitive and persistent
+operations fail closed with `ATOMIC_TRANSACTION_REQUIRED` when isolation is
+missing, before lifecycle hooks or writes run.
+
+The memory adapter remains useful for local, single-process development where
+the plugin documents serialized access. It is not a production isolation
+substitute. The generated scaffold rejects Form Builder with memory or MongoDB
+and Media with MongoDB; use Prisma, Drizzle, or Kysely for those production
+configurations. `@btst/codegen init` adds `transaction: true` automatically for
+AI Chat, Form Builder, and Media in the public `0.2.0-rc.3` artifact. RC3
+Kanban applications must add the flag manually. The forthcoming RC4 and stable
+Codegen releases generate it for all four atomic-write plugins.
+
## Usage
When you configure BTST, the `createBackendStack()` function collects all plugin database schemas and merges them into a unified schema. The adapter function receives this merged schema and returns an adapter that translates BTST's database operations to your ORM.
@@ -62,9 +113,9 @@ When you configure BTST, the `createBackendStack()` function collects all plugin
// Your plugins here
},
// The adapter receives the merged db schema from all plugins
- adapter: (db) => createPrismaAdapter(prisma, db, {
+ adapter: (db) => createPrismaAdapter(prisma, db, {
provider: "postgresql" // or "mysql", "sqlite", "cockroachdb", "mongodb"
- })
+ })({})
})
export { handler, dbSchema }
@@ -86,7 +137,9 @@ When you configure BTST, the `createBackendStack()` function collects all plugin
plugins: {
// Your plugins here
},
- adapter: (db) => createDrizzleAdapter(drizzleDb, db, {})
+ adapter: (db) => createDrizzleAdapter(drizzleDb, db, {
+ provider: "pg" // or "mysql", "sqlite"
+ })({})
})
export { handler, dbSchema }
@@ -111,7 +164,7 @@ When you configure BTST, the `createBackendStack()` function collects all plugin
plugins: {
// Your plugins here
},
- adapter: (db) => createKyselyAdapter(kyselyDb, db, {})
+ adapter: (db) => createKyselyAdapter(kyselyDb, db, {})({})
})
export { handler, dbSchema }