The full-stack Python framework designed for humans and agents. Follow for development updates. Follow @davegaeddert for more background.

Kansas
StreamingResponse bodies now run in the request's context. The generator queries the database on the same connection as the view, a cursor from Model.query.iterator() stays alive to the last chunk, and the request's trace covers sending the body.
13
Secrets in Plain are a type SECRET_KEY: Secret[str] `plain settings get` now masks them like `settings list` already did. Matters more with agents: everything a command prints lands in the transcript. `plain settings get --reveal` is the explicit ask
21
Plain now requires Python 3.14. The first payoff is sql() in plain-postgres: the SQL is the literal parts of a t-string, every interpolated value binds as a parameter, and passing a plain str is a type error.
28
Starting to rework some of the postgres APIs
1
22
A Plain package can now ship top-level `plain` commands that run without loading your app — a new entry point group that skips runtime setup. That matters exactly when the app is the problem: `plain env` still runs on a fresh clone with no encryption key, right when you need it.
1
15
We're getting rid of plain-redirection, for now. Agents are getting better by the day and the value prop of official framework packages is shifting — some things are better left to the user (agent). The 404 log was the valuable piece; that's solved by tracing now.
1
19
We recently renamed ORM internals across three releases, no deprecation cycle. Plain changelogs ship "Upgrade instructions" written for agents — /plain-upgrade reads the span you're crossing, fixes your code, and proves it with plain check.
35
plain docs cache --api prints the real signatures for the version you have installed. Docs ship inside each Plain package — your agent reads them instead of guessing from training data.
1
18
plain.loginlink: link expiration is now a class attribute — link_expires_in = 60 * 15 on your form, no method override needed. Docs now spell out the real semantics: links stay valid until expiry, deliberately, so mail scanners can't burn them before your user clicks.
16
The Plain CLI now works even when your app won't boot. `plain --help` still shows your commands, and `plain docs`, `plain install`, and `plain upgrade` don't load the app at all.
14
RedirectResponse refuses external URLs unless you pass allow_external=True. That check now blocks any scheme-prefixed URL: browsers normalize `http:/evil.com` to `evil.com`, so matching on `://` alone let single-slash URLs through.
1
30
Plain packages ship their own AI agent context. `plain agent install` syncs rules and skills from every installed package into your project's .claude/ — install plain.jobs and your coding agent learns how to write jobs.
1
31
Dev secrets can now live in git. `plain env set` encrypts a value into your committed .env.dev, one key per project. A fresh clone — or a hosted agent sandbox — just needs DEV_ENV_KEY and every dev credential comes with the repo.
1
28
A typing pass on Plain's ORM internals: `type: ignore`s replaced with real narrowing, `Field.concrete` removed, Meta's field collections collapsed to `fields` + `many_to_many`. The type checker kept pointing at design debt, so we removed the debt instead of ignoring it.
1
36
plain dev runs your whole local setup in one command: preflight checks, pending migrations, an auto-reloading server, Tailwind watch, and Postgres. It serves HTTPS by default with locally-trusted certs, so dev behaves like production from the first request.
1
2
33
Unique, check, and now foreign key violations in Plain all surface the same way: create()/update() attempt the write, Postgres rejects it, and you get a ValidationError on the field — "Author with id 5 does not exist." No pre-check SELECTs, no raced-duplicate 500s.
2
30
We found server connection errors dying on asyncio's default handler — a logger Plain never configured, invisible to any APM. 0.160.0 closes that: every exception in a Plain app lands in an OTel entry span or an ERROR log record. Both export. There is no third place.
1
36
plain portal gives you a Python shell into your production app over an encrypted tunnel. Starting one now requires an explicit --read-only or --read-write — no default. The database mode is right there in the command, so a human (or an agent) can judge it before it runs.
1
31
Response.status_code is read-only in Plain now. The status decides things everything downstream builds on — whether a body is allowed, how the response is framed on the wire — so it's fixed at construction. Renderers take it directly: self.render(form=form, status_code=422)
1
26
Every constraint in Plain is now checked at the write that violates it — never at COMMIT. FKs are NOT DEFERRABLE (deferral was a 2007 Django fixtures thing; Plain has no fixtures), and a bad FK in create()/update() is a ValidationError on the field, not a commit-time traceback.
2
68