Divine Relay Manager is the internal content-moderation console for Divine's Nostr
relay infrastructure. Moderators use it to review reports, drill into the surrounding
conversation, run the minor-safety age-review workflow, and take relay-level moderation
actions. It ships as two deployables: a React/Vite single-page app on Cloudflare Pages
and a Cloudflare Worker API that signs and issues NIP-86 relay-management commands.
The app deploys under the name divine-relay-admin.
- Reports queue — the default view. Loads NIP-56 kind-1984 reports and, for each, pulls the reported event, its thread ancestors and replies, the target's profile and recent activity, and the reporter's filing history into a single review screen.
- Moderation actions — ban/allow events and pubkeys through the Worker's NIP-86 RPC. Actions record an append-only decision log and sync linked Zendesk tickets as a side effect.
- Bulk moderation — an async job model (enqueue, queue-consumer, status polling) for actioning an account's media at scale via Cloudflare Queues.
- Age review — the minor-safety workflow, including the Greenlight consent-funnel dashboard, protected-minor visibility, minor-account onboarding, and keycast integration.
- Auto-hide — a
ReportWatcherDurable Object holds a persistent relay subscription to kind-1984 events and auto-hides high-priority reports from trusted apps unless a human has already reviewed the target. Reports against a Divine list (people or video) are never auto-hidden; they are left to a moderator. - Media preview — extracts media URLs from events and, when direct load fails, falls back to an authenticated Blossom admin proxy so moderators can view blocked content.
- AI context — Hive AI and AI-detection reports, transcript analysis, scene classification, and Claude-powered user summaries to speed up review.
- Events, Users, and Labels views — browse relay events, inspect users, and publish NIP-32 (kind-1985) moderation labels.
Two components deploy and version independently:
Frontend (React/Vite on CF Pages) Worker (Cloudflare Workers)
| |
|-- CF Access (edge auth) -------------->|-- CF Access (edge auth)
|-- direct relay WebSocket reads |-- verifyAdminAccess() (defense-in-depth)
| (useThread, useAuthor, useUserStats) |-- handleModerate() --> relay NIP-86 RPC
| |-- Zendesk ticket sync
v |-- media proxy (Blossom)
Nostr relay (Funnelcake) |-- ReportWatcher (Durable Object)
v
D1 (SQLite): decisions, targets, tickets, jobs
Cloudflare Queues (bulk moderation)
The frontend reads directly from the relay over WebSocket for event, thread, and profile
data, and calls the Worker only for privileged actions. It's built with React 18, TypeScript,
Vite, TailwindCSS 3, shadcn/ui (Radix primitives), TanStack Query 5, and React Router. Nostr
integration uses @nostrify/nostrify, @nostrify/react, and nostr-tools.
The Worker holds the admin signing key and issues NIP-86 relay-management commands
(signing kind-27235 auth events). It persists moderation state in D1, runs the auto-hide
ReportWatcher as a Durable Object, and processes bulk jobs off a Cloudflare Queue. Relevant
NIPs: NIP-11 (relay info), NIP-19 (bech32 identifiers), NIP-32 (labels), NIP-56 (reports),
NIP-86 (relay management), and NIP-98 (HTTP auth).
Access is layered: Cloudflare Access enforces edge auth on both the frontend and API domains,
and the Worker independently re-verifies admin access (verifyAdminAccess()) as defense in
depth. Domains and bindings live in the wrangler.*.toml files and env files — treat those as
the source of truth rather than hardcoding URLs.
Requires Node.js 22.
npm run dev # install deps and start the Vite dev server (http://localhost:5173)
npm run test # type-check, lint, run Vitest, and build
npx tsc -p tsconfig.app.json --noEmit # type-check onlyWorker development lives under worker/:
cd worker
npm run dev # wrangler dev --config wrangler.local.toml
npm run test:run # worker unit tests
npm run typecheck # tsc --noEmitFor the full local stack (Worker + Caddy HTTPS proxy + Vite), run ./scripts/dev-local.sh.
It requires mkcert and caddy (brew install mkcert caddy, then sudo mkcert -install)
and reads local CF Access and admin-key values from .env.local and worker/.dev.vars. The
HTTPS proxy exists because the frontend runs over TLS and browsers block an HTTPS page from
fetching an HTTP endpoint.
There are two separate variable systems; they must stay in sync.
Frontend (Vite). Non-secret production and staging URLs are committed in .env.production
(VITE_PROD_RELAY_URL, VITE_PROD_API_URL, VITE_STAGING_RELAY_URL, VITE_STAGING_API_URL).
Copy .env.example to .env.local (gitignored) for secrets and local overrides:
| Variable | Purpose |
|---|---|
VITE_CF_ACCESS_CLIENT_ID |
CF Access service-token ID for reaching Access-protected APIs |
VITE_CF_ACCESS_CLIENT_SECRET |
CF Access service-token secret |
VITE_LOCAL_RELAY_URL / VITE_LOCAL_API_URL |
Optional local-dev endpoint overrides |
VITE_ADMIN_API_KEY |
Optional; local admin auth for ./scripts/dev-local.sh |
VITE_ values must be plaintext at build time, so they come from env files, not the encrypted
Pages dashboard secrets.
Worker. Vars (RELAY_URL, CDN_DOMAIN, ALLOWED_ORIGINS, MANAGEMENT_PATH,
MODERATION_SERVICE_URL, and others) live in worker/wrangler.*.toml. Secrets are set via
wrangler secret put or the Cloudflare dashboard and include NOSTR_NSEC (admin signing key),
ANTHROPIC_API_KEY, the CF Access and Zendesk credentials, and BLOSSOM_WEBHOOK_SECRET.
Secrets Store bindings resolve via await binding.get(), not as plain strings. Never commit
secrets. The wrangler configs are the authoritative list of vars and bindings per environment.
The frontend deploys to Cloudflare Pages and the Worker to Cloudflare Workers. There is no
root worker wrangler.toml — each Worker environment has its own config, and you must pass
--config explicitly. The root wrangler.toml configures only the Pages build output.
Root public/ contains Vite static source files; npm run build emits the deployable frontend
artifact to dist/, including the SPA 404.html copy. The API Worker serves no static assets
and has no asset binding.
Deploy the Worker first, then the frontend back-to-back, because the two version independently and there is no version negotiation between them:
# 1. Worker (staging, verify, then prod)
cd worker
npx wrangler deploy --config wrangler.staging.toml
npx wrangler deploy --config wrangler.prod.toml
# 2. Frontend (Cloudflare Pages)
npx vite build
npx wrangler pages deploy dist --project-name divine-relay-admin --branch mainBulk moderation depends on a Cloudflare Queue that must exist before the first Worker deploy —
see DEPLOYMENT.md for queue creation, deploy ordering, and rollback details.
CI (.github/workflows/test.yml) runs the root test suite plus the Worker typecheck and tests
on every push and pull request to main. A separate workflow enforces Conventional Commit PR
titles.
Part of Divine — your playground for human creativity · Brand guidelines