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
25 changes: 25 additions & 0 deletions .changeset/keyless-mode-default.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
"clerk": minor
---

Make keyless mode the default for unauthenticated `clerk init` runs on keyless-capable frameworks, and add a `--login` flag to force the authenticated flow.

- Bootstrapping a new project while signed out now uses auto-generated temporary development keys instead of prompting for a browser login; running `clerk auth login` later claims the app automatically. Re-running init in an existing project while signed out still triggers the login flow.
- Agent-mode runs while signed out now set up keyless keys with a claim breadcrumb instead of printing manual setup guidance.
- `--keyless` now forces keyless mode even when signed in, including in existing projects. Combining it with `--login` or `--app` exits with a usage error.
- `--login` in agent mode while signed out exits with a usage error, since the browser login needs an interactive terminal.

Add keyless support to `clerk config pull` and `clerk config patch`, so an unclaimed keyless application can be configured without logging in.

- When the project isn't linked and no `--app` is passed, both commands use the instance secret key the project already holds and talk to the Backend API instead of the Platform API. This needs no account at all — no login and no `CLERK_PLATFORM_API_KEY`. If credentials happen to be present, the command still works and warns that linking would give the full configuration.
- Keyless payloads name Backend API resources directly: `instance`, `communication`, `restrictions`, `organization_settings`, `protect`, `oauth_application_settings`, and `instance_settings` (`test_mode`, `progressive_sign_up`, `from_email_address`, `restricted_to_allowlist`). Any other top-level key exits with a usage error naming the supported ones.
- `clerk config pull` returns the same envelope; `restrictions` and `instance_settings` are omitted because the Backend API has no read route for them.
- In keyless mode `--dry-run` previews the diff locally without sending anything, `--instance` exits with a usage error (the secret key already targets one instance), and `clerk config put` and `clerk config schema` explain that they need a claimed application.

Let more commands operate on an unclaimed keyless application, so an agent can bootstrap and configure Clerk end to end without an account.

- `clerk enable orgs` and `clerk disable orgs` now work without logging in, writing through the instance's own organization settings. `clerk enable/disable billing` still requires a claimed application and now says so directly, because Clerk's Backend API exposes no billing settings.
- `clerk whoami` reports the keyless instance — id, environment, publishable key, and where the key was found — instead of failing with "not logged in".
- `clerk env pull` writes a keyless application's locally-held keys into the project's env file instead of failing with "not linked".
- Every keyless-capable command now also finds the keys a Clerk SDK created for itself in `.clerk/.tmp/keyless.json`, so an application minted by running the dev server is reachable from the CLI.
- `clerk init --template <b2b-saas|b2c-saas|native|waitlist>` pre-configures the keyless application at creation — a `b2b-saas` app comes back with organizations already enabled. Keyless-only; combining it with `--login` exits with a usage error.
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,5 +1,4 @@
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 { confirm } from "../../lib/prompts.ts";
Expand All @@ -8,6 +7,7 @@ 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 +56,26 @@ 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.
*/
async function resolveBillingTarget(options: BillingOptions): Promise<InstanceTarget> {
const target = await resolveInstanceTarget(options);
if (target.kind === "keyless") {
throw new CliError(
"Billing can only be configured on a claimed application — Clerk's Backend API has no billing settings, so an unclaimed keyless application can't reach them.\n" +
"Run `clerk auth login` to claim this application, then re-run the command.",
{ code: ERROR_CODE.AUTH_REQUIRED },
);
}
return target;
}

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

const billing: Record<string, unknown> = {};
const payload: Record<string, unknown> = { billing };
Expand All @@ -73,7 +90,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 +139,7 @@ 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 resolveBillingTarget(options);

// 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
87 changes: 87 additions & 0 deletions packages/cli-core/src/commands/config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

Manage Clerk instance configuration.

Two modes exist, picked automatically:

- **Account mode** (default) — full instance config document via the Platform API. Used whenever the project is linked or `--app` is passed; requires an account (`clerk auth login` or `CLERK_PLATFORM_API_KEY`).
- **Keyless mode** — a reduced set of settings via the Backend API, using only the instance secret key the project already has on disk. No account, no login, and no platform API key required. See [Keyless mode](#keyless-mode).

## Commands

### `clerk config pull`
Expand Down Expand Up @@ -31,6 +36,7 @@ clerk config pull --keys auth_email session
- a linked Clerk project in the current directory, or
- `--app <id>` to target an application directly
- Authenticated via `CLERK_PLATFORM_API_KEY`, `clerk auth login`, or the interactive human-mode prompt
- **Or neither**: an unlinked project holding an instance secret key falls back to [keyless mode](#keyless-mode), which needs no account

#### API Endpoints

Expand Down Expand Up @@ -69,6 +75,7 @@ clerk config schema --keys auth_email session
- a linked Clerk project in the current directory, or
- `--app <id>` to target an application directly
- Authenticated via `CLERK_PLATFORM_API_KEY`, `clerk auth login`, or the interactive human-mode prompt
- Account-only: in an unlinked project holding an instance secret key this exits with an error explaining that the schema describes the account-level config document (see [keyless mode](#keyless-mode))

#### API Endpoints

Expand Down Expand Up @@ -109,6 +116,7 @@ clerk config patch --file partial-config.json --dry-run
- a linked Clerk project in the current directory, or
- `--app <id>` to target an application directly
- Authenticated via `CLERK_PLATFORM_API_KEY`, `clerk auth login`, or the interactive human-mode prompt
- **Or neither**: an unlinked project holding an instance secret key falls back to [keyless mode](#keyless-mode), which needs no account

#### API Endpoints

Expand Down Expand Up @@ -149,9 +157,88 @@ clerk config put --file full-config.json --dry-run
- a linked Clerk project in the current directory, or
- `--app <id>` to target an application directly
- Authenticated via `CLERK_PLATFORM_API_KEY`, `clerk auth login`, or the interactive human-mode prompt
- Account-only: in an unlinked project holding an instance secret key this exits with an error pointing at `clerk config patch` (see [keyless mode](#keyless-mode))

#### API Endpoints

| Method | Endpoint | Description |
| ------ | ----------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PUT` | `/v1/platform/applications/{appID}/instances/{instanceID}/config` | Replaces the full instance configuration. Sends `?dry_run=true` under `--dry-run` to validate and preview without persisting. Authenticated via `Bearer` token from `CLERK_PLATFORM_API_KEY`. |

---

## Keyless mode

A keyless application created by `clerk init` has no Clerk account behind it until someone claims it, so the Platform API — which authenticates an _account_ — cannot reach it. `clerk config pull` and `clerk config patch` fall back to Clerk's Backend API, authenticated with the instance secret key the project already keeps locally, so an unclaimed app can be configured without logging in.

### When it engages

Both must hold, otherwise the account-authenticated path runs unchanged:

1. No `--app` was passed.
2. No linked project in the current directory.

**Account credentials are not part of this decision.** Keyless mode works with or without `CLERK_PLATFORM_API_KEY` and with or without a `clerk auth login` session — the instance secret key is sufficient on its own. What rules it out is an explicit destination (`--app` or a linked profile), because that names an application the local secret key may not belong to.

When credentials _are_ present and the directory simply isn't linked, the command prints a warning that it's using the reduced key-based view and points at `clerk link`, so the narrower output is never a silent surprise.

The secret key is resolved the way the app itself would resolve one, and this order is shared by every keyless-capable command (`lib/keyless-target.ts`):

1. `CLERK_SECRET_KEY`, or the framework's secret key variable (e.g. `NUXT_CLERK_SECRET_KEY`), in the environment
2. `.env`, then `.env.local` — the later file wins
3. `.clerk/.tmp/keyless.json` — the keys a Clerk SDK minted for itself when the app ran with no keys configured

A key that doesn't start with `sk_` is rejected. The SDK file comes last because SDKs only create their own application when nothing else supplies keys.

### Payload shape

The Backend API has no single config document — it exposes independent resources — so keyless payloads name them directly instead of translating between the two shapes. Each top-level key maps 1:1 to one endpoint:

```sh
clerk config patch --json '{
"instance": { "support_email": "dev@acme.com" },
"organization_settings": { "enabled": true }
}'
```

| Top-level key | Endpoint | Readable | Covers |
| ---------------------------- | ----------------------------------------- | -------- | ----------------------------------------------------------------------------------- |
| `instance` | `/v1/instance` | Yes | Support email, home URL, allowed origins |
| `communication` | `/v1/instance/communication` | Yes | Blocked country codes and communication settings |
| `restrictions` | `/v1/instance/restrictions` | No | Allowlist / blocklist sign-up restrictions |
| `organization_settings` | `/v1/instance/organization_settings` | Yes | Organizations: enabled, membership limits, domains, creation defaults |
| `protect` | `/v1/instance/protect` | Yes | Bot protection |
| `oauth_application_settings` | `/v1/instance/oauth_application_settings` | Yes | Dynamic OAuth client registration |
| `instance_settings` | `/v1/beta_features/instance_settings` | No | `test_mode`, `progressive_sign_up`, `from_email_address`, `restricted_to_allowlist` |

Comment on lines +204 to +213

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

API Endpoints table is missing rows for 4 of 7 documented groups.

The Payload shape table lists communication, protect, oauth_application_settings, and instance_settings as supported/readable groups, and keyless.test.ts exercises GET/PATCH routes for all of them, but the "API Endpoints (keyless mode)" table only documents /v1/instance, /v1/instance/restrictions, and /v1/instance/organization_settings. Readers relying on this table alone will miss the other supported routes.

📝 Proposed additional rows
 | Method  | Endpoint                             | Description                                                                                |
 | ------- | ------------------------------------ | ------------------------------------------------------------------------------------------ |
 | `GET`   | `/v1/instance`                       | Reads instance settings for `config pull` and for the pre-write diff.                      |
 | `PATCH` | `/v1/instance`                       | Updates instance settings. Answers `204` with no body, so the group is re-read afterwards. |
 | `PATCH` | `/v1/instance/restrictions`          | Updates sign-up restrictions (allowlist, blocklist). Write-only — no read route exists.    |
+| `GET`   | `/v1/instance/communication`         | Reads communication settings for `config pull` and for the pre-write diff.                 |
+| `PATCH` | `/v1/instance/communication`         | Updates communication settings.                                                            |
 | `GET`   | `/v1/instance/organization_settings` | Reads organization settings for `config pull` and for the pre-write diff.                  |
 | `PATCH` | `/v1/instance/organization_settings` | Updates organization settings.                                                             |
+| `GET`   | `/v1/instance/protect`               | Reads bot-protection settings for `config pull` and for the pre-write diff.                 |
+| `PATCH` | `/v1/instance/protect`               | Updates bot-protection settings.                                                           |
+| `GET`   | `/v1/instance/oauth_application_settings` | Reads dynamic OAuth client registration settings.                                     |
+| `PATCH` | `/v1/instance/oauth_application_settings` | Updates dynamic OAuth client registration settings.                                  |
+| `PATCH` | `/v1/beta_features/instance_settings` | Updates `test_mode`, `progressive_sign_up`, `from_email_address`, `restricted_to_allowlist`. Beta route; write-only. |

Also applies to: 234-244

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli-core/src/commands/config/README.md` around lines 204 - 213,
Update the “API Endpoints (keyless mode)” table to include the documented routes
for the four omitted groups: communication, protect, oauth_application_settings,
and instance_settings. Keep their endpoint paths, readability values, and
coverage descriptions consistent with the corresponding Payload shape entries
and keyless.test.ts behavior.

Any other top-level key exits with a usage error naming the supported ones — the account-mode keys (`session`, `sign_up`, `auth_email`, …) are not silently translated.

`instance_settings` is backed by a beta route, and is the only way to reach those four auth-config fields without an account — which is why it's included.

`clerk config pull` returns the same envelope. `restrictions` is omitted because the Backend API has no read route for it; asking for it by name prints a warning rather than failing.

`GET /v1/instance` returns a subset of what `PATCH /v1/instance` accepts — `support_email`, for example, is writable but not readable. Fields the read omits have no "before" value to compare against, so they always appear as additions in the diff and never trigger "No changes detected". The write itself is unaffected: patching a field to the value it already holds is a no-op server-side.

### Differences from account mode

| Behavior | Account mode | Keyless mode |
| --------------- | ---------------------------------------- | ----------------------------------------------------------------- |
| Coverage | Full config document | Three Backend API resources |
| `--instance` | Selects dev/prod | Usage error — the secret key already targets exactly one instance |
| `--dry-run` | Server-side validation of the projection | Local diff only; nothing is sent |
| `config put` | Replaces the whole document | Errors — no full document exists to replace |
| `config schema` | Returns the JSON Schema | Errors — the schema describes the account-level document |

Run `clerk auth login` to claim the application; auto-claim links it, and every config command then uses account mode with full coverage.

### API Endpoints (keyless mode)

All requests go to the Clerk Backend API (default `https://api.clerk.dev`, overridable via `CLERK_BACKEND_API_URL`), authenticated with a `Bearer sk_…` instance secret key.

| Method | Endpoint | Description |
| ------- | ------------------------------------ | ------------------------------------------------------------------------------------------ |
| `GET` | `/v1/instance` | Reads instance settings for `config pull` and for the pre-write diff. |
| `PATCH` | `/v1/instance` | Updates instance settings. Answers `204` with no body, so the group is re-read afterwards. |
| `PATCH` | `/v1/instance/restrictions` | Updates sign-up restrictions (allowlist, blocklist). Write-only — no read route exists. |
| `GET` | `/v1/instance/organization_settings` | Reads organization settings for `config pull` and for the pre-write diff. |
| `PATCH` | `/v1/instance/organization_settings` | Updates organization settings. |
Loading