Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/accountless-every-framework.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"clerk": minor
---

Let `clerk init` set up an accountless application on every framework it supports. Vue, React, JavaScript, Expo, Express, Fastify, iOS, and Android now resolve to accountless the same way Next.js does: unauthenticated agent runs and new-project bootstraps mint temporary development keys without a login, `--accountless` is accepted everywhere, and the agent-mode "set up keys manually" fallback is gone. iOS and Android projects get only the publishable key in `.env`, since their default `.gitignore` doesn't cover it.
50 changes: 25 additions & 25 deletions packages/cli-core/src/commands/init/README.md

Large diffs are not rendered by default.

14 changes: 2 additions & 12 deletions packages/cli-core/src/commands/init/bootstrap-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ function runner(pm: PackageManager): string[] {
// create-astro) infer the PM from npm_config_user_agent, which is set automatically when run
// via bunx/pnpm dlx/yarn dlx/npx. This works in practice since we run them via the selected
// PM's runner, and our own installDependencies() step uses the correct PM regardless.
/** Frameworks that support keyless mode — used for bootstrap (new project from empty dir / --starter). */
export const BOOTSTRAP_KEYLESS_REGISTRY: BootstrapEntry[] = [
/** Frameworks `clerk init` can bootstrap (new project from an empty dir / --starter). */
export const BOOTSTRAP_REGISTRY: BootstrapEntry[] = [
{
label: "Next.js",
dep: "next",
Expand Down Expand Up @@ -105,10 +105,6 @@ export const BOOTSTRAP_KEYLESS_REGISTRY: BootstrapEntry[] = [
pm,
],
},
];

/** Frameworks that require API keys — keyless mode is not yet supported. */
export const BOOTSTRAP_AUTHENTICATED_REGISTRY: BootstrapEntry[] = [
{
label: "React",
dep: "react",
Expand Down Expand Up @@ -158,12 +154,6 @@ export const BOOTSTRAP_AUTHENTICATED_REGISTRY: BootstrapEntry[] = [
},
];

/** All bootstrap-capable frameworks (keyless + authenticated). */
export const BOOTSTRAP_REGISTRY: BootstrapEntry[] = [
...BOOTSTRAP_KEYLESS_REGISTRY,
...BOOTSTRAP_AUTHENTICATED_REGISTRY,
];

// Hardening flags come from PM_INSTALL_HARDENING_FLAGS (see package-manager.ts
// for the threat model). `clerk init --starter` runs this in projectDir,
// which is freshly scaffolded from an official template but lives inside the
Expand Down
6 changes: 3 additions & 3 deletions packages/cli-core/src/commands/init/frameworks/android.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import type { FrameworkScaffold, ProjectContext, ScaffoldPlan } from "./types.js
* publishable key is configured in Kotlin source (`Clerk.initialize(...)`),
* not an env file. Gradle files are user-managed build scripts with too many
* layout variants (Groovy/Kotlin DSL, version catalogs) to modify safely, so
* this scaffolder prints the exact quickstart steps; `clerk init` still links
* the app and pulls real keys so the user can copy the publishable key.
* this scaffolder prints the exact quickstart steps; `clerk init` still puts a
* publishable key in the env file (real or accountless) for the user to copy.
*
* Docs: https://clerk.com/docs/android/getting-started/quickstart
*/
Expand All @@ -25,7 +25,7 @@ export const android: FrameworkScaffold = {
'Add the Clerk Android SDK to app/build.gradle.kts: `implementation("com.clerk:clerk-android-ui:<latest-version>")` and `implementation("androidx.lifecycle:lifecycle-viewmodel-compose:<latest-version>")` (requires minSdk 24+ and Java 17+; latest version: https://github.com/clerk/clerk-android/releases)',
"Enable the Native API and register your Android app on the Native Applications page: https://dashboard.clerk.com/~/native-applications",
'Add `<uses-permission android:name="android.permission.INTERNET"/>` to AndroidManifest.xml and register an Application subclass via `android:name`',
`Initialize Clerk in your Application subclass: \`Clerk.initialize(this, publishableKey = "<publishable key>")\` — copy CLERK_PUBLISHABLE_KEY from ${ctx.envFile} after \`clerk env pull\``,
`Initialize Clerk in your Application subclass: \`Clerk.initialize(this, publishableKey = "<publishable key>")\` — copy CLERK_PUBLISHABLE_KEY from ${ctx.envFile}`,
"Full setup guide: https://clerk.com/docs/android/getting-started/quickstart",
],
};
Expand Down
2 changes: 1 addition & 1 deletion packages/cli-core/src/commands/init/frameworks/expo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ export const expo: FrameworkScaffold = {
}

postInstructions.push(
`Ensure ${ctx.framework.envVar} is set in your ${ctx.envFile} (pulled via \`clerk env pull\`)`,
`Ensure ${ctx.framework.envVar} is set in your ${ctx.envFile}`,
`Add sign-in and sign-up screens, and enable the Native API at https://dashboard.clerk.com/~/native-applications — see: ${EXPO_QUICKSTART_URL}`,
);

Expand Down
6 changes: 3 additions & 3 deletions packages/cli-core/src/commands/init/frameworks/ios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import type { FrameworkScaffold, ProjectContext, ScaffoldPlan } from "./types.js
* configured in Swift source (`Clerk.configure(publishableKey:)`), not an env
* file — and adding an SPM dependency requires editing the Xcode project
* bundle, which is not safe to automate. So instead of writing files, this
* scaffolder prints the exact quickstart steps; `clerk init` still links the
* app and pulls real keys so the user can copy the publishable key.
* scaffolder prints the exact quickstart steps; `clerk init` still puts a
* publishable key in the env file (real or accountless) for the user to copy.
*
* Docs: https://clerk.com/docs/ios/getting-started/quickstart
*/
Expand All @@ -25,7 +25,7 @@ export const ios: FrameworkScaffold = {
"Add the Clerk iOS SDK via Swift Package Manager: https://github.com/clerk/clerk-ios (add both ClerkKit and ClerkKitUI to your target)",
"Enable the Native API and register your iOS app (App ID Prefix + Bundle ID) on the Native Applications page: https://dashboard.clerk.com/~/native-applications",
"In Xcode, add the Associated Domains capability with `webcredentials:<your-frontend-api-url>`",
`Configure Clerk in your @main App struct: \`Clerk.configure(publishableKey: "<publishable key>")\` — copy CLERK_PUBLISHABLE_KEY from ${ctx.envFile} after \`clerk env pull\``,
`Configure Clerk in your @main App struct: \`Clerk.configure(publishableKey: "<publishable key>")\` — copy CLERK_PUBLISHABLE_KEY from ${ctx.envFile}`,
"Inject Clerk into the SwiftUI environment so views can read it via `@Environment(Clerk.self)`: `ContentView().environment(Clerk.shared)`",
"Full setup guide: https://clerk.com/docs/ios/getting-started/quickstart",
],
Expand Down
4 changes: 1 addition & 3 deletions packages/cli-core/src/commands/init/frameworks/javascript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,7 @@ export const javascriptVite: FrameworkScaffold = {
);
}

postInstructions.push(
`Ensure ${ctx.framework.envVar} is set in your ${ctx.envFile} (pulled via \`clerk env pull\`)`,
);
postInstructions.push(`Ensure ${ctx.framework.envVar} is set in your ${ctx.envFile}`);

return { actions, postInstructions };
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ export async function scaffoldServerFramework(
actions: action ? [action] : [],
postInstructions: [
...(wired ? [] : [`${config.manualWiring} See: ${config.docsUrl}`]),
`Ensure ${ctx.framework.envVar} and CLERK_SECRET_KEY are set in your ${ctx.envFile} (pulled via \`clerk env pull\`), and load them before Clerk imports — e.g. \`node --env-file=${ctx.envFile} ${entryPath ?? "index.js"}\``,
`Ensure ${ctx.framework.envVar} and CLERK_SECRET_KEY are set in your ${ctx.envFile}, and load them before Clerk imports — e.g. \`node --env-file=${ctx.envFile} ${entryPath ?? "index.js"}\``,
`Protect routes with \`getAuth()\` and \`clerkClient\`: ${config.docsUrl}`,
],
};
Expand Down
4 changes: 1 addition & 3 deletions packages/cli-core/src/commands/init/frameworks/react.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,7 @@ export const reactVite: FrameworkScaffold = {
);
}

postInstructions.push(
`Ensure ${ctx.framework.envVar} is set in your ${ctx.envFile} (pulled via \`clerk env pull\`)`,
);
postInstructions.push(`Ensure ${ctx.framework.envVar} is set in your ${ctx.envFile}`);

return { actions, postInstructions };
},
Expand Down
2 changes: 0 additions & 2 deletions packages/cli-core/src/commands/init/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,6 @@ describe("init", () => {
sdk: "@clerk/nextjs",
envVar: "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY",
envFile: ".env.local" as const,
supportsKeyless: true,
};
setup();
spyOn(context, "hasPackageJson").mockResolvedValue(false);
Expand Down Expand Up @@ -338,7 +337,6 @@ describe("init", () => {
sdk: "@clerk/nextjs",
envVar: "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY",
envFile: ".env.local" as const,
supportsKeyless: true,
};
setup();
spyOn(context, "hasPackageJson").mockResolvedValue(false);
Expand Down
83 changes: 18 additions & 65 deletions packages/cli-core/src/commands/init/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,9 @@ export async function init(options: InitOptions = {}) {
authed,
isBootstrap: bootstrap != null,
hasRealAppTarget,
framework: ctx.framework,
});

assertKeylessOnlyFlags(options, strategy, Boolean(ctx.framework.supportsKeyless));
assertKeylessOnlyFlags(options, strategy);

if (strategy === "authenticate") {
setTelemetryStage("link");
Expand All @@ -174,9 +173,6 @@ export async function init(options: InitOptions = {}) {
if (alreadySetUp) {
setTelemetryStage("already_set_up");
log.success("\nClerk is already set up in this project.");
if (agent && strategy === "manual") {
printBootstrapManualSetupInfo(ctx.framework);
}
await outro("Done");
return;
}
Expand Down Expand Up @@ -269,28 +265,15 @@ async function isAuthenticatedForAgent(): Promise<boolean> {
* resolution because that's the earliest point the real strategy — not just
* the flags that might influence it — is known.
*/
function assertKeylessOnlyFlags(
options: InitOptions,
strategy: InitStrategy,
supportsAccountless: boolean,
): void {
function assertKeylessOnlyFlags(options: InitOptions, strategy: InitStrategy): void {
if (strategy === "keyless") return;

// "Add --accountless" is only valid remediation when accountless setup is
// actually reachable from here — not when the framework doesn't support it
// or when --app/--login are what forced the authenticated flow (both
// conflict with --accountless in assertUsableFlags above). Framework
// support is checked directly, not via strategy: an unsupported framework
// resolves to "manual" only in agent mode — in human mode it resolves to
// "authenticate", which would otherwise suggest an --accountless flag the
// framework rejects.
// "Add --accountless" is only valid remediation when --app/--login aren't
// what forced the authenticated flow (both conflict with --accountless in
// assertUsableFlags above).
let reason: string;
// Null when dropping the offending flag is the only remediation.
let remedy: string | null;
if (!supportsAccountless) {
reason = "this framework does not support accountless setup";
remedy = null;
} else if (options.app) {
let remedy: string;
if (options.app) {
reason = "--app was set, which cannot be combined with --accountless";
remedy = "drop --app to allow accountless setup";
} else if (options.login) {
Expand All @@ -302,15 +285,14 @@ function assertKeylessOnlyFlags(
remedy = "add --accountless to force an accountless app";
}

const tail = (flag: string): string => (remedy ? `${remedy}, or drop ${flag}.` : `drop ${flag}.`);
if (options.template) {
throwUsageError(
`--template only applies to accountless applications, but ${reason}; ${tail("--template")}`,
`--template only applies to accountless applications, but ${reason}; ${remedy}, or drop --template.`,
);
}
if (options.fresh) {
throwUsageError(
`--fresh only applies to accountless applications, but ${reason}; ${tail("--fresh")}`,
`--fresh only applies to accountless applications, but ${reason}; ${remedy}, or drop --fresh.`,
);
}
}
Expand Down Expand Up @@ -403,29 +385,16 @@ function printBootstrapNextSteps(
printNextSteps(steps);
}

function printBootstrapManualSetupInfo(framework: FrameworkInfo): void {
// Only reachable for frameworks without accountless support: capable ones resolve to
// the "keyless" or "authenticate" strategy in agent mode instead.
const lines = [
`\n Set up Clerk for ${framework.name}:`,
` ${framework.name} requires API keys — set them up manually:`,
" clerk init --app <app_id>",
" clerk env pull",
];
log.info(lines.map(dim).join("\n"));
}

// --- Strategy ---

type InitStrategy = "keyless" | "manual" | "authenticate";
type InitStrategy = "keyless" | "authenticate";

// Picks how `clerk init` will reach a working Clerk setup:
// Picks how `clerk init` will reach a working Clerk setup. The CLI mints the accountless app
// itself and only needs the framework's env var names, so every framework qualifies:
// - "keyless" → temporary development keys, no login. Forced via `--accountless`, or the default
// for unauthenticated runs on a keyless-capable framework (human bootstrap and
// all agent runs). A legacy `.clerk/keyless.json` breadcrumb lets the next
// `clerk auth login` claim the app automatically.
// - "manual" → agent mode on a non-keyless framework without a real app target — scaffold
// locally and print guidance instead of running OAuth.
// for unauthenticated runs (human bootstrap and all agent runs). A legacy
// `.clerk/keyless.json` breadcrumb lets the next `clerk auth login` claim the app
// automatically.
// - "authenticate" → log in (interactively if needed) and link a real Clerk application. Forced
// via `--login`, and the default whenever accountless setup doesn't apply.
function pickStrategy({
Expand All @@ -435,27 +404,17 @@ function pickStrategy({
authed,
isBootstrap,
hasRealAppTarget,
framework,
}: {
optsAccountless: boolean;
optsLogin: boolean;
agent: boolean;
authed: boolean;
isBootstrap: boolean;
hasRealAppTarget: boolean;
framework: FrameworkInfo;
}): InitStrategy {
if (optsAccountless) {
if (!framework.supportsKeyless) {
throwUsageError(
`--accountless is not supported for ${framework.name}. Run \`clerk auth login\` and use \`clerk init --app <app_id>\` instead.`,
);
}
return "keyless";
}
if (optsAccountless) return "keyless";
if (optsLogin || hasRealAppTarget) return "authenticate";
if (agent && !framework.supportsKeyless) return "manual";
if (!authed && framework.supportsKeyless && (agent || isBootstrap)) return "keyless";
if (!authed && (agent || isBootstrap)) return "keyless";
return "authenticate";
}

Expand All @@ -473,9 +432,6 @@ async function runStrategy(
keylessOptions: KeylessRunOptions,
): Promise<void> {
switch (strategy) {
case "manual":
printBootstrapManualSetupInfo(ctx.framework);
return;
case "authenticate":
await pull({ file: ctx.envFile, cwd: ctx.cwd });
return;
Expand Down Expand Up @@ -692,10 +648,7 @@ export function registerInit(program: Program): void {
.option("--name <project-name>", "Project name for --starter (skips prompt)")
.option("--app <id>", "Application ID to link (skips interactive picker)")
.option("--starter", "Create a new project from a starter template")
.option(
"--accountless",
"Force accountless development keys, even when logged in (only for supported frameworks)",
)
.option("--accountless", "Force accountless development keys, even when logged in")
.addOption(createOption("--keyless", "Deprecated alias for --accountless").hideHelp())
.option(
"--login",
Expand Down
Loading
Loading