diff --git a/README.md b/README.md
index 1cb0856f..d2c77335 100644
--- a/README.md
+++ b/README.md
@@ -140,16 +140,12 @@ for QueryClient wiring, database adapters, all three frameworks, and auth.
## Database schemas & migrations
-Optional CLI to generate schemas and run migrations from enabled plugins:
+Generate schemas and run migrations through the v3 codegen CLI. It runs the
+aligned Better DB CLI in isolation, so its dependencies and `btst` binary do
+not enter your application graph:
```bash
-npm install -D @btst/cli
-```
-
-Generate drizzle schema:
-
-```bash
-npx @btst/cli generate --orm drizzle --config lib/stack.ts --output db/schema.ts
+npx @btst/codegen@next generate --orm drizzle --config lib/stack.ts --output db/schema.ts
```
Supports Prisma, Drizzle, MongoDB and Kysely SQL dialects.
diff --git a/docs/content/docs/cli.mdx b/docs/content/docs/cli.mdx
index f2c6f38c..2c918731 100644
--- a/docs/content/docs/cli.mdx
+++ b/docs/content/docs/cli.mdx
@@ -12,7 +12,9 @@ BTST has two CLI packages:
- `@btst/codegen` owns `init` scaffolding (`npx @btst/codegen init`)
- `@btst/cli` owns low-level DB schema generation and migrations
-`@btst/codegen generate` and `@btst/codegen migrate` are passthrough entrypoints to the existing `@btst/cli` flow.
+`@btst/codegen generate` and `@btst/codegen migrate` run the aligned
+`@btst/cli@2.2.3` release in isolation. This avoids adding its dependency graph
+or competing `btst` binary to the application.
## Init (Codegen)
@@ -60,7 +62,8 @@ npx @btst/codegen generate --orm=prisma --config=lib/stack.ts --output=schema.pr
npx @btst/codegen migrate --config=lib/stack.ts --database-url=postgres://...
```
-When a delegated command fails, fix the underlying issue and run the equivalent `npx @btst/cli ...` command directly.
+When a delegated command fails, fix the underlying issue and run the equivalent
+`npx @btst/cli@2.2.3 ...` command directly.
## About Better DB
@@ -68,27 +71,11 @@ BTST uses [Better DB (`@btst/db`)](https://github.com/better-stack-ai/better-aut
The CLI works with the `dbSchema` exported from your BTST configuration, which is built using Better DB's schema definition API. All plugin schemas are automatically merged into a unified schema that the CLI can process.
-Install the CLI as a dev dependency:
-
-
-
- ```bash
- npm install -D @btst/cli
- ```
-
-
-
- ```bash
- pnpm add -D @btst/cli
- ```
-
-
-
- ```bash
- yarn add -D @btst/cli
- ```
-
-
+For v3 applications, prefer the codegen passthrough commands above. If a v2
+application lists `@btst/cli` in its dependencies, remove it during migration;
+the pinned one-off CLI keeps Better DB dependencies from polluting the consumer
+graph. You can still invoke the low-level CLI directly with
+`npx @btst/cli@2.2.3`.
## Parameters
@@ -106,13 +93,13 @@ Generate database schemas for your ORM from your BTST `dbSchema`:
```bash
- npx @btst/cli generate --config=lib/stack.ts --orm=prisma --output=schema.prisma
+ npx @btst/cli@2.2.3 generate --config=lib/stack.ts --orm=prisma --output=schema.prisma
```
```bash
- npx @btst/cli generate --config=lib/stack.ts --orm=drizzle --output=src/db/schema.ts
+ npx @btst/cli@2.2.3 generate --config=lib/stack.ts --orm=drizzle --output=src/db/schema.ts
```
@@ -122,17 +109,17 @@ Generate database schemas for your ORM from your BTST `dbSchema`:
**Using DATABASE_URL environment variable:**
```bash
- DATABASE_URL=sqlite:./dev.db npx @btst/cli generate --config=lib/stack.ts --orm=kysely --output=migrations/schema.sql
+ DATABASE_URL=sqlite:./dev.db npx @btst/cli@2.2.3 generate --config=lib/stack.ts --orm=kysely --output=migrations/schema.sql
```
**Or using --database-url flag:**
```bash
- npx @btst/cli generate --config=lib/stack.ts --orm=kysely --output=migrations/schema.sql --database-url=sqlite:./dev.db
+ npx @btst/cli@2.2.3 generate --config=lib/stack.ts --orm=kysely --output=migrations/schema.sql --database-url=sqlite:./dev.db
```
```bash
- npx @btst/cli generate --config=lib/stack.ts --orm=kysely --output=migrations/schema.sql --database-url=postgres://user:pass@localhost:5432/db
+ npx @btst/cli@2.2.3 generate --config=lib/stack.ts --orm=kysely --output=migrations/schema.sql --database-url=postgres://user:pass@localhost:5432/db
```
@@ -145,17 +132,17 @@ Migrate your database schema directly (Kysely only). For Prisma and Drizzle, use
**Using DATABASE_URL environment variable:**
```bash
-DATABASE_URL=sqlite:./dev.db npx @btst/cli migrate --config=lib/stack.ts
+DATABASE_URL=sqlite:./dev.db npx @btst/cli@2.2.3 migrate --config=lib/stack.ts
```
**Or using --database-url flag:**
```bash
-npx @btst/cli migrate --config=lib/stack.ts --database-url=sqlite:./dev.db
+npx @btst/cli@2.2.3 migrate --config=lib/stack.ts --database-url=sqlite:./dev.db
```
```bash
-npx @btst/cli migrate --config=lib/stack.ts --database-url=postgres://user:pass@localhost:5432/db
+npx @btst/cli@2.2.3 migrate --config=lib/stack.ts --database-url=postgres://user:pass@localhost:5432/db
```
### Generate SQL to File
@@ -163,7 +150,7 @@ npx @btst/cli migrate --config=lib/stack.ts --database-url=postgres://user:pass@
Instead of running migrations directly, generate SQL to a file:
```bash
-npx @btst/cli migrate --config=lib/stack.ts --output=migrations.sql --database-url=sqlite:./dev.db
+npx @btst/cli@2.2.3 migrate --config=lib/stack.ts --output=migrations.sql --database-url=sqlite:./dev.db
```
## Gotchas
@@ -175,11 +162,11 @@ Because the CLI executes your config file to extract the `dbSchema`, there are a
- **Environment variables**: If your config file or its imports have conditional checks for available environment variables (e.g., checking if `process.env.SOME_VAR` exists), you should also pass those environment variables when running CLI commands:
```bash
-SOME_VAR=value npx @btst/cli generate --config=lib/stack.ts --orm=prisma --output=schema.prisma
+SOME_VAR=value npx @btst/cli@2.2.3 generate --config=lib/stack.ts --orm=prisma --output=schema.prisma
```
or using dotenv-cli:
```bash
-npx dotenv-cli -e .env.local -- npx @btst/cli generate --orm drizzle --config lib/stack.ts --output db/btst-schema.ts
+npx dotenv-cli -e .env.local -- npx @btst/cli@2.2.3 generate --orm drizzle --config lib/stack.ts --output db/btst-schema.ts
```
diff --git a/docs/content/docs/databases/adapters.mdx b/docs/content/docs/databases/adapters.mdx
index 47615db8..b06a2305 100644
--- a/docs/content/docs/databases/adapters.mdx
+++ b/docs/content/docs/databases/adapters.mdx
@@ -14,7 +14,7 @@ BTST consists of separate npm packages under the `@btst` namespace:
- **`@btst/stack`** - Core package (install this first)
- **`@btst/adapter-*`** - Database adapters (install one based on your ORM)
-- **`@btst/cli`** - CLI tools for schema generation (dev dependency)
+- **`@btst/cli`** - schema tooling invoked in isolation through `@btst/codegen`
- **`@btst/db`** - Internal database abstraction layer (installed as a dependency of other packages)
See the [Installation guide](/installation) for setup instructions.
diff --git a/docs/content/docs/plugins/better-auth-ui.mdx b/docs/content/docs/plugins/better-auth-ui.mdx
index 7d4d3b27..f87b6dd0 100644
--- a/docs/content/docs/plugins/better-auth-ui.mdx
+++ b/docs/content/docs/plugins/better-auth-ui.mdx
@@ -56,20 +56,64 @@ Before starting, ensure you have:
- A database adapter (e.g., Drizzle with `@btst/adapter-drizzle`)
-### 1. Install the Package
+### 1. Install the v3 RC Dependency Cohort
```bash
-pnpm add @btst/better-auth-ui
+pnpm add --save-exact \
+ @btst/stack@next \
+ @btst/yar@1.3.2 \
+ @btst/adapter-drizzle@2.2.3 \
+ @btst/better-auth-ui@2.0.0-rc.1 \
+ @tanstack/react-query@5.100.14 \
+ better-auth@1.6.16 \
+ @better-auth/core@1.6.16 \
+ @better-auth/api-key@1.6.16 \
+ @better-auth/drizzle-adapter@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 \
+ drizzle-orm@0.45.2
```
-Or with npm/yarn:
+This exact cohort keeps BTST, Better Auth, and Drizzle on one copy of their
+shared types. If you use another database, replace the two Drizzle packages
+with the matching `@btst/adapter-*` `2.2.3` and `@better-auth/*-adapter`
+`1.6.16` packages. The BTST initializer installs this aligned set when Better
+Auth UI is selected.
+
+If you are upgrading from v2, remove a locally installed `@btst/cli`. The v3
+codegen commands run the aligned `@btst/cli@2.2.3` release in isolation, which
+keeps its database tooling and `btst` binary out of the application dependency
+graph:
```bash
-npm install @btst/better-auth-ui
-# or
-yarn add @btst/better-auth-ui
+pnpm remove @btst/cli
```
+With npm, use the same exact versions:
+
+```bash
+npm install --save-exact \
+ @btst/stack@next \
+ @btst/yar@1.3.2 \
+ @btst/adapter-drizzle@2.2.3 \
+ @btst/better-auth-ui@2.0.0-rc.1 \
+ @tanstack/react-query@5.100.14 \
+ better-auth@1.6.16 \
+ @better-auth/core@1.6.16 \
+ @better-auth/api-key@1.6.16 \
+ @better-auth/drizzle-adapter@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 \
+ drizzle-orm@0.45.2
+```
+
+For an npm-based v2 application, use `npm uninstall @btst/cli` during the
+migration.
+
### 2. Configure the Stack Client
Import and register the auth plugins in your `stack-client.tsx` file:
@@ -126,8 +170,9 @@ Configure the plugin overrides in your catch-all layout file. The `auth` overrid
```tsx title="app/p/layout.tsx"
"use client"
- import { StackProvider, type StackAuthProvider } from "@btst/stack/context"
+ import { StackProvider } from "@btst/stack/context"
import { nextRouter } from "@btst/stack/next"
+ import { createBetterAuthProvider } from "@btst/better-auth-ui"
import type {
AuthPluginOverrides,
AccountPluginOverrides,
@@ -142,15 +187,9 @@ Configure the plugin overrides in your catch-all layout file. The `auth` overrid
organization: OrganizationPluginOverrides
}
- const stackAuth = {
- getIdentity: async () => {
- const { data } = await authClient.getSession()
- return data?.user ?? null
- },
+ const stackAuth = createBetterAuthProvider(authClient, {
loginPath: "/p/auth/sign-in",
- can: ({ resource, action, identity }) =>
- Boolean(identity && authorize(identity, resource, action)),
- } satisfies StackAuthProvider
+ })
export default function PagesLayout({ children }: { children: ReactNode }) {
// Better Auth UI-specific configuration shared by its three plugins
@@ -277,10 +316,10 @@ The exact sub-paths come from the view paths constants in the library and match
### Auth Plugin (`AuthPluginOverrides`)
-`Link`, `navigate`, and `replace` in this table are optional APIs of the
-external `@btst/better-auth-ui` package. They are not the removed built-in BTST
-override fields; the recommended setup above uses `StackProvider.router` and
-the package defaults instead.
+The standalone upstream UI provider still supports its own navigation
+overrides. BTST plugin overrides intentionally omit those fields: the bridge
+reads navigation, notifications, localization, and session refresh from the
+top-level `StackProvider` instead.
| Option | Type | Default | Description |
|--------|------|---------|-------------|
@@ -313,11 +352,6 @@ the package defaults instead.
| `optimistic` | `boolean` | `false` | Optimistic user updates |
| `hooks` | `Partial` | — | Custom data fetching hooks |
| `mutators` | `Partial` | — | Custom mutation handlers |
-| `Link` | `Link` | `` | Custom link component |
-| `navigate` | `(href: string) => void` | `location.href` | Navigation function |
-| `replace` | `(href: string) => void` | `navigate` | Replace navigation |
-| `toast` | `RenderToast` | Sonner | Custom toast renderer |
-| `onSessionChange` | `() => void` | — | Session change callback |
| `onRouteError` | `(name, error, ctx) => void` | — | Route error callback |
| `pageProps` | See [Per-Page Props](#per-page-props) | — | Per-page className/classNames/localization |
diff --git a/packages/cli/scripts/test-init.sh b/packages/cli/scripts/test-init.sh
index 59d27744..74f3b884 100644
--- a/packages/cli/scripts/test-init.sh
+++ b/packages/cli/scripts/test-init.sh
@@ -125,26 +125,21 @@ STACK_PEERS=$(node -e 'const fs=require("fs");const p=JSON.parse(fs.readFileSync
# installInitDependencies() in package-installer.ts does at runtime.
PLUGIN_EXTRA_PACKAGES=$(node -e '
const { PLUGINS } = require("./node_modules/@btst/codegen/dist/lib.cjs");
-const extras = PLUGINS.flatMap(p => p.extraPackages || []);
+const extras = PLUGINS.flatMap(p => p.extraInstallSpecs || p.extraPackages || []);
process.stdout.write([...new Set(extras)].join(" "));
')
# Install adapter, plugin extras (includes @btst/better-auth-ui, better-auth, and any
# plugin-specific deps like @ai-sdk/openai + ai), and @btst/stack peers.
# next-themes is generated by shadcn init (mode-toggle.tsx, sonner.tsx) but not auto-installed.
-# @btst/better-auth-ui must be present before the next step for peer resolution.
-npm install @btst/adapter-memory next-themes $PLUGIN_EXTRA_PACKAGES $STACK_PEERS --legacy-peer-deps
-BETTER_AUTH_UI_PEERS=$(node -e '
-const fs=require("fs");
-const p=JSON.parse(fs.readFileSync("node_modules/@btst/better-auth-ui/package.json","utf8"));
-const skip=new Set(["react","react-dom","tailwindcss","@btst/stack","@btst/yar","better-auth","@tanstack/react-query"]);
-const optionalPrefixes=["@triplit","@instantdb","@daveyplate"];
-const keys=Object.keys(p.peerDependencies||{}).filter(d=>!skip.has(d)&&!optionalPrefixes.some(pre=>d.startsWith(pre)));
-process.stdout.write(keys.join(" "));
-')
-if [ -n "$BETTER_AUTH_UI_PEERS" ]; then
- npm install $BETTER_AUTH_UI_PEERS --legacy-peer-deps
-fi
-success "Installed runtime deps (adapter + plugin extras + @btst/stack and @btst/better-auth-ui peers)"
+# Re-enable strict peer resolution here so this fixture catches incompatible cohorts.
+rm .npmrc
+npm install --save-exact @btst/adapter-memory@2.2.3 next-themes $PLUGIN_EXTRA_PACKAGES $STACK_PEERS
+success "Installed aligned runtime deps with strict peer resolution"
+
+BTST_CLI_VERSION=$(npx --yes @btst/cli@2.2.3 --version)
+test "$BTST_CLI_VERSION" = "2.2.3"
+test ! -e node_modules/@btst/cli
+success "Ran @btst/cli@2.2.3 without adding it to the consumer graph"
step "Asserting generated files and patches"
test -f "lib/stack.ts"
diff --git a/packages/cli/src/templates/nextjs/pages-layout.tsx.hbs b/packages/cli/src/templates/nextjs/pages-layout.tsx.hbs
index 333c8992..367da54c 100644
--- a/packages/cli/src/templates/nextjs/pages-layout.tsx.hbs
+++ b/packages/cli/src/templates/nextjs/pages-layout.tsx.hbs
@@ -7,12 +7,11 @@ import { ChatLayout } from "@btst/stack/plugins/ai-chat/client"
{{/if}}
import { QueryClientProvider } from "@tanstack/react-query"
{{#if hasBetterAuthUi}}
-import { useRouter{{#if hasAiChat}}, usePathname{{/if}} } from "next/navigation"
-{{else}}
+import { createBetterAuthProvider } from "@btst/better-auth-ui"
+{{/if}}
{{#if hasAiChat}}
import { usePathname } from "next/navigation"
{{/if}}
-{{/if}}
import { getOrCreateQueryClient } from "{{alias}}lib/query-client"
function getBaseURL() {
@@ -27,14 +26,19 @@ function getBaseURL() {
return "http://localhost:3000"
}
+{{#if hasBetterAuthUi}}
+// TODO: replace this placeholder with your Better Auth client import.
+const authClient = undefined as any
+const stackAuth = createBetterAuthProvider(authClient, {
+ loginPath: "/pages/auth/sign-in",
+})
+
+{{/if}}
export default function BtstPagesLayout({
children,
}: {
children: React.ReactNode
}) {
-{{#if hasBetterAuthUi}}
- const router = useRouter()
-{{/if}}
const queryClient = getOrCreateQueryClient()
{{#if hasAiChat}}
const hasApiKey = typeof process !== "undefined" && !!process.env.NEXT_PUBLIC_HAS_OPENAI_KEY
@@ -49,6 +53,9 @@ export default function BtstPagesLayout({
basePath="/pages"
router={nextRouter()}
api={{{providerApiLiteral}}}
+{{#if hasBetterAuthUi}}
+ auth={stackAuth}
+{{/if}}
{{#if pagesLayoutOverrides}}
overrides={
{
diff --git a/packages/cli/src/templates/react-router/pages-layout.tsx.hbs b/packages/cli/src/templates/react-router/pages-layout.tsx.hbs
index 8edf2902..27853abb 100644
--- a/packages/cli/src/templates/react-router/pages-layout.tsx.hbs
+++ b/packages/cli/src/templates/react-router/pages-layout.tsx.hbs
@@ -4,7 +4,10 @@ import { reactRouter } from "@btst/stack/react-router"
import { ChatLayout } from "@btst/stack/plugins/ai-chat/client"
{{/if}}
import { QueryClientProvider } from "@tanstack/react-query"
-import { Outlet{{#if hasBetterAuthUi}}, useNavigate{{/if}}{{#if hasAiChat}}, useLocation{{/if}} } from "react-router"
+{{#if hasBetterAuthUi}}
+import { createBetterAuthProvider } from "@btst/better-auth-ui"
+{{/if}}
+import { Outlet{{#if hasAiChat}}, useLocation{{/if}} } from "react-router"
import { getOrCreateQueryClient } from "{{alias}}lib/query-client"
function getBaseURL() {
@@ -19,10 +22,15 @@ function getBaseURL() {
return "http://localhost:5173"
}
-export default function BtstPagesLayout() {
{{#if hasBetterAuthUi}}
- const navigate = useNavigate()
+// TODO: replace this placeholder with your Better Auth client import.
+const authClient = undefined as any
+const stackAuth = createBetterAuthProvider(authClient, {
+ loginPath: "/pages/auth/sign-in",
+})
+
{{/if}}
+export default function BtstPagesLayout() {
const queryClient = getOrCreateQueryClient()
{{#if hasAiChat}}
const hasApiKey = !!import.meta.env.VITE_HAS_OPENAI_KEY
@@ -37,6 +45,9 @@ export default function BtstPagesLayout() {
basePath="/pages"
router={reactRouter()}
api={{{providerApiLiteral}}}
+{{#if hasBetterAuthUi}}
+ auth={stackAuth}
+{{/if}}
{{#if pagesLayoutOverrides}}
overrides={
{
diff --git a/packages/cli/src/templates/tanstack/pages-layout.tsx.hbs b/packages/cli/src/templates/tanstack/pages-layout.tsx.hbs
index 5d2c3a71..53677f16 100644
--- a/packages/cli/src/templates/tanstack/pages-layout.tsx.hbs
+++ b/packages/cli/src/templates/tanstack/pages-layout.tsx.hbs
@@ -1,10 +1,13 @@
-import { createFileRoute, Outlet{{#if hasBetterAuthUi}}, useNavigate{{/if}}{{#if hasAiChat}}, useLocation{{/if}} } from "@tanstack/react-router"
+import { createFileRoute, Outlet{{#if hasAiChat}}, useLocation{{/if}} } from "@tanstack/react-router"
import { StackProvider } from "@btst/stack/context"
import { tanstackRouter } from "@btst/stack/tanstack"
{{#if hasAiChat}}
import { ChatLayout } from "@btst/stack/plugins/ai-chat/client"
{{/if}}
import { QueryClientProvider } from "@tanstack/react-query"
+{{#if hasBetterAuthUi}}
+import { createBetterAuthProvider } from "@btst/better-auth-ui"
+{{/if}}
import { getOrCreateQueryClient } from "{{alias}}lib/query-client"
export const Route = createFileRoute("/pages")({
@@ -23,10 +26,15 @@ function getBaseURL() {
return "http://localhost:3000"
}
-function BtstPagesLayout() {
{{#if hasBetterAuthUi}}
- const navigate = useNavigate()
+// TODO: replace this placeholder with your Better Auth client import.
+const authClient = undefined as any
+const stackAuth = createBetterAuthProvider(authClient, {
+ loginPath: "/pages/auth/sign-in",
+})
+
{{/if}}
+function BtstPagesLayout() {
const queryClient = getOrCreateQueryClient()
{{#if hasAiChat}}
const hasApiKey = !!import.meta.env.VITE_HAS_OPENAI_KEY
@@ -41,6 +49,9 @@ function BtstPagesLayout() {
basePath="/pages"
router={tanstackRouter()}
api={{{providerApiLiteral}}}
+{{#if hasBetterAuthUi}}
+ auth={stackAuth}
+{{/if}}
{{#if pagesLayoutOverrides}}
overrides={
{
diff --git a/packages/cli/src/types.ts b/packages/cli/src/types.ts
index 8c2c89a1..cf26e0b5 100644
--- a/packages/cli/src/types.ts
+++ b/packages/cli/src/types.ts
@@ -41,4 +41,5 @@ export interface ScaffoldPlan {
pagesLayoutPath?: string;
cssImports: string[];
extraPackages: string[];
+ extraPackageVersions?: Record;
}
diff --git a/packages/cli/src/utils/__tests__/package-installer.test.ts b/packages/cli/src/utils/__tests__/package-installer.test.ts
new file mode 100644
index 00000000..77c17717
--- /dev/null
+++ b/packages/cli/src/utils/__tests__/package-installer.test.ts
@@ -0,0 +1,58 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+const { execa } = vi.hoisted(() => ({ execa: vi.fn() }));
+
+vi.mock("execa", () => ({ execa }));
+
+import { installInitDependencies } from "../package-installer";
+
+describe("installInitDependencies", () => {
+ beforeEach(() => {
+ execa.mockReset();
+ execa.mockResolvedValue({});
+ });
+
+ it("installs the coherent v3 auth and Drizzle release cohort", async () => {
+ await installInitDependencies({
+ cwd: "/tmp/example",
+ packageManager: "pnpm",
+ adapter: "drizzle",
+ plugins: ["better-auth-ui"],
+ });
+
+ expect(execa).toHaveBeenCalledWith(
+ "pnpm",
+ [
+ "add",
+ "@btst/stack@next",
+ "@btst/yar@1.3.2",
+ "@tanstack/react-query@5.100.14",
+ "@btst/adapter-drizzle@2.2.3",
+ "drizzle-orm@0.45.2",
+ "@btst/better-auth-ui@2.0.0-rc.1",
+ "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",
+ "@better-auth/drizzle-adapter@1.6.16",
+ ],
+ { cwd: "/tmp/example", stdio: "inherit" },
+ );
+ expect(execa).toHaveBeenCalledTimes(1);
+ });
+
+ it("saves npm runtime versions exactly", async () => {
+ await installInitDependencies({
+ cwd: "/tmp/example",
+ packageManager: "npm",
+ adapter: "memory",
+ plugins: [],
+ });
+
+ expect(execa.mock.calls[0]?.[1]).toContain("--save-exact");
+ expect(execa).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/packages/cli/src/utils/__tests__/passthrough.test.ts b/packages/cli/src/utils/__tests__/passthrough.test.ts
new file mode 100644
index 00000000..27b65999
--- /dev/null
+++ b/packages/cli/src/utils/__tests__/passthrough.test.ts
@@ -0,0 +1,30 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+const { execa } = vi.hoisted(() => ({ execa: vi.fn() }));
+
+vi.mock("execa", () => ({ execa }));
+
+import { runCliPassthrough } from "../passthrough";
+
+describe("runCliPassthrough", () => {
+ beforeEach(() => {
+ execa.mockReset();
+ execa.mockResolvedValue({});
+ });
+
+ it("runs the aligned Better DB CLI outside the consumer dependency graph", async () => {
+ await expect(
+ runCliPassthrough({
+ cwd: "/tmp/example",
+ command: "generate",
+ args: ["--orm=drizzle"],
+ }),
+ ).resolves.toBe(0);
+
+ expect(execa).toHaveBeenCalledWith(
+ "npx",
+ ["--yes", "@btst/cli@2.2.3", "generate", "--orm=drizzle"],
+ { cwd: "/tmp/example", stdio: "inherit" },
+ );
+ });
+});
diff --git a/packages/cli/src/utils/__tests__/scaffold-plan.test.ts b/packages/cli/src/utils/__tests__/scaffold-plan.test.ts
index cb3e078f..db2adaa9 100644
--- a/packages/cli/src/utils/__tests__/scaffold-plan.test.ts
+++ b/packages/cli/src/utils/__tests__/scaffold-plan.test.ts
@@ -392,17 +392,26 @@ describe("scaffold plan", () => {
// No apiBaseURL/apiBasePath in better-auth-ui client entries
expect(stackClientFile?.content).not.toContain('apiBasePath: "/api/data"');
- // Pages layout overrides — three blocks with authClient placeholder
- expect(pagesLayoutFile?.content).toContain("authClient: undefined as any");
- expect(pagesLayoutFile?.content).toContain('basePath: "/pages/auth"');
- expect(pagesLayoutFile?.content).toContain('basePath: "/pages/account"');
- expect(pagesLayoutFile?.content).toContain('basePath: "/pages/org"');
+ // The provider owns identity while the three plugin overrides share its client.
+ expect(pagesLayoutFile?.content).toContain(
+ 'import { createBetterAuthProvider } from "@btst/better-auth-ui"',
+ );
+ expect(pagesLayoutFile?.content).toContain(
+ "const authClient = undefined as any",
+ );
expect(pagesLayoutFile?.content).toContain(
- "replace: (path: string) => router.replace(path)",
+ "const stackAuth = createBetterAuthProvider(authClient, {",
);
expect(pagesLayoutFile?.content).toContain(
- "onSessionChange: () => router.refresh()",
+ 'loginPath: "/pages/auth/sign-in"',
);
+ expect(pagesLayoutFile?.content).toContain("auth={stackAuth}");
+ expect(pagesLayoutFile?.content).toContain("authClient,");
+ expect(pagesLayoutFile?.content).toContain('basePath: "/pages/auth"');
+ expect(pagesLayoutFile?.content).toContain('basePath: "/pages/account"');
+ expect(pagesLayoutFile?.content).toContain('basePath: "/pages/org"');
+ expect(pagesLayoutFile?.content).not.toContain("replace: (path: string)");
+ expect(pagesLayoutFile?.content).not.toContain("onSessionChange:");
});
it("does not include apiBaseURL/apiBasePath in better-auth-ui client entries when mixed with other plugins", async () => {
@@ -547,37 +556,33 @@ describe("scaffold plan", () => {
expect(layoutFile?.content).not.toContain("router.replace");
});
- it("uses window.location.reload for onSessionChange in react-router better-auth-ui", async () => {
- const plan = await buildScaffoldPlan({
- framework: "react-router",
- adapter: "memory",
- plugins: ["better-auth-ui"],
- alias: "~/",
- cssFile: "app/app.css",
- });
-
- const layoutFile = plan.files.find((f) => f.path.endsWith("_layout.tsx"));
- expect(layoutFile?.content).toContain("window.location.reload()");
- expect(layoutFile?.content).not.toContain("router.refresh()");
- expect(layoutFile?.content).toContain("navigate(path, { replace: true })");
- });
-
- it("uses window.location.reload for onSessionChange in tanstack better-auth-ui", async () => {
- const plan = await buildScaffoldPlan({
- framework: "tanstack",
- adapter: "memory",
- plugins: ["better-auth-ui"],
- alias: "@/",
- cssFile: "src/styles/globals.css",
- });
+ it.each(["nextjs", "react-router", "tanstack"] as const)(
+ "uses the top-level auth provider in %s better-auth-ui scaffolds",
+ async (framework) => {
+ const plan = await buildScaffoldPlan({
+ framework,
+ adapter: "memory",
+ plugins: ["better-auth-ui"],
+ alias: framework === "react-router" ? "~/" : "@/",
+ cssFile:
+ framework === "nextjs"
+ ? "app/globals.css"
+ : framework === "react-router"
+ ? "app/app.css"
+ : "src/styles/globals.css",
+ });
- const layoutFile = plan.files.find((f) => f.path.endsWith("route.tsx"));
- expect(layoutFile?.content).toContain("window.location.reload()");
- expect(layoutFile?.content).not.toContain("router.refresh()");
- expect(layoutFile?.content).toContain(
- "navigate({ to: path, replace: true })",
- );
- });
+ const layoutFile = plan.files.find(
+ (file) => file.path === plan.pagesLayoutPath,
+ );
+ expect(layoutFile?.content).toContain(
+ 'import { createBetterAuthProvider } from "@btst/better-auth-ui"',
+ );
+ expect(layoutFile?.content).toContain("auth={stackAuth}");
+ expect(layoutFile?.content).not.toContain("onSessionChange:");
+ expect(layoutFile?.content).not.toContain("replace: (path: string)");
+ },
+ );
it("includes better-auth-ui in the PLUGINS registry", () => {
const allKeys = PLUGINS.map((p) => p.key);
@@ -1084,6 +1089,25 @@ describe("scaffold plan", () => {
expect(plan.extraPackages.length).toBe(new Set(plan.extraPackages).size);
});
+ it("keeps Better Auth package names separate from their v3 install versions", async () => {
+ const plan = await buildScaffoldPlan({
+ framework: "nextjs",
+ adapter: "memory",
+ plugins: ["better-auth-ui"],
+ alias: "@/",
+ cssFile: "app/globals.css",
+ });
+
+ expect(plan.extraPackages).toContain("@btst/better-auth-ui");
+ expect(plan.extraPackages).not.toContain("@btst/better-auth-ui@2.0.0-rc.1");
+ expect(plan.extraPackageVersions).toMatchObject({
+ "@btst/better-auth-ui": "2.0.0-rc.1",
+ "better-auth": "1.6.16",
+ "@better-auth/core": "1.6.16",
+ "better-call": "1.3.6",
+ });
+ });
+
it("returns empty cssImports and extraPackages when no plugins selected", async () => {
const plan = await buildScaffoldPlan({
framework: "nextjs",
diff --git a/packages/cli/src/utils/constants.ts b/packages/cli/src/utils/constants.ts
index dfcb9000..5404cfb4 100644
--- a/packages/cli/src/utils/constants.ts
+++ b/packages/cli/src/utils/constants.ts
@@ -4,9 +4,13 @@ export interface AdapterMeta {
key: Adapter;
label: string;
packageName: string;
+ installSpec?: string;
+ betterAuthInstallSpec?: string;
ormForGenerate?: "prisma" | "drizzle" | "kysely";
- /** Additional npm packages that must be installed when this adapter is selected. */
+ /** Additional package names required when this adapter is selected. */
extraPackages?: string[];
+ /** Version-qualified forms of extraPackages used by the installer. */
+ extraInstallSpecs?: string[];
}
export interface PluginMeta {
@@ -18,8 +22,10 @@ export interface PluginMeta {
clientImportPath?: string;
clientSymbol?: string;
configKey: string;
- /** Additional npm packages that must be installed when this plugin is selected. */
+ /** Additional package names required when this plugin is selected. */
extraPackages?: string[];
+ /** Version-qualified forms of extraPackages used by the installer. */
+ extraInstallSpecs?: string[];
/** Whether this plugin has sample seed data available for the playground. */
hasSeedData?: boolean;
}
@@ -29,11 +35,15 @@ export const ADAPTERS: readonly AdapterMeta[] = [
key: "memory",
label: "Memory (local dev / testing)",
packageName: "@btst/adapter-memory",
+ installSpec: "@btst/adapter-memory@2.2.3",
+ betterAuthInstallSpec: "@better-auth/memory-adapter@1.6.16",
},
{
key: "prisma",
label: "Prisma",
packageName: "@btst/adapter-prisma",
+ installSpec: "@btst/adapter-prisma@2.2.3",
+ betterAuthInstallSpec: "@better-auth/prisma-adapter@1.6.16",
ormForGenerate: "prisma",
extraPackages: ["@prisma/adapter-pg", "pg"],
},
@@ -41,18 +51,26 @@ export const ADAPTERS: readonly AdapterMeta[] = [
key: "drizzle",
label: "Drizzle",
packageName: "@btst/adapter-drizzle",
+ installSpec: "@btst/adapter-drizzle@2.2.3",
+ betterAuthInstallSpec: "@better-auth/drizzle-adapter@1.6.16",
ormForGenerate: "drizzle",
+ extraPackages: ["drizzle-orm"],
+ extraInstallSpecs: ["drizzle-orm@0.45.2"],
},
{
key: "kysely",
label: "Kysely",
packageName: "@btst/adapter-kysely",
+ installSpec: "@btst/adapter-kysely@2.2.3",
+ betterAuthInstallSpec: "@better-auth/kysely-adapter@1.6.16",
ormForGenerate: "kysely",
},
{
key: "mongodb",
label: "MongoDB",
packageName: "@btst/adapter-mongodb",
+ installSpec: "@btst/adapter-mongodb@2.2.3",
+ betterAuthInstallSpec: "@better-auth/mongo-adapter@1.6.16",
},
];
@@ -153,7 +171,26 @@ export const PLUGINS: readonly PluginMeta[] = [
clientImportPath: "@btst/better-auth-ui/client",
clientSymbol: "authClientPlugin",
configKey: "auth",
- extraPackages: ["@btst/better-auth-ui", "better-auth"],
+ extraPackages: [
+ "@btst/better-auth-ui",
+ "better-auth",
+ "@better-auth/core",
+ "@better-auth/api-key",
+ "@better-auth/passkey",
+ "@better-auth/utils",
+ "@better-fetch/fetch",
+ "better-call",
+ ],
+ extraInstallSpecs: [
+ "@btst/better-auth-ui@2.0.0-rc.1",
+ "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",
+ ],
},
{
key: "route-docs",
diff --git a/packages/cli/src/utils/package-installer.ts b/packages/cli/src/utils/package-installer.ts
index cf9b7b3d..0783bcc5 100644
--- a/packages/cli/src/utils/package-installer.ts
+++ b/packages/cli/src/utils/package-installer.ts
@@ -7,12 +7,21 @@ function getInstallCommand(
packages: string[],
): { command: string; args: string[] } {
if (packageManager === "pnpm") {
- return { command: "pnpm", args: ["add", ...packages] };
+ return {
+ command: "pnpm",
+ args: ["add", ...packages],
+ };
}
if (packageManager === "yarn") {
- return { command: "yarn", args: ["add", ...packages] };
+ return {
+ command: "yarn",
+ args: ["add", ...packages],
+ };
}
- return { command: "npm", args: ["install", ...packages] };
+ return {
+ command: "npm",
+ args: ["install", "--save-exact", ...packages],
+ };
}
export async function installInitDependencies(input: {
@@ -31,16 +40,20 @@ export async function installInitDependencies(input: {
const pluginExtraPackages = input.plugins.flatMap((key) => {
const meta = PLUGINS.find((p) => p.key === key);
- return meta?.extraPackages ?? [];
+ return meta?.extraInstallSpecs ?? meta?.extraPackages ?? [];
});
const packages = [
- "@btst/stack",
- "@btst/yar",
- "@tanstack/react-query",
- adapterMeta.packageName,
- ...(adapterMeta.extraPackages ?? []),
+ "@btst/stack@next",
+ "@btst/yar@1.3.2",
+ "@tanstack/react-query@5.100.14",
+ adapterMeta.installSpec ?? adapterMeta.packageName,
+ ...(adapterMeta.extraInstallSpecs ?? adapterMeta.extraPackages ?? []),
...pluginExtraPackages,
+ ...(input.plugins.includes("better-auth-ui") &&
+ adapterMeta.betterAuthInstallSpec
+ ? [adapterMeta.betterAuthInstallSpec]
+ : []),
];
const { command, args } = getInstallCommand(input.packageManager, packages);
await execa(command, args, { cwd: input.cwd, stdio: "inherit" });
diff --git a/packages/cli/src/utils/passthrough.ts b/packages/cli/src/utils/passthrough.ts
index 41fec26c..e3e4da83 100644
--- a/packages/cli/src/utils/passthrough.ts
+++ b/packages/cli/src/utils/passthrough.ts
@@ -2,6 +2,8 @@ import { execa } from "execa";
import { ADAPTERS } from "./constants";
import type { Adapter } from "../types";
+const BETTER_DB_CLI_SPEC = "@btst/cli@2.2.3";
+
export function adapterNeedsGenerate(adapter: Adapter): boolean {
if (adapter === "memory") return false;
return Boolean(ADAPTERS.find((item) => item.key === adapter)?.ormForGenerate);
@@ -34,7 +36,12 @@ export async function runCliPassthrough(input: {
command: "generate" | "migrate";
args: string[];
}): Promise {
- const effectiveCommand = ["@btst/cli", input.command, ...input.args];
+ const effectiveCommand = [
+ "--yes",
+ BETTER_DB_CLI_SPEC,
+ input.command,
+ ...input.args,
+ ];
console.log(`Delegating to: npx ${effectiveCommand.join(" ")}`);
try {
await execa("npx", effectiveCommand, {
@@ -44,7 +51,7 @@ export async function runCliPassthrough(input: {
return 0;
} catch (error) {
console.error(
- `Delegated ${input.command} failed. Resolve the error, then run npx @btst/cli ${input.command} ... again.`,
+ `Delegated ${input.command} failed. Resolve the error, then run npx --yes ${BETTER_DB_CLI_SPEC} ${input.command} ... again.`,
);
return 1;
}
diff --git a/packages/cli/src/utils/scaffold-plan.ts b/packages/cli/src/utils/scaffold-plan.ts
index 85bfdb09..34a9f217 100644
--- a/packages/cli/src/utils/scaffold-plan.ts
+++ b/packages/cli/src/utils/scaffold-plan.ts
@@ -60,17 +60,6 @@ function getPublicSiteURLVar(framework: Framework) {
return "VITE_PUBLIC_SITE_URL";
}
-function getReplaceExpr(framework: Framework): string {
- if (framework === "nextjs") return "router.replace(path)";
- if (framework === "react-router") return "navigate(path, { replace: true })";
- return "navigate({ to: path, replace: true })";
-}
-
-function getSessionChangeExpr(framework: Framework): string {
- if (framework === "nextjs") return "router.refresh()";
- return "window.location.reload()";
-}
-
function getPagesLayoutFilePath(framework: Framework): string {
if (framework === "nextjs") return "app/pages/layout.tsx";
if (framework === "react-router") return "app/routes/pages/_layout.tsx";
@@ -212,28 +201,20 @@ function buildPluginTemplateContext(
if (m.key === "route-docs") {
return "";
}
- const rep = getReplaceExpr(framework);
- const ses = getSessionChangeExpr(framework);
const layoutFile = getPagesLayoutFilePath(framework);
if (m.key === "better-auth-ui") {
return `\t\t\t\t\tauth: {
-\t\t\t\t\t\tauthClient: undefined as any,
-\t\t\t\t\t\treplace: (path: string) => ${rep},
-\t\t\t\t\t\tonSessionChange: () => ${ses},
+\t\t\t\t\t\tauthClient,
\t\t\t\t\t\tbasePath: "/pages/auth",
\t\t\t\t\t\tredirectTo: "/pages/account/settings",
\t\t\t\t\t},
\t\t\t\t\taccount: {
-\t\t\t\t\t\tauthClient: undefined as any,
-\t\t\t\t\t\treplace: (path: string) => ${rep},
-\t\t\t\t\t\tonSessionChange: () => ${ses},
+\t\t\t\t\t\tauthClient,
\t\t\t\t\t\tbasePath: "/pages/account",
\t\t\t\t\t\taccount: { fields: ["image", "name"] },
\t\t\t\t\t},
\t\t\t\t\torganization: {
-\t\t\t\t\t\tauthClient: undefined as any,
-\t\t\t\t\t\treplace: (path: string) => ${rep},
-\t\t\t\t\t\tonSessionChange: () => ${ses},
+\t\t\t\t\t\tauthClient,
\t\t\t\t\t\tbasePath: "/pages/org",
\t\t\t\t\t\torganization: { basePath: "/pages/org" },
\t\t\t\t\t},`;
@@ -647,6 +628,18 @@ export async function buildScaffoldPlan(
),
),
);
+ const extraPackageVersions = Object.fromEntries(
+ PLUGINS.filter((plugin) => input.plugins.includes(plugin.key)).flatMap(
+ (plugin) =>
+ (plugin.extraInstallSpecs ?? []).map((spec) => {
+ const versionSeparator = spec.lastIndexOf("@");
+ return [
+ spec.slice(0, versionSeparator),
+ spec.slice(versionSeparator + 1),
+ ];
+ }),
+ ),
+ );
return {
files,
@@ -655,5 +648,6 @@ export async function buildScaffoldPlan(
pagesLayoutPath: frameworkPaths.pagesLayoutPath,
cssImports,
extraPackages,
+ extraPackageVersions,
};
}
diff --git a/packages/stack/package.json b/packages/stack/package.json
index a0fb7a92..106ca7cc 100644
--- a/packages/stack/package.json
+++ b/packages/stack/package.json
@@ -833,7 +833,7 @@
}
},
"dependencies": {
- "@btst/db": "2.2.2",
+ "@btst/db": "2.2.3",
"@milkdown/crepe": "^7.17.1",
"@milkdown/kit": "^7.17.1",
"remend": "^1.0.1",
@@ -843,7 +843,7 @@
"@ai-sdk/react": ">=2.0.0",
"@aws-sdk/client-s3": ">=3.0.0",
"@aws-sdk/s3-request-presigner": ">=3.0.0",
- "@btst/yar": ">=1.3.0",
+ "@btst/yar": ">=1.3.2",
"@hookform/resolvers": ">=5.0.0",
"@radix-ui/react-dialog": ">=1.1.0",
"@radix-ui/react-label": ">=2.1.0",
@@ -854,7 +854,7 @@
"@tanstack/react-router": ">=1.0.0",
"@vercel/blob": ">=0.14.0",
"ai": ">=5.0.0",
- "better-call": ">=1.3.5",
+ "better-call": "1.3.6",
"class-variance-authority": ">=0.7.0",
"clsx": ">=2.1.0",
"cmdk": ">=1.1.0",
@@ -903,8 +903,8 @@
"@ai-sdk/react": "^2.0.94",
"@aws-sdk/client-s3": "^3.1011.0",
"@aws-sdk/s3-request-presigner": "^3.1011.0",
- "@btst/adapter-memory": "2.2.2",
- "@btst/yar": "1.3.0",
+ "@btst/adapter-memory": "2.2.3",
+ "@btst/yar": "1.3.2",
"@tanstack/react-router": "1.168.10",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
diff --git a/packages/stack/scripts/test-registry.sh b/packages/stack/scripts/test-registry.sh
index 2f356c5d..da423914 100755
--- a/packages/stack/scripts/test-registry.sh
+++ b/packages/stack/scripts/test-registry.sh
@@ -193,6 +193,7 @@ main() {
react-hook-form \
@hookform/resolvers \
zod \
+ better-call@1.3.6 \
lucide-react \
sonner \
clsx \
diff --git a/packages/stack/src/__tests__/package-metadata.test.ts b/packages/stack/src/__tests__/package-metadata.test.ts
new file mode 100644
index 00000000..b21d271d
--- /dev/null
+++ b/packages/stack/src/__tests__/package-metadata.test.ts
@@ -0,0 +1,21 @@
+import { readFile } from "node:fs/promises";
+import { resolve } from "node:path";
+import { describe, expect, it } from "vitest";
+
+describe("published dependency alignment", () => {
+ it("uses the Better DB release that shares the Better Auth 1.6.16 cohort", async () => {
+ const manifest = JSON.parse(
+ await readFile(resolve("package.json"), "utf8"),
+ ) as {
+ dependencies?: Record;
+ devDependencies?: Record;
+ peerDependencies?: Record;
+ };
+
+ expect(manifest.dependencies?.["@btst/db"]).toBe("2.2.3");
+ expect(manifest.devDependencies?.["@btst/adapter-memory"]).toBe("2.2.3");
+ expect(manifest.devDependencies?.["@btst/yar"]).toBe("1.3.2");
+ expect(manifest.peerDependencies?.["@btst/yar"]).toBe(">=1.3.2");
+ expect(manifest.peerDependencies?.["better-call"]).toBe("1.3.6");
+ });
+});
diff --git a/playground/src/app/actions.ts b/playground/src/app/actions.ts
index f83ee24a..59846350 100644
--- a/playground/src/app/actions.ts
+++ b/playground/src/app/actions.ts
@@ -20,6 +20,7 @@ export interface GenerateResult {
routes: string[];
cssImports: string[];
extraPackages: string[];
+ extraPackageVersions: Record;
hasAiChat: boolean;
seedRouteFiles: SeedRouteFile[];
seedRunnerScript: string | null;
@@ -178,6 +179,7 @@ export async function generateProject(
routes,
cssImports: plan.cssImports,
extraPackages: plan.extraPackages,
+ extraPackageVersions: plan.extraPackageVersions ?? {},
hasAiChat: withRouteDocs.includes("ai-chat" as PluginKey),
seedRouteFiles,
seedRunnerScript,
diff --git a/playground/src/components/playground-client.tsx b/playground/src/components/playground-client.tsx
index 0deaa12a..c994f54b 100644
--- a/playground/src/components/playground-client.tsx
+++ b/playground/src/components/playground-client.tsx
@@ -42,6 +42,7 @@ interface GeneratedState {
routes: string[];
cssImports: string[];
extraPackages: string[];
+ extraPackageVersions: Record;
hasAiChat: boolean;
seedRouteFiles: SeedRouteFile[];
seedRunnerScript: string | null;
@@ -379,6 +380,7 @@ export function PlaygroundClient({
generatedFiles={generated.files}
cssImports={generated.cssImports}
extraPackages={generated.extraPackages}
+ extraPackageVersions={generated.extraPackageVersions}
hasAiChat={generated.hasAiChat}
previewPath={activePreviewRoute}
seedRouteFiles={generated.seedRouteFiles}
diff --git a/playground/src/components/stackblitz-embed.tsx b/playground/src/components/stackblitz-embed.tsx
index 859da994..cd271c62 100644
--- a/playground/src/components/stackblitz-embed.tsx
+++ b/playground/src/components/stackblitz-embed.tsx
@@ -18,6 +18,7 @@ interface StackBlitzEmbedProps {
generatedFiles: FileWritePlanItem[];
cssImports: string[];
extraPackages: string[];
+ extraPackageVersions: Record;
hasAiChat?: boolean;
previewPath?: string | null;
extraButtons?: React.ReactNode;
@@ -33,6 +34,7 @@ export function StackBlitzEmbed({
generatedFiles,
cssImports,
extraPackages,
+ extraPackageVersions,
hasAiChat = false,
previewPath,
extraButtons,
@@ -67,6 +69,7 @@ export function StackBlitzEmbed({
generatedFiles,
cssImports,
extraPackages,
+ extraPackageVersions,
hasAiChat,
seedRouteFiles,
seedRunnerScript,
@@ -218,6 +221,7 @@ export function StackBlitzEmbed({
generatedFiles,
cssImports,
extraPackages,
+ extraPackageVersions,
hasAiChat,
seedRouteFiles,
seedRunnerScript,
diff --git a/playground/src/lib/stackblitz-template.ts b/playground/src/lib/stackblitz-template.ts
index a9dd284f..88150020 100644
--- a/playground/src/lib/stackblitz-template.ts
+++ b/playground/src/lib/stackblitz-template.ts
@@ -18,15 +18,17 @@ function buildNextjsProjectFiles(
generatedFiles: FileWritePlanItem[],
cssImports: string[],
extraPackages: string[] = [],
+ extraPackageVersions: Record = {},
hasAiChat = false,
seedFiles: SeedRouteFile[] = [],
seedRunnerScript: string | null = null,
): ProjectFiles {
const cssImportLines = cssImports.map((c) => `@import "${c}";`).join("\n");
const baseDependencies: Record = {
- "@btst/stack": "latest",
- "@btst/adapter-memory": "latest",
- "@tanstack/react-query": "^5.0.0",
+ "@btst/stack": "next",
+ "@btst/adapter-memory": "2.2.3",
+ "@btst/yar": "1.3.2",
+ "@tanstack/react-query": "5.100.14",
next: "15.3.4",
react: "19.2.4",
"react-dom": "19.2.4",
@@ -35,7 +37,10 @@ function buildNextjsProjectFiles(
"lucide-react": "latest",
};
const pluginDependencies = Object.fromEntries(
- Array.from(new Set(extraPackages)).map((pkgName) => [pkgName, "latest"]),
+ Array.from(new Set(extraPackages)).map((pkgName) => [
+ pkgName,
+ extraPackageVersions[pkgName] ?? "latest",
+ ]),
);
const dependencies = Object.fromEntries(
Object.entries({
@@ -435,6 +440,7 @@ function buildReactRouterProjectFiles(
generatedFiles: FileWritePlanItem[],
cssImports: string[],
extraPackages: string[] = [],
+ extraPackageVersions: Record = {},
hasAiChat = false,
seedFiles: SeedRouteFile[] = [],
seedRunnerScript: string | null = null,
@@ -442,14 +448,18 @@ function buildReactRouterProjectFiles(
): ProjectFiles {
const cssImportLines = cssImports.map((c) => `@import "${c}";`).join("\n");
const pluginDependencies = Object.fromEntries(
- Array.from(new Set(extraPackages)).map((pkgName) => [pkgName, "latest"]),
+ Array.from(new Set(extraPackages)).map((pkgName) => [
+ pkgName,
+ extraPackageVersions[pkgName] ?? "latest",
+ ]),
);
const baseDependencies: Record = {
- "@btst/adapter-memory": "latest",
- "@btst/stack": "latest",
+ "@btst/adapter-memory": "2.2.3",
+ "@btst/stack": "next",
+ "@btst/yar": "1.3.2",
"@react-router/node": "^7.0.0",
"@react-router/serve": "^7.0.0",
- "@tanstack/react-query": "^5.0.0",
+ "@tanstack/react-query": "5.100.14",
react: "^19.0.0",
"react-dom": "^19.0.0",
"react-router": "^7.0.0",
@@ -783,6 +793,7 @@ function buildTanstackProjectFiles(
generatedFiles: FileWritePlanItem[],
cssImports: string[],
extraPackages: string[] = [],
+ extraPackageVersions: Record = {},
hasAiChat = false,
seedFiles: SeedRouteFile[] = [],
seedRunnerScript: string | null = null,
@@ -790,13 +801,17 @@ function buildTanstackProjectFiles(
): ProjectFiles {
const cssImportLines = cssImports.map((c) => `@import "${c}";`).join("\n");
const pluginDependencies = Object.fromEntries(
- Array.from(new Set(extraPackages)).map((pkgName) => [pkgName, "latest"]),
+ Array.from(new Set(extraPackages)).map((pkgName) => [
+ pkgName,
+ extraPackageVersions[pkgName] ?? "latest",
+ ]),
);
const baseDependencies: Record = {
- "@btst/adapter-memory": "latest",
- "@btst/stack": "latest",
+ "@btst/adapter-memory": "2.2.3",
+ "@btst/stack": "next",
+ "@btst/yar": "1.3.2",
"@tailwindcss/postcss": "^4",
- "@tanstack/react-query": "^5.0.0",
+ "@tanstack/react-query": "5.100.14",
"@tanstack/react-router": "^1.0.0",
"@tanstack/react-router-ssr-query": "^1.0.0",
"@tanstack/react-start": "^1.0.0",
@@ -1126,6 +1141,7 @@ export function buildProjectFiles(
generatedFiles: FileWritePlanItem[],
cssImports: string[],
extraPackages: string[] = [],
+ extraPackageVersions: Record = {},
hasAiChat = false,
seedFiles: SeedRouteFile[] = [],
seedRunnerScript: string | null = null,
@@ -1136,6 +1152,7 @@ export function buildProjectFiles(
generatedFiles,
cssImports,
extraPackages,
+ extraPackageVersions,
hasAiChat,
seedFiles,
seedRunnerScript,
@@ -1147,6 +1164,7 @@ export function buildProjectFiles(
generatedFiles,
cssImports,
extraPackages,
+ extraPackageVersions,
hasAiChat,
seedFiles,
seedRunnerScript,
@@ -1157,6 +1175,7 @@ export function buildProjectFiles(
generatedFiles,
cssImports,
extraPackages,
+ extraPackageVersions,
hasAiChat,
seedFiles,
seedRunnerScript,
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b6275bb1..0c2b7aaa 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -339,8 +339,8 @@ importers:
packages/stack:
dependencies:
'@btst/db':
- specifier: 2.2.2
- version: 2.2.2(706a9ce1fcc616cc33147c0a84ffbdf8)
+ specifier: 2.2.3
+ version: 2.2.3(11577220e6bea834a9815f29293913ef)
'@hookform/resolvers':
specifier: '>=5.0.0'
version: 5.2.2(react-hook-form@7.66.1(react@19.2.7))
@@ -436,11 +436,11 @@ importers:
specifier: ^3.1011.0
version: 3.1011.0
'@btst/adapter-memory':
- specifier: 2.2.2
- version: 2.2.2(018b1bc68345b791db4bf45b13a5d907)
+ specifier: 2.2.3
+ version: 2.2.3(11577220e6bea834a9815f29293913ef)
'@btst/yar':
- specifier: 1.3.0
- version: 1.3.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react@19.2.7)
+ specifier: 1.3.2
+ version: 1.3.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react@19.2.7)
'@tanstack/react-router':
specifier: 1.168.10
version: 1.168.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
@@ -1454,15 +1454,29 @@ packages:
'@better-auth/core': '>=1.6.0'
better-auth: '>=1.6.0'
+ '@btst/adapter-memory@2.2.3':
+ resolution: {integrity: sha512-qBgmfN9Wx6mCG3NoNv+12b+3iUoXGQGWBbnT/nPd1Olk+tJo8GjiLXY824gqRYLWNjpk0LIUsmo+lDMQlNrbIA==}
+ peerDependencies:
+ '@better-auth/core': 1.6.16
+ '@better-auth/utils': 0.4.1
+ better-auth: 1.6.16
+
'@btst/db@2.2.2':
resolution: {integrity: sha512-NLT9FXK4c60wP1DQ3lTRPd9Hqa+54VoqtQTT0w9xilk3Vm6DfNnWE9npwf8Nc+5noUBVtvgESUYssQHcOjNSbA==}
- '@btst/yar@1.3.0':
- resolution: {integrity: sha512-TD6/whPS6ES7uDFkL/1QIWSvrDyg7QDvtn9s7frAcGwbg4+0VNsC8DOhmJ7MlWb3qF5JuGQXM2hB62P4vSeWPQ==}
+ '@btst/db@2.2.3':
+ resolution: {integrity: sha512-tbx0o9mJnv3y5WVole5TZQwMMVmboamVJxsSeyMZH3O4qF9CNdEg6NfiR9zm/b81eO7ie6CftTWT9R7pJ4msSA==}
peerDependencies:
- '@types/react': ^19.1.16
- '@types/react-dom': ^19.1.9
- react: 19.2.7
+ '@better-auth/core': 1.6.16
+ '@better-auth/utils': 0.4.1
+ better-auth: 1.6.16
+
+ '@btst/yar@1.3.2':
+ resolution: {integrity: sha512-2YqkTht2PdKizlkwF5EG/fmLPzK4SG7YCkOojwDVRrlj0s3YpszhSak0uongrqslLoexozxN0RixbOeEnz9Vew==}
+ peerDependencies:
+ '@types/react': ^18.0.0 || ^19.0.0
+ '@types/react-dom': ^18.0.0 || ^19.0.0
+ react: ^18.0.0 || ^19.0.0
'@chevrotain/cst-dts-gen@10.5.0':
resolution: {integrity: sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw==}
@@ -13234,6 +13248,13 @@ snapshots:
- vitest
- vue
+ '@btst/adapter-memory@2.2.3(11577220e6bea834a9815f29293913ef)':
+ dependencies:
+ '@better-auth/core': 1.6.16(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.2.2)(@opentelemetry/api@1.9.0)(better-call@1.3.6(zod@4.4.3))(jose@6.2.0)(kysely@0.29.2)(nanostores@1.1.1)
+ '@better-auth/utils': 0.4.1
+ '@btst/db': 2.2.3(11577220e6bea834a9815f29293913ef)
+ better-auth: 1.6.16(@opentelemetry/api@1.9.0)(@prisma/client@6.19.0(prisma@7.5.0(@types/react@19.2.14)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.9.3))(typescript@5.9.3))(@tanstack/react-start@1.167.16(crossws@0.4.9(srvx@0.11.20))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@7.3.1(@types/node@24.12.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.2)))(mongodb@6.21.0(socks@2.8.7))(mysql2@3.15.3)(next@16.0.10(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(prisma@7.5.0(@types/react@19.2.14)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.9.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(solid-js@1.9.12)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.7.0)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(msw@2.12.10(@types/node@24.12.0)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.24(typescript@5.9.3))
+
'@btst/db@2.2.2(706a9ce1fcc616cc33147c0a84ffbdf8)':
dependencies:
'@better-auth/core': 1.6.16(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.2.2)(@opentelemetry/api@1.9.0)(better-call@1.3.6(zod@4.4.3))(jose@6.2.0)(kysely@0.29.2)(nanostores@1.1.1)
@@ -13300,7 +13321,13 @@ snapshots:
- vitest
- vue
- '@btst/yar@1.3.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react@19.2.7)':
+ '@btst/db@2.2.3(11577220e6bea834a9815f29293913ef)':
+ dependencies:
+ '@better-auth/core': 1.6.16(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.2.2)(@opentelemetry/api@1.9.0)(better-call@1.3.6(zod@4.4.3))(jose@6.2.0)(kysely@0.29.2)(nanostores@1.1.1)
+ '@better-auth/utils': 0.4.1
+ better-auth: 1.6.16(@opentelemetry/api@1.9.0)(@prisma/client@6.19.0(prisma@7.5.0(@types/react@19.2.14)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.9.3))(typescript@5.9.3))(@tanstack/react-start@1.167.16(crossws@0.4.9(srvx@0.11.20))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@7.3.1(@types/node@24.12.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.2)))(mongodb@6.21.0(socks@2.8.7))(mysql2@3.15.3)(next@16.0.10(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(prisma@7.5.0(@types/react@19.2.14)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.9.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(solid-js@1.9.12)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.7.0)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(msw@2.12.10(@types/node@24.12.0)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.24(typescript@5.9.3))
+
+ '@btst/yar@1.3.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react@19.2.7)':
dependencies:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
diff --git a/scripts/codegen/setup-nextjs.sh b/scripts/codegen/setup-nextjs.sh
index c8c2272e..7be8a90c 100755
--- a/scripts/codegen/setup-nextjs.sh
+++ b/scripts/codegen/setup-nextjs.sh
@@ -124,7 +124,7 @@ pkg.scripts["start:e2e"] = "rm -rf .next && next build && NODE_ENV=test NODE_OPT
// btst init --skip-install doesn't add packages to package.json, so add them manually.
const btstDeps = {
"@btst/stack": "workspace:*",
- "@btst/adapter-memory": "^2.2.2",
+ "@btst/adapter-memory": "2.2.3",
};
// Ensure required runtime deps
diff --git a/scripts/codegen/setup-react-router.sh b/scripts/codegen/setup-react-router.sh
index 4318944d..908966ff 100755
--- a/scripts/codegen/setup-react-router.sh
+++ b/scripts/codegen/setup-react-router.sh
@@ -124,7 +124,7 @@ pkg.scripts["start:e2e"] = "rm -rf build && rm -rf .react-router && react-router
// btst init --skip-install doesn't add packages to package.json, so add them manually.
const btstDeps = {
"@btst/stack": "workspace:*",
- "@btst/adapter-memory": "^2.2.2",
+ "@btst/adapter-memory": "2.2.3",
};
// Ensure required runtime deps
diff --git a/scripts/codegen/setup-tanstack.sh b/scripts/codegen/setup-tanstack.sh
index 17448887..0c1c136b 100755
--- a/scripts/codegen/setup-tanstack.sh
+++ b/scripts/codegen/setup-tanstack.sh
@@ -134,7 +134,7 @@ pkg.scripts["start:e2e"] = "rm -rf .output && rm -rf .nitro && rm -rf .tanstack
// btst init --skip-install doesn't add packages to package.json, so add them manually.
const btstDeps = {
"@btst/stack": "workspace:*",
- "@btst/adapter-memory": "^2.2.2",
+ "@btst/adapter-memory": "2.2.3",
};
// Ensure required runtime deps