{"id":51025214,"url":"https://github.com/jenova-marie/agora-server","last_synced_at":"2026-06-21T19:00:54.314Z","repository":{"id":359885372,"uuid":"1247173917","full_name":"jenova-marie/agora-server","owner":"jenova-marie","description":"Self-hosted open-source backend for community \u0026 social apps (posts, comments, reactions, follows, spaces, realtime chat, semantic search). API-compatible with the Replyke SDK. Built on Supabase.","archived":false,"fork":false,"pushed_at":"2026-06-16T09:17:49.000Z","size":3434,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"root","last_synced_at":"2026-06-16T09:23:49.347Z","etag":null,"topics":["backend","comments","community","drizzle-orm","moderation","nodejs","open-source","pgvector","postgres","realtime","replyke","replyke-sdk","replyke-server","self-hosted","semantic-search","social-network","socketio","sublay","supabase","typescript"],"latest_commit_sha":null,"homepage":"https://demo.agora-oss.org","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"agpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jenova-marie.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"docs/SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-23T01:41:21.000Z","updated_at":"2026-06-16T09:20:52.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/jenova-marie/agora-server","commit_stats":null,"previous_names":["jenova-marie/agora","jenova-marie/agora-server"],"tags_count":24,"template":false,"template_full_name":null,"purl":"pkg:github/jenova-marie/agora-server","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jenova-marie%2Fagora-server","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jenova-marie%2Fagora-server/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jenova-marie%2Fagora-server/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jenova-marie%2Fagora-server/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jenova-marie","download_url":"https://codeload.github.com/jenova-marie/agora-server/tar.gz/refs/heads/root","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jenova-marie%2Fagora-server/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34622271,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-21T02:00:05.568Z","response_time":54,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["backend","comments","community","drizzle-orm","moderation","nodejs","open-source","pgvector","postgres","realtime","replyke","replyke-sdk","replyke-server","self-hosted","semantic-search","social-network","socketio","sublay","supabase","typescript"],"created_at":"2026-06-21T19:00:53.415Z","updated_at":"2026-06-21T19:00:54.282Z","avatar_url":"https://github.com/jenova-marie.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"assets/agora.png\" alt=\"Agora logo\" width=\"200\" height=\"200\" /\u003e\n\u003c/p\u003e\n\n\u003ch1 align=\"center\"\u003eAgora\u003c/h1\u003e\n\n\u003cp align=\"center\"\u003e\u003cem\u003eThe open social layer. Own your community.\u003c/em\u003e\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://github.com/jenova-marie/agora-server/actions/workflows/ci.yml\"\u003e\u003cimg src=\"https://img.shields.io/github/actions/workflow/status/jenova-marie/agora-server/ci.yml?label=ci\u0026logo=github\u0026logoColor=white\" alt=\"CI Status\" /\u003e\u003c/a\u003e\n  \u003ca href=\"https://demo.agora-oss.org\"\u003e\u003cimg src=\"https://img.shields.io/badge/▶_live_demo-demo.agora--oss.org-7C3AED.svg\" alt=\"Live demo\" /\u003e\u003c/a\u003e\n  \u003ca href=\"LICENSE\"\u003e\u003cimg src=\"https://img.shields.io/badge/License-AGPL_v3-7C3AED.svg\" alt=\"License: AGPL-3.0-only\" /\u003e\u003c/a\u003e\n  \u003ca href=\"https://supabase.com\"\u003e\u003cimg src=\"https://img.shields.io/badge/built%20on-Supabase-3ECF8E.svg\" alt=\"Built on Supabase\" /\u003e\u003c/a\u003e\n  \u003ca href=\"#status\"\u003e\u003cimg src=\"https://img.shields.io/badge/backend-feature--complete-success.svg\" alt=\"Status: feature-complete\" /\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  ▶️ \u003cstrong\u003eTry it live: \u003ca href=\"https://demo.agora-oss.org\"\u003edemo.agora-oss.org\u003c/a\u003e\u003c/strong\u003e\n\u003c/p\u003e\n\nA working social app — sign in, browse the feed, comment, react, semantic-search, and chat in\nrealtime — all driven by the [`agora-sdk`](https://github.com/jenova-marie/agora-sdk) against a live\nAgora backend.\n\n**Agora is an open-source, self-hosted, 1:1-compatible replacement for the closed source Replyke backend, built on Supabase.**\n\nReplyke is a hosted backend for community \u0026 social features. Agora reimplements that backend so the\n[`agora-sdk`](https://github.com/jenova-marie/agora-sdk) (a repointed fork of the Replyke SDK) talks\nto **your** server instead of `api.replyke.com` — byte-for-byte the same REST paths, response shapes,\nauth semantics, and socket.io events. You keep Replyke's opinionated feature set (posts, threaded\ncomments, reactions \u0026 feeds, follows \u0026 connections, nested spaces, real-time chat, notifications,\nmoderation \u0026 stewardship, semantic search) and run it all on infrastructure you control, under a\ngenuinely open license.\n\n**AGPL-3.0 — and that's the whole point.** Replyke open-sources its *SDK* (Apache-2.0) but keeps the\nbackend you'd actually depend on closed and hosted. Agora is the entire backend, in the open, under a\nlicense with teeth: self-host it freely, forever — but anyone who runs a modified Agora as a service\nhas to share their changes back. No vendor lock-in, no per-seat pricing, no data leaving your project,\nand no \"open-source\" asterisk. **The community edition is AGPL-3.0 and always will be.**\n\n## Why\n\nSupabase hands you ~40% of a social backend for free: Postgres, Auth (GoTrue), Storage, Realtime\ninfrastructure, and pgvector. The other ~60% — the social schema, the denormalized counts, the\npermission model, and the opinionated endpoints that sit in front of all of it — is what makes\nReplyke worth using. **That 60% is what Agora builds**, and it's the part you'd otherwise rent.\n\n## Governance is first-class, not a bolt-on\n\nMost community backends — Replyke included — ship content and social primitives and leave *governance*\nto you: you build the reporting flow, the moderation dashboard, and any conflict-resolution process\nyourself, against whatever the hosted API exposes. A real community can't run without these, so Agora\nmakes them **core surface area** — endpoints, schema, and admin UI in the box, enforced at the server\ntrust boundary rather than reconstructed per app:\n\n- **Moderation** — report queues for entities, comments, and chat messages; server-enforced\n  removed-content hiding (a removed row is omitted from every list, 404'd on single reads, and filtered\n  inside the semantic-search RPC — operators bypass to review); space-scoped moderator roles plus a\n  project-wide operator god-view; and **AI Agent Moderator** that assesses all new content for\n  inappropriate violations (configurable categories, confidence thresholds) and either **auto-hides** or\n  **flags for human review** depending on the AI score (tunable per-project in Settings) — escalates to\n  the Stewardship caseload for conflict resolution.\n- **Stewardship** — a distinct **conflict-resolution** layer: moderation judges *content*; stewardship\n  tends *people and relationships*. A DB-granted **steward** role between member and operator; a\n  **caseload** that moves a dispute (complainant ↔ respondent over some content) through\n  `open → in_mediation → closed` with **transformative-ordered outcomes** (repair → separation →\n  protection → escalation); an **asymmetry / \"targeting\"** flag for power-aware, anti-false-balance\n  handling; **private mediation channels** — built on the existing chat — to actually talk a conflict\n  through (1:1 *caucus* with each party, or a consensual *joint room* in hybrid mode, never for a\n  targeting case); **configurable participant notifications** (power-aware / symmetric / resolution-only)\n  that keep the parties informed without ever leaking who raised a case; an append-only case timeline;\n  and **escalate-to-removal** that takes the subject content (post, comment, or chat message) down\n  through the moderation path. All of it operator-tunable per project. See\n  [`docs/STEWARDSHIP.md`](docs/STEWARDSHIP.md).\n\nBoth live behind the same `/v7/:projectId/...` contract and in the Postgres schema, so they're available\nto every client from day one — not gated behind an external service's limits.\n\n## Private means private — end-to-end-encrypted chat\n\nA community backend that keeps everyone's direct messages in **readable plaintext** is a breach waiting\nto happen: one database leak, one subpoena, one over-broad admin, one \"smart\" feature that quietly reads\nDMs, and the trust is gone. Storing readable private messages is the bad default we refuse to ship — so\nAgora's **secure chat** is genuinely **end-to-end encrypted**, and the server *cannot read it*.\n\nIt's built as a **blind delivery service** on **MLS (RFC 9420)** — the IETF messaging-layer-security\nstandard, the same family of guarantees as Signal but designed for large, dynamic groups:\n\n- **The server is blind.** It stores and relays only **opaque ciphertext**. It enforces *who* may post\n  and the *order* of group changes, but never sees *what* — it learns social-graph + timing metadata\n  only (the Signal-server model). All cryptography is client-side; the server depends on no crypto library.\n- **No AI in your DMs — on purpose.** Secure chat runs **no** LLM moderation, embeddings, or semantic\n  search. Those features require reading content, which is exactly the thing we're refusing to do. That\n  trade is the point, not a limitation.\n- **A separate path *and* a separate process, zero contract drift.** Secure chat lives at\n  `/v7/:projectId/secure-chat/*`, entirely apart from the Replyke-compatible plaintext chat (which is\n  untouched), and runs as its own deployable service ([`@agora/secure-chat`](apps/secure-chat)) so the\n  blind relay can be isolated and scaled on its own. Use whichever a conversation needs.\n- **Your keys, your history.** Keys live on the client. Two recovery paths — both opaque to the server —\n  restore history onto a re-provisioned device: an optional **passphrase-encrypted backup**, and\n  **device-to-device history restore** (a peer seals your back-history into an ephemeral, targeted blob\n  the server relays blindly; see [`apps/secure-chat/docs/RESTORE.md`](apps/secure-chat/docs/RESTORE.md)).\n  The schema is multi-device-ready.\n- **The crypto stays swappable.** All MLS lives behind a small `SecureChatCrypto` interface, so the\n  concrete core (ts-mls vs OpenMLS-WASM) is a deferred, reversible choice — the server never changes.\n\nFull design, threat model, schema, endpoints, and roadmap: **[`docs/SECURE_CHAT.md`](docs/SECURE_CHAT.md)**.\n\n## Your social graph, pointed back at the community — not at you\n\nEvery platform mines the social graph *at* the user — to target, rank, and sell them. Agora points the\nsame structure **back at the community, for the community**, in service of its health. It's an\n**optional, off-by-default** layer: wire up a Neo4j instance (`NEO4J_URI`) and the **Garden** comes\nalive; leave it unset and nothing in the graph layer runs (every `/social/*` endpoint cleanly 503s and\nthe admin panels stay hidden).\n\nThere's a second purpose, just as deliberate: **to teach by showing.** Every member already *lives*\ninside a social graph on every platform they use — they just never get to see it. That graph is the\nasset being silently harvested, scored, brokered, and turned back on them as targeted advertising and\nbehavioral nudging, with their consent buried in a terms-of-service no one reads. By rendering the\ngraph **in the open, for once aimed at the community's wellbeing instead of at extracting from it**,\nthe Garden makes the thing concrete: *this* is what a map of who-talks-to-whom looks like, *this* is\nhow much it reveals, and *this* is exactly the structure that surveillance-capitalism platforms mine\nand sell without ever letting you look at it. Seeing it pointed at *care* is the clearest way to grasp\nhow dangerous it is when it's pointed at *profit* — and how flatly that use conflicts with the\ninterests of the people it's built from. The Garden is consciousness-raising as much as it is a feature:\ndata literacy you can feel, not a lecture.\n\nThe whole design exists because Agora's communities skew **trans, queer, sex-worker, and recovery** —\nfor them graph exposure is a doxxing/outing vector, and mass-reporting is the primary harassment\ntactic (any score that counts reports lights up on the *target*, not the harasser). So every rule\nbends toward one non-negotiable:\n\n\u003e **The asymmetry principle — every possible misreading must land on *kindness*, never a scarlet\n\u003e letter.** Friction only ever *dims* a node; it never reddens it.\n\n- **One public signal: warmth.** Loneliness and conflict both render as *dim* — indistinguishable on\n  purpose, because both mean \"bring care here.\" There is no per-person \"bad actor\" score, in any view.\n- **A zoom ladder that is a privacy ladder.** ☀️ **Community Weather** (one project-wide warmth scalar\n  with band + trend) → 🏡 **Neighborhood** (your *own* ties only, each rendered by its **dyadic**\n  brightness `B(you, them)` — never the friend's global score, which closes the friction side-channel\n  entirely). *(✨ Constellation — anonymous cluster blobs — is designed and next.)*\n- **Friction is quarantined and decays.** A user report projects a directed `FRICTION` edge that can\n  only *dim* an existing tie and fades at a ~14-day half-life (\"a bad week is not a permanent\n  identity\"); it never creates a tie, and never becomes a per-person verdict.\n- **One graph, opposite deployments.** The same structure serves a **vulnerable-population community**\n  and a **corporation's internal platform** — nearly opposite privacy needs — resolved by a per-project\n  `social_config` **tier** (community ↔ corporate, admin Settings → Social Graph), not by picking a side.\n\nUnder the hood the **scorer** owns the write side (it projects `INTERACTED` / `FOLLOWS` / `CONNECTED` /\n`FRICTION` edges into Neo4j off the same pgmq queue it already scores content on), and `@agora/api`\nowns the read side (`/v7/:projectId/social/*`, member/operator-gated, computed live or cached with band\nhysteresis). Full design, ethics, and threat model:\n**[`docs/SOCIAL-GRAPH.md`](docs/SOCIAL-GRAPH.md)** (the consolidation plan),\n**[`docs/AGORA-SOCIAL.md`](docs/AGORA-SOCIAL.md)** (the condensed design corpus), and\n**[`docs/DOZERDB.md`](docs/DOZERDB.md)** (DozerDB + OpenGDS setup — plugins, memory tuning, TLS).\n\n## The contract is the constraint\n\nAgora's whole reason to exist is that the forked SDK's typed hooks work **unchanged**. So the\ncontract is non-negotiable and fully specified:\n\n- **[`docs/MANIFEST.md`](docs/MANIFEST.md)** — every REST endpoint (method + path, marked\n  ✅ SDK-confirmed vs 🔶 inferred), the socket.io event names, and the auth / pagination / error\n  envelopes. This is the checklist.\n- **[`docs/MODELS.md`](docs/MODELS.md)** — field-level response shapes; the source of truth for both\n  API output and the database schema.\n\nMatch these exactly or the SDK's hooks break — that discipline is what makes the \"1:1\" claim real.\n\n## What's inside\n\nAgora is a **pnpm monorepo** — three apps and two shared packages. Each app has its own README\nwith setup, configuration, and development details:\n\n| Package | What it is | Docs |\n|---|---|---|\n| **[`@agora/api`](apps/api)** | The backend — Hono + Supabase + socket.io. Every endpoint, permission check, and bit of business logic. The reference package. | [apps/api/README.md](apps/api/README.md) |\n| **[`@agora/secure-chat`](apps/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](apps/secure-chat/README.md) |\n| **[`@agora/admin`](apps/admin)** | The admin dashboard — Vite + React. Moderation queue + AI queue, the **stewardship caseload** (cases, mediation channels, steward grants), feed tuning, webhook config, community \u0026 analytics dashboards, plus the optional **Social Graph** panel + **Community Weather** card (gated on `VITE_SOCIAL_GRAPH_ENABLED`). | [apps/admin/README.md](apps/admin/README.md) |\n| **`@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. | — |\n| **`@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. | — |\n\n```\nagora/\n├── docs/\n│   ├── MANIFEST.md          # the exact REST + socket.io contract (SDK-confirmed vs inferred)\n│   ├── MODELS.md            # field-level response shapes (drive both the API and the schema)\n│   ├── SECURE_CHAT.md       # the end-to-end-encrypted chat design + reference\n│   ├── STEWARDSHIP.md       # the conflict-resolution caseload design\n│   ├── SCORER.md            # the Python moderation + social-graph subsystem (services/scorer)\n│   ├── SOCIAL-GRAPH.md      # the optional Neo4j social-graph layer (\"the Garden\")\n│   ├── SELF-HOSTING.md      # run fully self-contained (native auth · S3/MinIO · local Postgres)\n│   └── DOZERDB.md           # DozerDB + OpenGDS setup (plugins, memory tuning, TLS)\n├── packages/\n│   ├── contract/            # @agora-server/contract — shared API types + zod schemas\n│   └── core/                # @agora/core   — shared kernel (env · logger · db+schema · auth · suspensions · redis)\n└── apps/\n    ├── api/                 # @agora/api          — the backend\n    ├── secure-chat/         # @agora/secure-chat  — the E2E-encrypted chat service (own process)\n    └── admin/               # @agora/admin        — the admin frontend\n```\n\n\u003e The **client** secure-chat packages (`@agora-sdk/secure-chat-crypto` — the `SecureChatCrypto` seam +\n\u003e mock, with the real ts-mls/OpenMLS core to come) live in the separate **`agora-sdk-plus`** repo, not\n\u003e here; this repo only devDepends on the mock for tests. See [Ecosystem](#ecosystem) and\n\u003e [`docs/SECURE_CHAT.md`](docs/SECURE_CHAT.md).\n\nThe client SDK lives in a **separate** companion repository,\n[`jenova-marie/agora-sdk`](https://github.com/jenova-marie/agora-sdk) (see [Ecosystem](#ecosystem)).\n\n## Quick start\n\n```bash\ncorepack enable          # activate the pinned pnpm\npnpm install             # install all workspaces (from the repo root)\npnpm -r build            # build every package (contract first, topologically)\n\n# Backend — the only hard requirement is a Supabase DATABASE_URL\ncd apps/api\ncp .env.example .env      # fill in DATABASE_URL\npnpm db:migrate:run       # apply migrations (idempotent; safe to re-run)\npnpm dev                  # http://localhost:4000/v7   (GET /health to verify)\n\n# Admin dashboard (optional)\ncd ../admin \u0026\u0026 pnpm dev   # http://localhost:5173\n```\n\nEach app's README covers its own configuration, commands, and Docker image. Start with\n**[apps/api](apps/api/README.md)** — it's the backend everything else points at.\n\n## Architecture\n\n```\nclient + forked Replyke SDK\n   │  HTTPS  /v7/:projectId/\u003cdomain\u003e/...        (+ socket.io for chat realtime)\n   ▼\n@agora/api  (Hono)   endpoints · business logic · permission checks\n   │  Drizzle ORM (postgres.js, Supabase transaction pooler :6543, prepare:false)   ← owner role, bypasses RLS\n   ▼\nSupabase Postgres   schema · triggers · RPC · pgvector · PostGIS · RLS\n        ├── Supabase Auth     (passwords, confirmation/reset emails, OAuth)\n        └── Supabase Storage  (file/image bytes)\n        Voyage AI ──▶ embeddings        Anthropic ──▶ /search/ask answers\n        pgmq ──▶ services/scorer ──▶ AI moderation · social-graph edges ──▶ Neo4j   (optional)\n                                                  @agora/api reads Neo4j back for /social/*\n\n@agora/admin ─(operator JWT)─▶ @agora/api\n```\n\n- **Drizzle owns all DB access** via a direct `postgres.js` connection. The Supabase JS client is\n  *only* for Auth/Storage and is lazily constructed.\n- **The server is the trust boundary.** The API connects as the table-owner role (so RLS never\n  constrains it) and enforces every ownership / role check in the handlers. RLS is enabled as\n  defense-in-depth with public-read policies.\n- **Multi-tenant by `project_id`** — every table has it; the SDK addresses `/v7/:projectId/...`. A\n  single-project deployment just has one `projects` row.\n- **Supabase is the default, not a hard dependency.** Auth and Storage sit behind provider seams\n  (`getAuthProvider()` / `getStorage()`): choose **native** email/password auth\n  (`DEFAULT_AUTH_PROVIDER=native`) and an **S3-compatible** object store (`STORAGE_PROVIDER=s3` →\n  MinIO/AWS) and the *same* server runs fully self-contained on a local Postgres — no Supabase at all.\n  See [`docs/SELF-HOSTING.md`](docs/SELF-HOSTING.md).\n\nSee [apps/api/README.md](apps/api/README.md#architecture) for the full backend architecture and\nhandler conventions.\n\n## Features\n\nEvery domain below is implemented and validated against live cloud Supabase. The **REST surface is\ncomplete** — no stubbed endpoints remain.\n\n| Domain | Highlights |\n|---|---|\n| **entities** | feed with full filter grammar + **pluggable ranking** (`hot`/`top`/`new`/`controversial`/`decay`/`gravity`/`wilson`/`bayesian`, per-project + per-request tunable), CRUD, drafts, foreign/short-id lookup, reactions, saved state |\n| **comments** | threaded (adjacency list + recursive CTE full-tree endpoint), reactions, Reddit-style soft delete, `sortBy` |\n| **users / follows** | profiles, follow graph + counts, suggestions |\n| **connections** | bidirectional friend-request state machine (none → pending → connected/declined) with directional status |\n| **spaces** | nested spaces (depth cap + cycle guard), membership (join/approve/ban/roles), rules, moderation queues, **digest config** |\n| **collections** | nestable saved-entity folders |\n| **notifications** | fan-out across every write path, inbox, unread count, mark read |\n| **reports** | report queue + resolution (entities, comments, chat messages) |\n| **auth** | sign-up/in/out, refresh rotation + reuse-detection, change/reset password, email verify, external RS256, OAuth provider sign-in/link |\n| **chat** | conversations (direct/group/space), members, messages, reactions, typing, read state — **socket.io realtime** |\n| **secure chat** | **end-to-end-encrypted** chat (MLS / RFC 9420) on a separate path **and a separate process** (the independently-deployable [`@agora/secure-chat`](apps/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`](docs/SECURE_CHAT.md)) |\n| **search** | semantic content search across entities/comments/messages (Voyage + pgvector), RAG `/ask` (Anthropic, SSE), text search for spaces/users |\n| **storage** | file uploads + image variants (sharp → webp, 5 sizing modes); **pluggable backend** — Supabase Storage or any S3-compatible store (MinIO/AWS) via `STORAGE_PROVIDER` |\n| **webhooks** | project webhooks (HMAC validation gates + `*.complete` broadcasts) + per-space digests |\n| **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 |\n| **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`](docs/STEWARDSHIP.md)) |\n| **social graph** *(optional · Neo4j)* | the **Garden** — community-health analytics pointed *back at the community*: **Community Weather** (project-wide warmth scalar + band/trend, cached with hysteresis) 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`](docs/SOCIAL-GRAPH.md)) |\n\nDenormalized counts (reaction counts, reply counts, member counts, thread counts, reputation) are\nmaintained atomically by Postgres **triggers** — never recomputed per request.\n\n## Security \u0026 access control\n\nThe trust boundary is the server: it talks to Postgres as the RLS-bypassing owner role and enforces\nevery read/write rule in the handlers, with RLS underneath as a verified backstop.\n\n- **Tokens** — Agora mints short-lived access tokens (30 m) + refresh tokens (30 d) with rotation,\n  reuse-detection, and a 30 s racing-tabs grace window.\n- **Anonymous reads, authenticated writes** — public content is readable without a token (matching\n  Replyke's contract); every mutation is `requireAuth`.\n- **Space privacy** — a members-only space is invisible to non-members on every path (feed, single\n  reads, reactions, comment creation, semantic search).\n- **Private chat** — conversation messages are readable only by active members, enforced on the REST\n  routes *and* inside the search RPC.\n- **End-to-end-encrypted secure chat** — an optional, separate chat surface where the server stores only\n  **ciphertext** and *cannot* read messages at all (MLS / RFC 9420; deny-all RLS on every secure table).\n  See [`docs/SECURE_CHAT.md`](docs/SECURE_CHAT.md).\n- **Moderation visibility** — removed content is always hidden from non-privileged readers (omitted from\n  lists, 404'd on single reads, filtered in the search RPC); operators bypass to review.\n- **Operators \u0026 stewards** — a deployment-operator allowlist grants a project-wide moderation/admin\n  god-view; a DB-granted **steward** role (between member and operator) is route-scoped to the\n  conflict-resolution caseload.\n- **RLS backstop** — denies `anon`/`authenticated` any private-space, removed, or draft row directly.\n\nFull detail (including the OAuth callback setup) lives in\n[apps/api/README.md](apps/api/README.md#configuration-env).\n\n## Docker\n\nThe repo's `docker-compose.yml` builds and wires the whole stack. Every service is **profile-gated**, so\na bare `docker compose up` starts nothing — pick a profile. The **`full`** profile is the API stack —\napi (`:4000`) + admin (`:8080`) + the scorer services + neo4j + cron. With Supabase backing\nPostgres/Auth/Storage there's no local DB to run:\n\n```bash\ndocker compose --profile full up --build                # the API stack, from the repo root\ndocker compose run --rm agora node scripts/migrate.mjs  # apply migrations (naming agora enables `full`)\n```\n\nThe admin service is the Vite SPA on nginx, reverse-proxying `/v7` + `/socket.io` to the api\n(same origin, no CORS). Each app's README documents building and running its image standalone.\n\nThe **`secure`** profile is the **standalone [`@agora/secure-chat`](apps/secure-chat)** deploy — Redis +\nthe secure-chat process (`:4002`), no API stack. Like `agora`, it doesn't bundle a database: it persists\nthe `secure_*` tables to whatever **`DATABASE_URL`** points at, which in v1 is the **shared main Postgres**\n(or Supabase) — already migrated. (Run it alongside the API with `--profile full --profile secure`, where\nthe admin nginx routes `/v7/:projectId/secure-chat/*` + the WebSocket `/secure-socket/` to it.)\n\n```bash\ndocker compose --profile secure up --build                       # DATABASE_URL → shared/Supabase Postgres\ndocker compose --profile secure --profile selfhost up --build    # + a LOCAL db (minio tags along, unused)\n```\n\nFor a **fully self-contained** stack (no Supabase at all) add **`selfhost`** (local Postgres + MinIO).\nAgora swaps Supabase out through its provider seams — **native** email/password auth\n(`DEFAULT_AUTH_PROVIDER=native`) and **S3-compatible** storage (`STORAGE_PROVIDER=s3` → MinIO/AWS) — so\nthe *same* server image runs against either backend. See [`docs/SELF-HOSTING.md`](docs/SELF-HOSTING.md):\n\n```bash\ndocker compose --profile full --profile selfhost up --build\n```\n\nFor production, an **optional TLS edge proxy** (Caddy) gives the stack a single HTTPS front door with\n**automatic Let's Encrypt certs**, HSTS + security headers, a body-size cap, and an authoritative\n`X-Forwarded-For`. It's the `edge` profile, run alongside `full`:\n\n```bash\n# in .env: SERVER_NAME=your.domain  and  RATE_LIMIT_TRUSTED_HOPS=2\ndocker compose --profile full --profile edge up --build\n```\n\nFor Tor hidden services — or any deploy where Let's Encrypt can't reach you — a second\n[`Caddyfile.onion`](deploy/proxy/Caddyfile.onion) serves a cert you supply at startup instead of\nauto-ACME (selected via the `CADDYFILE` env var, same `edge` profile). See\n[`deploy/proxy/README.md`](deploy/proxy/README.md). (Optionally add `--profile scale` for Redis as\nthe cross-replica rate-limit store.)\n\n## Ecosystem\n\nAgora is **three separate repos** — kept separate on purpose, *not* one monorepo:\n\n- **`agora-server`** (this repo) — the backend + admin. The contract's server side.\n- **[`agora-sdk`](https://github.com/jenova-marie/agora-sdk)** — the forked, repointed Replyke SDK,\n  published as `@agora-sdk/*` (`core` / `react-js` / `react-native` / `expo`). The base URL flows in\n  via the provider prop; no `api.replyke.com` left. Its own release cycle.\n- **`agora-sdk-plus`** — additive, Agora-only SDK features built on `@agora-sdk/*` (kept out of the\n  Replyke fork so that stays a tiny, documented divergence). Home of **end-to-end-encrypted chat**:\n  `@agora-sdk/secure-chat-crypto` (the `SecureChatCrypto` seam + mock; real ts-mls/OpenMLS core to come)\n  and the transport/provider/hooks. Depends on `@agora-server/contract` for the wire types — never the reverse.\n- **[`agora-demo`](https://github.com/jenova-marie/agora-demo)** — a standalone Vite + React app: the\n  **1:1 compatibility harness** (and what powers the [live demo](https://demo.agora-oss.org)). Eight\n  tabs, each exercising one SDK surface against a running server. It installs the **published**\n  `@agora-sdk/*` (not a workspace link), so it catches server↔SDK contract drift exactly as a\n  third-party app would.\n\n**Why a fork?** The published Replyke SDK hardcodes `https://api.replyke.com/v7`; `agora-sdk`\nrepoints that base URL (see `docs/MANIFEST.md §0`). Because it does, the URL shape, auth token\nsemantics, `{ data, pagination }` / `{ error, code }` envelopes, response object shapes, and\nsocket.io event names all line up 1:1 — that's the entire point.\n\nPoint the SDK at your server with `VITE_API_BASE_URL` (defaults to `http://localhost:4000/v7`) and\npass a `projectId` + a signed user token to the provider; the SDK's typed hooks (`useEntity`,\n`useComments`, `useChat`, …) then work unchanged. See the\n[agora-sdk README](https://github.com/jenova-marie/agora-sdk#quick-start) for a full quick-start.\n\n## Status\n\n- ✅ **Backend feature-complete** — every domain implemented and validated against live cloud\n  Supabase; the REST surface has no remaining stubs.\n- ✅ Realtime chat, semantic + RAG search, auth (token rotation + external RS256 + OAuth), storage,\n  project webhooks, space digests, and RLS (public-read + authenticated self-access) verified end-to-end.\n- ✅ **Access control** — space read/post privacy, private-chat membership gating (incl. search),\n  server-enforced removed-content hiding, the operator god-view, and the route-scoped steward role,\n  all enforced server-side.\n- ✅ **Governance** — moderation (report queues + optional LLM auto-moderation) and the stewardship\n  caseload (cases, private mediation channels, participant notifications) are wired and operator-gated\n  in the admin dashboard.\n- 🌱 **Social graph (optional · Neo4j)** — the `social_config` foundation, **Community Weather**, and\n  **Neighborhood** are live and env-gated behind `NEO4J_URI` (scorer writes the `INTERACTED` / `FOLLOWS`\n  / `CONNECTED` / `FRICTION` edges, the API reads them); Constellation + admin graph analytics are\n  designed and next ([`docs/SOCIAL-GRAPH.md`](docs/SOCIAL-GRAPH.md)).\n- ✅ **Supabase-optional** — provider seams run the same server on **native** email/password auth\n  (`DEFAULT_AUTH_PROVIDER=native`) + **S3-compatible** storage (`STORAGE_PROVIDER=s3`) + a local\n  Postgres, fully self-contained; DB layer validated end-to-end ([`docs/SELF-HOSTING.md`](docs/SELF-HOSTING.md)).\n- ✅ Idempotent Drizzle migrations `0000`–`0048`; unit + integration test suites green.\n- ✅ Client SDK published + repointed — validated 1:1 by the\n  [`agora-demo`](https://github.com/jenova-marie/agora-demo) compatibility harness.\n- ⬜ Ops backlog: deployment guides, and RLS write policies (only needed if the Supabase Data API is\n  opened for writes).\n\n## Contributing\n\n**Contributors welcome — Agora is built in the open, and we'd genuinely love your help.** 🌱\nBug fixes, new admin-app slices, docs, test coverage, deployment guides, or closing a contract gap\nagainst Replyke — there's room to jump in, whatever your level.\n\n- 🐛 **Found a bug or a contract mismatch?** [Open an issue](https://github.com/jenova-marie/agora-server/issues).\n  For SDK-compat drift, include the endpoint and the expected-vs-actual shape from\n  [`docs/MODELS.md`](docs/MODELS.md).\n- ✨ **Want to build something?** Browse the [open issues](https://github.com/jenova-marie/agora-server/issues)\n  or the **[Status](#status)** backlog. Friendly first areas: admin-app features, test coverage, and\n  the deployment guide.\n- 📋 **Before you start,** read **[CONTRIBUTING.md](CONTRIBUTING.md)** — dev setup, the contract\n  rules, coding conventions, the migration workflow, and how to open a PR.\n\nThe one hard rule: **the contract is the constraint.** Any change to request/response shapes, REST\npaths, or socket.io events must keep [`agora-sdk`](https://github.com/jenova-marie/agora-sdk) working\n1:1 — see [The contract is the constraint](#the-contract-is-the-constraint). Everyone is welcome here;\nbe kind, assume good faith, and build something we'd all want to use. 💜\n\n## License\n\n**[AGPL-3.0-only](LICENSE)** for the server (`@agora/api`, `@agora/admin`) — you\ncan self-host forever, but running a *modified* Agora as a network service means sharing your changes\nback. The shared wire contract ([`@agora-server/contract`](packages/contract)) stays **Apache-2.0** so the\n[`agora-sdk`](https://github.com/jenova-marie/agora-sdk) and any third-party client can build against\nit freely. **The community edition is AGPL-3.0 and always will be.**\n\nContributions are accepted under the **[Developer Certificate of Origin](https://developercertificate.org/)** —\nsign off your commits with `git commit -s`. There is **no CLA**: your contributions stay yours,\nlicensed AGPL-3.0, and they can't be relicensed out from under you. See\n[CONTRIBUTING.md](CONTRIBUTING.md#licensing).\n\n\u003e **AGPL §13 note for operators:** because users interact with Agora over a network, your deployment\n\u003e must offer them its corresponding source. Agora surfaces a source link by default — keep it pointing\n\u003e at your fork.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjenova-marie%2Fagora-server","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjenova-marie%2Fagora-server","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjenova-marie%2Fagora-server/lists"}