AppView development
The reference AppView lives in colibri-social/appview. It’s a Rust service built on Rocket, talking to PostgreSQL through SeaORM and to the firehose through Tap. See the architecture page for how the pieces fit together, and the specification for what the endpoints have to do.
Running it
Section titled “Running it”-
Clone it and create your configuration:
Terminal window git clone https://github.com/colibri-social/appview.gitcd appviewcp .env.example .env -
Fill in
.env. For the dev compose file the local values are:DATABASE_URL=postgres://root:colibri@127.0.0.1:5432/colibriTAP_HOSTNAME=127.0.0.1:2480TAP_ADMIN_PASSWORD=<anything, Tap reads the same variable>RUST_LOG=colibri_appview=debug,rocket=infoGenerate the two keys the AppView refuses to start without:
Terminal window openssl rand -hex 32 # K256_PRIVATE_KEYopenssl rand -base64 32 # CREDENTIAL_ENCRYPTION_KEYPDS_LOCandAPPVIEW_HANDLE_DOMAINare also checked at boot. See Running a PDS for what to put there. -
Start PostgreSQL and Tap, plus a local PDS if you want to create communities:
Terminal window docker compose -f docker-compose.dev.yml up -d# ...with a local PDS + PLC directory (see Running a PDS):docker compose -f docker-compose.dev.yml -f docker-compose.pds.dev.yml up -dPostgres is published on
5432and Tap on2480. Theappservice in the dev file sits behind theappCompose profile, so it stays down and you run the AppView yourself. -
Run the AppView. Migrations are applied on boot, so there is no separate step:
Terminal window cargo runIt listens on
http://127.0.0.1:8000, prints a banner at/, and serves the API under/xrpc/.
Two quick checks that it came up correctly:
curl http://127.0.0.1:8000/xrpc/social.colibri.server.describeServercurl http://127.0.0.1:8000/.well-known/did.jsonThe first should report "software": "colibri-appview". The second is the DID document derived from APPVIEW_DID and K256_PRIVATE_KEY.
Configuration that matters locally
Section titled “Configuration that matters locally”.env.example documents every variable, these are the ones that change how the service behaves while you develop.
| Variable | Effect |
|---|---|
RUST_LOG |
Rocket and AppView log filters. colibri_appview=debug is what you want while working on handlers. |
DATABASE_MAX_CONNECTIONS |
Pool ceiling (default 20, .env.example suggests 40). Keep it comfortably above TAP_WORKERS, since every Tap worker holds a connection while it writes. |
TAP_WORKERS |
How many workers process firehose events. Lower it if Tap floods your machine. |
REFILL_FROM_SCRATCH |
Set to any non-empty value to clear the local record_data cache at boot and have Tap re-backfill every DID the AppView knows about. A recovery knob, not something you want on by default. It’s slow, and destructive against a local PDS, whose repos Tap cannot re-fetch. |
HUMMING_ENABLED |
Set false to stop the AppView from opening outbound connections to other AppViews (see Humming). Sensible for local work. |
VAPID_*, FCM_SERVICE_ACCOUNT_JSON, KLIPY_API_KEY |
Optional. Unset means background Web Push, Android push and the GIF picker are disabled, which the AppView says clearly in its boot logs. |
SFU_ANNOUNCED_IP |
Only needed if you test voice from another device, 127.0.0.1 is fine on one machine. |
Source layout
Section titled “Source layout”Directorysrc
- main.rs Rocket setup, boot-time invariant checks, route mounting
Directorylib/ the bulk of the logic
- …
Directoryxrpc/ one module per lexicon method
- …
Directorymodels/ SeaORM entities for the AppView’s own tables
- …
Directorymigrations/ SeaORM migrations, applied automatically on boot
- …
- well_known.rs the
did:webdocument - sfu.rs the mediasoup voice SFU
Directoryscripts/ operational helpers for community credentials
- …
Directorytests/ fixtures for the unit tests that live next to the code
- …
A new endpoint is usually: a handler module under src/xrpc/social/colibri/..., its route registered in main.rs, plus an entry in the AppView specification so other implementations can follow.
Migrations
Section titled “Migrations”Migrations are plain SeaORM migration structs in src/migrations/, named m<UTC timestamp>_<description>.rs. Adding one means creating the file, then registering it in src/migrations/mod.rs twice: once as a mod declaration and once in the migrations() vector. Order in that vector is the order they run in, so append rather than insert.
They run inside the AppView’s boot sequence, which means a failing migration prevents startup. There is no separate migration binary to invoke.
Tests, formatting, linting
Section titled “Tests, formatting, linting”CI runs three jobs, all of which you can reproduce locally:
cargo fmt --all --checkcargo clippy -- -D warningscargo test --all-featurescargo test also runs tests/lexicons.rs, which checks every route against
the lexicons vendored in lexicons/. It reads files only, no database, no
network, and fails just when a route would actually break a client, so a
lexicon that has not caught up with the AppView is reported rather than
rejected. Run ./scripts/sync-lexicons.sh to refresh the vendored copy,
Testing covers the rules and the escape hatch in
full.
src/lib/response_snapshots.rs pins what the response structs actually
serialise to, against tests/fixtures/responses/, and checks each one still
carries every property its lexicon marks required. It lives in src/ because
a binary crate’s tests/ cannot import crate types. After a deliberate shape
change, regenerate with UPDATE_FIXTURES=1 cargo test and review the diff.
Unit tests live in #[cfg(test)] modules next to the code they cover. Handlers are written against injected function seams (create_with takes create_account_fn, create_record_fn, and friends) precisely so they can be tested without a PDS or a database, wiremock stands in for HTTP services where a seam isn’t enough. When you add a handler, prefer that shape over reaching for the real thing.
Working without a PDS
Section titled “Working without a PDS”PDS_LOC is only used when a community is created, deleted, or migrated. It’s merely checked for presence at boot. Everything else (login, reading and writing messages in an existing community, notifications, voice) works with a placeholder value. If community creation isn’t what you’re working on, that’s the cheapest setup. Otherwise run the local PDS overlay: Running a PDS.
Write-through indexing
Section titled “Write-through indexing”Read endpoints serve from the local record_data index, which Tap fills from the firehose. But every write the AppView makes on a community’s behalf, including the bootstrap records in community.create, is also indexed as it lands, via community_write::cache_upsert. Two things follow:
- A new community, channel or role is readable the moment the call returns, rather than after firehose latency.
- The upsert is keyed on
(did, nsid, rkey), the same unique key Tap’s indexer writes through, so a later firehose delivery of the same record is a no-op.
When you add a write path, follow that pattern: it’s what makes a local PDS (whose repos Tap can’t reach) usable at all.
Operational scripts
Section titled “Operational scripts”scripts/ holds two helpers for the credentials the AppView stores encrypted in community_credentials:
import-community-credentials.shinserts a community’s PDS credentials into a deployment, encrypting them with that deployment’sCREDENTIAL_ENCRYPTION_KEY. The manual counterpart toregisterCredentials. It does not register the DID with Tap, so records that aren’t inrecord_datayet won’t be backfilled.get-community-password.shis the read side: it decrypts and prints a community’s app password.
Helpful tips
Section titled “Helpful tips”Below, you’ll find guides on situations you might run into.
Resetting local state
Section titled “Resetting local state”In certain cases, you may want to start clean on your local state.
- If you use an external PDS, not the local one: use
get-community-password.sh(see Operational scripts) to extract the passwords for the communities and save them for later. - Stop the appview, then stop the docker compose if it is running.
- Delete the volume associated with the compose file.
- Start the compose file
- Start the appview
- Wait for backfill to complete
See also
Section titled “See also”- AppView Architecture: services, sizing, and Humming.
- AppView Specification: what every endpoint and event must do.
- Self-Hosting: running an instance for real users.