Skip to content

Architecture

One process, four faces over one store. abilian_cdn.app.serve is the entry point a server runs; create_app builds the Litestar application it wraps.

Layers

app.py  settings.py  db.py  models.py  routes.py  delivery.py  client.py

core/      pure: paths, signing, sigv4, tokens, sessions, passwords, sync
policy/    who may do what, how much, how fast: auth, quotas, ratelimit
storage/   where bytes live: bucket, cache, blobs, objects, origin, reconcile
records/   what happened: audit, stats

api/       the token API        ┐
s3/        the S3 face          ├ the three faces, each a package
ui/        the browser          ┘
cli/       the command line

Dependencies point inward. A face may call storage, policy and records; none of them calls a face, and no face calls another. store_object lives in storage/ for that reason — the S3 face and the browser both need it, and having them import it from the token API made one face reach into another.

core/ imports nothing from the rest. Everything in it can be tested by calling it with arguments and looking at what comes back, which is why the boundary is worth keeping.

The write path

An upload passes through one function whatever face it arrived by:

  1. The body is spooled to a temporary file on the cache volume, refusing anything past the size limit or the quota.
  2. The file is uploaded to S3.
  3. It is moved into the local cache.
  4. The index row is written, with the audit entry, in one transaction.

storage/blobs.py owns that order. If the process dies between steps, the worst outcome is bytes in S3 that no row references — an orphan cdn reconcile can find. The other order produces a row pointing at nothing, which every reader trips over and no sweeper can repair.

The read path

Delivery is the one path that must stay out of the database's way.

  1. The zone is resolved from the Host header, or from the first path segment.
  2. A private zone's signature and expiry are checked.
  3. The local cache answers if it holds the bytes; otherwise they come from S3 or the origin and are kept.
  4. Range and conditional headers are honoured.
  5. The response is counted in memory. A background task writes the counters every few seconds.

No write to the database happens on a read. Delivery also never sets a cookie: every proxy between here and the reader treats Set-Cookie as "this is personal, do not cache it", which is the opposite of what a CDN is for.

Authorization

Decided once, at the request boundary, producing a Principal the rest of the request trusts. The rule that resolves a zone lives in policy/auth.py and is worded by each face — 404 and 403 in JSON for the token API, NoSuchBucket and AccessDenied in XML for S3 — so there is one rule and two vocabularies.

A zone belonging to another organisation is reported as missing rather than forbidden: whether a zone exists is not something an unrelated tenant should learn from the difference between two error codes.

Two faces on two hostnames

serve() returns the application unwrapped unless CDN_S3_HOST is set. With it, an ASGI wrapper rewrites requests arriving on that hostname into the /_/s3 namespace before routing, carrying the original path along so signature verification still sees what the client signed. Middleware runs after routing, which is why this sits outside the application rather than inside it.

Storage

holds durable
PostgreSQL the index, accounts, tokens, statistics, the audit log yes, back it up
S3 the bytes of every storage zone yes
the cache volume local copies and their sidecars no, disposable

Schema changes go through Alembic migrations and never create_all: a process that creates its own tables at boot diverges from the migration history, and nobody finds out until a restore.