From 61141999024392ad2ff80be6f9e8cb832241c4f3 Mon Sep 17 00:00:00 2001
From: olliethedev <5933733+olliethedev@users.noreply.github.com>
Date: Mon, 31 Aug 2026 19:11:34 -0400
Subject: [PATCH] feat(cli): restore Better Auth UI scaffold
---
.github/workflows/init.yml | 37 ++
docs/content/docs/breaking-changes.mdx | 34 +-
docs/content/docs/cli.mdx | 8 +-
docs/content/docs/meta.json | 1 +
docs/content/docs/plugins/better-auth-ui.mdx | 174 ++++++++
docs/content/docs/plugins/index.mdx | 8 +-
packages/cli/package.json | 3 +-
.../scripts/test-better-auth-ui-fixtures.mjs | 400 ++++++++++++++++++
packages/cli/scripts/test-init.sh | 19 +-
packages/cli/src/commands/init.ts | 2 +-
.../templates/nextjs/form-demo-client.tsx.hbs | 4 +-
.../nextjs/pages-client-layout.tsx.hbs | 11 +
.../templates/nextjs/preview-client.tsx.hbs | 4 +-
.../nextjs/public-chat-client.tsx.hbs | 4 +-
.../react-router/form-demo-route.tsx.hbs | 4 +-
.../react-router/pages-layout.tsx.hbs | 12 +-
.../react-router/preview-route.tsx.hbs | 4 +-
.../react-router/public-chat-route.tsx.hbs | 4 +-
.../templates/shared/lib/auth-client.ts.hbs | 13 +
.../tanstack/form-demo-route.tsx.hbs | 4 +-
.../templates/tanstack/pages-layout.tsx.hbs | 12 +-
.../templates/tanstack/pages-route.tsx.hbs | 11 +-
.../templates/tanstack/preview-route.tsx.hbs | 4 +-
.../tanstack/public-chat-route.tsx.hbs | 4 +-
packages/cli/src/types.ts | 3 +-
.../src/utils/__tests__/init-command.test.ts | 12 +
.../utils/__tests__/package-installer.test.ts | 32 +-
.../src/utils/__tests__/scaffold-plan.test.ts | 176 +++++++-
packages/cli/src/utils/constants.ts | 49 +++
packages/cli/src/utils/scaffold-plan.ts | 124 ++++--
playground/src/lib/plugin-selection.ts | 2 +
31 files changed, 1070 insertions(+), 109 deletions(-)
create mode 100644 docs/content/docs/plugins/better-auth-ui.mdx
create mode 100644 packages/cli/scripts/test-better-auth-ui-fixtures.mjs
create mode 100644 packages/cli/src/templates/shared/lib/auth-client.ts.hbs
create mode 100644 packages/cli/src/utils/__tests__/init-command.test.ts
diff --git a/.github/workflows/init.yml b/.github/workflows/init.yml
index 5feb97837..bd46dc5d4 100644
--- a/.github/workflows/init.yml
+++ b/.github/workflows/init.yml
@@ -7,6 +7,8 @@ on:
- 'packages/cli/**'
- 'docs/content/docs/cli.mdx'
- 'docs/content/docs/installation.mdx'
+ - 'docs/content/docs/breaking-changes.mdx'
+ - 'docs/content/docs/plugins/better-auth-ui.mdx'
- '.github/workflows/init.yml'
concurrency:
@@ -59,3 +61,38 @@ jobs:
name: btst-init-fixtures
path: /tmp/test-btst-init-*/
retention-days: 3
+
+ better-auth-ui-fixtures:
+ name: Better Auth UI packed fixtures
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+
+ - name: Setup pnpm
+ uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
+
+ - name: Setup Node.js 22
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ with:
+ node-version: 22
+ cache: 'pnpm'
+
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile
+
+ - name: Run packed framework fixtures
+ run: pnpm --filter @btst/codegen test:better-auth-ui-fixtures
+ env:
+ BTST_KEEP_FIXTURES: 1
+ CI: true
+
+ - name: Upload artifacts on failure
+ if: failure()
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ with:
+ name: better-auth-ui-fixtures
+ path: /tmp/btst-better-auth-ui-*/
+ retention-days: 3
diff --git a/docs/content/docs/breaking-changes.mdx b/docs/content/docs/breaking-changes.mdx
index 7b38635f6..dff813883 100644
--- a/docs/content/docs/breaking-changes.mdx
+++ b/docs/content/docs/breaking-changes.mdx
@@ -421,8 +421,8 @@ deployment origins, never a server stack or request headers.
BTST core is authentication-provider agnostic. Better Auth and Better Auth UI
are not core dependencies or hidden identity bridges; adapt the provider your
application already uses through `createClientAuth` and `createServerAuth`.
-The separate Better Auth UI companion migration happens downstream of the
-completed core DX migration.
+Applications that already run Better Auth can separately opt into the migrated
+Better Auth UI companion for auth and account pages.
---
@@ -676,15 +676,19 @@ above. JavaScript applications must update removed keys too; removed callback
names are not invoked at runtime. Keep hook denials exception-based while
renaming them.
-### 8. Replace the retired provider-specific auth scaffold
+### 8. Replace the v2 provider-specific auth bridge
-The v3 CLI no longer offers `better-auth-ui`, installs
-`@btst/better-auth-ui` or Better Auth packages, or generates auth, account,
-and organization routes. BTST authorization is provider-agnostic: keep your
-authentication provider and UI in application code, then adapt its session
-through the generic client and server identity resolvers.
+The v3 CLI no longer generates the old Better Auth-to-BTST authorization
+provider. BTST authorization is provider-agnostic: adapt your application
+session through the generic client and server identity resolvers.
-Before:
+The stable-v3 CLI does offer an optional `better-auth-ui` companion scaffold
+for applications that already own a Better Auth server. That selection creates
+only the Better Auth browser client plus auth and account UI routes; it does not
+generate the server, database, schema, migrations, providers, secrets,
+organization plugin, or a BTST identity bridge.
+
+Remove the old bridge:
```bash
npx @btst/codegen init --plugins blog,better-auth-ui
@@ -698,7 +702,7 @@ import { createBetterAuthProvider } from "@btst/better-auth-ui"
```
-After:
+Keep authorization application-owned:
```ts title="authorization.client.ts"
export const clientAuth = createClientAuth({
@@ -717,9 +721,9 @@ export const serverAuth = createServerAuth({
Pass `clientAuth` to `StackProvider`, pass `serverAuth` to
`createBackendStack({ auth: serverAuth })`, and keep
-sign-in, account, and organization routes in your chosen authentication
-framework. Remove the retired plugin from existing `btst init` commands and
-delete its generated imports, overrides, CSS import, and package dependencies.
+authorization independent from the optional Better Auth UI routes. See
+[Better Auth UI Companion](/plugins/better-auth-ui) for the supported scaffold
+and exact dependency cohort.
### Migration checklist
@@ -735,7 +739,9 @@ delete its generated imports, overrides, CSS import, and package dependencies.
- Change backend hook denials from boolean returns to thrown errors.
- Replace every retired Form Builder, Kanban, and Media lifecycle spelling
using the RC3 mapping tables.
-- Replace the provider-specific auth scaffold with application-owned identity resolvers and routes.
+- Keep the Better Auth backend and identity resolvers application-owned. When
+ selected, use the optional companion scaffold for browser auth/account routes
+ and connect it to that existing backend.
- Run your framework build, TypeScript checks, and tests.
---
diff --git a/docs/content/docs/cli.mdx b/docs/content/docs/cli.mdx
index af4287a3b..322b6d243 100644
--- a/docs/content/docs/cli.mdx
+++ b/docs/content/docs/cli.mdx
@@ -32,7 +32,7 @@ Common flags:
|------|-------------|
| `--framework` | `nextjs`, `react-router`, or `tanstack` |
| `--adapter` | `memory`, `prisma`, `drizzle`, `kysely`, or `mongodb` |
-| `--plugins` | Comma-separated plugin keys: `blog`, `ai-chat`, `cms`, `form-builder`, `ui-builder`, `kanban`, `comments`, `media`, `route-docs`, `open-api` (or `all`) |
+| `--plugins` | Comma-separated plugin keys: `blog`, `ai-chat`, `cms`, `form-builder`, `ui-builder`, `kanban`, `comments`, `media`, `route-docs`, `open-api`, `better-auth-ui` (or `all`) |
| `--cwd` | Target directory |
| `--skip-install` | Skip package installation step |
| `--yes` | Non-interactive defaults (useful in CI) |
@@ -55,6 +55,12 @@ Generated v3 layouts never repeat framework router, API, or identity wiring in
plugin overrides. Replace only the plugin-specific TODO values (for example an
upload function or user resolver).
+`better-auth-ui` is an optional companion selection. It generates the auth and
+account client plugins, a browser client for an existing `/api/auth` endpoint,
+and the framework-native session refresh callback. It never generates a Better
+Auth server, database schema, migrations, providers, secrets, or BTST identity
+adapter. See [Better Auth UI Companion](/plugins/better-auth-ui).
+
## Generate and Migrate via Codegen
If you prefer one command surface, these delegate to `@btst/cli`:
diff --git a/docs/content/docs/meta.json b/docs/content/docs/meta.json
index 4e84731a4..212b7165f 100644
--- a/docs/content/docs/meta.json
+++ b/docs/content/docs/meta.json
@@ -21,6 +21,7 @@
"plugins/media",
"plugins/open-api",
"plugins/route-docs",
+ "plugins/better-auth-ui",
"plugins/development",
"---[Database]Databases---",
"databases/adapters",
diff --git a/docs/content/docs/plugins/better-auth-ui.mdx b/docs/content/docs/plugins/better-auth-ui.mdx
new file mode 100644
index 000000000..870a20e91
--- /dev/null
+++ b/docs/content/docs/plugins/better-auth-ui.mdx
@@ -0,0 +1,174 @@
+---
+title: Better Auth UI Companion
+description: Add optional auth and account pages to an application that already runs Better Auth.
+---
+
+import { Callout } from "fumadocs-ui/components/callout";
+import { Tabs, Tab } from "fumadocs-ui/components/tabs";
+
+[`@btst/better-auth-ui`](https://github.com/better-stack-ai/better-auth-ui)
+is the separately maintained Better Auth UI companion for BTST v3. It adds
+resolved auth and account routes while Better Auth UI continues to read its own
+session and native permissions from your Better Auth client.
+
+
+ This integration assumes your application already owns a Better Auth server
+ endpoint. It does not generate a Better Auth backend, database adapter,
+ schema, migrations, authentication providers, secrets, or deployment
+ configuration.
+
+
+## Generate the minimal integration
+
+Select the companion explicitly; it is never part of the default scaffold.
+
+```bash
+npx @btst/codegen init --plugins better-auth-ui
+```
+
+The generated result:
+
+- registers only `authClientPlugin()` and `accountClientPlugin()`;
+- creates one browser client for the existing `/api/auth` endpoint;
+- mounts routes under the resolved BTST site path (`/pages/auth/*` and
+ `/pages/account/*` by default);
+- configures API, site, and QueryClient runtime only once in
+ `createClientStack()`; and
+- refreshes the framework explicitly after a Better Auth session change.
+
+Organization, API-key, passkey, multi-session, and other Better Auth extensions
+are not enabled by the generated code. Add one only after the matching Better
+Auth server and client plugin are configured in your application.
+
+
+ RC4 publishes API-key and passkey as required declaration peers because its
+ synthetic full `AuthClient` type exposes their surfaces. The CLI therefore
+ installs their aligned 1.6.16 packages to keep strict dependency trees clean,
+ but it does not import, register, or enable either runtime feature. Activation
+ remains an explicit application choice and requires the matching Better Auth
+ server/client plugins.
+
+
+## Supported release cohort
+
+The corrected companion candidate is `@btst/better-auth-ui@2.0.0-rc.4`. Its
+stable-v3 compatibility contract retains these exact versions:
+
+| Package | Version |
+| --- | --- |
+| `better-auth`, `@better-auth/core` | `1.6.16` |
+| `@better-auth/api-key`, `@better-auth/passkey` | `1.6.16` |
+| `@better-auth/utils` | `0.4.1` |
+| `@better-fetch/fetch` | `1.2.2` |
+| `better-call` | `1.3.6` |
+| `@btst/db` and BTST database adapters | `2.2.3` |
+
+Do not combine this companion release with a Better Auth 1.7 dependency graph.
+The CLI installs the corrected auth cohort without changing the retained
+`@btst/db@2.2.3` or adapter versions.
+
+For a manual installation, add the companion and exact auth cohort alongside
+your existing BTST dependencies:
+
+```bash
+pnpm add @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
+```
+
+The package declares its component-library peers. Resolve any peer warning
+against the companion's published manifest. These optional data-adapter
+subpaths add their own peers; do not install or import them unless you select
+that integration:
+
+| Optional subpath | Additional peers |
+| --- | --- |
+| `@btst/better-auth-ui/tanstack` | `@daveyplate/better-auth-tanstack@^1.3.6` |
+| `@btst/better-auth-ui/instantdb` | `@instantdb/react@>=0.18.0` |
+| `@btst/better-auth-ui/triplit` | `@triplit/client@>=1.0.0`, `@triplit/react@>=1.0.0` |
+
+## Browser client and resolved routes
+
+The CLI generates the following application-owned seam:
+
+```ts title="lib/auth-client.ts"
+import { createAuthClient } from "better-auth/react"
+
+export function createAppAuthClient(baseURL?: string) {
+ return createAuthClient({
+ ...(baseURL ? { baseURL } : {}),
+ basePath: "/api/auth",
+ })
+}
+```
+
+Change `basePath` only when your existing Better Auth handler uses a different
+path. The companion route bases are not configured here: they derive from the
+site runtime passed once to `createClientStack()`.
+
+```tsx title="lib/stack-client.tsx"
+import { accountClientPlugin, authClientPlugin } from "@btst/better-auth-ui/client"
+import { createClientStack } from "@btst/stack/client"
+
+return createClientStack({
+ api: { baseURL: apiOrigin, basePath: "/api/data" },
+ site: { baseURL: siteOrigin, basePath: "/pages" },
+ queryClient,
+ plugins: {
+ auth: authClientPlugin(),
+ account: accountClientPlugin(),
+ },
+})
+```
+
+## Provider overrides
+
+The resolved stack infers both override keys. Configure the Better Auth client
+once under `auth`; account-specific settings remain under `account`.
+
+```tsx
+
+ {children}
+
+```
+
+Use the framework-native synchronization generated for your target:
+
+
+
+ ```ts
+ onSessionChange: () => router.refresh()
+ ```
+
+
+ ```ts
+ onSessionChange: () => revalidator.revalidate()
+ ```
+
+
+ ```ts
+ onSessionChange: () => router.invalidate()
+ ```
+
+
+
+The bridge performs no hidden BTST identity refetch. If business plugins use
+BTST authorization, map the Better Auth session separately with BTST's generic
+`createClientAuth` and `createServerAuth` contracts and keep server
+authorization authoritative.
diff --git a/docs/content/docs/plugins/index.mdx b/docs/content/docs/plugins/index.mdx
index 9ce5c9713..b8b780520 100644
--- a/docs/content/docs/plugins/index.mdx
+++ b/docs/content/docs/plugins/index.mdx
@@ -4,7 +4,7 @@ description: Available plugins and features for BTST
---
import { Card, Cards } from "fumadocs-ui/components/card";
-import { BookOpen, Database, Hammer, Bot, FileText, FileCode, Route, Layout, Columns3, MessageSquare, ImageIcon } from "lucide-react";
+import { BookOpen, Database, Hammer, Bot, FileText, FileCode, Route, Layout, Columns3, MessageSquare, ImageIcon, ShieldCheck } from "lucide-react";
BTST provides a collection of full-stack plugins that you can easily integrate into your React application. Each plugin includes routes, APIs, database schemas, components, and hooks—everything you need to add complete features to your app.
@@ -72,6 +72,12 @@ With more plugins coming soon, you can add complete features to your app in minu
icon={}
description="Auto-generated client route documentation with interactive navigation."
/>
+ }
+ description="Optional auth and account routes for applications that already run Better Auth."
+ />
argument !== "--");
+ if (requested.length === 0) return FRAMEWORKS;
+ for (const framework of requested) {
+ if (!FRAMEWORKS.includes(framework)) {
+ throw new Error(
+ `Unknown framework ${framework}. Expected: ${FRAMEWORKS.join(", ")}`,
+ );
+ }
+ }
+ return [...new Set(requested)];
+}
+
+function logStep(message) {
+ console.log(`\n[better-auth-ui-fixture] ${message}`);
+}
+
+async function run(command, args, options = {}) {
+ logStep([command, ...args].join(" "));
+ try {
+ return await execFileAsync(command, args, {
+ cwd: options.cwd,
+ env: {
+ ...process.env,
+ CI: "true",
+ COREPACK_ENABLE_AUTO_PIN: "0",
+ ...options.env,
+ },
+ maxBuffer: 64 * 1024 * 1024,
+ stdio: options.capture ? "pipe" : "inherit",
+ });
+ } catch (error) {
+ if (error.stdout?.trim()) console.error(error.stdout.trim());
+ if (error.stderr?.trim()) console.error(error.stderr.trim());
+ throw new Error(`Command failed: ${command} ${args.join(" ")}`, {
+ cause: error,
+ });
+ }
+}
+
+async function packLocalPackage(packageDirectory, artifactsDirectory) {
+ const result = await run(
+ "npm",
+ ["pack", "--quiet", "--pack-destination", artifactsDirectory],
+ { cwd: packageDirectory, capture: true },
+ );
+ const tarballName = result.stdout.trim().split(/\s+/).at(-1);
+ if (!tarballName?.endsWith(".tgz")) {
+ throw new Error(`npm pack did not report a tarball: ${result.stdout}`);
+ }
+ return realpath(join(artifactsDirectory, basename(tarballName)));
+}
+
+async function packPublicCompanion(artifactsDirectory) {
+ const result = await run(
+ "npm",
+ [
+ "pack",
+ `@btst/better-auth-ui@${BETTER_AUTH_UI_VERSION}`,
+ "--quiet",
+ "--pack-destination",
+ artifactsDirectory,
+ ],
+ { cwd: REPOSITORY_ROOT, capture: true },
+ );
+ const tarballName = result.stdout.trim().split(/\s+/).at(-1);
+ if (!tarballName?.endsWith(".tgz")) {
+ throw new Error(`npm pack did not report a tarball: ${result.stdout}`);
+ }
+ return realpath(join(artifactsDirectory, basename(tarballName)));
+}
+
+async function prepareArtifacts(tempRoot) {
+ await run("pnpm", ["--filter", "@btst/stack", "build"], {
+ cwd: REPOSITORY_ROOT,
+ });
+ await run("pnpm", ["--filter", "@btst/codegen", "build"], {
+ cwd: REPOSITORY_ROOT,
+ });
+
+ const artifactsDirectory = join(tempRoot, "artifacts");
+ await mkdir(artifactsDirectory, { recursive: true });
+ return {
+ stack: await packLocalPackage(
+ join(REPOSITORY_ROOT, "packages/stack"),
+ artifactsDirectory,
+ ),
+ codegen: await packLocalPackage(CLI_DIRECTORY, artifactsDirectory),
+ betterAuthUi: await packPublicCompanion(artifactsDirectory),
+ };
+}
+
+async function patchManifest(projectDirectory, artifacts, framework) {
+ const manifestPath = join(projectDirectory, "package.json");
+ const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
+ manifest.private = true;
+ manifest.dependencies = {
+ ...manifest.dependencies,
+ "@btst/adapter-memory": "2.2.3",
+ "@btst/better-auth-ui": `file:${artifacts.betterAuthUi}`,
+ "@btst/db": AUTH_COHORT["@btst/db"],
+ "@btst/stack": `file:${artifacts.stack}`,
+ "@btst/yar": "1.3.2",
+ "@better-auth/api-key": AUTH_COHORT["@better-auth/api-key"],
+ "@better-auth/core": AUTH_COHORT["@better-auth/core"],
+ "@better-auth/passkey": AUTH_COHORT["@better-auth/passkey"],
+ "@better-auth/utils": AUTH_COHORT["@better-auth/utils"],
+ "@better-fetch/fetch": AUTH_COHORT["@better-fetch/fetch"],
+ "@tanstack/react-query": "5.102.0",
+ "better-auth": AUTH_COHORT["better-auth"],
+ "better-call": AUTH_COHORT["better-call"],
+ "next-themes": "0.4.6",
+ };
+ manifest.devDependencies = {
+ ...manifest.devDependencies,
+ "@btst/codegen": `file:${artifacts.codegen}`,
+ ...(framework === "tanstack" ? { eslint: "10.0.1" } : {}),
+ };
+ await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
+}
+
+function collectNamedVersions(tree, packageName, versions = new Set()) {
+ if (!tree || typeof tree !== "object") return versions;
+ if (Array.isArray(tree)) {
+ for (const item of tree) collectNamedVersions(item, packageName, versions);
+ return versions;
+ }
+ for (const [name, dependency] of Object.entries(tree.dependencies ?? {})) {
+ if (name === packageName && typeof dependency.version === "string") {
+ versions.add(dependency.version);
+ }
+ collectNamedVersions(dependency, packageName, versions);
+ }
+ return versions;
+}
+
+async function assertCohort(projectDirectory) {
+ const problems = [];
+ for (const [packageName, expected] of Object.entries(AUTH_COHORT)) {
+ const versions = new Set();
+ for (const depth of ["0", "Infinity"]) {
+ const result = await run(
+ "pnpm",
+ ["list", packageName, "--json", "--depth", depth],
+ { cwd: projectDirectory, capture: true },
+ );
+ collectNamedVersions(JSON.parse(result.stdout), packageName, versions);
+ }
+ const found = [...versions].sort();
+ if (found.length !== 1 || found[0] !== expected) {
+ problems.push(
+ `${packageName}: expected only ${expected}; found ${found.join(", ") || "nothing"}`,
+ );
+ }
+ }
+ if (problems.length > 0) {
+ throw new Error(`Auth cohort mismatch:\n- ${problems.join("\n- ")}`);
+ }
+}
+
+async function assertGeneratedBoundary(projectDirectory, config) {
+ const authClient = await readFile(
+ join(projectDirectory, config.authClientPath),
+ "utf8",
+ );
+ const stackClient = await readFile(
+ join(projectDirectory, config.stackClientPath),
+ "utf8",
+ );
+ const provider = await readFile(
+ join(projectDirectory, config.providerPath),
+ "utf8",
+ );
+ const css = await readFile(join(projectDirectory, config.cssFile), "utf8");
+ const backend = await readFile(
+ join(
+ projectDirectory,
+ config.stackClientPath.replace("stack-client.tsx", "stack.ts"),
+ ),
+ "utf8",
+ );
+
+ const requirements = [
+ [authClient.includes("createAuthClient"), "browser auth client"],
+ [authClient.includes('basePath: "/api/auth"'), "existing endpoint seam"],
+ [stackClient.includes("auth: authClientPlugin()"), "auth plugin"],
+ [stackClient.includes("account: accountClientPlugin()"), "account plugin"],
+ [
+ !stackClient.includes("organizationClientPlugin"),
+ "no organization plugin",
+ ],
+ [provider.includes(config.refresh), "framework session refresh"],
+ [provider.includes("account: true"), "account override"],
+ [!provider.includes("organization:"), "no organization override"],
+ [!provider.includes("apiKey:"), "no API-key opt-in"],
+ [!provider.includes("passkey:"), "no passkey opt-in"],
+ [css.includes("@btst/better-auth-ui/css"), "companion CSS"],
+ [!backend.includes("better-auth"), "no Better Auth backend"],
+ ];
+ const missing = requirements.filter(([ok]) => !ok).map(([, label]) => label);
+ if (missing.length > 0) {
+ throw new Error(`Generated boundary failed: ${missing.join(", ")}`);
+ }
+}
+
+async function scaffoldFramework(tempRoot, framework, artifacts) {
+ const config = FIXTURE_CONFIG[framework];
+ const projectName = `better-auth-ui-${framework}`;
+ const projectDirectory = join(tempRoot, projectName);
+
+ await run(
+ "pnpm",
+ [
+ "dlx",
+ `shadcn@${SHADCN_VERSION}`,
+ "init",
+ "-t",
+ config.template,
+ "--no-monorepo",
+ "--base",
+ "radix",
+ "--preset",
+ "nova",
+ "--name",
+ projectName,
+ "--yes",
+ ],
+ { cwd: tempRoot },
+ );
+ await run(
+ "pnpm",
+ [
+ "dlx",
+ `shadcn@${SHADCN_VERSION}`,
+ "add",
+ "dropdown-menu",
+ "--yes",
+ "--overwrite",
+ ],
+ { cwd: projectDirectory },
+ );
+
+ await rm(join(projectDirectory, ".git"), { recursive: true, force: true });
+ await rm(join(projectDirectory, "node_modules"), {
+ recursive: true,
+ force: true,
+ });
+ await rm(join(projectDirectory, "pnpm-lock.yaml"), { force: true });
+ await rm(join(projectDirectory, "package-lock.json"), { force: true });
+ await patchManifest(projectDirectory, artifacts, framework);
+
+ await run("pnpm", ["install", "--strict-peer-dependencies"], {
+ cwd: projectDirectory,
+ });
+ await run(
+ "pnpm",
+ [
+ "exec",
+ "btst",
+ "init",
+ "--yes",
+ "--framework",
+ framework,
+ "--adapter",
+ "memory",
+ "--plugins",
+ "better-auth-ui",
+ "--skip-install",
+ ],
+ { cwd: projectDirectory },
+ );
+
+ await assertGeneratedBoundary(projectDirectory, config);
+ await assertCohort(projectDirectory);
+ await run("pnpm", ["run", "build"], {
+ cwd: projectDirectory,
+ env: config.env,
+ });
+ const generatedManifest = JSON.parse(
+ await readFile(join(projectDirectory, "package.json"), "utf8"),
+ );
+ const typecheckCommand = generatedManifest.scripts?.typecheck
+ ? ["run", "typecheck"]
+ : ["exec", "tsc", "--noEmit"];
+ await run("pnpm", typecheckCommand, {
+ cwd: projectDirectory,
+ env: config.env,
+ });
+ logStep(`${framework} packed fixture passed`);
+}
+
+async function main() {
+ assertNode22();
+ const frameworks = selectedFrameworks();
+ const tempRoot = await mkdtemp(join(tmpdir(), "btst-better-auth-ui-"));
+ let passed = false;
+ try {
+ const artifacts = await prepareArtifacts(tempRoot);
+ for (const framework of frameworks) {
+ await scaffoldFramework(tempRoot, framework, artifacts);
+ }
+ passed = true;
+ logStep(`all packed fixtures passed: ${frameworks.join(", ")}`);
+ } finally {
+ if (passed || process.env.BTST_KEEP_FIXTURES !== "1") {
+ await rm(tempRoot, { recursive: true, force: true });
+ } else {
+ console.error(`Fixture retained for debugging: ${tempRoot}`);
+ }
+ }
+}
+
+await main();
diff --git a/packages/cli/scripts/test-init.sh b/packages/cli/scripts/test-init.sh
index 2638fb375..5349aa7a1 100644
--- a/packages/cli/scripts/test-init.sh
+++ b/packages/cli/scripts/test-init.sh
@@ -13,7 +13,7 @@ ROOT_DIR="$(cd "$PACKAGE_DIR/../.." && pwd)"
TEST_DIR="/tmp/test-btst-init-$(date +%s)"
TEST_PASSED=false
SHADCN_VERSION="4.0.5"
-MEMORY_PLUGIN_LIST="blog,ai-chat,cms,ui-builder,kanban,comments,media,route-docs,open-api"
+MEMORY_PLUGIN_LIST="blog,ai-chat,cms,ui-builder,kanban,comments,media,route-docs,open-api,better-auth-ui"
cleanup() {
if [ "$TEST_PASSED" = true ]; then
@@ -129,17 +129,6 @@ if [ "$(cat "$TEST_DIR/init-memory-before.hash")" != "$(cat "$TEST_DIR/init-memo
fi
success "Memory + Form Builder failed before scaffolding"
-step "Rejecting the retired provider-specific authentication plugin"
-if npx @btst/codegen init --yes --framework nextjs --adapter memory --plugins better-auth-ui --skip-install > "$TEST_DIR/init-retired-auth.log" 2>&1; then
- error "Expected the retired authentication plugin selection to fail"
- exit 1
-fi
-if ! grep -q "Unknown plugin(s): better-auth-ui" "$TEST_DIR/init-retired-auth.log"; then
- error "Expected retired authentication plugin guidance was not printed"
- exit 1
-fi
-success "Retired authentication plugin cannot be selected"
-
step "Running compatible memory btst init (first pass)"
npx @btst/codegen init --yes --framework nextjs --adapter memory --plugins "$MEMORY_PLUGIN_LIST" --skip-install 2>&1 | tee "$TEST_DIR/init-first.log"
if ! node -e 'const fs=require("fs");const s=fs.readFileSync(process.argv[1],"utf8");process.exit(s.includes("Running @btst/codegen init")?0:1)' "$TEST_DIR/init-first.log"; then
@@ -171,6 +160,7 @@ success "Ran @btst/cli@2.2.4 without adding it to the consumer graph"
step "Asserting generated files and patches"
test -f "lib/stack.ts"
test -f "lib/stack-client.tsx"
+test -f "lib/auth-client.ts"
test -f "lib/stack-client.server.ts"
test -f "lib/query-client.ts"
test -f "app/api/data/[[...all]]/route.ts"
@@ -181,10 +171,13 @@ test -f "app/pages/client-layout.tsx"
node -e 'const fs=require("fs");const s=fs.readFileSync("lib/stack.ts","utf8");process.exit(s.includes("import { createBackendStack } from \"@btst/stack/api\"")?0:1)'
node -e 'const fs=require("fs");const s=fs.readFileSync("lib/stack.ts","utf8");process.exit(s.includes("mediaBackendPlugin({ storageAdapter: localAdapter() })")?0:1)'
node -e 'const fs=require("fs");const s=fs.readFileSync("lib/stack-client.tsx","utf8");process.exit(s.includes("createClientStack")&&s.includes("NEXT_PUBLIC_BASE_URL")&&!s.includes("getStackClientForRequest")?0:1)'
+node -e 'const fs=require("fs");const s=fs.readFileSync("lib/stack-client.tsx","utf8");process.exit(s.includes("auth: authClientPlugin()")&&s.includes("account: accountClientPlugin()")&&!s.includes("organizationClientPlugin")?0:1)'
+node -e 'const fs=require("fs");const s=fs.readFileSync("lib/auth-client.ts","utf8");process.exit(s.includes("createAuthClient")&&s.includes("/api/auth")?0:1)'
node -e 'const fs=require("fs");const s=fs.readFileSync("lib/stack-client.server.ts","utf8");process.exit(s.includes("getStackClientForRequest")&&s.includes("resolveTrustedClientOrigins")&&s.includes("filterCredentialForwardingHeaders")&&s.includes("NEXT_PUBLIC_BASE_URL")?0:1)'
node -e 'const fs=require("fs");const request=fs.readFileSync("app/(request)/pages/layout.tsx","utf8"),staticLayout=fs.readFileSync("app/(static)/pages/layout.tsx","utf8"),client=fs.readFileSync("app/pages/client-layout.tsx","utf8");process.exit(request.includes("getServerClientOriginsFromHeaders(await headers())")&&staticLayout.includes("getServerClientOrigins()")&&!staticLayout.includes("next/headers")&&client.includes("getStackClient(queryClient, clientOrigins)")?0:1)'
node -e 'const fs=require("fs");const s=fs.readFileSync("app/globals.css","utf8");process.exit(s.includes("@btst/stack/plugins/ui-builder/css")?0:1)'
-node -e 'const fs=require("fs"),path=require("path");const roots=["app","lib","package.json"];const retired=["@btst","better-auth-ui"].join("/");const read=(p)=>fs.statSync(p).isDirectory()?fs.readdirSync(p).flatMap((n)=>read(path.join(p,n))):[fs.readFileSync(p,"utf8")];process.exit(roots.flatMap(read).some((s)=>s.includes(retired))?1:0)'
+node -e 'const fs=require("fs");const s=fs.readFileSync("app/pages/client-layout.tsx","utf8");process.exit(s.includes("authClient")&&s.includes("frameworkRouter.refresh()")&&s.includes("account: true")&&!s.includes("organization:")?0:1)'
+node -e 'const fs=require("fs");const s=fs.readFileSync("app/globals.css","utf8");process.exit(s.includes("@btst/better-auth-ui/css")?0:1)'
success "Generation + patch checks passed"
step "Adding third-party public extension fixture"
diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts
index 0f4365853..416244236 100644
--- a/packages/cli/src/commands/init.ts
+++ b/packages/cli/src/commands/init.ts
@@ -179,7 +179,7 @@ export function createInitCommand() {
)
.option(
"--plugins ",
- "Comma-separated plugin keys, or 'all'",
+ "Comma-separated plugin keys (use better-auth-ui for auth + account with an existing Better Auth backend), or 'all'",
parsePluginOption,
)
.option("--skip-install", "Skip dependency install")
diff --git a/packages/cli/src/templates/nextjs/form-demo-client.tsx.hbs b/packages/cli/src/templates/nextjs/form-demo-client.tsx.hbs
index 182dd4492..9716bce1d 100644
--- a/packages/cli/src/templates/nextjs/form-demo-client.tsx.hbs
+++ b/packages/cli/src/templates/nextjs/form-demo-client.tsx.hbs
@@ -29,10 +29,10 @@ export default function FormDemoPageClient({
getStackClient(queryClient, clientOrigins),
[clientOrigins.apiOrigin, clientOrigins.siteOrigin, queryClient],
)
+{{#if hasBetterAuthUi}}
+ const frameworkRouter = useRouter()
+ const authClient = useMemo(
+ () => createAppAuthClient(clientOrigins.siteOrigin),
+ [clientOrigins.siteOrigin],
+ )
+{{/if}}
{{#if hasAiChat}}
const hasApiKey = typeof process !== "undefined" && !!process.env.NEXT_PUBLIC_HAS_OPENAI_KEY
const pathname = usePathname()
diff --git a/packages/cli/src/templates/nextjs/preview-client.tsx.hbs b/packages/cli/src/templates/nextjs/preview-client.tsx.hbs
index 08a7f8db2..63ed2fd11 100644
--- a/packages/cli/src/templates/nextjs/preview-client.tsx.hbs
+++ b/packages/cli/src/templates/nextjs/preview-client.tsx.hbs
@@ -33,10 +33,10 @@ export default function PreviewPageClient({
getStackClient(queryClient, { apiOrigin, siteOrigin }),
[apiOrigin, queryClient, siteOrigin],
)
+{{#if hasBetterAuthUi}}
+ const revalidator = useRevalidator()
+ const authClient = useMemo(
+ () => createAppAuthClient(siteOrigin),
+ [siteOrigin],
+ )
+{{/if}}
{{#if hasAiChat}}
const hasApiKey = !!import.meta.env.VITE_HAS_OPENAI_KEY
const location = useLocation()
diff --git a/packages/cli/src/templates/react-router/preview-route.tsx.hbs b/packages/cli/src/templates/react-router/preview-route.tsx.hbs
index f3a0945f6..459364f62 100644
--- a/packages/cli/src/templates/react-router/preview-route.tsx.hbs
+++ b/packages/cli/src/templates/react-router/preview-route.tsx.hbs
@@ -33,10 +33,10 @@ export default function PreviewPage() {
getStackClient(queryClient, { apiOrigin, siteOrigin }),
[apiOrigin, queryClient, siteOrigin],
)
+{{#if hasBetterAuthUi}}
+ const frameworkRouter = useRouter()
+ const authClient = useMemo(
+ () => createAppAuthClient(siteOrigin),
+ [siteOrigin],
+ )
+{{/if}}
{{#if hasAiChat}}
const hasApiKey = !!import.meta.env.VITE_HAS_OPENAI_KEY
const location = useLocation()
diff --git a/packages/cli/src/templates/tanstack/pages-route.tsx.hbs b/packages/cli/src/templates/tanstack/pages-route.tsx.hbs
index eded418b8..b8ab3e894 100644
--- a/packages/cli/src/templates/tanstack/pages-route.tsx.hbs
+++ b/packages/cli/src/templates/tanstack/pages-route.tsx.hbs
@@ -1,7 +1,7 @@
import { createFileRoute } from "@tanstack/react-router"
import { createTanStackPageOptions } from "@btst/stack/tanstack"
import type { QueryClient } from "@tanstack/react-query"
-import { createIsomorphicFn } from "@tanstack/react-start"
+import { createIsomorphicFn, createServerOnlyFn } from "@tanstack/react-start"
import { getRequest } from "@tanstack/react-start/server"
import { getOrCreateQueryClient } from "{{alias}}lib/query-client"
import { getStackClient } from "{{alias}}lib/stack-client"
@@ -21,13 +21,20 @@ const getLoaderRequestContext = createIsomorphicFn()
const getNavigationClientStack = async (queryClient: QueryClient) =>
getStackClient(queryClient, await getTrustedClientOrigins())
+const getRequestStackClient = createServerOnlyFn(
+ (
+ queryClient: QueryClient,
+ requestContext: { headers: Headers; requestOrigin: string },
+ ) => getStackClientForRequest(queryClient, requestContext),
+)
+
export const Route = createFileRoute("/pages/$")(
createTanStackPageOptions({
getStackClient,
getLoaderStackClient: async (queryClient) => {
const requestContext = await getLoaderRequestContext()
return requestContext
- ? getStackClientForRequest(queryClient, requestContext)
+ ? getRequestStackClient(queryClient, requestContext)
: getNavigationClientStack(queryClient)
},
getQueryClient: getOrCreateQueryClient,
diff --git a/packages/cli/src/templates/tanstack/preview-route.tsx.hbs b/packages/cli/src/templates/tanstack/preview-route.tsx.hbs
index ad9579fc1..d7c769851 100644
--- a/packages/cli/src/templates/tanstack/preview-route.tsx.hbs
+++ b/packages/cli/src/templates/tanstack/preview-route.tsx.hbs
@@ -31,10 +31,10 @@ function PreviewPage() {
{
+ it("advertises the optional Better Auth UI scaffold boundary", () => {
+ const help = createInitCommand().helpInformation();
+
+ expect(help).toContain("better-auth-ui");
+ expect(help).toContain("auth + account");
+ expect(help).toContain("existing Better Auth backend");
+ });
+});
diff --git a/packages/cli/src/utils/__tests__/package-installer.test.ts b/packages/cli/src/utils/__tests__/package-installer.test.ts
index 2659ab4fe..60c9ec2a7 100644
--- a/packages/cli/src/utils/__tests__/package-installer.test.ts
+++ b/packages/cli/src/utils/__tests__/package-installer.test.ts
@@ -5,7 +5,6 @@ const { execa } = vi.hoisted(() => ({ execa: vi.fn() }));
vi.mock("execa", () => ({ execa }));
import { installInitDependencies } from "../package-installer";
-import { PLUGINS } from "../constants";
describe("installInitDependencies", () => {
beforeEach(() => {
@@ -13,26 +12,41 @@ describe("installInitDependencies", () => {
execa.mockResolvedValue({});
});
- it("never installs a provider-specific authentication cohort", async () => {
+ it("installs the corrected Better Auth UI cohort only when selected", async () => {
await installInitDependencies({
cwd: "/tmp/example",
packageManager: "pnpm",
adapter: "drizzle",
- plugins: PLUGINS.map((plugin) => plugin.key),
+ plugins: ["better-auth-ui"],
});
const installArguments = execa.mock.calls[0]?.[1] as string[];
expect(installArguments).toContain("@btst/adapter-drizzle@2.2.3");
expect(installArguments).toContain("drizzle-orm@0.45.2");
- expect(installArguments.join(" ")).not.toContain(
- ["@btst", "better-auth-ui"].join("/"),
- );
- expect(installArguments.join(" ")).not.toContain(
- ["better", "auth"].join("-"),
- );
+ expect(installArguments).toContain("@btst/better-auth-ui@2.0.0-rc.4");
+ expect(installArguments).toContain("better-auth@1.6.16");
+ expect(installArguments).toContain("@better-auth/core@1.6.16");
+ expect(installArguments).toContain("@better-auth/utils@0.4.1");
+ expect(installArguments).toContain("@better-fetch/fetch@1.2.2");
+ expect(installArguments).toContain("better-call@1.3.6");
+ expect(installArguments).toContain("@better-auth/api-key@1.6.16");
+ expect(installArguments).toContain("@better-auth/passkey@1.6.16");
expect(execa).toHaveBeenCalledTimes(1);
});
+ it("does not install Better Auth UI for the default scaffold", async () => {
+ await installInitDependencies({
+ cwd: "/tmp/example",
+ packageManager: "pnpm",
+ adapter: "memory",
+ plugins: [],
+ });
+
+ const installArguments = execa.mock.calls[0]?.[1] as string[];
+ expect(installArguments.join(" ")).not.toContain("better-auth-ui");
+ expect(installArguments.join(" ")).not.toContain("better-auth@");
+ });
+
it("saves npm runtime versions exactly", async () => {
await installInitDependencies({
cwd: "/tmp/example",
diff --git a/packages/cli/src/utils/__tests__/scaffold-plan.test.ts b/packages/cli/src/utils/__tests__/scaffold-plan.test.ts
index c8dd592b0..48ee7259c 100644
--- a/packages/cli/src/utils/__tests__/scaffold-plan.test.ts
+++ b/packages/cli/src/utils/__tests__/scaffold-plan.test.ts
@@ -1,7 +1,7 @@
import { describe, expect, it } from "vitest";
import { readFile } from "node:fs/promises";
import { buildScaffoldPlan } from "../scaffold-plan";
-import { PLUGINS } from "../constants";
+import { PLUGINS, PLUGIN_ROUTES } from "../constants";
describe("scaffold plan", () => {
it.each(["memory", "mongodb"] as const)(
@@ -75,6 +75,21 @@ describe("scaffold plan", () => {
},
);
+ it.each(["prisma", "drizzle", "kysely"] as const)(
+ "enables isolated transactions for Kanban in the %s scaffold",
+ async (adapter) => {
+ const plan = await buildScaffoldPlan({
+ framework: "nextjs",
+ adapter,
+ plugins: ["kanban"],
+ alias: "@/",
+ cssFile: "app/globals.css",
+ });
+ const stackFile = plan.files.find((file) => file.path === "lib/stack.ts");
+ expect(stackFile?.content).toContain("transaction: true");
+ },
+ );
+
it("rejects Media with the unsupported MongoDB generated configuration", async () => {
await expect(
buildScaffoldPlan({
@@ -425,6 +440,10 @@ describe("scaffold plan", () => {
expect(pageRoute?.content).toContain("new URL(request.url).origin");
} else {
expect(pageRoute?.content).toContain("createIsomorphicFn");
+ expect(pageRoute?.content).toContain("createServerOnlyFn");
+ expect(pageRoute?.content).toContain(
+ "getRequestStackClient(queryClient, requestContext)",
+ );
expect(pageRoute?.content).toContain("getRequest()");
expect(pageRoute?.content).toContain(
"getStackClient(queryClient, await getTrustedClientOrigins())",
@@ -522,6 +541,18 @@ describe("scaffold plan", () => {
expect(source).not.toContain("StackProvider<");
});
+ it("runs the three-framework Better Auth UI packed fixtures in CI", async () => {
+ const workflow = await readFile(
+ new URL("../../../../../.github/workflows/init.yml", import.meta.url),
+ "utf8",
+ );
+
+ expect(workflow).toContain("better-auth-ui-fixtures:");
+ expect(workflow).toContain(
+ "pnpm --filter @btst/codegen test:better-auth-ui-fixtures",
+ );
+ });
+
it("does not register ui-builder as a backend plugin entry", async () => {
const plan = await buildScaffoldPlan({
framework: "nextjs",
@@ -944,13 +975,142 @@ describe("scaffold plan", () => {
expect(layoutFile?.content).not.toContain("router.replace");
});
- it("excludes provider-specific authentication integrations from generated projects", () => {
- const allKeys = PLUGINS.map((p) => p.key);
- expect(allKeys).not.toContain(["better", "auth", "ui"].join("-"));
- expect(JSON.stringify(PLUGINS)).not.toContain(
- ["@btst", "better-auth-ui"].join("/"),
- );
- });
+ it.each([
+ {
+ framework: "nextjs" as const,
+ authRefresh: "frameworkRouter.refresh()",
+ authClientPath: "lib/auth-client.ts",
+ },
+ {
+ framework: "react-router" as const,
+ authRefresh: "revalidator.revalidate()",
+ authClientPath: "app/lib/auth-client.ts",
+ },
+ {
+ framework: "tanstack" as const,
+ authRefresh: "frameworkRouter.invalidate()",
+ authClientPath: "src/lib/auth-client.ts",
+ },
+ ])(
+ "generates the minimal Better Auth UI bridge for $framework",
+ async ({ framework, authRefresh, authClientPath }) => {
+ const plan = await buildScaffoldPlan({
+ framework,
+ adapter: "drizzle",
+ plugins: ["better-auth-ui"],
+ alias: framework === "react-router" ? "~/" : "@/",
+ cssFile:
+ framework === "nextjs"
+ ? "app/globals.css"
+ : framework === "react-router"
+ ? "app/app.css"
+ : "src/styles.css",
+ });
+
+ const stack = plan.files.find((file) =>
+ file.path.endsWith("lib/stack.ts"),
+ );
+ const client = plan.files.find((file) =>
+ file.path.endsWith("lib/stack-client.tsx"),
+ );
+ const authClient = plan.files.find(
+ (file) => file.path === authClientPath,
+ );
+ const provider = plan.files.find((file) =>
+ file.content.includes(" plugin.key)).toContain("better-auth-ui");
+ expect(PLUGIN_ROUTES["better-auth-ui"]).toEqual(
+ expect.arrayContaining([
+ "/pages/auth/sign-in",
+ "/pages/account/settings",
+ "/pages/account/security",
+ ]),
+ );
+ expect(stack?.content).not.toContain("better-auth");
+ expect(stack?.content).not.toContain("transaction: true");
+ expect(client?.content).toContain(
+ 'import { accountClientPlugin, authClientPlugin } from "@btst/better-auth-ui/client"',
+ );
+ expect(client?.content).toContain("auth: authClientPlugin(),");
+ expect(client?.content).toContain("account: accountClientPlugin(),");
+ expect(client?.content).not.toContain("organizationClientPlugin");
+ expect(authClient?.content).toContain(
+ 'import { createAuthClient } from "better-auth/react"',
+ );
+ expect(authClient?.content).toContain('basePath: "/api/auth"');
+ expect(provider?.content).toContain("authClient");
+ expect(provider?.content).toContain(authRefresh);
+ expect(provider?.content).toContain(
+ 'redirectTo: "/pages/account/settings"',
+ );
+ expect(provider?.content).toContain("account: {");
+ expect(provider?.content).not.toContain("organization:");
+ expect(provider?.content).not.toContain("apiKey:");
+ expect(provider?.content).not.toContain("passkey:");
+ expect(plan.cssImports).toContain("@btst/better-auth-ui/css");
+ expect(plan.extraPackageVersions).toMatchObject({
+ "@btst/better-auth-ui": "2.0.0-rc.4",
+ "better-auth": "1.6.16",
+ });
+ },
+ );
+
+ it.each([
+ {
+ framework: "nextjs" as const,
+ primaryProviderPath: "app/pages/client-layout.tsx",
+ cssFile: "app/globals.css",
+ },
+ {
+ framework: "react-router" as const,
+ primaryProviderPath: "app/routes/pages/_layout.tsx",
+ cssFile: "app/app.css",
+ },
+ {
+ framework: "tanstack" as const,
+ primaryProviderPath: "src/routes/pages/route.tsx",
+ cssFile: "src/styles/globals.css",
+ },
+ ])(
+ "keeps Better Auth UI state out of embedded $framework providers",
+ async ({ framework, primaryProviderPath, cssFile }) => {
+ const plan = await buildScaffoldPlan({
+ framework,
+ adapter: "drizzle",
+ plugins: [
+ "better-auth-ui",
+ "ai-chat",
+ "cms",
+ "ui-builder",
+ "form-builder",
+ "kanban",
+ ],
+ alias: framework === "react-router" ? "~/" : "@/",
+ cssFile,
+ });
+
+ const primaryProvider = plan.files.find(
+ (file) => file.path === primaryProviderPath,
+ );
+ const embeddedProviders = plan.files.filter(
+ (file) =>
+ file.path !== primaryProviderPath &&
+ file.content.includes(" = {
"route-docs": ["/pages/route-docs"],
/** open-api registers an API route, not a page route */
"open-api": ["/api/data/reference"],
+ "better-auth-ui": [
+ "/pages/auth/sign-in",
+ "/pages/auth/sign-up",
+ "/pages/auth/forgot-password",
+ "/pages/auth/reset-password",
+ "/pages/auth/magic-link",
+ "/pages/auth/email-otp",
+ "/pages/auth/two-factor",
+ "/pages/auth/recover-account",
+ "/pages/auth/callback",
+ "/pages/auth/sign-out",
+ "/pages/auth/accept-invitation",
+ "/pages/auth/email-verification",
+ "/pages/account/settings",
+ "/pages/account/security",
+ "/pages/account/api-keys",
+ "/pages/account/organizations",
+ "/pages/account/teams",
+ ],
};
diff --git a/packages/cli/src/utils/scaffold-plan.ts b/packages/cli/src/utils/scaffold-plan.ts
index f1e881cfb..9b0d29cd8 100644
--- a/packages/cli/src/utils/scaffold-plan.ts
+++ b/packages/cli/src/utils/scaffold-plan.ts
@@ -23,6 +23,7 @@ function getFrameworkPaths(framework: Framework, cssFile: string) {
return {
stackPath: `${prefix}lib/stack.ts`,
stackClientPath: `${prefix}lib/stack-client.tsx`,
+ authClientPath: `${prefix}lib/auth-client.ts`,
stackClientServerPath: `${prefix}lib/stack-client.server.ts`,
stackClientOriginsPath: undefined,
queryClientPath: `${prefix}lib/query-client.ts`,
@@ -39,6 +40,7 @@ function getFrameworkPaths(framework: Framework, cssFile: string) {
return {
stackPath: "app/lib/stack.ts",
stackClientPath: "app/lib/stack-client.tsx",
+ authClientPath: "app/lib/auth-client.ts",
stackClientServerPath: "app/lib/stack-client.server.ts",
stackClientOriginsPath: undefined,
queryClientPath: "app/lib/query-client.ts",
@@ -54,6 +56,7 @@ function getFrameworkPaths(framework: Framework, cssFile: string) {
return {
stackPath: "src/lib/stack.ts",
stackClientPath: "src/lib/stack-client.tsx",
+ authClientPath: "src/lib/auth-client.ts",
stackClientServerPath: "src/lib/stack-client.server.ts",
stackClientOriginsPath: "src/lib/stack-client.origins.ts",
queryClientPath: "src/lib/query-client.ts",
@@ -121,6 +124,7 @@ function buildPluginTemplateContext(
const hasFormBuilder = selectedPlugins.includes("form-builder");
const hasBlog = selectedPlugins.includes("blog");
const hasKanban = selectedPlugins.includes("kanban");
+ const hasBetterAuthUi = selectedPlugins.includes("better-auth-ui");
const hasSitemap = hasBlog || hasCms || hasKanban;
const backendMetas = metas.filter(
@@ -139,6 +143,29 @@ function buildPluginTemplateContext(
const backendImportLines = backendMetas
.map((m) => `import { ${m.backendSymbol} } from "${m.backendImportPath}"`)
.join("\n");
+ const embeddedOverrides = clientMetas
+ .map((m) => {
+ const layoutFile = getPagesLayoutFilePath(framework);
+ if (m.key === "blog") {
+ return `\t\t\t\t\t${m.configKey}: {
+\t\t\t\t\t\tuploadImage: async () => {
+\t\t\t\t\t\t\tthrow new Error("TODO: implement blog.uploadImage override in ${layoutFile}")
+\t\t\t\t\t\t},
+\t\t\t\t\t},`;
+ }
+ if (m.key === "kanban") {
+ return `\t\t\t\t\t${m.configKey}: {
+\t\t\t\t\t\tuploadImage: async () => {
+\t\t\t\t\t\t\tthrow new Error("TODO: implement kanban.uploadImage override in ${layoutFile}")
+\t\t\t\t\t\t},
+\t\t\t\t\t\tresolveUser: async () => null,
+\t\t\t\t\t\tsearchUsers: async () => [],
+\t\t\t\t\t},`;
+ }
+ return "";
+ })
+ .filter(Boolean)
+ .join("\n");
return {
hasAiChat,
@@ -159,8 +186,15 @@ function buildPluginTemplateContext(
]
.filter(Boolean)
.join("\n"),
- clientImports: clientMetas
- .map((m) => `import { ${m.clientSymbol} } from "${m.clientImportPath}"`)
+ clientImports: [
+ hasBetterAuthUi
+ ? 'import { accountClientPlugin, authClientPlugin } from "@btst/better-auth-ui/client"'
+ : "",
+ clientMetas
+ .map((m) => `import { ${m.clientSymbol} } from "${m.clientImportPath}"`)
+ .join("\n"),
+ ]
+ .filter(Boolean)
.join("\n"),
backendEntries: metas
.map((m) => {
@@ -200,48 +234,48 @@ function buildPluginTemplateContext(
})
.filter(Boolean)
.join("\n"),
- clientEntries: clientMetas
- .map((m) => {
- if (m.key === "ai-chat") {
- return `\t\t\t${m.configKey}: ${m.clientSymbol}({ mode: "public" }),`;
- }
- return `\t\t\t${m.configKey}: ${m.clientSymbol}(),`;
- })
+ clientEntries: [
+ hasBetterAuthUi
+ ? "\t\t\tauth: authClientPlugin(),\n\t\t\taccount: accountClientPlugin(),"
+ : "",
+ clientMetas
+ .map((m) => {
+ if (m.key === "ai-chat") {
+ return `\t\t\t${m.configKey}: ${m.clientSymbol}({ mode: "public" }),`;
+ }
+ return `\t\t\t${m.configKey}: ${m.clientSymbol}(),`;
+ })
+ .join("\n"),
+ ]
+ .filter(Boolean)
.join("\n"),
clientApiEndpointEntries: clientMetas
.filter((m) => m.backendSymbol && m.key !== "ui-builder")
.map((m) => `\t\t\t\t${m.configKey}: crossOriginApiEndpoint,`)
.join("\n"),
- pagesLayoutOverrides: clientMetas
- .map((m) => {
- if (m.key === "route-docs" || m.key === "media") {
- return "";
- }
- const layoutFile = getPagesLayoutFilePath(framework);
- if (m.key === "comments") {
- return "";
- }
- if (m.key === "blog") {
- return `\t\t\t\t\t${m.configKey}: {
-\t\t\t\t\t\tuploadImage: async () => {
-\t\t\t\t\t\t\tthrow new Error("TODO: implement blog.uploadImage override in ${layoutFile}")
-\t\t\t\t\t\t},
-\t\t\t\t\t},`;
- }
- if (m.key === "kanban") {
- return `\t\t\t\t\t${m.configKey}: {
-\t\t\t\t\t\tuploadImage: async () => {
-\t\t\t\t\t\t\tthrow new Error("TODO: implement kanban.uploadImage override in ${layoutFile}")
-\t\t\t\t\t\t},
-\t\t\t\t\t\tresolveUser: async () => null,
-\t\t\t\t\t\tsearchUsers: async () => [],
-\t\t\t\t\t},`;
- }
- if (m.key === "ai-chat") return "";
- return "";
- })
+ pagesLayoutOverrides: [
+ hasBetterAuthUi
+ ? `\t\t\t\t\tauth: {
+\t\t\t\t\t\tauthClient,
+\t\t\t\t\t\tredirectTo: "/pages/account/settings",
+\t\t\t\t\t\tonSessionChange: () => ${
+ framework === "nextjs"
+ ? "frameworkRouter.refresh()"
+ : framework === "react-router"
+ ? "revalidator.revalidate()"
+ : "frameworkRouter.invalidate()"
+ },
+\t\t\t\t\t},
+\t\t\t\t\taccount: {
+\t\t\t\t\t\taccount: true,
+\t\t\t\t\t},`
+ : "",
+ embeddedOverrides,
+ ]
.filter(Boolean)
.join("\n"),
+ embeddedOverrides,
+ hasBetterAuthUi,
};
}
@@ -257,7 +291,9 @@ function buildAdapterTemplateContext(
const hasFormBuilder = selectedPlugins.includes("form-builder");
const hasMedia = selectedPlugins.includes("media");
const hasAiChat = selectedPlugins.includes("ai-chat");
- const needsIsolatedTransactions = hasFormBuilder || hasMedia || hasAiChat;
+ const hasKanban = selectedPlugins.includes("kanban");
+ const needsIsolatedTransactions =
+ hasFormBuilder || hasMedia || hasAiChat || hasKanban;
if (
(hasFormBuilder && (adapter === "memory" || adapter === "mongodb")) ||
@@ -388,6 +424,18 @@ export async function buildScaffoldPlan(
),
description: "BTST client stack configuration",
},
+ ...(pluginContext.hasBetterAuthUi
+ ? [
+ {
+ path: frameworkPaths.authClientPath,
+ content: await renderTemplate(
+ "shared/lib/auth-client.ts.hbs",
+ sharedContext,
+ ),
+ description: "Better Auth browser client for an existing endpoint",
+ },
+ ]
+ : []),
{
path: frameworkPaths.stackClientServerPath,
content: await renderTemplate(
diff --git a/playground/src/lib/plugin-selection.ts b/playground/src/lib/plugin-selection.ts
index e4373d040..daba88fec 100644
--- a/playground/src/lib/plugin-selection.ts
+++ b/playground/src/lib/plugin-selection.ts
@@ -7,6 +7,8 @@ export const PLAYGROUND_UNSUPPORTED_PLUGINS: Partial<
> = {
"form-builder":
"Requires a database adapter with isolated transactions, which the browser playground does not provide.",
+ "better-auth-ui":
+ "Requires an existing application-owned Better Auth endpoint, which the browser playground does not provide.",
};
/**