# Security scan

Every deploy is scanned before anything goes live, whether it comes from the CLI, Git or MCP. A critical finding stops the deploy and your current version keeps serving. Everything else is reported and the deploy goes ahead.

## What is checked

| Check | Severity | What it looks for |
| --- | --- | --- |
| `frontend-secret` | Critical | Secret-looking strings in your built frontend: private keys, Stripe secret keys, AWS, OpenAI, Anthropic, GitHub, Slack and SendGrid keys, Flatstack tokens, database URLs with passwords, service-role JWTs, hard-coded `secret`/`password` values, and the value of any of your environment's secrets. Publishable keys, such as Stripe `pk_` keys and Firebase API keys, are fine. |
| `admin-without-role-check` | Critical | A function clients can call that uses `ctx.db.asAdmin()` without `auth: "admin"` or a check on `ctx.user.role`. |
| `public-write-table` | Critical | A table with `write: "public"`, or a write rule that always returns `true`. |
| `exposed-table-without-rules` | Critical | A table with `expose: true` and nothing limiting who reads or writes it: no `access` block and no `owner()` column, or rules that always return `true`. |
| `unauthenticated-db-write` | High | A function with `auth: "none"` that inserts, updates or deletes rows. |
| `weak-cors` | High | `*` as an allowed origin in production: in the project's CORS origins, in the `cors` block of `flatstack.config.ts`, from Hono's `cors()` without an `origin`, or an `Access-Control-Allow-Origin: *` header set by a route. Previews aren't checked. |
| `vulnerable-dependency` | High | An installed package with a critical vulnerability in the [OSV database](https://osv.dev): rated critical by GitHub, or with a CVSS v3 score of 9.0 or more. |
| `missing-input-schema` | Medium | A function with no `input` schema. |
| `ssrf-fetch` | Medium | A function or route that fetches a URL taken from the request, with no allowlist of hosts in sight. A fixed origin with a variable path is fine. |

Each finding names the file and line, says how to fix it, and links to its check on this page. Secrets are never shown in full: a finding shows the first few characters and the length, or the name of the secret it matched.

## Fixing the common ones

- **Secret in the frontend.** Treat it as leaked and rotate it. Then store the new value with `flatstack secrets set`, read it with `ctx.secrets.get()` in a function, and call the function from your app.
- **asAdmin() in a client-callable function.** Use `ctx.db.asUser()`, which applies your access rules. If the function really is for admins, set `auth: "admin"`, or check `ctx.user.role` before the admin query.
- **Public writes.** Use `write: "owner"` or `"authenticated"`, or a function that checks the caller.
- **Missing input schema.** Add `input: z.object({ … })`. A function that takes nothing can use `z.object({})`.

## Deploying anyway

If you need to ship with a critical finding, `flatstack deploy --force-insecure` asks you to type the project's slug. Over the API, send `"forceInsecure": { "confirm": "<project slug>" }`. A wrong or missing slug leaves the deploy blocked. Forced deploys are recorded in the audit log as `deploy.forced_insecure`, with the findings that were overridden.

## Where results appear

- **CLI:** printed after the build, with the findings and how to fix them.
- **API:** a deploy has a `scan` field with `blocking`, `counts` by severity and `findings`. The response to `POST /v1/projects/{id}/deploys` also has `scan.skipped`, listing checks that couldn't run, and `warnings`, one line per finding.
- **Blocked deploys** return `422 deploy_blocked_by_scanner` with `details.deployId` and `details.scan`. They're kept in the deploy list with `status: "blocked"`, and in the audit log as `deploy.blocked`.
- **Dashboard and MCP:** the same findings; MCP responses carry them in `warnings`.
- **Pull requests:** a comment with a table of findings.

## What the scan needs

`flatstack deploy` sends the scan's input with the bundle: your `flatstack/` sources and `flatstack.config.ts`, the text files of your built frontend (`dist/` or `build/`), and your installed packages with their versions, read from `package-lock.json`, `pnpm-lock.yaml` or `node_modules`. If you deploy through the API without them, the checks that need them are listed in `scan.skipped` and the deploy goes ahead. If the OSV database can't be reached, the dependency check is skipped the same way.

Rollbacks aren't scanned again: they put back code that was scanned when it was first deployed, and keep that deploy's findings.