# The flatstack CLI

`flatstack` signs in, creates and links projects, deploys, runs migrations, and manages variables, secrets, domains and files. Every command takes `--json`, so agents can run it as easily as people.

## Installing

The CLI is the npm package `@flatstackdev/cli`, and the command it installs is `flatstack`. It needs Node.js 22 or later.

```bash
npx @flatstackdev/cli@latest init my-app   # or: npm create flatstack@latest my-app
```

A project made by `init` lists `@flatstackdev/cli` as a dev dependency, so after `npm install` every `npx flatstack …` in that folder runs the project's own copy, at the version the project pins. Elsewhere, run it as `npx @flatstackdev/cli@latest <command>`, or install it once with `npm install -g @flatstackdev/cli` and type `flatstack <command>`.

Outside a Flatstack project, don't run plain `npx flatstack`: the unscoped `flatstack` package on npm belongs to someone else. Code in your project imports the server SDK as `@flatstackdev/server`.

## Signing in

```bash
flatstack login
```

The CLI shows a code like `BCDF-GHJK` and opens the dashboard's `/device` page. Check the code matches, then choose **Approve**. The CLI receives a personal access token with the scopes your role allows (see [Teams and roles](../api/teams.md)), named `CLI: flatstack CLI on <your computer>`. It expires after 90 days, like any token. Codes last 10 minutes.

The token is stored in the OS keychain: the macOS Keychain, or the Secret Service (GNOME Keyring, KWallet) through `secret-tool` on Linux. Where there is no keychain, such as Windows or a container without a keyring daemon, it goes in `credentials.json` in your Flatstack config directory, readable only by you (file mode 0600). `FLATSTACK_CREDENTIALS=file` always uses the file. The config directory is `$XDG_CONFIG_HOME/flatstack`, else `~/.config/flatstack`; `FLATSTACK_CONFIG_DIR` overrides it.

`flatstack logout` revokes the token and removes it from this computer. `flatstack whoami` shows the account and role you're using, your plan, and how many projects it allows and how many are free. With `--json`:

```json
{
  "apiUrl": "https://api.flatstack.dev",
  "accountId": "acc_…",
  "role": "developer",
  "tokenId": "tok_…",
  "plan": { "id": "pro", "projects": { "allowed": 3, "used": 1, "free": 2 } },
  "project": { "id": "prj_…", "slug": "my-app" }
}
```

`used` and `free` are `null` when the token can't list projects, as with a deploy token. `project` is the directory's link, or `null`.

For CI and agents without a browser, create a token with [`flatstack tokens create`](#tokens) or in the dashboard and set `FLATSTACK_TOKEN`; it wins over a stored login. A [deploy token](../api/authentication.md#tokens) with the `deploy` scope can run `deploy`, `rollback <deployId>` and `db migrate` (and `env list` with `env:read`); it can't read projects, so pass the project's ID or link the directory first.

Some things need a signed-in member in the dashboard, not a token, so the CLI can't do them: managing members, changing plan or cards, removing add-ons, and transferring a project to another account (an owner does that in the dashboard: **Settings** on the project to send it, **Projects**, then **Transfers** to accept; see [moving a project](../api/projects.md#moving-a-project-to-another-account)). Owners can buy add-ons with `addons buy`, and delete and restore projects with [`projects delete --yes` and `projects restore`](#commands).

## Choosing a project

Most commands act on one project. The CLI uses, in order:

1. `--project <id or slug>`
2. `FLATSTACK_PROJECT`
3. The link `flatstack link <id or slug>` writes to `.flatstack/project.json`, found in the current directory or a parent. Commit it to share the link.

`flatstack projects create <name> --link` creates a project and links the directory in one step.

## Commands

| Command | What it does |
| --- | --- |
| `login`, `logout`, `whoami` | Sign in through the dashboard, sign out, show who you are and your plan. |
| `launch [dir] [--name] [--slug] [--region] [--engine] [--template] [--assets-dir] [--yes] [--no-install]` | From an empty folder to a live URL: write the starter, create and link a project, deploy. Run again, it only deploys (see [Launching](#launching)). |
| `tokens create [--name] --scope <scope>… [--expires <days>] [--deploy --project <id>]`, `tokens list [--all]`, `tokens revoke <tokenId> --yes` | Personal access tokens and deploy tokens (see [Tokens](#tokens)). |
| `projects list [--deleted]` | The account's projects. |
| `projects create <name> [--slug] [--region eu\|us] [--engine d1\|postgres] [--parked\|--active] [--link]` | Create and provision a project. It takes an active slot if one is free, else a parked one; `--parked` or `--active` chooses. Postgres needs a plan that includes it, and an active slot. |
| `projects park`, `projects promote` | Move the project to a [parked](../api/projects.md#active-and-parked-projects) slot or back to an active one. Same ID, URL, data and deploys. |
| `projects archive --yes` | [Archive](../api/projects.md#archived-projects) the project: export it, keep the export 12 months, remove the live database, files and code, and free its slot. Owners only. |
| `projects restore [--active\|--parked]` | Restore a deleted project (within 30 days), or an archived one into a free slot with its database, users, files and last deploy. `--project` takes a deleted project's slug too. Owners only. |
| `projects delete --yes` | Delete the project. It stops serving at once and can be restored for 30 days. Owners only. |
| `export [--out <file.zip>]` | [Export](../api/projects.md#exporting-a-project) the project: one ZIP with a SQL dump, users with password hashes, files, the live code and config, and `SELF_HOST.md`. Prints a link that lasts an hour, or saves the ZIP with `--out`. Works while the account is read-only. Owners only. |
| `projects get`, `projects rename <name>`, `projects provision` | Show, rename, or finish provisioning a project. |
| `projects indexing <on\|off>` | Let search engines index the project's site, or stop them, from the next deploy ([quick publish](publish.md#search-engines)). |
| `sandbox create [--template] [--ref]` | Without signing in, a 24-hour [sandbox](../agents/sandbox.md): prints its URL, claim link and token (once). Signed in, a parked project in your account instead. |
| `sandbox claim <claim link or token>` | Move an unclaimed sandbox into your account, as a parked project. |
| `link <project>` | Link this directory to a project. |
| `init [dir] [--template] [--force]` | Write a [starter app](starter.md) into a new directory (see below). |
| `dev [--port] [--assets-dir]` | Run the project locally, with sign-in and its static files, routed as a deploy routes them (see below). |
| `deploy [--env] [--allow-destructive] [--force-insecure --confirm <slug>] [--commit] [--assets-dir <dir>] [--skip-build]` | Build, scan, migrate and deploy, with the jobs in `flatstack/cron.ts`; prints the URL. Your built frontend goes too: `--assets-dir`, or else `dist`, `build` or `public`, whichever exists first. Only files that changed since the last upload are sent ([how it's served](serving.md#your-sites-files)). A Next.js, Astro or React Router app is built with its Cloudflare adapter first and its pages render in the same Worker; `--skip-build` deploys the last build ([server-rendered apps](frameworks.md)). |
| `publish <dir> [--project] [--name]` | Publish a folder of static files as it is, with no build, to a new project or the one given; prints the URL ([quick publish](publish.md)). |
| `deploys list`, `deploys get <deployId>` | Deploy history and one deploy's status and scan. |
| `rollback [deployId]` | Put an earlier deploy back live; with no ID, the one before the live deploy. |
| `db diff [--name] [--engine]` | Write the next migration from `flatstack/schema.ts`. Local only. |
| `db migrate [--env] [--dry-run] [--allow-destructive]` | Apply pending migrations without deploying code. |
| `db extensions list`, `db extensions enable <name...>` | [Postgres extensions](postgres.md#extensions): which are on, and turning them on for every environment. `deploy` also enables the ones under `database.extensions` in `flatstack.config.ts`. |
| `env list/set/unset`, `secrets list/set/unset` | Variables and secrets, project-wide or for one environment with `--env`. |
| `auth settings` | How [your app's users sign in](auth.md): methods, email verification, OAuth providers, Turnstile, trusted origins, email templates and pages. Client secrets show only as set. |
| `auth set [--app-name] [--email-password on\|off] [--magic-link on\|off] [--email-otp on\|off] [--passkeys on\|off] [--email-verification required\|optional] [--trusted-origin <url>…] [--password-reset-page] [--invitation-page] [--template <kind=name>…]` | Change sign-in settings. Only the ones you pass change; an empty value puts one back to its default. |
| `auth providers set <google\|github\|apple\|microsoft> --client-id <id> [--tenant-id] [--app-bundle-identifier] [--keep-secret]`, `auth providers remove <provider> --yes` | Turn on sign-in with your own OAuth client, or turn it off. The client secret is asked for without showing it, or piped in (see [Secrets](#secrets)). |
| `auth turnstile set --site-key <key> [--keep-secret]`, `auth turnstile remove --yes` | Check sign-ups and sign-ins with Cloudflare Turnstile. The secret key is read as a client secret is. |
| `cron jobs`, `cron runs [--job] [--status] [--limit] [--cursor]` | [Cron jobs](cron.md) with their next and last run, and run history. Jobs change by deploying `flatstack/cron.ts`. |
| `logs [--env] [--since] [--level] [--search] [--limit]` | An environment's [logs](../api/logs.md), newest first: console output, requests, exceptions and cron runs, with secrets redacted. `--since` takes `15m`, `2h`, `7d` or an ISO time (default an hour); `--level warn` shows warnings and errors. |
| `domains list/add/remove` | Custom domains; `add` prints the DNS record to create. |
| `status` | The setup checklist: each step done or not, and the next one ([setup checklist](../api/activation.md)). |
| `usage [--project]` | This period's usage against each capped limit. |
| `billing [--open]` | Your limits now and a link to billing in the dashboard. |
| `addons list` | [Add-ons](../api/billing.md#add-ons) you can buy, with prices, and the ones you hold. |
| `addons buy <id> [--quantity <n>] [--project <id\|slug>] --yes [--open]` | Buy an add-on on the account's card. Without `--yes` it says the price and buys nothing. Packs print a Checkout link (`--open` opens it); always-on Postgres takes `--project`. Owners only. |
| `storage buckets`, `storage ls <bucket> [prefix]`, `storage url <bucket> <key>`, `storage rm <bucket> <key...>` | Browse, link to and delete files. |
| `import supabase`, `import lovable [--dry-run] [--env] [--project-url] [--s3-region] [--functions-dir] [--detach] [--no-write] [--overwrite]` | [Import](importing.md) a Supabase or Lovable Cloud project. Credentials come from `SUPABASE_DB_URL`, `SUPABASE_URL`, `SUPABASE_SERVICE_ROLE_KEY` (or the `SUPABASE_S3_*` keys), or a hidden prompt. `--dry-run` prints the report and writes nothing; otherwise it runs to the end and writes the generated files here, leaving files that already differ unless `--overwrite`. |
| `import clerk [--export <file.csv>] [--webhook <url>] [--dry-run] [--env] [--detach] [--no-write] [--overwrite]` | [Import](importing.md#clerk) a Clerk application's users, sign-ins, metadata and organisations. The secret key comes from `CLERK_SECRET_KEY` or a hidden prompt; `--export` is the user export CSV from Clerk's dashboard, which carries the passwords. |
| `import status [importId]`, `import report <importId> [--write]`, `import resume <importId>` | An import's progress, its `MIGRATION_REPORT.md` and files, and carrying on one that stopped. |

`flatstack <command> --help` lists a command's options. `flatstack --help --json` describes every command as JSON. `--api-url` or `FLATSTACK_API_URL` points the CLI at another API, such as a local one.

Commands for features that aren't available yet (`export`, `logs --tail`, `branch`, `codegen`, `db studio/shell/seed`) arrive with those features.

## Launching

```bash
npx flatstack launch my-app --json
```

`launch` does what `init`, `projects create --link` and `deploy` do, in one command that's safe to run again:

1. If the folder (`my-app`, or the current one) is empty or doesn't exist, it writes the [starter app](starter.md) into it. A folder holding only `.git` counts as empty.
2. If the folder isn't linked to a project, it creates one and links the folder to it. The name is `--name`, else the `name` in `package.json`, else the folder's name, and the subdomain is `--slug`, else made from the name. If that subdomain is taken, `launch` stops with `conflict` and suggests a free one in `details.suggestion`; pass it as `--slug`, or add `--yes` to take a free one like it.
3. If `package.json` has dependencies and there's no `node_modules` yet, it installs them, with the package manager the project's lockfile points to, else the one that ran `launch` (`npx`, `pnpm dlx`, `bunx`), else npm. The installer's output goes to stderr, so `--json` output stays one object. `--no-install` skips this step; if the install fails, `launch` stops with `deploy_failed` before deploying, and the folder stays linked so the next run carries on.
4. It deploys, as `flatstack deploy` does, and prints the URL.

Running it again in a launched folder only deploys. With `--json` it prints one object:

```json
{
  "projectId": "prj_…",
  "slug": "my-app",
  "url": "https://my-app.flatstack.app",
  "deployId": "dep_…",
  "created": true,
  "initialised": true,
  "warnings": [],
  "next": {
    "id": "add-domain",
    "label": "Add your domain",
    "command": "flatstack domains add <domain>",
    "mcpTool": "add_domain",
    "docs": null
  }
}
```

`created` and `initialised` say whether this run created the project and wrote the starter. `warnings` holds anything to tell the user, such as scan findings or a subdomain that was taken. `next` is explained under [Next steps](#next-steps).

Creating a project needs a free project slot on your plan; `flatstack whoami --json` shows how many are free. Without one, `launch` fails with `plan_limit_reached` before writing any files or creating anything: delete a project, buy the Extra project add-on or upgrade, or link the folder to an existing project with `flatstack link <project>` and run `launch` again. Errors and exit codes are the same as for the commands it stands for.

## Tokens

`flatstack tokens create` makes a [personal access token](../api/authentication.md#tokens) for CI or another agent and prints it once. Copy it then: it can't be shown again.

```bash
flatstack tokens create --name ci --scope deploy --scope env:read --expires 30d
```

- `--scope` grants one scope; repeat it or separate scopes with commas. You can only grant scopes your role has.
- `--expires` is the lifetime in days (`30d` or `30`); the default is 90 days and the most 365.
- `--deploy` makes a deploy token for one project instead (`--project`, else the linked one), carrying `deploy` unless you give other deploy scopes.

The CLI signs in with a token itself, so a token it creates can't carry a scope the CLI's token lacks, or expire after it. For a wider or longer-lived token, use the dashboard. Without `--json`, stdout holds only the token, so `flatstack tokens create --scope deploy > token.txt` captures just it; the reminder to copy it goes to stderr. The value is never logged or written anywhere else.

`flatstack tokens list` shows your own tokens and, if you can deploy, the account's deploy tokens, with their first characters but never their values. The one this CLI uses is marked. `--all` adds revoked and expired ones.

`flatstack tokens revoke <tokenId> --yes` revokes a token; it stops working at once. Revoking the CLI's own token signs it out.

## Secrets

`flatstack secrets set STRIPE_KEY` asks for the value without showing it. You can also pipe it in: `cat key.txt | flatstack secrets set STRIPE_KEY`. Avoid putting the value on the command line, where it ends up in your shell history. Secret values are never printed. Variables and secrets apply from the next deploy.

OAuth client secrets and the Turnstile secret key work the same way: `flatstack auth providers set google --client-id 123.apps.googleusercontent.com` asks for the client secret, or takes it piped in (`cat google-secret.txt | flatstack auth providers set google --client-id …`). There's no flag for it, so it never lands in your shell history. It's stored encrypted and never shown again, by the CLI, the API or MCP; `auth settings` shows it only as set. To change a client ID and keep the stored secret, add `--keep-secret`. Sign-in settings apply from the next deploy.

## Actions that can't be undone

`env unset`, `secrets unset`, `auth providers remove`, `auth turnstile remove`, `domains remove`, `storage rm`, `tokens revoke`, `projects archive` and `projects delete` need `--yes`. Without it they change nothing and fail with `confirmation_required`. A destructive migration needs `--allow-destructive`, and deploying past a critical [security finding](security-scan.md) needs `--force-insecure` and the project's slug: typed at the prompt, or passed as `--confirm <slug>` outside a terminal.

## Next steps

`login`, `launch`, `deploy`, `projects create` and a finished `import supabase`, `import lovable` or `import clerk` end with one line saying what to do next, such as `Next: deploy your app with flatstack deploy.` With `--json` the same step is in `next`: its `id`, `label`, the `command` to run, the `mcpTool` that does it and a `docs` link when the step happens in your code. The step is the earliest one still undone on your [setup checklist](../api/activation.md) (`flatstack status` shows it all), and `next` is `null` once every step is done. If the checklist can't be read, the command still succeeds and suggests only a step it certainly left undone (a new project hasn't deployed; a project `launch` just created has no domain).

## Output and errors

Without `--json`, results go to stdout as text and progress to stderr. With `--json`, stdout holds one JSON document: the result, or an error in the same shape the API uses:

```json
{ "error": { "code": "confirmation_required", "message": "…", "docsUrl": "…" } }
```

Codes are the API's (for example `unauthenticated`, `not_found`, `deploy_blocked_by_scanner`), so a script can branch on them. Exit codes: `0` success, `1` the command failed, `2` a usage mistake such as an unknown flag or a missing argument (`validation_failed`).

## Local development

`flatstack dev` runs your functions and routes on workerd, the runtime Cloudflare uses, bound the way a deploy binds them. It uses a local D1 database kept in `.flatstack/dev/`, applies `flatstack/migrations` when it starts and when they change, and rebuilds when anything in `flatstack/` changes. A build error is printed and the last good build keeps serving. Plain variables come from `.env` and secrets from `.dev.vars`, both `KEY=value` files.

[Sign-in](auth.md) works as it does when deployed: `flatstack dev` creates the sign-in tables in the local database before your migrations, as a deploy does, and serves `/auth/*`. It uses the settings a new project starts with (email and password, magic links, one-time codes and passkeys on, verification optional) plus your roles and hooks in `flatstack/auth.ts`. Settings you change in the dashboard, such as Google sign-in or Turnstile, apply to deployed environments only. Emails sign-in would send are kept instead: `GET /auth/fs/dev/outbox?to=<email>` lists them, newest first, with their links and codes. The key that signs local sessions is made on first run and kept in `.flatstack/dev/auth-secret`, so you stay signed in across restarts.

Static files come from the folder you pass with `--assets-dir`, or else the first of `dist`, `build` and `public`, as `deploy` picks them. They're routed as a deploy routes them: `/api/*`, `/fn/*`, `/auth/*`, `/storage/*`, `/realtime` and `/_fs/*` always reach your code, a file answers its own path, and the `hosting` block of `flatstack.config.ts` adds SPA fallback, redirects, rewrites and headers ([how your app is served](serving.md#your-sites-files)). Dev copies the files to `.flatstack/dev/site/` to serve them and doesn't add the default security headers.

A [server-rendered app](frameworks.md) is served from its last build (`.open-next`, `dist` or `build`), bundled with your functions as a deploy bundles it, with your variables and secrets by name and Cloudflare Images for resizing. Dev doesn't build it: run the build (`npx opennextjs-cloudflare build --skipWranglerConfigCheck`, or your `build` script), and dev reloads. For hot reload, use your framework's own dev server.

[File storage](storage.md) is kept locally in `.flatstack/dev/`, which also holds a Next.js app's ISR cache. Signed upload and download URLs and image transforms need a deployed project and answer `feature_unavailable`.

Not in `flatstack dev` yet: Postgres through PGlite (with the Postgres engine), the email capture UI for emails your functions send with `ctx.email`, and realtime.

## Starting a project

```bash
npx @flatstackdev/cli@latest init my-app
```

writes the [starter app](starter.md), a to-do list with sign-in, into `my-app/` and prints the next steps. `npm create flatstack@latest my-app` does the same through the `create-flatstack` package and takes the same options after `--`. With no directory it writes into the current one. The directory must be empty or not exist yet; `--force` writes into one that has files, replacing any with the same names. The template comes with the CLI, so nothing is downloaded. `--template` picks a template: `starter` (the default), `next` (Next.js with server actions, streaming, ISR and an AI SDK chat route), `astro` or `react-router` ([server-rendered apps](frameworks.md)). The new project's `package.json` asks for `@flatstackdev/server` and `@flatstackdev/cli` at the CLI's own version.

From there, `npm install`, `npx flatstack login` and `npx flatstack launch` put it live; `init` prints those steps. `launch` in an empty folder also writes the starter itself, so `npx @flatstackdev/cli@latest launch my-app` alone goes from nothing to a URL once you're signed in.