Skip to content
Merged
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
10 changes: 10 additions & 0 deletions .changeset/keyless-mode-default.md
Comment thread
rafa-thayto marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"clerk": major
---

Make keyless mode the default for unauthenticated `clerk init` runs on keyless-capable frameworks, and let an unclaimed keyless application be configured end to end without an account.

- `clerk init` while signed out now bootstraps with auto-generated development keys instead of forcing a browser login; running `clerk auth login` later claims the application. `--keyless` forces keyless mode even when signed in, `--login` forces the authenticated flow, and `--template <b2b-saas|b2c-saas|native|waitlist>` shapes the application at creation. Re-running init keeps an existing unclaimed application — whether `clerk init` or a Clerk SDK minted it — unless `--fresh` explicitly asks for a replacement.
- `clerk config pull` and `clerk config patch` work on an unclaimed application through the Backend API, addressing its resources directly: `instance`, `communication`, `restrictions`, `organization_settings`, `protect`, `oauth_application_settings`, and `instance_settings`. Unsupported groups and unrecognized `instance` fields exit with a usage error naming what the API accepts, instead of printing success while the write was silently dropped; applied writes are verified against the API's own response.
- `clerk enable orgs`/`disable orgs`, `clerk whoami`, `clerk env pull`, `clerk doctor`, `clerk open`, `clerk users`, and `clerk api` all operate on an unclaimed keyless application, resolving the project's own secret key from its env files or the SDK's `.clerk/.tmp/keyless.json`. An exported `CLERK_SECRET_KEY` keeps its existing precedence, including over a linked profile. Commands that genuinely need a claimed application (billing, `config put`, `config schema`, `users open`) say so and name the reason instead of failing with "not linked".
- `clerk whoami` and `clerk env pull` detect a publishable/secret key pair belonging to two different applications — `whoami` warns, `env pull` refuses to write the pair. `whoami` in an unlinked directory also points out a local secret key when one is present, since `clerk api` and `clerk users` will use that key's instance rather than the signed-in account.
22 changes: 16 additions & 6 deletions packages/cli-core/src/commands/api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ Make authenticated HTTP requests to Clerk APIs directly from the command line.
By default, targets the Clerk Backend API (`https://api.clerk.dev/v1/`) using
the instance secret key. Use `--platform` to target the Platform API instead.

Works with no login and no linked project on an **unclaimed keyless
application** — the one an SDK creates for itself the first time you run
`next dev` (or similar) with no keys configured — by reading the secret key it
already left on disk. See [Authentication](#authentication) below.

## Usage

```sh
Expand Down Expand Up @@ -81,14 +86,19 @@ clerk api --fapi /environment --app app_123 --instance dev
Secret key resolution order (Backend API, the default):

1. `--secret-key` flag (explicit)
2. `CLERK_SECRET_KEY` environment variable
3. Auto-resolve from `--app <id>` via the Platform API (see below)
2. Auto-resolve from `--app <id>` via the Platform API (see below)
3. This project's own keyless secret key — from `CLERK_SECRET_KEY`, `.env.local`
(or the framework's detected env var name), or the SDK's own `.clerk/.tmp/keyless.json`
4. Auto-resolve from linked project profile via the Platform API (see below)

Steps 3 and 4 both exchange a Platform API token for the target instance's
secret key, so either needs Platform API auth to be available. Step 3 works
from any directory (no `clerk link` required); step 4 uses the app ID stored
by `clerk link`.
Step 2 exchanges a Platform API token for the target instance's secret key and
works from any directory (no `clerk link` required). Step 3 is what makes
`clerk api` work out of the box against an **unclaimed keyless application** —
the one an SDK creates for itself on first `next dev` (or similar) with no keys
configured — with no login and no Platform API auth at all; it only applies when
the directory isn't linked and `--app` wasn't passed, since either of those names
an explicit destination the on-disk key might not belong to. Step 4 uses the app
ID stored by `clerk link` and needs Platform API auth like step 2.

Platform API auth (used by `--platform` mode, and by steps 3 and 4 above):

Expand Down
6 changes: 6 additions & 0 deletions packages/cli-core/src/commands/billing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ Toggle Clerk billing for organizations and/or users on the linked instance.
The handlers are wired to top-level `clerk enable billing` and `clerk disable
billing` commands.

**Requires a claimed application.** Unlike `clerk enable/disable orgs`, these
commands cannot run against an unclaimed keyless application: billing settings
exist only in the account-level config document, and Clerk's Backend API exposes
no billing resource an instance secret key could reach. In a keyless project both
commands exit with an `auth_required` error pointing at `clerk auth login`.

For arbitrary billing config edits (plans, trials, payment-method requirements)
use `clerk config patch --json '{"billing":{...}}'` until a dedicated
`clerk billing settings` command lands.
Expand Down
29 changes: 23 additions & 6 deletions packages/cli-core/src/commands/billing/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { resolveAppContext } from "../../lib/config.ts";
import { throwUsageError } from "../../lib/errors.ts";
import { CliError, ERROR_CODE, throwUsageError } from "../../lib/errors.ts";
import { isAgent, isHuman } from "../../mode.ts";
import { log } from "../../lib/log.ts";
import { keylessCopy } from "../../lib/copy.ts";
import { confirm } from "../../lib/prompts.ts";
import { detectPackageManager } from "../../lib/package-manager.ts";
import { NEXT_STEPS } from "../../lib/next-steps.ts";
import { withGutter } from "../../lib/spinner.ts";
import { resolveSkillsRunner, runSkillsAdd } from "../../lib/skills.ts";
import { applyConfigPatch } from "../config/apply-patch.ts";
import { resolveInstanceTarget, type InstanceTarget } from "../../lib/keyless-target.ts";

interface BillingOptions {
app?: string;
Expand Down Expand Up @@ -56,9 +57,24 @@ function describeTargets(targets: Target[]): string {
return parts.length === 2 ? `${parts[0]} and ${parts[1]}` : parts[0]!;
}

/**
* Billing settings live only in the account-level config document — Clerk's
* Backend API exposes no billing resource — so these commands can't run against
* an unclaimed keyless application the way the org toggles can. Pure assertion:
* resolving the target is `resolveInstanceTarget`'s job, not billing's.
*/
function assertBillingTarget(target: InstanceTarget): void {
if (target.kind === "keyless") {
throw new CliError(keylessCopy.billingNeedsClaimedApplication(), {
code: ERROR_CODE.AUTH_REQUIRED,
});
}
}
Comment thread
rafa-thayto marked this conversation as resolved.

export async function billingEnable(options: BillingOptions): Promise<void> {
const targets = parseForTargets(options.for);
const ctx = await resolveAppContext(options);
const target = await resolveInstanceTarget(options);
assertBillingTarget(target);

const billing: Record<string, unknown> = {};
const payload: Record<string, unknown> = { billing };
Expand All @@ -73,7 +89,7 @@ export async function billingEnable(options: BillingOptions): Promise<void> {

await withGutter("Enabling billing", async ({ setNextSteps }) => {
const applied = await applyConfigPatch({
ctx,
target,
payload,
verb: `Enabling billing for ${describeTargets(targets)}`,
successMessage: `Billing enabled for ${describeTargets(targets)}`,
Expand Down Expand Up @@ -122,7 +138,8 @@ async function offerBillingSkillInstall(options: BillingOptions): Promise<void>

export async function billingDisable(options: BillingOptions): Promise<void> {
const targets = parseForTargets(options.for);
const ctx = await resolveAppContext(options);
const target = await resolveInstanceTarget(options);
assertBillingTarget(target);

// No cascade: leave organization_settings untouched.
const billing: Record<string, unknown> = {};
Expand All @@ -131,7 +148,7 @@ export async function billingDisable(options: BillingOptions): Promise<void> {

await withGutter("Disabling billing", async () => {
await applyConfigPatch({
ctx,
target,
payload: { billing },
verb: `Disabling billing for ${describeTargets(targets)}`,
successMessage: `Billing disabled for ${describeTargets(targets)}`,
Expand Down
Loading