The open social layer. Own your community.
Warning
Agora is in beta. The backend is feature-complete and validated, but APIs, schema, and
deployment details may still change ahead of a 1.0 release. Pin a version, expect occasional
breaking changes, and please report anything you hit.
A working social app — sign in, browse the feed, comment, react, semantic-search, and chat in
realtime — all driven by the agora-sdk against a live
Agora backend.
Agora is an open-source, self-hosted community & social backend that's 1:1-compatible with the Replyke SDK, built on Supabase.
The Replyke SDK is excellent — thoughtful, typed, and battle-tested. Agora is an independent,
open-source backend that speaks the same API, so the agora-sdk
(a repointed fork of the Replyke SDK) talks to your server instead of api.replyke.com —
byte-for-byte the same REST paths, response shapes, auth semantics, and socket.io events. You get
the Replyke SDK's opinionated feature set (posts, threaded comments, reactions & feeds, follows &
connections, nested spaces, real-time chat, notifications, moderation & stewardship, semantic search)
and run it all on infrastructure you control, under a genuinely open license.
AGPL-3.0 — and that's the whole point. You own the backend, the data, and the code. No vendor lock-in, no per-seat pricing, no data leaving your project, and no "open-source" asterisk. The community edition is AGPL-3.0 and always will be.
Supabase hands you ~40% of a social backend for free: Postgres, Auth (GoTrue), Storage, Realtime infrastructure, and pgvector. The other ~60% — the social schema, the denormalized counts, the permission model, and the opinionated endpoints that sit in front of all of it — is what makes real communities work. That 60% is what Agora builds, and you own it: open source you control, privacy and security by default, data stays yours, and zero vendor lock-in.
Most community backends ship content and social primitives and leave governance to you: you build the reporting flow, the moderation dashboard, and any conflict-resolution process yourself, against whatever the hosted API exposes. A real community can't run without these, so Agora makes them core surface area — endpoints, schema, and admin UI in the box, enforced at the server trust boundary rather than reconstructed per app:
- Moderation — report queues for entities, comments, and chat messages; server-enforced removed-content hiding (a removed row is omitted from every list, 404'd on single reads, and filtered inside the semantic-search RPC — operators bypass to review); space-scoped moderator roles plus a project-wide operator god-view; and AI Agent Moderator that assesses all new content for inappropriate violations (configurable categories, confidence thresholds) and either auto-hides or flags for human review depending on the AI score (tunable per-project in Settings) — escalates to the Stewardship caseload for conflict resolution.
- Stewardship — a distinct conflict-resolution layer: moderation judges content; stewardship
tends people and relationships. A DB-granted steward role between member and operator; a
caseload that moves a dispute (complainant ↔ respondent over some content) through
open → in_mediation → closedwith transformative-ordered outcomes (repair → separation → protection → escalation); an asymmetry / "targeting" flag for power-aware, anti-false-balance handling; private mediation channels — built on the existing chat — to actually talk a conflict through (1:1 caucus with each party, or a consensual joint room in hybrid mode, never for a targeting case); configurable participant notifications (power-aware / symmetric / resolution-only) that keep the parties informed without ever leaking who raised a case; an append-only case timeline; and escalate-to-removal that takes the subject content (post, comment, or chat message) down through the moderation path. All of it operator-tunable per project. Seedocs/STEWARDSHIP.md.
Both live behind the same /v7/:projectId/... contract and in the Postgres schema, so they're available
to every client from day one — not gated behind an external service's limits.
A community backend that keeps everyone's direct messages in readable plaintext is a breach waiting to happen: one database leak, one subpoena, one over-broad admin, one "smart" feature that quietly reads DMs, and the trust is gone. Storing readable private messages is the bad default we refuse to ship — so Agora's secure chat is end-to-end encrypted, and is built so the server cannot read it.
Secure Chat implements MLS (RFC 9420) via
ts-mls. Neitherts-mlsnor Agora's integration around it has received an independent security audit. The design is documented below and the server stores only ciphertext — but design intent is not the same as verified implementation. Do not rely on Secure Chat where compromise would put someone at risk. Independent cryptographic review is explicitly welcome — seeSECURITY.md.
It's built as a blind delivery service on MLS (RFC 9420) — the IETF messaging-layer-security standard, the same family of guarantees as Signal but designed for large, dynamic groups:
- The server is blind. It stores and relays only opaque ciphertext. It enforces who may post and the order of group changes, but never sees what — it learns social-graph + timing metadata only (the Signal-server model). All cryptography is client-side; the server depends on no crypto library.
- No AI in your DMs — on purpose. Secure chat runs no LLM moderation, embeddings, or semantic search. Those features require reading content, which is exactly the thing we're refusing to do. That trade is the point, not a limitation.
- A separate path and a separate process, zero contract drift. Secure chat lives at
/v7/:projectId/secure-chat/*, entirely apart from the Replyke-compatible plaintext chat (which is untouched), and runs as its own deployable service (@agora/secure-chat) so the blind relay can be isolated and scaled on its own. Use whichever a conversation needs. - Your keys, your history. Keys live on the client. Two recovery paths — both opaque to the server —
restore history onto a re-provisioned device: an optional passphrase-encrypted backup, and
device-to-device history restore (a peer seals your back-history into an ephemeral, targeted blob
the server relays blindly; see
apps/secure-chat/docs/RESTORE.md). The schema is multi-device-ready. - The crypto stays swappable. All MLS lives behind a small
SecureChatCryptointerface, so the concrete core (ts-mls vs OpenMLS-WASM) is a deferred, reversible choice — the server never changes.
Full design, threat model, schema, endpoints, and roadmap: docs/SECURE_CHAT.md.
Note
The social graph is in alpha. The graph is functional and env-gated off by default, but its APIs, scoring, and admin surfaces are still evolving and may change ahead of a stable release. Everything below the social layer (the backend, chat, moderation, stewardship) is beta and stable.
Every platform mines the social graph at the user — to target, rank, and sell them. Agora points the
same structure back at the community, for the community, in service of its health. It's an
optional, off-by-default layer: wire up a Neo4j instance (NEO4J_URI) and the Garden comes
alive; leave it unset and nothing in the graph layer runs (every /social/* endpoint cleanly 503s and
the admin panels stay hidden).
There's a second purpose, just as deliberate: to teach by showing. Every member already lives inside a social graph on every platform they use — they just never get to see it. That graph is the asset being silently harvested, scored, brokered, and turned back on them as targeted advertising and behavioral nudging, with their consent buried in a terms-of-service no one reads. By rendering the graph in the open, for once aimed at the community's wellbeing instead of at extracting from it, the Garden makes the thing concrete: this is what a map of who-talks-to-whom looks like, this is how much it reveals, and this is exactly the structure that surveillance-capitalism platforms mine and sell without ever letting you look at it. Seeing it pointed at care is the clearest way to grasp how dangerous it is when it's pointed at profit — and how flatly that use conflicts with the interests of the people it's built from. The Garden is consciousness-raising as much as it is a feature: data literacy you can feel, not a lecture.
The whole design exists because Agora's communities skew trans, queer, sex-worker, and recovery — for them graph exposure is a doxxing/outing vector, and mass-reporting is the primary harassment tactic (any score that counts reports lights up on the target, not the harasser). So every rule bends toward one non-negotiable:
The asymmetry principle — every possible misreading must land on kindness, never a scarlet letter. Friction only ever dims a node; it never reddens it.
- One public signal: warmth. Loneliness and conflict both render as dim — indistinguishable on purpose, because both mean "bring care here." There is no per-person "bad actor" score, in any view.
- A zoom ladder that is a privacy ladder. ☀️ Community Weather (one project-wide warmth scalar
with band + trend) → ✨ Constellation (anonymous cluster blobs — size + warmth only, never
individual nodes; an adaptive k-floor of 2–5 by community size, hard floor 2, so small
communities see an accurate shape while large ones keep a privacy-meaningful floor) → 🏡
Neighborhood (your own ties only, each rendered by its dyadic brightness
B(you, them)— never the friend's global score, which closes the friction side-channel entirely). - Friction is quarantined and decays. A user report projects a directed
FRICTIONedge that can only dim an existing tie and fades at a ~14-day half-life ("a bad week is not a permanent identity"); it never creates a tie, and never becomes a per-person verdict. - One graph, opposite deployments. The same structure serves a vulnerable-population community
and a corporation's internal platform — nearly opposite privacy needs — resolved by a per-project
social_configtier (community ↔ corporate, admin Settings → Social Graph), not by picking a side.
Under the hood the scorer owns the write side (it projects INTERACTED / FOLLOWS / CONNECTED /
FRICTION edges into Neo4j off the same pgmq queue it already scores content on), and @agora/api
owns the read side (/v7/:projectId/social/*, member/operator-gated, computed live or cached with band
hysteresis). Full design, ethics, and threat model:
docs/SOCIAL-GRAPH.md (the consolidation plan),
docs/AGORA-SOCIAL.md (the condensed design corpus), and
docs/DOZERDB.md (DozerDB + OpenGDS setup — plugins, memory tuning, TLS).
Agora's whole reason to exist is that the forked SDK's typed hooks work unchanged. So the contract is non-negotiable and fully specified:
docs/MANIFEST.md— every REST endpoint (method + path, marked ✅ SDK-confirmed vs 🔶 inferred), the socket.io event names, and the auth / pagination / error envelopes. This is the checklist.docs/MODELS.md— field-level response shapes; the source of truth for both API output and the database schema.
Match these exactly or the SDK's hooks break — that discipline is what makes the "1:1" claim real.
Agora is a pnpm monorepo — three apps and two shared packages. Each app has its own README with setup, configuration, and development details:
| Package | What it is | Docs |
|---|---|---|
@agora/api |
The backend — Hono + Supabase + socket.io. Every endpoint, permission check, and bit of business logic. The reference package. | apps/api/README.md |
@agora/secure-chat |
The independently-deployable end-to-end-encrypted chat service — its own Hono + socket.io process serving /v7/:projectId/secure-chat/* (a reverse proxy routes that prefix to it) and the /secure realtime namespace on engine.io path /secure-socket/. A blind MLS delivery service that stores/relays only ciphertext; consumes @agora/core. |
apps/secure-chat/README.md |
@agora/admin |
The admin dashboard — Vite + React. Moderation queue + AI queue, the stewardship caseload (cases, mediation channels, steward grants), feed tuning, webhook config, community & analytics dashboards, plus the optional Social Graph panel + Community Weather card (gated on VITE_SOCIAL_GRAPH_ENABLED). |
apps/admin/README.md |
@agora/core |
The shared kernel consumed by both @agora/api and @agora/secure-chat: env schema, logger, the Drizzle db client + full schema (single source of truth for all tables), http error/context, auth + project middleware, validation, suspensions (incl. the Redis fail-closed index), and the Redis client. |
— |
@agora-server/contract |
Shared API types + zod request schemas (no hono/drizzle). Built first; consumed across the workspace — and the permissive (Apache-2.0) wire surface the SDK builds on. | — |
agora/
├── docs/
│ ├── MANIFEST.md # the exact REST + socket.io contract (SDK-confirmed vs inferred)
│ ├── MODELS.md # field-level response shapes (drive both the API and the schema)
│ ├── SECURE_CHAT.md # the end-to-end-encrypted chat design + reference
│ ├── STEWARDSHIP.md # the conflict-resolution caseload design
│ ├── SCORER.md # the Python moderation + social-graph subsystem (services/scorer)
│ ├── SOCIAL-GRAPH.md # the optional Neo4j social-graph layer ("the Garden")
│ ├── SELF-HOSTING.md # run fully self-contained (native auth · S3/MinIO · local Postgres)
│ ├── TELEMETRY.md # built-in OpenTelemetry (traces/metrics/logs) → Grafana + Alloy
│ └── DOZERDB.md # DozerDB + OpenGDS setup (plugins, memory tuning, TLS)
├── packages/
│ ├── contract/ # @agora-server/contract — shared API types + zod schemas
│ └── core/ # @agora/core — shared kernel (env · logger · db+schema · auth · suspensions · redis)
└── apps/
├── api/ # @agora/api — the backend
├── secure-chat/ # @agora/secure-chat — the E2E-encrypted chat service (own process)
└── admin/ # @agora/admin — the admin frontend
The client secure-chat packages (
@agora-sdk/secure-chat-crypto— theSecureChatCryptoseam + mock, with the real ts-mls/OpenMLS core to come) live in the separateagora-sdk-plusrepo, not here; this repo only devDepends on the mock for tests. See Ecosystem anddocs/SECURE_CHAT.md.
The client SDK lives in a separate companion repository,
jenova-marie/agora-sdk (see Ecosystem).
corepack enable # activate the pinned pnpm
pnpm install # install all workspaces (from the repo root)
pnpm -r build # build every package (contract first, topologically)
# Backend — the dev template defaults to a local Postgres + MinIO (no cloud account needed).
# Bring the backing services up first: docker compose -f docker-compose.dev.yml --profile selfhost up
cd apps/api
cp ../../.env.dev.example .env # local-PG dev; fill the <GENERATE> secrets (cloud Supabase = in-file switch)
pnpm db:migrate:run # apply migrations to the local db (idempotent; safe to re-run)
pnpm dev # http://localhost:4000/v7 (GET /health to verify)
# Admin dashboard (optional)
cd ../admin && pnpm dev # http://localhost:5173Each app's README covers its own configuration, commands, and Docker image. Start with apps/api — it's the backend everything else points at.
Agora is local-Postgres-first: it runs on a local Postgres (the supabase/postgres image) + MinIO
out of the box, with no cloud account required. Cloud Supabase is an opt-in switch, not the default.
There's one template per compose file — pick by how you run Agora, not by data plane (each template
does both). Copy it to .env and fill the <…> placeholders:
Template → .env |
Compose file | Runs the app | Default data plane |
|---|---|---|---|
cp .env.dev.example .env |
docker-compose.dev.yml |
on the host (HMR) | local PG + MinIO (native auth) |
cp .env.selfhost.example .env |
docker-compose.yml |
in a container (built from source) | local PG + MinIO + GoTrue |
cp .env.prod.example .env |
docker-compose.prod.yml |
in a container (pulled image) | local PG + MinIO + GoTrue |
The selfhost/prod data plane bundles GoTrue (Supabase Auth as its own container) behind the Caddy
front door — email+password and Google/GitHub/Apple SSO with no cloud dependency; dev keeps
Agora's zero-infra native auth (SSO is opt-in there too — see the docs). The api reaches it via
the internal SUPABASE_URL shim while SUPABASE_PUBLIC_AUTH_URL supplies the public origin for
browser-facing OAuth URLs, and OAUTH_REDIRECT_ALLOWED_ORIGINS gates where the callback may hand
tokens back to. The bundled @agora/admin SPA can offer the same social buttons on its login screen
(AGORA_ADMIN_OAUTH_PROVIDERS). See
docs/SELF-HOSTING.md → SSO / social login.
Switching a template to cloud Supabase is an in-file edit: comment its LOCAL data-plane block,
uncomment the CLOUD block (the full SUPABASE_* + pooler vars are right there), and run
--profile supabase instead of --profile selfhost. No separate template.
Compose reads the root .env for ${VAR} interpolation, and (in selfhost) the services'
env_file too — one copy covers everything there. Separately, each app loads its own .env from its
package dir via dotenv — for the backend that's apps/api/.env. In selfhost the API runs as a
container reading the root .env through env_file. In prod there is no env_file: each service
enumerates every var it consumes explicitly (${VAR:?required} / ${VAR:-default}), so no secret has to
live in a .env file on disk — .env still only supplies ${VAR} interpolation if you keep one. In
dev the API runs on the host (pnpm --filter @agora/api dev) and needs its own
apps/api/.env — copy the template there too (cd apps/api && cp ../../.env.dev.example .env), or symlink it
(ln -sf ../../.env apps/api/.env — an optional, gitignored convenience, not created by default). Same for
@agora/secure-chat when you run it on the host.
Every template carries an AGORA_ENV marker (dev/selfhost/prod). The destructive DB scripts
(drop/genesis) combine it with the DATABASE_URL host to gate a --force: a local throwaway
(db/localhost host, non-prod) drops unattended, while a protected target — any cloud/remote DB,
or an AGORA_ENV=prod deployment even on a local DB — requires typing the project ref to confirm.
The browser-facing admin SPA has its own build-time vars — see apps/admin/.env.example.
client + forked Replyke SDK
│ HTTPS /v7/:projectId/<domain>/... (+ socket.io for chat realtime)
▼
@agora/api (Hono) endpoints · business logic · permission checks
│ Drizzle ORM (postgres.js, Supabase transaction pooler :6543, prepare:false) ← owner role, bypasses RLS
▼
Supabase Postgres schema · triggers · RPC · pgvector · PostGIS · RLS
├── Supabase Auth (passwords, confirmation/reset emails, OAuth)
└── Supabase Storage (file/image bytes)
Voyage AI ──▶ embeddings Anthropic ──▶ /search/ask answers
pgmq ──▶ services/scorer ──▶ AI moderation · social-graph edges ──▶ Neo4j (optional)
@agora/api reads Neo4j back for /social/*
@agora/admin ─(operator JWT)─▶ @agora/api
- Drizzle owns all DB access via a direct
postgres.jsconnection. The Supabase JS client is only for Auth/Storage and is lazily constructed. - The server is the trust boundary. The API connects as the table-owner role (so RLS never constrains it) and enforces every ownership / role check in the handlers. RLS is enabled as defense-in-depth with public-read policies.
- Multi-tenant by
project_id— every table has it; the SDK addresses/v7/:projectId/.... A single-project deployment just has oneprojectsrow. - Supabase is the default, not a hard dependency. Auth and Storage sit behind provider seams
(
getAuthProvider()/getStorage()): choose native email/password auth (DEFAULT_AUTH_PROVIDER=native) and an S3-compatible object store (STORAGE_PROVIDER=s3→ MinIO/AWS) and the same server runs fully self-contained on a local Postgres — no Supabase cloud (no hosted Auth/Storage/DB, no account).⚠️ That local Postgres is still thesupabase/postgresimage (the migrations hard-depend on its bundled pgvector/PostGIS/pgmq + theauthroles — a vanilla Postgres won't migrate), which theselfhostprofile pins for you. Seedocs/SELF-HOSTING.md.
See apps/api/README.md for the full backend architecture and handler conventions.
Every domain below is implemented and validated against live cloud Supabase. The REST surface is complete — no stubbed endpoints remain.
| Domain | Highlights |
|---|---|
| entities | feed with full filter grammar + pluggable ranking (createdAt/hot/top/controversial/decay/gravity/wilson/bayesian, per-project + per-request tunable, sortDir), CRUD, drafts, foreign/short-id lookup, reactions, saved state |
| comments | threaded (adjacency list + recursive CTE full-tree endpoint), reactions, configurable delete (CONTENT_DELETE_MODE: hard default = row + media removed; soft = Reddit-style tombstone), sortBy (createdAt/top/controversial + sortDir; new/old kept as RFC 8594-deprecated aliases) |
| users / follows | profiles, follow graph + counts, suggestions |
| connections | bidirectional friend-request state machine (none → pending → connected/declined) with directional status |
| spaces | nested spaces (depth cap + cycle guard), membership (join/approve/ban/roles), rules, moderation queues, digest config |
| collections | nestable saved-entity folders |
| events | community events with RSVPs (going/maybe/not_going, capacity-capped going), invites (host-only, invite-removal revokes the RSVP), co-hosts (last-host guard), public/members/invite visibility (gated against space-read access), cover + gallery images, and a PostGIS location for radius (km) list filtering |
| notifications | fan-out across every write path, inbox, unread count, mark read; web + native push (Web Push/VAPID wired, FCM + APNs credential-gated) driven by a push-worthy allowlist (reactions, milestones, and all steward events stay in-app only) |
| reports | report queue + resolution (entities, comments, chat messages) |
| auth | sign-up/in/out, refresh rotation + reuse-detection, change/reset password, email verify, external RS256, OAuth provider sign-in/link |
| chat | conversations (direct/group/space), members, messages, reactions, typing, read state — socket.io realtime |
| secure chat | end-to-end-encrypted chat (MLS / RFC 9420) on a separate path and a separate process (the independently-deployable @agora/secure-chat service) — the server stores/relays only ciphertext and never reads content; 1:1 + groups + space channels, one-time key packages, passphrase + device-to-device history restore, a dedicated /secure socket.io namespace (engine.io path /secure-socket/) (docs/SECURE_CHAT.md) |
| search | semantic content search across entities/comments/messages (Voyage + pgvector), RAG /ask (Anthropic, SSE), text search for spaces/users |
| storage | file uploads + image variants (sharp → webp, 5 sizing modes); pluggable backend — Supabase Storage or any S3-compatible store (MinIO/AWS) via STORAGE_PROVIDER |
| webhooks | project webhooks (HMAC validation gates + *.complete broadcasts) + per-space digests |
| moderation | report resolution + server-enforced removed-content hiding (lists, single reads, and the search RPC); space-moderator + operator roles; AI Agent Moderator that flags inappropriate content on post — configurable violation categories, confidence thresholds, and auto-actions (immediate hide or human review) — tunable per-project in Settings; escalation to Stewards for conflict resolution |
| stewardship | first-class conflict resolution — a DB-granted steward role (between member and operator), a caseload (open → in_mediation → closed), transformative outcomes, a "targeting" power-imbalance flag, private mediation channels (caucus + consensual joint room, built on chat), configurable participant notifications (power-aware/symmetric/resolution-only, never leaking who raised a case), append-only timeline, and escalate-to-removal for posts/comments/chat messages (docs/STEWARDSHIP.md) |
| social graph (optional · Neo4j · alpha) | the Garden — community-health analytics pointed back at the community: Community Weather (project-wide warmth scalar + band/trend, cached with hysteresis), Constellation (anonymous GDS-Louvain cluster blobs with an adaptive 2–5 k-floor + on-demand project-admin recompute), and Neighborhood (your own ties by dyadic brightness); one public signal (warmth), friction quarantined + decaying, per-project privacy tier (community ↔ corporate). Scorer projects the edges, the API reads them. Off unless NEO4J_URI is set (docs/SOCIAL-GRAPH.md) |
Denormalized counts (reaction counts, reply counts, member counts, thread counts, reputation) are maintained atomically by Postgres triggers — never recomputed per request.
The trust boundary is the server: it talks to Postgres as the RLS-bypassing owner role and enforces every read/write rule in the handlers, with RLS underneath as a verified backstop.
- Tokens — Agora mints short-lived access tokens (30 m) + refresh tokens (30 d) with rotation, reuse-detection, and a 30 s racing-tabs grace window.
- Anonymous reads, authenticated writes — public content is readable without a token (matching
Replyke's contract); every mutation is
requireAuth. - Space privacy — a members-only space is invisible to non-members on every path (feed, single reads, reactions, comment creation, semantic search).
- Private chat — conversation messages are readable only by active members, enforced on the REST routes and inside the search RPC.
- End-to-end-encrypted secure chat — an optional, separate chat surface where the server stores only
ciphertext, built so it cannot read messages at all (MLS / RFC 9420; deny-all RLS on every
secure table). Unaudited — see the status note above.
See
docs/SECURE_CHAT.md. - Moderation visibility — removed content is always hidden from non-privileged readers (omitted from lists, 404'd on single reads, filtered in the search RPC); operators bypass to review.
- Operators & stewards — a deployment-operator allowlist grants a project-wide moderation/admin god-view; a DB-granted steward role (between member and operator) is route-scoped to the conflict-resolution caseload.
- RLS backstop — denies
anon/authenticatedany private-space, removed, or draft row directly.
Full detail (including the OAuth callback setup) lives in apps/api/README.md.
Full telemetry is built in — across every service. Agora ships all three OpenTelemetry signals —
traces, metrics, and logs — wired up out of the box on
@jenova-marie/wonder-logger (the Node
services) and the OpenTelemetry Python SDK (the scorer). Nothing to install and no instrumentation to
write: each service starts an OTel SDK on boot, auto-instruments HTTP + the database, exposes
metrics, and pushes OTLP to a collector. The API (agora-api), the secure-chat delivery service
(agora-secure-chat), and the scorer (agora-scorer-*) each show up as their own service in Grafana.
- Traces — auto-instrumented HTTP/DB spans, plus manual
withSpan()for the realtime (socket.io) path that HTTP instrumentation never sees. OTLP → Tempo. - Metrics — service-level RED + runtime plus custom instruments (embedding latency, moderation
decisions, feed-algorithm mix, live socket connections), on a Prometheus endpoint (
:9464) and OTLP push → Mimir. (Distinct from the per-projectapi_usageproduct metering — a separate system.) - Logs — structured logs (console + OTLP → Loki), secrets redacted at the boundary, with
automatic trace ↔ log correlation (
trace_id/span_idon every line, in every service).
It's off by default (bare deploys stay dark) and ships with a one-command Grafana stack — Alloy
- Tempo/Mimir/Loki/Grafana behind the
observabilitycompose profile:
docker compose --profile selfhost --profile observability up --build # bring up the stack
echo 'OTEL_SDK_DISABLED=false' >> .env # the single toggle, then restartThe OTLP endpoints are pre-wired to the bundled Grafana Alloy
collector, so that one flag lights up Grafana at :3000 — or point the OTEL_* env at your own
collector instead. Full setup, the per-service signal table, and verification:
docs/TELEMETRY.md.
📋
docs/CHEAT-SHEET.mdmaps every deployment recipe to the env vars it needs and where to obtain each value — start there if you just want to stand a stack up.
The repo's docker-compose.yml builds and wires the whole stack with a two-axis profile model, so a
bare docker compose up starts nothing:
- Axis 1 — data plane + API (required, pick exactly one):
supabase(external Supabase Postgres + Storage) orselfhost(local Postgres + MinIO). Either brings up the API itself — api (:4000), the Caddy front door (proxy), and cron. - Axis 2 — optional add-ons (compose freely on top):
scorer(moderation + social graph: scorer ×3 + neo4j),secure-chat(the E2E delivery process + redis),scale(Redis rate-limit store), orfull= every add-on at once.
With Supabase backing Postgres/Auth/Storage there's no local DB to run:
docker compose --profile supabase up --build # just the API, Supabase-backed
docker compose run --rm agora node scripts/migrate.mjs # apply migrationsThe proxy service is a Caddy front door — the single public entrypoint that serves the admin SPA
(baked into its image) and routes every service (/v7 + /socket.io → api, secure-chat, /moderator,
/media), with automatic Let's Encrypt certs, HSTS + security headers, a body-size cap, and an
authoritative X-Forwarded-For (one hop → set RATE_LIMIT_TRUSTED_HOPS=1). It rides the data-plane
profiles, so it's up whenever the API is. Each app's README documents building its image standalone.
The secure-chat profile is the standalone @agora/secure-chat deploy — Redis
- the secure-chat process (
:4002), no API stack. Likeagora, it doesn't bundle a database: it persists thesecure_*tables to whateverDATABASE_URLpoints at, which in v1 is the shared main Postgres (or Supabase) — already migrated. Alongside the API it travels infull, so afulldeploy routes/v7/:projectId/secure-chat/*+ the WebSocket/secure-socket/to it through the Caddy front door automatically.
docker compose --profile secure-chat up --build # split box → remote DATABASE_URL, no API
docker compose --profile full --profile supabase up # everything (incl. secure-chat), Supabase-backedFor a fully self-contained stack (no Supabase cloud) use selfhost (local Postgres + MinIO).
Agora swaps Supabase out through its provider seams — native email/password auth
(DEFAULT_AUTH_PROVIDER=native) and S3-compatible storage (STORAGE_PROVIDER=s3 → MinIO/AWS) — so
the same server image runs against either backend. The local db is still the supabase/postgres
image, though (not a vanilla Postgres — the migrations need its pgvector/PostGIS/pgmq + auth roles);
"self-contained" means no Supabase cloud, not no Supabase Postgres image. See
docs/SELF-HOSTING.md:
docker compose --profile selfhost up --build # just the API, self-contained
docker compose --profile full --profile selfhost up # + every optional add-onFor a real domain set SERVER_NAME=your.domain (DNS → this host so ACME can validate on :80). ACME_CA=https://acme-v02.api.letsencrypt.org/directory for real, trusted certs
before going live. For Tor
hidden services — or any deploy where Let's Encrypt can't reach you — a second
Caddyfile.onion serves a cert you supply at startup instead of
auto-ACME (selected via the CADDYFILE env var); plain HTTP behind your own TLS terminator: set
SERVER_NAME=:80. See deploy/proxy/README.md. Point
AGORA_PUBLIC_APP_URL at your public consumer app's origin so the admin's "Open in app" deep links land
on your real site (unset → the local demo dev server, http://localhost:5174/); it's read at runtime
from /config.js, so docker compose up -d proxy retargets even a pulled image with no rebuild. The
rest of the admin's settings ride the same seam as optional AGORA_ADMIN_* vars — project id, API and
moderator bases, feature flags, demo login (full table).
(Optionally add --profile scale for Redis as the cross-replica rate-limit store.)
Agora is three separate repos — kept separate on purpose, not one monorepo:
agora-server(this repo) — the backend + admin. The contract's server side.agora-sdk— the forked, repointed Replyke SDK, published as@agora-sdk/*(core/react-js/react-native/expo). The base URL flows in via the provider prop; noapi.replyke.comleft. Its own release cycle.agora-sdk-plus— additive, Agora-only SDK features built on@agora-sdk/*(kept out of the Replyke fork so that stays a tiny, documented divergence). Home of end-to-end-encrypted chat:@agora-sdk/secure-chat-crypto(theSecureChatCryptoseam + mock; real ts-mls/OpenMLS core to come) and the transport/provider/hooks. Depends on@agora-server/contractfor the wire types — never the reverse.agora-demo— a standalone Vite + React app: the 1:1 compatibility harness (and what powers the live demo). Eight tabs, each exercising one SDK surface against a running server. It installs the published@agora-sdk/*(not a workspace link), so it catches server↔SDK contract drift exactly as a third-party app would.
Why a fork? The published Replyke SDK hardcodes https://api.replyke.com/v7; agora-sdk
repoints that base URL (see docs/MANIFEST.md §0). Because it does, the URL shape, auth token
semantics, { data, pagination } / { error, code } envelopes, response object shapes, and
socket.io event names all line up 1:1 — that's the entire point.
Point the SDK at your server with VITE_API_BASE_URL (defaults to http://localhost:4000/v7) and
pass a projectId + a signed user token to the provider; the SDK's typed hooks (useEntity,
useComments, useChat, …) then work unchanged. See the
agora-sdk README for a full quick-start.
- ✅ Backend feature-complete — every domain implemented and validated against live cloud Supabase; the REST surface has no remaining stubs.
- ✅ Realtime chat, semantic + RAG search, auth (token rotation + external RS256 + OAuth), storage, project webhooks, space digests, and RLS (public-read + authenticated self-access) verified end-to-end.
- ✅ Access control — space read/post privacy, private-chat membership gating (incl. search), server-enforced removed-content hiding, the operator god-view, and the route-scoped steward role, all enforced server-side.
- ✅ Governance — moderation (report queues + optional LLM auto-moderation) and the stewardship caseload (cases, private mediation channels, participant notifications) are wired and operator-gated in the admin dashboard.
- 🌱 Social graph (optional · Neo4j · alpha) — the full Garden is live and env-gated behind
NEO4J_URI(scorer writes theINTERACTED/FOLLOWS/CONNECTED/FRICTIONedges, the API reads them): Community Weather, Constellation (GDS-Louvain blobs, adaptive 2–5 k-floor, project-admin recompute), and Neighborhood, plus the operator-only corporate analytics tier (influence / silos / engagement + React dashboards) (docs/SOCIAL-GRAPH.md). - ✅ Supabase-optional — provider seams run the same server on native email/password auth
(
DEFAULT_AUTH_PROVIDER=native) + S3-compatible storage (STORAGE_PROVIDER=s3) + a local Postgres, fully self-contained; DB layer validated end-to-end (docs/SELF-HOSTING.md). - ✅ Idempotent Drizzle migrations
0000–0055; unit + integration test suites green. - ✅ Client SDK published + repointed — validated 1:1 by the
agora-democompatibility harness. - ⬜ Ops backlog: deployment guides, and RLS write policies (only needed if the Supabase Data API is opened for writes).
Contributors welcome — Agora is built in the open, and we'd genuinely love your help. 🌱 Bug fixes, new admin-app slices, docs, test coverage, deployment guides, or closing a contract gap against Replyke — there's room to jump in, whatever your level.
- 🐛 Found a bug or a contract mismatch? Open an issue.
For SDK-compat drift, include the endpoint and the expected-vs-actual shape from
docs/MODELS.md. - ✨ Want to build something? Browse the open issues or the Status backlog. Friendly first areas: admin-app features, test coverage, and the deployment guide.
- 📋 Before you start, read CONTRIBUTING.md — dev setup, the contract rules, coding conventions, the migration workflow, and how to open a PR.
- 🛠️ Setting up to hack on it? docs/DEVELOPMENT.md is the full developer guide — the three local run modes, seeding, debugging, researching the codebase, and testing.
- 📖 New here? The wiki is a curated
handbook (getting started, deployment, architecture, the subsystems) that links into the deep
docs/. Its source lives inwiki/and is auto-published — edit it via PR, see CONTRIBUTING.md.
The one hard rule: the contract is the constraint. Any change to request/response shapes, REST
paths, or socket.io events must keep agora-sdk working
1:1 — see The contract is the constraint. Everyone is welcome here;
be kind, assume good faith, and build something we'd all want to use. 💜
AGPL-3.0-only for the server (@agora/api, @agora/admin) — you
can self-host forever, but running a modified Agora as a network service means sharing your changes
back. The shared wire contract (@agora-server/contract) stays Apache-2.0 so the
agora-sdk and any third-party client can build against
it freely. The community edition is AGPL-3.0 and always will be.
Contributions are accepted under the Developer Certificate of Origin —
sign off your commits with git commit -s. There is no CLA: your contributions stay yours,
licensed AGPL-3.0, and they can't be relicensed out from under you. See
CONTRIBUTING.md.
AGPL §13 note for operators: because users interact with Agora over a network, your deployment must offer them its corresponding source. Agora surfaces a source link by default — keep it pointing at your fork.
Agora started as a solo project for my own needs — I'm also building a recovery app, and it needs a private community to call home. But I want to be clear about one thing: I did not build this system alone. The Claudes helped me.
Building software with agents is not an easy task. It takes the skills, knowledge, and experience to be done right. I am proud to work alongside The Claudes, and I believe the quality of this repo will speak for itself.
Thank you, Anthropic, for making my dreams come true — and for making Agora possible. 💜
— Jenova
Agora Server is built on excellent open-source software. See:
- CREDITS.md — Human-friendly acknowledgments of all direct dependencies and the teams behind them, including a shout-out to Claude Code
- OSS-LICENSES.txt — Machine-readable license inventory for compliance and automated license scanning
