Security

How the security works.

An app on @popcomputer/web runs on Cloudflare Workers, so there’s no server for you to patch, and its database has no address on the internet. The framework turns on the protections web apps most often get wrong, and makes the safe way to write each feature the easy one, for you and for your AI.

There’s no server to look after

Most break-ins start with the machine under the code: an old operating system, an open port, a plugin nobody updated. An app on Workers doesn’t have one of its own. Cloudflare runs everything under your code, and keeps the engine your app runs on up to date, usually within a day of a fix.

Each deploy is a fixed copy of your code. A running app can’t rewrite its own files, so there’s nowhere for a backdoor to hide, and an uploaded file is kept as data, never run as code.

A database nobody else can reach

Your data lives in Cloudflare D1. It has no address on the internet and no admin page beside it: only your app and your Cloudflare account can reach it.

Queries are built with Drizzle, which sends values apart from the SQL, so a form field can’t change what a query does. A lint rule reports SQL built by joining strings, before the code is committed.

On by default, in every app

These are the framework’s, so a feature can’t forget them. Each can be loosened on purpose, and a lint rule asks why when it is.

  • Forged requests refused: a form sent from another site is turned away.
  • Password guessing slowed, for each address and each account.
  • Every form checked against its schema before your code sees it. A field the schema doesn’t declare never reaches the database.
  • Text shown as text, so what a visitor types can’t run as a script.
  • Uploads accepted by type and size, and served as what they are. A file that could run, such as HTML or SVG, is sandboxed.
  • Other sites can’t frame your pages, and the browser is told not to guess a file’s type.

Private records stay private

In most apps, every page has to remember to check who’s asking. Forget once and anyone can read someone else’s record by changing a number in the address. Here, the route that names a record also says who may load it, and the lookup itself enforces it. Another person’s record is the same “not found” as one that doesn’t exist.

Passwords and sessions come from Better Auth, which hashes every password. Two-step sign-in, magic links and Sign in with Google are there when you want them.

Safe file uploads, on Workers and R2

An upload goes wrong in three ways: a file far bigger than you meant to take, a type you didn’t mean to take, or a page with a script in it that your app serves back from its own address, where the script runs as if your app wrote it.

Each form’s schema names the file types it accepts and the largest size. A family such as “any image” leaves out SVG, which a browser can run script in. A request over 1 MiB is refused before it’s read, unless that form raises its limit, and Cloudflare caps a request’s body at 100 MB on its Free and Pro plans.

Keep each file in R2, Cloudflare’s file storage, under a name your app makes, never the one it arrived with, and check who may read it as you would any record. When your app serves it back, the browser is told its type and not to guess. Images other than SVG, PDFs, plain text, JSON, audio and video are shown as they are; anything else, such as HTML or SVG, is sandboxed, so a script inside can’t act as your app. Validating input and Serving an upload back show how.

Rate limits on sign-in attempts

A limit that counts each address doesn’t stop someone guessing one account’s password from thousands of addresses, and every guess costs your app a password hash. The framework counts both ways, cheapest first:

  • A Cloudflare Workers Rate Limiting binding counts each address’s attempts to sign in, sign up or check a password, and each account’s sign-ins, at Cloudflare’s edge, without writing anything.
  • Your database counts each account’s sign-ins before the password is checked, so guesses sent at once count against each other. After 10 failures, that account’s sign-in is refused for a minute, doubling with each further failure up to 15 minutes.
  • It’s a cooldown, not a lock: someone who knows an address can slow its owner down, never shut them out. A refused attempt gets a 429 that says when to try again. Limiting password guessing has the settings.

Audited, and fixed in the open

The framework has had two security audits so far. 1.5.0 fixed everything the first found, and 1.6.0 everything the second found. Each fix was proven by re-running the audit’s own reproductions against the release before and the release after. The changelog lists every finding.

1.7.0 fixed four more, in the state the framework keeps between requests: guesses sent at once that a sign-in count let past, Back in one tab showing a previous member’s pages, an emailed sign-in link a mail scanner used up, and a sign-in redirect that left the site. Models of that state, checked over every order of their steps, and a fuzz suite that runs sequences of steps against the real code found them. Each fix comes with tests that fail on 1.6.0.

The security tests also attack the framework the way known bugs attacked other frameworks. The vulnerabilities it’s tested against lists each one, with the test that tries it.

What’s still yours to get right

No framework makes an app unhackable, and this one doesn’t claim to. It takes away the server, and the mistakes that are easy to make in every feature. What’s left is yours:

  • Your app’s own rules, such as who counts as an admin.
  • What data you collect, and who you share it with.
  • Your Cloudflare and GitHub accounts: turn on two-step sign-in for both.
  • Reading what your AI changes before you deploy it. Vibe coding safely covers how, with a checklist for any stack.

Questions people ask

Is an app built with AI secure?

It depends on what the AI builds on. If every page has to remember its own checks, an AI will eventually forget one. On @popcomputer/web, the checks that matter most belong to the framework, and the type checker and lint rules report the usual mistakes before the code is committed.

How do I secure file uploads on Cloudflare Workers and R2?

Accept only the types and size you need, keep each file in R2 under a key your app makes, check who may read it before serving it, and sandbox anything a browser could run, such as HTML or SVG. On @popcomputer/web, the form’s schema names the types and size, and serving a file back sets its type and sandboxes the rest. Serving an upload back shows how.

How do I rate limit sign-in attempts on Cloudflare Workers?

Count by address and by account. A Workers Rate Limiting binding counts at Cloudflare’s edge, but its limits are approximate and kept apart in each Cloudflare location, so a count that holds for each account belongs in your database. On @popcomputer/web, one setting does both: Limiting password guessing shows it.

Do I need to set up HTTPS?

No. Cloudflare serves every app over HTTPS, on its workers.dev address or on your own domain, and renews the certificate for you.

Where are passwords kept?

In your app’s own database, hashed by Better Auth, so they can’t be read back. Nobody, you included, can see a member’s password.

Can I add two-step sign-in?

Yes. Better Auth’s two-factor plugin adds it, and the framework’s docs show how, as they do magic links and Sign in with Google.