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: 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 withctx.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, setauth: "admin", or checkctx.user.rolebefore 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 usez.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
scanfield withblocking,countsby severity andfindings. The response toPOST /v1/projects/{id}/deploysalso hasscan.skipped, listing checks that couldn’t run, andwarnings, one line per finding. - Blocked deploys return
422 deploy_blocked_by_scannerwithdetails.deployIdanddetails.scan. They’re kept in the deploy list withstatus: "blocked", and in the audit log asdeploy.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.
As Markdown: apps/security-scan.md