Skip to content

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 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.

As Markdown: apps/security-scan.md