A Fastly Compute edge router that fronts every *.divine.video and *.dvines.org
request. It inspects the host and path at the edge, decides whether the request is
for the main site, a platform service, or a user's subdomain, and either serves a
small response itself (WebFinger, NIP-05, ATProto DID) or forwards to the right
backend.
This is the public edge tier, separate from the GitOps-managed GKE stack. It is
published on its own as a Fastly Compute service, independent of
divine-iac-coreconfig.
- Subdomain routing — classifies each host as an apex domain, a reserved system subdomain, a username subdomain, or a deeper multi-level host, and routes accordingly.
- Per-service backends — sends media, invite, and API traffic to dedicated backends while everything else falls through to the main site.
- WebFinger — serves
/.well-known/webfingerforacct:user@divine.videodirectly from the username KV store (RFC 7033 JRD), with proper 404s for unknown or inactive users. - NIP-05 — serves
/.well-known/nostr.jsonon username subdomains for Nostr identity verification, reading pubkeys and relays from KV. - ATProto handle resolution — serves
/.well-known/atproto-didon username subdomains for users whose ATProto state is ready. - ActivityPub passthrough — forwards
/ap,/ap/*, and nodeinfo paths on the apex to the ActivityPub gateway. - Edge caching — bypasses cache for
/.well-known/*, ActivityPub, and WebSocket traffic, and applies a short cacheable TTL to public API GET requests.
Host classification happens in classify_host. Requests are handled in this order:
GET /.well-known/webfinger— answered at the edge from the username KV./ap,/ap/*,/.well-known/nodeinfo,/nodeinfo,/nodeinfo/*— forwarded to the ActivityPub gateway backend.- Everything else — passthrough to the main site backend.
Reserved single-level subdomains are routed by name:
| Subdomain | Backend |
|---|---|
media, blossom |
Blossom / media server |
invite |
Invite faucet service |
api |
Funnelcake API; on api.divine.video only, the exact mobile API routes and sound library paths below use the mobile API and sound proxy backends |
www, cdn, admin, support, relay, analytics, funnel, gateway, names, login, pds, feed, labeler |
Main site |
stream |
Retired. Router returns 410 Gone and does not passthrough. |
Any single-level subdomain that is not reserved is treated as a username:
/.well-known/atproto-did— returns the user's DID if their ATProto state isready, otherwise 404./.well-known/nostr.json— returns the user's NIP-05 record.- Any other path — looks the username up in KV. Active users are forwarded to the
main site backend with an
X-Original-Hostheader so the web app can render the subdomain profile; unknown or inactive users get a 404 page.
Deeper hosts (a.b.divine.video) and hosts outside the owned domains fall through to
the main site backend.
Requests to username.divine.video/.well-known/nostr.json return a standard NIP-05
document. The subdomain name is looked up in KV, and the response echoes the queried
?name= parameter (defaulting to the subdomain), which supports the
_@username.divine.video form:
{
"names": {
"_": "hex-encoded-pubkey"
},
"relays": {
"hex-encoded-pubkey": ["wss://relay.example.com"]
}
}The service is a single Rust binary (src/main.rs) compiled to WebAssembly and run
on Fastly Compute. It reads the request Host and path, classifies the host, and
then either builds a response in-process (WebFinger, NIP-05, ATProto DID, 404s) or
rewrites headers and forwards to a backend.
On passthrough it sets Host to the backend's expected hostname and overwrites
X-Original-Host, X-Forwarded-Host, and X-Forwarded-Proto with values derived
at the edge. X-Original-Host preserves the public hostname across downstream
proxy rewrites for exact-URL authentication such as NIP-98; overwriting it on
every backend prevents a client-supplied value from reaching trusted consumers.
Caching is decided per request:
/.well-known/*and ActivityPub paths on public Divine hosts, plus WebSocket upgrades, are passed uncached.- Public API GET requests on
api.divine.video(excluding/api/docs, authenticated, and WebSocket requests) are cacheable with a 30-second fallback TTL. - Other passthrough responses are cacheable; the Fastly 0.13
stale-if-errordefault is disabled so origin 5xx responses keep surfacing.
Username, NIP-05, WebFinger, and ATProto lookups all read the same Fastly KV store.
Backends are defined in fastly.toml.
- Rust
1.88.0with thewasm32-wasip1target (pinned inrust-toolchain.toml). - The Fastly CLI.
cargo build --profile release --target wasm32-wasip1
cargo testfastly compute serveThe local server uses the KV fixture bound in fastly.toml (kv_usernames.json) and
the local backend definitions.
Backends are declared in fastly.toml for both the local server and Fastly setup:
| Backend | Purpose |
|---|---|
main_site |
Main Divine web app |
username_handler |
Username / profile origin |
blossom |
Blossom media server |
invite_service |
Invite faucet |
funnelcake_api |
API origin (relay.divine.video) |
mobile_api |
Mobile-facing moderation and support identity API |
sound_proxy |
Sound library API (sounds.divine.video, a Cloudflare Worker) |
activitypub_gateway |
ActivityPub gateway worker |
On api.divine.video, the router sends only these public client contracts to
mobile_api; all other API traffic stays on Funnelcake:
GET /v1/account/moderation-statusPOST /v1/minor-review-cases/{caseId}/parent-contactPOST /api/zendesk/pre-auth
Their OPTIONS preflights follow the same route; wrong methods and every other
path stay on Funnelcake.
Also on api.divine.video only, these sound library paths go to sound_proxy:
GET /api/sounds/providersGET /api/sounds/trendingGET /api/sounds/{soundEventId}/videos
The rest of the /api/sounds namespace stays on Funnelcake — notably
/api/sounds itself, which is a live Funnelcake endpoint and the upstream the
proxy's own trending handler fetches, and /api/sounds/{id}/stats. Both
per-path lists are scoped by the canonical-host check in api_backend_for, so
api.dvines.org continues to route to Funnelcake in full.
/api/sounds/search also remains on Funnelcake until the sound proxy's search
provider credential is configured; widening the allowlist after that is a
separate, observable cutover.
Sound proxy responses bypass Fastly caching and rely on the Cloudflare origin's
own Cache-Control policy. This avoids layering the router's 24-hour
stale-if-error window over a separately cached origin.
Username records are read from KV under the key user:<username> with this shape:
{
"pubkey": "hex-encoded-pubkey",
"relays": ["wss://relay.example.com"],
"status": "active",
"atproto_did": "did:plc:...",
"atproto_state": "ready"
}Only records with status active are served. ATProto DID resolution additionally
requires atproto_state to be ready and atproto_did to be present.
Deploy with the Fastly CLI:
fastly compute publish --non-interactive && fastly purge --all[setup.backends] bootstraps new Fastly services but is not applied to this
already-associated service. Before deploying a package that first references a
new static backend, add that backend to the editable service version and verify
its address, host override, certificate hostname, and SNI hostname all target
the intended origin. Never activate code that names a backend absent from the
same service version.
Because username, WebFinger, and ATProto responses come from KV, publish this service
after the handle and ATProto state have been written by divine-name-server.
MIT
Part of Divine — your playground for human creativity · Brand guidelines