Two halves of the same evening.
Uploads were one-way. A `type: file` field put an object in the bucket
and wrote its key into the record, and nothing ever read it back - a
desk showed `0aba5734-...-drawing.pdf` as a string with no link, so a
builder could receive drawings and never open one.
`GET /attachment/{bucket}/{record}/{field}` streams it. The key is not
the credential and is not taken from the caller: it is read out of the
record named, so a file is reachable only through a record somebody is
allowed to see, and a key that leaks off a desk is a link to nothing.
And there was no rate limiting anywhere. A burst of 30 per caller IP,
refilled one every two seconds, outside the session layer so anyone
over it is turned away before a handler is entered. Well above a
person filling in a form - a page load fetches several resources at
once - and well below being a nuisance. PORTAL_RATE_BURST and
PORTAL_RATE_SECONDS retune it; either 0 turns it off for an instance
behind something that already does this.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
portal documentation
Portal turns a Git repo of YAML into a running site. Almost everything here is about that YAML: what a content repo may say, and what the engine does with it.
| Architecture | What the engine is made of and where each piece lives in the source. |
| Content reference | Every key a content repo may write, and what it does. |
| State machines | aggregates.yaml: stages, who moves a case, mail, deadlines, memberships. |
| Access | How the Cedar policy is compiled from content, and who may ask it what. |
| Operations | Releasing, deploying, adding a site, environment variables. |
| Filesystem routes | Why the questions/ tree is the router. |
Two repos in the same family carry their own docs: