Skip to content

Commit b23dd72

Browse files
feat(cli): add server-side Clerk support for standard backends (#956)
* fix(cli): refresh Clerk templates for current SDKs * fix * fix * ok * fix(cli): complete Clerk support and docs * fix(cli): address Clerk review feedback
1 parent 65151ca commit b23dd72

77 files changed

Lines changed: 5966 additions & 1001 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,13 @@ npx create-better-t-stack@latest
3737

3838
## Features
3939

40-
- Frontend: React (TanStack Router, React Router, TanStack Start), Next.js, Nuxt, Svelte, Solid, Astro, React Native (NativeWind/Unistyles), or none
41-
- Backend: Hono, Express, Fastify, Elysia, Next API Routes, Convex, or none
40+
- Frontend: React (TanStack Router, React Router, TanStack Start), Next.js, Nuxt, Svelte, Solid, Astro, React Native (Bare, NativeWind, Unistyles), or none
41+
- Backend: Hono, Express, Fastify, Elysia, Self (fullstack web app), Convex, or none
4242
- API: tRPC or oRPC (or none)
4343
- Runtime: Bun, Node.js, or Cloudflare Workers
4444
- Databases: SQLite, PostgreSQL, MySQL, MongoDB (or none)
4545
- ORMs: Drizzle, Prisma, Mongoose (or none)
46-
- Auth: Better-Auth (optional)
46+
- Auth: Better Auth or Clerk (optional)
4747
- Addons: Turborepo, Nx, PWA, Tauri, Electrobun, Biome, Lefthook, Husky, Starlight, Fumadocs, Ultracite, Oxlint, MCP, OpenTUI, WXT, Skills
4848
- Examples: Todo, AI
4949
- DB Setup: Turso, Neon, Supabase, Prisma PostgreSQL, MongoDB Atlas, Cloudflare D1, Docker
@@ -82,8 +82,8 @@ bun dev:web
8282

8383
Please read the Contribution Guide first and open an issue before starting new features to ensure alignment with project goals.
8484

85-
- Docs: [`Contributing`](/apps/web/content/docs/contributing.mdx)
86-
- Repo guide: [`.github/CONTRIBUTING.md`](.github/CONTRIBUTING.md)
85+
- Docs: [`./apps/web/content/docs/contributing.mdx`](./apps/web/content/docs/contributing.mdx)
86+
- Repo guide: [`./.github/CONTRIBUTING.md`](./.github/CONTRIBUTING.md)
8787

8888
## Star History
8989

apps/cli/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,14 @@ Follow the prompts to configure your project or use the `--yes` flag for default
3232
| Category | Options |
3333
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
3434
| **TypeScript** | End-to-end type safety across all parts of your application |
35-
| **Frontend** | • React with TanStack Router<br>• React with React Router<br>• React with TanStack Start (SSR)<br>• Next.js<br>• SvelteKit<br>• Nuxt (Vue)<br>• SolidJS<br>• React Native with NativeWind (via Expo)<br>• React Native with Unistyles (via Expo)<br>• None |
36-
| **Backend** | • Hono<br>• Express<br>• Elysia<br>• Next.js API routes<br>• Convex<br>• Fastify<br>• None |
35+
| **Frontend** | • React with TanStack Router<br>• React with React Router<br>• React with TanStack Start (SSR)<br>• Next.js<br>• SvelteKit<br>• Nuxt (Vue)<br>• SolidJS<br>• Astro<br>• React Native bare Expo<br>• React Native with NativeWind (via Expo)<br>• React Native with Unistyles (via Expo)<br>• None |
36+
| **Backend** | • Hono<br>• Express<br>• Elysia<br>• Fastify<br>• Self (fullstack inside the web app)<br>• Convex<br>• None |
3737
| **API Layer** | • tRPC (type-safe APIs)<br>• oRPC (OpenAPI-compatible type-safe APIs)<br>• None |
3838
| **Runtime** | • Bun<br>• Node.js<br>• Cloudflare Workers<br>• None |
3939
| **Database** | • SQLite<br>• PostgreSQL<br>• MySQL<br>• MongoDB<br>• None |
4040
| **ORM** | • Drizzle (TypeScript-first)<br>• Prisma (feature-rich)<br>• Mongoose (for MongoDB)<br>• None |
4141
| **Database Setup** | • Turso (SQLite)<br>• Cloudflare D1 (SQLite)<br>• Neon (PostgreSQL)<br>• Supabase (PostgreSQL)<br>• Prisma Postgres<br>• MongoDB Atlas<br>• None (manual setup) |
42-
| **Authentication** | Better-Auth (email/password, with more options coming soon) |
42+
| **Authentication** | Better Auth<br>• Clerk |
4343
| **Styling** | Tailwind CSS with a shared shadcn/ui package for React web apps |
4444
| **Addons** | • PWA support<br>• Tauri (desktop applications)<br>• Electrobun (lightweight desktop shell)<br>• Starlight and Fumadocs (documentation sites)<br>• Biome, Oxlint, Ultracite (linting and formatting)<br>• Lefthook, Husky (Git hooks)<br>• MCP, Skills (agent tooling)<br>• OpenTUI, WXT (platform extensions)<br>• Turborepo or Nx (monorepo orchestration) |
4545
| **Examples** | • Todo app<br>• AI Chat interface (using Vercel AI SDK) |

apps/cli/src/helpers/core/post-installation.ts

Lines changed: 101 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,8 @@ export async function displayPostInstallInstructions(
104104
const starlightInstructions = addons?.includes("starlight")
105105
? getStarlightInstructions(runCmd)
106106
: "";
107-
const clerkInstructions = isConvex && config.auth === "clerk" ? getClerkInstructions() : "";
107+
const clerkInstructions =
108+
config.auth === "clerk" ? getClerkInstructions(frontend || [], backend, api) : "";
108109
const polarInstructions =
109110
config.payments === "polar" && config.auth === "better-auth"
110111
? getPolarInstructions(backend)
@@ -445,8 +446,105 @@ function getBunWebNativeWarning() {
445446
)} 'bun' might cause issues with web + native apps in a monorepo.\n Use 'pnpm' if problems arise.`;
446447
}
447448

448-
function getClerkInstructions() {
449-
return `${pc.bold("Clerk Authentication Setup:")}\n${pc.cyan("•")} Follow the guide: ${pc.underline("https://docs.convex.dev/auth/clerk")}\n${pc.cyan("•")} Set CLERK_JWT_ISSUER_DOMAIN in Convex Dashboard\n${pc.cyan("•")} Set CLERK_PUBLISHABLE_KEY in apps/*/.env`;
449+
function getClerkQuickstartUrl(frontend: Frontend[]) {
450+
if (frontend.includes("next")) return "https://clerk.com/docs/nextjs/getting-started/quickstart";
451+
if (frontend.includes("react-router")) {
452+
return "https://clerk.com/docs/react-router/getting-started/quickstart";
453+
}
454+
if (frontend.includes("tanstack-start")) {
455+
return "https://clerk.com/docs/tanstack-react-start/getting-started/quickstart";
456+
}
457+
if (frontend.includes("tanstack-router")) {
458+
return "https://clerk.com/docs/react/getting-started/quickstart";
459+
}
460+
if (
461+
frontend.includes("native-bare") ||
462+
frontend.includes("native-uniwind") ||
463+
frontend.includes("native-unistyles")
464+
) {
465+
return "https://clerk.com/docs/expo/getting-started/quickstart";
466+
}
467+
468+
return "https://clerk.com/docs";
469+
}
470+
471+
function getClerkInstructionLines(
472+
frontend: Frontend[],
473+
backend: Backend,
474+
api: ProjectConfig["api"],
475+
) {
476+
const lines: string[] = [];
477+
478+
if (frontend.includes("next")) {
479+
lines.push("Set NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY in apps/web/.env");
480+
}
481+
482+
if (
483+
frontend.some((value) => ["react-router", "tanstack-router", "tanstack-start"].includes(value))
484+
) {
485+
lines.push("Set VITE_CLERK_PUBLISHABLE_KEY in apps/web/.env");
486+
}
487+
488+
if (
489+
frontend.some((value) => ["native-bare", "native-uniwind", "native-unistyles"].includes(value))
490+
) {
491+
lines.push("Set EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY in apps/native/.env");
492+
}
493+
494+
if (backend === "convex") {
495+
return [
496+
"Set CLERK_JWT_ISSUER_DOMAIN in Convex Dashboard",
497+
...lines,
498+
...(frontend.some((value) => ["next", "react-router", "tanstack-start"].includes(value))
499+
? ["Set CLERK_SECRET_KEY in apps/web/.env for Clerk server middleware"]
500+
: []),
501+
];
502+
}
503+
504+
const hasClerkServerFrontend = frontend.some((value) =>
505+
["next", "react-router", "tanstack-start"].includes(value),
506+
);
507+
const serverEnvPath = backend === "self" ? "apps/web/.env" : "apps/server/.env";
508+
const needsServerSideClerkAuth = backend !== "none";
509+
const needsClerkBackendPublishableKey = ["express", "fastify"].includes(backend);
510+
const needsClerkRequestVerification =
511+
api !== "none" && ["self", "hono", "elysia"].includes(backend);
512+
513+
if (hasClerkServerFrontend && backend === "self") {
514+
lines.push(
515+
"Set CLERK_SECRET_KEY in apps/web/.env for Clerk server middleware and server-side Clerk auth",
516+
);
517+
} else {
518+
if (hasClerkServerFrontend) {
519+
lines.push("Set CLERK_SECRET_KEY in apps/web/.env for Clerk server middleware");
520+
}
521+
522+
if (needsServerSideClerkAuth) {
523+
lines.push(`Set CLERK_SECRET_KEY in ${serverEnvPath} for server-side Clerk auth`);
524+
}
525+
}
526+
527+
if (needsClerkRequestVerification) {
528+
lines.push(
529+
`Set CLERK_PUBLISHABLE_KEY in ${serverEnvPath} for server-side Clerk request verification`,
530+
);
531+
}
532+
533+
if (needsClerkBackendPublishableKey) {
534+
lines.push(`Set CLERK_PUBLISHABLE_KEY in ${serverEnvPath} for Clerk backend middleware`);
535+
}
536+
537+
return lines;
538+
}
539+
540+
function getClerkInstructions(frontend: Frontend[], backend: Backend, api: ProjectConfig["api"]) {
541+
const lines = [
542+
`${pc.bold("Clerk Authentication Setup:")}`,
543+
`${pc.cyan("•")} Follow the guide: ${pc.underline(getClerkQuickstartUrl(frontend))}`,
544+
...getClerkInstructionLines(frontend, backend, api).map((line) => `${pc.cyan("•")} ${line}`),
545+
];
546+
547+
return lines.join("\n");
450548
}
451549

452550
function getBetterAuthConvexInstructions(hasWeb: boolean, webPort: string, packageManager: string) {

apps/cli/src/prompts/auth.ts

Lines changed: 48 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -12,74 +12,70 @@ export async function getAuthChoice(
1212
if (backend === "none") {
1313
return "none" as Auth;
1414
}
15-
if (backend === "convex") {
16-
const supportedBetterAuthFrontends = frontend?.some((f) =>
17-
[
18-
"tanstack-router",
19-
"tanstack-start",
20-
"next",
21-
"native-bare",
22-
"native-uniwind",
23-
"native-unistyles",
24-
].includes(f),
25-
);
15+
const supportedBetterAuthFrontends = frontend?.some((f) =>
16+
[
17+
"tanstack-router",
18+
"tanstack-start",
19+
"next",
20+
"nuxt",
21+
"svelte",
22+
"solid",
23+
"native-bare",
24+
"native-uniwind",
25+
"native-unistyles",
26+
].includes(f),
27+
);
2628

27-
const hasClerkCompatibleFrontends = frontend?.some((f) =>
28-
[
29-
"react-router",
30-
"tanstack-router",
31-
"tanstack-start",
32-
"next",
33-
"native-bare",
34-
"native-uniwind",
35-
"native-unistyles",
36-
].includes(f),
37-
);
29+
const hasClerkCompatibleFrontends = frontend?.some((f) =>
30+
[
31+
"react-router",
32+
"tanstack-router",
33+
"tanstack-start",
34+
"next",
35+
"native-bare",
36+
"native-uniwind",
37+
"native-unistyles",
38+
].includes(f),
39+
);
3840

39-
const options = [];
41+
const options = [];
4042

43+
if (backend === "convex") {
4144
if (supportedBetterAuthFrontends) {
4245
options.push({
4346
value: "better-auth",
4447
label: "Better-Auth",
4548
hint: "comprehensive auth framework for TypeScript",
4649
});
4750
}
51+
} else {
52+
options.push({
53+
value: "better-auth",
54+
label: "Better-Auth",
55+
hint: "comprehensive auth framework for TypeScript",
56+
});
57+
}
4858

49-
if (hasClerkCompatibleFrontends) {
50-
options.push({
51-
value: "clerk",
52-
label: "Clerk",
53-
hint: "More than auth, Complete User Management",
54-
});
55-
}
56-
57-
if (options.length === 0) {
58-
return "none" as Auth;
59-
}
60-
61-
options.push({ value: "none", label: "None", hint: "No auth" });
62-
63-
const response = await navigableSelect({
64-
message: "Select authentication provider",
65-
options,
66-
initialValue: "none",
59+
if (hasClerkCompatibleFrontends) {
60+
options.push({
61+
value: "clerk",
62+
label: "Clerk",
63+
hint: "More than auth, Complete User Management",
6764
});
68-
if (isCancel(response)) throw new UserCancelledError({ message: "Operation cancelled" });
69-
return response as Auth;
7065
}
7166

67+
if (options.length === 0) {
68+
return "none" as Auth;
69+
}
70+
71+
options.push({ value: "none", label: "None", hint: "No auth" });
72+
7273
const response = await navigableSelect({
7374
message: "Select authentication provider",
74-
options: [
75-
{
76-
value: "better-auth",
77-
label: "Better-Auth",
78-
hint: "comprehensive auth framework for TypeScript",
79-
},
80-
{ value: "none", label: "None" },
81-
],
82-
initialValue: DEFAULT_CONFIG.auth,
75+
options,
76+
initialValue: options.some((option) => option.value === DEFAULT_CONFIG.auth)
77+
? DEFAULT_CONFIG.auth
78+
: "none",
8379
});
8480

8581
if (isCancel(response)) throw new UserCancelledError({ message: "Operation cancelled" });

apps/cli/src/utils/compatibility-rules.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ export function isFrontendAllowedWithBackend(
179179
) {
180180
if (backend === "convex" && (frontend === "solid" || frontend === "astro")) return false;
181181

182-
if (auth === "clerk" && backend === "convex") {
182+
if (auth === "clerk") {
183183
const incompatibleFrontends = ["nuxt", "svelte", "solid", "astro"];
184184
if (incompatibleFrontends.includes(frontend)) return false;
185185
}

apps/cli/src/utils/config-validation.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -327,15 +327,9 @@ export function validateBackendConstraints(
327327
): ValidationResult {
328328
const { backend } = config;
329329

330-
if (config.auth === "clerk" && backend !== "convex") {
331-
return validationErr(
332-
"Clerk authentication is only supported with the Convex backend. Please use '--backend convex' or choose a different auth provider.",
333-
);
334-
}
335-
336-
if (backend === "convex" && config.auth === "clerk" && config.frontend) {
330+
if (config.auth === "clerk" && config.frontend) {
337331
const incompatibleFrontends = config.frontend.filter((f) =>
338-
["nuxt", "svelte", "solid"].includes(f),
332+
["nuxt", "svelte", "solid", "astro"].includes(f),
339333
);
340334
if (incompatibleFrontends.length > 0) {
341335
return validationErr(

apps/cli/test/api.test.ts

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,31 @@
1-
import { describe, it } from "bun:test";
1+
import { describe, expect, it } from "bun:test";
22

3+
import { createVirtual } from "../src/index";
34
import type { API, Backend, Database, Examples, Frontend, ORM, Runtime } from "../src/types";
45
import { expectError, expectSuccess, runTRPCTest, type TestConfig } from "./test-utils";
56

7+
function collectFiles(
8+
node:
9+
| { type: "file"; path: string; content: string }
10+
| { type: "directory"; path: string; children: unknown[] },
11+
rootPath: string,
12+
files = new Map<string, string>(),
13+
) {
14+
if (node.type === "file") {
15+
const relativePath = node.path.startsWith(`${rootPath}/`)
16+
? node.path.slice(rootPath.length + 1)
17+
: node.path;
18+
files.set(relativePath, node.content);
19+
return files;
20+
}
21+
22+
for (const child of node.children as Parameters<typeof collectFiles>[0][]) {
23+
collectFiles(child, rootPath, files);
24+
}
25+
26+
return files;
27+
}
28+
629
describe("API Configurations", () => {
730
describe("tRPC API", () => {
831
const reactFrontends = ["tanstack-router", "react-router", "tanstack-start", "next"];
@@ -538,6 +561,42 @@ describe("API Configurations", () => {
538561
});
539562

540563
describe("API Edge Cases", () => {
564+
it("should scaffold Fastify oRPC context with matching request shapes", async () => {
565+
const result = await createVirtual({
566+
projectName: "fastify-orpc-request-shape",
567+
api: "orpc",
568+
frontend: ["tanstack-router"],
569+
backend: "fastify",
570+
runtime: "node",
571+
database: "sqlite",
572+
orm: "drizzle",
573+
auth: "none",
574+
addons: ["none"],
575+
examples: ["none"],
576+
dbSetup: "none",
577+
webDeploy: "none",
578+
serverDeploy: "none",
579+
install: false,
580+
git: false,
581+
packageManager: "bun",
582+
payments: "none",
583+
});
584+
585+
if (result.isErr()) {
586+
throw result.error;
587+
}
588+
589+
const files = collectFiles(result.value.root, result.value.root.path);
590+
const serverFile = files.get("apps/server/src/index.ts");
591+
const contextFile = files.get("packages/api/src/context.ts");
592+
593+
expect(serverFile).toContain("context: await createContext(request.headers)");
594+
expect(contextFile).toContain('import type { IncomingHttpHeaders } from "node:http";');
595+
expect(contextFile).toContain(
596+
"export async function createContext(req: IncomingHttpHeaders)",
597+
);
598+
});
599+
541600
it("should handle API with complex frontend combinations", async () => {
542601
const result = await runTRPCTest({
543602
projectName: "api-complex-frontend",

0 commit comments

Comments
 (0)