Skip to content

Repository files navigation

Divine Router

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.

What it does

  • 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/webfinger for acct:user@divine.video directly from the username KV store (RFC 7033 JRD), with proper 404s for unknown or inactive users.
  • NIP-05 — serves /.well-known/nostr.json on username subdomains for Nostr identity verification, reading pubkeys and relays from KV.
  • ATProto handle resolution — serves /.well-known/atproto-did on 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.

Routing

Host classification happens in classify_host. Requests are handled in this order:

Apex domains (divine.video, dvines.org)

  • 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.

System subdomains (sub.divine.video, sub.dvines.org)

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.

Username subdomains (alice.divine.video, alice.dvines.org)

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 is ready, 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-Host header so the web app can render the subdomain profile; unknown or inactive users get a 404 page.

Multi-level and unknown hosts

Deeper hosts (a.b.divine.video) and hosts outside the owned domains fall through to the main site backend.

NIP-05 verification

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"]
  }
}

Architecture

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-error default 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.

Getting started

Prerequisites

  • Rust 1.88.0 with the wasm32-wasip1 target (pinned in rust-toolchain.toml).
  • The Fastly CLI.

Build and test

cargo build --profile release --target wasm32-wasip1
cargo test

Run locally

fastly compute serve

The local server uses the KV fixture bound in fastly.toml (kv_usernames.json) and the local backend definitions.

Configuration

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-status
  • POST /v1/minor-review-cases/{caseId}/parent-contact
  • POST /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/providers
  • GET /api/sounds/trending
  • GET /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.

Deployment

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.

License

MIT


Part of Divine — your playground for human creativity · Brand guidelines

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages