Reference file: architecture.md
Architecture
Tempest is a Phoenix application that implements AT Protocol PDS behavior through small contexts: XRPC routing, accounts/auth, identity, repository storage, blobs, sync, and operator tooling.
Concepts
A PDS hosts user repositories. Each account has a DID, a mutable handle, a signed repository, and blob data referenced by records. Other services discover where a repo lives through the account's DID document and then read repo data through XRPC sync endpoints or the firehose.
XRPC is the HTTP RPC layer. Repository data is content-addressed and exported as CAR. Firehose events announce changes after they are durably sequenced.
Key protocol objects:
- DID: stable decentralized identifier for an account.
- Handle: mutable human-readable name that resolves to a DID.
- Repo: signed personal data store for one DID.
- MST: Merkle Search Tree used to represent current repo records.
- CID: content identifier for blocks.
- TID: timestamp identifier used for sortable record keys and revisions.
- CAR: content-addressed archive used for repo/block export.
Request flow
AT Protocol client
│
▼
TempestWeb.Endpoint (Bandit)
│
▼
TempestWeb.Router
├─ /xrpc/:method -> XrpcController
├─ /xrpc/...subscribeRepos -> FirehoseController / WebSocket
├─ /.well-known/atproto-did -> WellKnownController
├─ /account/* -> OperatorAccountController
├─ /admin/* -> AdminController
└─ / -> HomeLive
│
▼
Contexts
├─ Accounts -> account.sqlite via Ecto
├─ Identity -> DID docs, handles, signing keys
├─ Records -> per-DID repo SQLite files
├─ Blobs -> account.sqlite metadata + local bytes
├─ Sync -> CAR/block/blob reads
├─ Security -> MFA, email tokens, grants, event log
├─ Admin -> status, repo ops, backups
└─ Sequencer -> sequencer.sqlite + PubSub fanout
Controllers should not build commits directly. They validate/dispatch, then contexts handle storage, signing, sequencing, and response data.
Storage shape
Tempest uses SQLite in three roles:
| System | Storage | Contents |
|---|---|---|
Ecto + ecto_sqlite3 |
account.sqlite |
accounts, sessions, keys, auth, blob metadata |
Raw Exqlite.Sqlite3 |
repos/<did>.sqlite |
blocks, records, commits, MST metadata |
Raw Exqlite.Sqlite3 |
sequencer.sqlite |
global event log / firehose cursor |
Repo databases are per-DID because accounts are created dynamically and each repo has a lifecycle independent of ordinary account metadata.
Write cycle
A record write (createRecord, putRecord, deleteRecord, or applyWrites)
performs a complete repo commit cycle:
- Load current MST entries from repo storage.
- Insert, replace, or delete the record entry.
- Rebuild the MST for correctness.
- Sign a new commit block with the account signing key.
- Store new CBOR blocks.
- Update the record index and repo metadata.
- Write a durable sequencer event.
- Fan out the event to live firehose subscribers.
Tempest currently rebuilds the MST from scratch on writes. That is a deliberate correctness-first tradeoff.
Key modules
| Module | Role |
|---|---|
Tempest.Xrpc.Registry |
XRPC method table and dispatch metadata |
Tempest.Accounts |
accounts, sessions, auth contexts, app passwords |
Tempest.Security |
security inventory, MFA, email tokens, delegated access |
Tempest.Admin |
admin status, compatibility status, repo and backup helpers |
Tempest.Identity.KeyStore |
account signing key generation and storage |
Tempest.Records |
record write/read boundary |
Tempest.RepoStorage |
per-DID SQLite repository storage |
Tempest.RepoCore.Mst |
Merkle Search Tree implementation |
Tempest.RepoCore.Drisl |
deterministic DAG-CBOR subset |
Tempest.RepoCore.Car |
CAR v1 archive encoding/decoding |
Tempest.RepoCore.Commit |
signed repo commit blocks |
Tempest.Blobs |
blob metadata, reference lifecycle, GC |
Tempest.Sequencer |
durable event log and PubSub fanout |
Tempest.Lexicon.Validator |
runtime Lexicon validation |
Auth shape
Legacy sessions use two credentials:
- access token: short-lived
Phoenix.Tokentoken - refresh token: opaque random token stored only as a SHA-256 hash
Refresh tokens rotate. Reuse of an old rotated refresh token revokes the token family. OAuth and app passwords share the centralized permission boundary added for modern atproto clients.
Supervision
Tempest.Supervisor
├── TempestWeb.Telemetry
├── Tempest.Repo
├── DNSCluster
├── Phoenix.PubSub
└── TempestWeb.Endpoint
Firehose WebSocket connections are spawned by the web server and subscribe to PubSub for live events after durable sequencer insertion.
Verification
hurl --test --variable base_url=http://localhost:4000 test/smoke/health.hurl