Skip to content

Commit 97a50ec

Browse files
authored
Merge pull request #223 from udecode/codex/fix-auth-cli-cold-start
2 parents 8ffb18a + 0f1bed4 commit 97a50ec

6 files changed

Lines changed: 206 additions & 9 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"kitcn": patch
3+
---
4+
5+
## Patches
6+
7+
- Fix `kitcn add auth` in fresh apps so the CLI does not require `better-auth` to be installed before the auth scaffold planner runs.

docs/solutions/integration-issues/auth-peer-and-fixture-sync-parity-20260323.md

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
title: Auth scaffold peer installs and fixture sync must follow the packaged CLI path
3-
last_updated: 2026-04-01
3+
last_updated: 2026-04-17
44
category: integration-issues
55
tags:
66
- auth
@@ -14,6 +14,7 @@ symptoms:
1414
- auth apps hit `Cannot find package '@opentelemetry/api'`
1515
- fresh Bun apps can warn during later `kitcn add ...` runs because `bun.lock` already carries Better Auth peers
1616
- plain non-auth apps can still fail codegen because the packaged CLI bundle imports Better Auth too early
17+
- fresh `kitcn add auth` runs can fail before scaffold with `Cannot find package 'better-auth'`
1718
- `fixtures:sync` can say snapshots are fresh while `fixtures:check` still reports drift
1819
module: cli-fixtures-auth
1920
resolved: 2026-03-31
@@ -50,6 +51,10 @@ There were three separate mistakes:
5051
4. Fresh app baselines still omitted `@opentelemetry/api`, so Bun could keep
5152
warning on later `bun add` operations even after the auth-specific planning
5253
fix landed.
54+
5. First-pass auth schema registration still called
55+
`loadDefaultManagedAuthOptions()` when `auth.ts` did not exist yet. That
56+
pulled the managed Convex auth plugin into the published CLI before
57+
`better-auth` was installed.
5358

5459
## Solution
5560

@@ -74,6 +79,14 @@ Then cut the hot auth import out of the CLI bundle:
7479
lazy dynamic import that only runs when auth schema reconciliation actually
7580
happens
7681

82+
Then keep first-pass auth scaffold on a static schema fallback:
83+
84+
- if `convex/functions/auth.ts` is missing, resolve the default managed auth
85+
schema from baked extension units instead of calling the Better Auth-backed
86+
fallback loader
87+
- only reach for `loadDefaultManagedAuthOptions()` after auth is already
88+
present and the app can legitimately load Better Auth runtime code
89+
7790
Finally, make fixture sync mirror fixture check:
7891

7992
- sync the generated app through the same packaged local install + validation
@@ -84,11 +97,12 @@ Finally, make fixture sync mirror fixture check:
8497

8598
- `bun test packages/kitcn/src/cli/registry/dependencies.test.ts packages/kitcn/src/cli/supported-dependencies.test.ts`
8699
- `bun test ./packages/kitcn/src/cli/cli.commands.ts --test-name-pattern 'run\\(add auth --yes --no-codegen\\) patches the next baseline with minimal auth scaffolding|run\\(add auth --preset convex --yes\\) adopts a raw next convex app without kitcn baseline churn'`
87-
- `bun test packages/kitcn/src/cli/registry/items/auth/reconcile-auth-schema.test.ts`
100+
- `bun test packages/kitcn/src/cli/registry/items/auth/reconcile-auth-schema.test.ts packages/kitcn/src/cli/registry/items/auth/auth-item.test.ts`
88101
- `bun --cwd packages/kitcn build`
89102
- fresh packed CLI smoke: `KITCN_INSTALL_SPEC=<tarball> bunx --bun --package <tarball> kitcn init -t next --yes`
90103
- fresh packed CLI smoke: `bunx kitcn add auth --yes --no-codegen`
91104
- fresh packed CLI smoke: `bunx kitcn codegen`
105+
- fresh packed CLI smoke: `node node_modules/kitcn/dist/cli.mjs add auth --yes`
92106

93107
## Prevention
94108

@@ -103,3 +117,5 @@ Finally, make fixture sync mirror fixture check:
103117
5. If Bun warnings only disappear after manually adding a package once, fix the
104118
generated app baseline or the CLI preflight. Do not teach users to ignore
105119
the warning.
120+
6. First-pass auth scaffold must not require Better Auth runtime code just to
121+
compute the default managed schema.

packages/kitcn/src/cli/registry/items/auth/auth-item.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,9 @@ import { AUTH_START_SERVER_TEMPLATE } from './auth-start-server.template.js';
4949
import { AUTH_START_SERVER_CALL_TEMPLATE } from './auth-start-server-call.template.js';
5050
import {
5151
loadAuthOptionsFromDefinition,
52-
loadDefaultManagedAuthOptions,
5352
preserveUserOwnedAuthScaffoldFiles,
5453
reconcileAuthScaffoldFiles,
55-
renderManagedAuthSchemaUnits,
54+
resolveManagedAuthSchemaUnits,
5655
} from './reconcile-auth-schema.js';
5756

5857
const INIT_HTTP_API_USE_BLOCK_RE =
@@ -161,9 +160,6 @@ async function buildAuthSchemaRegistrationPlanFile(
161160
const schemaPath = getSchemaFilePath(params.functionsDir);
162161
const source = fs.readFileSync(schemaPath, 'utf8');
163162
const authDefinitionPath = resolve(params.functionsDir, 'auth.ts');
164-
const authOptions =
165-
(await loadAuthOptionsFromDefinition(authDefinitionPath)) ??
166-
(await loadDefaultManagedAuthOptions());
167163
const authSchemaLock = params.lockfile.plugins.auth?.schema ?? null;
168164
const result = await reconcileRootSchemaOwnership({
169165
claimMatchingManaged:
@@ -176,8 +172,9 @@ async function buildAuthSchemaRegistrationPlanFile(
176172
promptAdapter: params.promptAdapter,
177173
schemaPath,
178174
source,
179-
tables: await renderManagedAuthSchemaUnits({
180-
authOptions,
175+
tables: await resolveManagedAuthSchemaUnits({
176+
authDefinitionPath,
177+
loadAuthOptions: loadAuthOptionsFromDefinition,
181178
}),
182179
yes: params.yes,
183180
});

packages/kitcn/src/cli/registry/items/auth/auth-schema.template.ts

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,137 @@ export function authExtension() {
8383
}
8484
`;
8585

86+
export const DEFAULT_MANAGED_AUTH_EXTENSION_TEMPLATE = `// This file is auto-generated. Do not edit this file manually.
87+
// To regenerate the schema, run:
88+
// \`npx kitcn add auth --yes\`
89+
90+
import {
91+
boolean,
92+
convexTable,
93+
defineSchemaExtension,
94+
index,
95+
text,
96+
timestamp,
97+
} from "kitcn/orm";
98+
99+
export const userTable = convexTable(
100+
"user",
101+
{
102+
name: text().notNull(),
103+
email: text().notNull().unique(),
104+
emailVerified: boolean().notNull(),
105+
image: text(),
106+
createdAt: timestamp().notNull(),
107+
updatedAt: timestamp().notNull(),
108+
userId: text(),
109+
},
110+
(userTable) => [
111+
index("email_name").on(userTable.email, userTable.name),
112+
index("name").on(userTable.name),
113+
]
114+
);
115+
116+
export const sessionTable = convexTable(
117+
"session",
118+
{
119+
expiresAt: timestamp().notNull(),
120+
token: text().notNull().unique(),
121+
createdAt: timestamp().notNull(),
122+
updatedAt: timestamp().notNull(),
123+
ipAddress: text(),
124+
userAgent: text(),
125+
userId: text().notNull().references(() => userTable.id),
126+
},
127+
(sessionTable) => [
128+
index("expiresAt").on(sessionTable.expiresAt),
129+
index("expiresAt_userId").on(sessionTable.expiresAt, sessionTable.userId),
130+
index("userId").on(sessionTable.userId),
131+
]
132+
);
133+
134+
export const accountTable = convexTable(
135+
"account",
136+
{
137+
accountId: text().notNull(),
138+
providerId: text().notNull(),
139+
userId: text().notNull().references(() => userTable.id),
140+
accessToken: text(),
141+
refreshToken: text(),
142+
idToken: text(),
143+
accessTokenExpiresAt: timestamp(),
144+
refreshTokenExpiresAt: timestamp(),
145+
scope: text(),
146+
password: text(),
147+
createdAt: timestamp().notNull(),
148+
updatedAt: timestamp().notNull(),
149+
},
150+
(accountTable) => [
151+
index("accountId").on(accountTable.accountId),
152+
index("accountId_providerId").on(accountTable.accountId, accountTable.providerId),
153+
index("providerId_userId").on(accountTable.providerId, accountTable.userId),
154+
index("userId").on(accountTable.userId),
155+
]
156+
);
157+
158+
export const verificationTable = convexTable(
159+
"verification",
160+
{
161+
identifier: text().notNull(),
162+
value: text().notNull(),
163+
expiresAt: timestamp().notNull(),
164+
createdAt: timestamp().notNull(),
165+
updatedAt: timestamp().notNull(),
166+
},
167+
(verificationTable) => [
168+
index("expiresAt").on(verificationTable.expiresAt),
169+
index("identifier").on(verificationTable.identifier),
170+
]
171+
);
172+
173+
export const jwksTable = convexTable(
174+
"jwks",
175+
{
176+
publicKey: text().notNull(),
177+
privateKey: text().notNull(),
178+
createdAt: timestamp().notNull(),
179+
expiresAt: timestamp(),
180+
}
181+
);
182+
183+
export function authExtension() {
184+
return defineSchemaExtension("auth", {
185+
user: userTable,
186+
session: sessionTable,
187+
account: accountTable,
188+
verification: verificationTable,
189+
jwks: jwksTable,
190+
}).relations((r) => ({
191+
user: {
192+
sessions: r.many.session({
193+
from: r.user.id,
194+
to: r.session.userId,
195+
}),
196+
accounts: r.many.account({
197+
from: r.user.id,
198+
to: r.account.userId,
199+
}),
200+
},
201+
session: {
202+
user: r.one.user({
203+
from: r.session.userId,
204+
to: r.user.id,
205+
}),
206+
},
207+
account: {
208+
user: r.one.user({
209+
from: r.account.userId,
210+
to: r.user.id,
211+
}),
212+
},
213+
}));
214+
}
215+
`;
216+
86217
export const AUTH_CONVEX_SCHEMA_TEMPLATE = `import { defineTable } from 'convex/server';
87218
import { v } from 'convex/values';
88219

packages/kitcn/src/cli/registry/items/auth/reconcile-auth-schema.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
preserveUserOwnedAuthScaffoldFiles,
1111
reconcileAuthScaffoldFiles,
1212
renderManagedAuthSchemaFile,
13+
resolveManagedAuthSchemaUnits,
1314
} from './reconcile-auth-schema';
1415

1516
const baseAuthOptions = {
@@ -100,6 +101,23 @@ describe('reconcile auth schema', () => {
100101
expect(content).toContain('"jwks"');
101102
});
102103

104+
test('uses static default managed auth schema units when auth definition is missing', async () => {
105+
const units = await resolveManagedAuthSchemaUnits({
106+
authDefinitionPath: '/repo/convex/functions/auth.ts',
107+
loadAuthOptions: async () => null,
108+
renderManagedUnits: async () => {
109+
throw new Error('should not load better-auth-backed schema units');
110+
},
111+
});
112+
113+
expect(units.find((unit) => unit.key === 'jwks')?.declaration).toContain(
114+
'export const jwksTable = convexTable('
115+
);
116+
expect(units.find((unit) => unit.key === 'user')?.registration).toContain(
117+
'user: userTable'
118+
);
119+
});
120+
103121
test('loads auth options from strict env-backed auth definitions', async () => {
104122
const dir = mkTempDir();
105123
const authDefinitionPath = path.join(dir, 'convex', 'functions', 'auth.ts');

packages/kitcn/src/cli/registry/items/auth/reconcile-auth-schema.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { createSchemaExtensionOrm } from '../../../../auth/create-schema-orm';
88
import { createProjectJiti } from '../../../utils/project-jiti.js';
99
import { createTypeScriptProxy } from '../../../utils/typescript-runtime.js';
1010
import type { RootSchemaTableUnit } from '../../schema-ownership.js';
11+
import { DEFAULT_MANAGED_AUTH_EXTENSION_TEMPLATE } from './auth-schema.template.js';
1112

1213
type AuthSchemaTemplateId = 'auth-schema' | 'auth-schema-convex';
1314
type UserOwnedAuthTemplateId =
@@ -286,6 +287,33 @@ export const renderManagedAuthSchemaUnits = async ({
286287
})
287288
);
288289

290+
export const renderDefaultManagedAuthSchemaUnits = () =>
291+
parseRootSchemaUnitsFromExtension(DEFAULT_MANAGED_AUTH_EXTENSION_TEMPLATE);
292+
293+
export const resolveManagedAuthSchemaUnits = async ({
294+
authDefinitionPath,
295+
loadAuthOptions = loadAuthOptionsFromDefinition,
296+
renderManagedUnits = renderManagedAuthSchemaUnits,
297+
}: {
298+
authDefinitionPath: string;
299+
loadAuthOptions?: (
300+
authDefinitionPath: string
301+
) => Promise<BetterAuthOptions | null>;
302+
renderManagedUnits?: (params: {
303+
authOptions: BetterAuthOptions;
304+
}) => Promise<RootSchemaTableUnit[]>;
305+
}) => {
306+
const authOptions = await loadAuthOptions(authDefinitionPath);
307+
308+
if (!authOptions) {
309+
return renderDefaultManagedAuthSchemaUnits();
310+
}
311+
312+
return renderManagedUnits({
313+
authOptions,
314+
});
315+
};
316+
289317
export const loadDefaultManagedAuthConfigProvider = async () =>
290318
withAuthSchemaEnv(async () => getAuthConfigProvider());
291319

0 commit comments

Comments
 (0)