Pulse is a community moderation platform that unifies data from Discourse forums, Telegram channels, and on-chain NEAR activity, then routes it through AI-assisted workflows for human review. The repository contains every service, shared package, and deployment artifact needed to run the stack yourself.
- Real-time ingestion from Discourse, Telegram, and NEAR balance activity
- AI-powered content rating queue with structured moderation outcomes
- Next.js 16 dashboard built on React Server Components and Better Auth
- Shared PostgreSQL schema with generated TypeScript and Rust types
- Docker, Docker Compose, and Railway deployment paths for every long-running service
┌──────────────────────────────────────────────────────────────────────────────────────┐
│ PostgreSQL Database │
│ (Single Source of Truth) │
└──────────────────────────────────────────────────────────────────────────────────────┘
↑ ↑ ↑ ↑ ↑
┌──────┴──────┐ ┌────┴────┐ ┌──────┴─────┐ ┌─────┴──────┐ ┌─────────┴─────────┐
│ Web App │ │The Brain│ │ Discourse │ │ Telegram │ │ NEAR Balance Sync │
│ (Next.js) │ │(Node.js)│ │ Listener │ │ Listener │ │ (Rust) │
└─────────────┘ └─────────┘ └────────────┘ └────────────┘ └───────────────────┘
| Path | Description |
|---|---|
apps/web |
Next.js moderation dashboard with Better Auth integration |
apps/the-brain |
Node.js worker that processes the content rating queue with AI providers |
apps/discourse-listener |
Rust service that ingests topics, posts, likes, and revisions from Discourse |
apps/telegram-listener |
Rust MTProto listener for Telegram channels and reactions |
apps/near-balance-listener |
Rust worker that snapshots NEAR account balances via FastNEAR |
packages/shared |
Shared TypeScript helpers, database types, feature flags, and AI model registry |
packages/better-auth-near |
Better Auth extension that adds NEAR wallet authentication |
migrations / seeds |
Kysely migrations and optional seed scripts |
docker-compose.yml |
End-to-end local container stack |
- Node.js 20 or newer
- pnpm 10.x
- Rust stable toolchain
- PostgreSQL 15+ (local or remote)
- Docker (optional, for containerized workflows)
pnpm install
cp .env.example .env
pnpm db:migrate:latest
pnpm db:generate
pnpm devUseful follow-up commands:
pnpm the-brain:dev
pnpm discourse-listener:dev
pnpm telegram-listener:dev
pnpm near-balance-listener:dev
pnpm db:seed:run # optional demo/bootstrap dataEnvironment variable details live in docs/environment.md.
This monorepo ships as independently deployable services plus a shared PostgreSQL database.
| Component | Role | Public ingress | Main requirements |
|---|---|---|---|
web |
Next.js moderation UI | Yes | DATABASE_URL, BETTER_AUTH_SECRET, BETTER_AUTH_URL, NEXT_PUBLIC_APP_URL, FLAGS_SECRET, GitHub OAuth |
the-brain |
AI rating worker with /health endpoint |
No | DATABASE_URL, OPENROUTER_API_KEY, PORT or HEALTH_PORT |
discourse-listener |
Discourse ingestion worker | No | DATABASE_URL, DISCOURSE_BASE_URL |
telegram-listener |
Telegram ingestion worker | No | DATABASE_URL, Telegram credentials, durable session storage or TELEGRAM_SESSION_DATA |
near-balance-listener |
NEAR balance worker | No | DATABASE_URL, optional FASTNEAR_API_KEY |
db-migrate |
One-off schema migration job | No | Run before any schema-dependent rollout |
db-seed |
Optional bootstrap/demo data job | No | Non-production only unless you intentionally want seed data |
Use plain Docker when you want to run each container explicitly instead of relying on docker-compose.yml.
Build the images:
docker build -f migrations/Dockerfile -t pulse/db-migrate .
docker build -f apps/web/Dockerfile -t pulse/web .
docker build -f apps/the-brain/Dockerfile -t pulse/the-brain .
docker build -f apps/discourse-listener/Dockerfile -t pulse/discourse-listener .
docker build -f apps/telegram-listener/Dockerfile -t pulse/telegram-listener .
docker build -f apps/near-balance-listener/Dockerfile -t pulse/near-balance-listener .Create a network and start PostgreSQL:
docker network create pulse-local
docker volume create pulse-postgres
docker run -d \
--name pulse-db \
--network pulse-local \
-p 55432:5432 \
-e POSTGRES_DB=codeofconduct \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=postgres \
-v pulse-postgres:/var/lib/postgresql/data \
postgres:16-alpineWait for PostgreSQL and run migrations:
until docker exec pulse-db pg_isready -U postgres -d codeofconduct; do sleep 1; done
docker run --rm \
--name pulse-migrate \
--network pulse-local \
-e DATABASE_URL=postgresql://postgres:postgres@pulse-db:5432/codeofconduct \
pulse/db-migrateOptional seed step:
docker run --rm \
--name pulse-seed \
--network pulse-local \
-e DATABASE_URL=postgresql://postgres:postgres@pulse-db:5432/codeofconduct \
pulse/db-migrate \
npx kysely seed runStart the main services:
docker run -d \
--name pulse-web \
--network pulse-local \
-p 3000:3000 \
-e DATABASE_URL=postgresql://postgres:postgres@pulse-db:5432/codeofconduct \
-e DEV_AUTH_BYPASS=false \
-e BETTER_AUTH_SECRET=... \
-e BETTER_AUTH_URL=https://pulse.example.com \
-e NEXT_PUBLIC_APP_URL=https://pulse.example.com \
-e FLAGS_SECRET=... \
-e GITHUB_CLIENT_ID=... \
-e GITHUB_CLIENT_SECRET=... \
pulse/web
docker run -d \
--name pulse-the-brain \
--network pulse-local \
-p 3001:3001 \
-e DATABASE_URL=postgresql://postgres:postgres@pulse-db:5432/codeofconduct \
-e OPENROUTER_API_KEY=... \
-e PORT=3001 \
pulse/the-brain
docker run -d \
--name pulse-discourse-listener \
--network pulse-local \
-e DATABASE_URL=postgresql://postgres:postgres@pulse-db:5432/codeofconduct \
-e DISCOURSE_BASE_URL=https://gov.near.org \
-e RUST_LOG=info \
pulse/discourse-listenerFor a local smoke test without wiring OAuth first, add
-e DEV_AUTH_BYPASS=true -e IS_LOCAL=true -e DOCKER_ENV=development to the
pulse-web container. Keep those values disabled in any public environment.
Optional workers:
docker run -d \
--name pulse-near-balance-listener \
--network pulse-local \
-e DATABASE_URL=postgresql://postgres:postgres@pulse-db:5432/codeofconduct \
-e FASTNEAR_API_KEY="${FASTNEAR_API_KEY:-}" \
-e RUST_LOG=info \
pulse/near-balance-listenerTelegram requires credentials plus durable session storage:
docker volume create pulse-telegram-session
docker run -d \
--name pulse-telegram-listener \
--network pulse-local \
-e DATABASE_URL=postgresql://postgres:postgres@pulse-db:5432/codeofconduct \
-e TELEGRAM_API_ID=... \
-e TELEGRAM_API_HASH=... \
-e TELEGRAM_CHANNELS=@nearprotocol \
-e TELEGRAM_SESSION_FILE=/var/lib/telegram/telegram_session.bin \
-v pulse-telegram-session:/var/lib/telegram \
pulse/telegram-listenerUseful checks:
docker ps --filter name=pulse-
curl http://127.0.0.1:3000/healthz
curl http://127.0.0.1:3001/health
docker logs -f pulse-the-brainCleanup:
docker rm -f \
pulse-web \
pulse-the-brain \
pulse-discourse-listener \
pulse-telegram-listener \
pulse-near-balance-listener \
pulse-db \
2>/dev/null || true
docker network rm pulse-local 2>/dev/null || true
docker volume rm pulse-postgres pulse-telegram-session 2>/dev/null || trueThe repo includes docker-compose.yml for a local multi-container stack:
db,db-migrate,web,the-brain, anddiscourse-listenerstart by defaulttelegram-listeneris behind thetelegramprofilenear-balance-listeneris behind thenearprofiledb-seedis behind theseedprofile and stays optional- PostgreSQL data and Telegram session state use named volumes
Build the images:
docker compose buildStart the default stack:
docker compose up -dEnable optional services only when you need them:
docker compose --profile seed run --rm db-seed
docker compose --profile near up -d near-balance-listener
docker compose --profile telegram up -d telegram-listenerUseful checks:
docker compose ps
docker compose logs -f web the-brain discourse-listener
curl http://127.0.0.1:3000/healthzStop and clean up:
docker compose --profile seed --profile near --profile telegram down
docker compose --profile seed --profile near --profile telegram down -vNotes for this compose stack:
- The containers always use the internal Postgres hostname
db; they do not inherit your host-localDATABASE_URL. - PostgreSQL is exposed to the host on
postgresql://postgres:postgres@127.0.0.1:55432/codeofconductby default. db-seedis explicit on purpose. Production deployments should not seed automatically on every rollout.telegram-listenerstores session state in thetelegram_sessionvolume at/var/lib/telegram/telegram_session.bin.- The local compose stack sets
DEV_AUTH_BYPASS=true,IS_LOCAL=true, andDOCKER_ENV=developmentforwebso containerized smoke tests do not require OAuth setup. the-brainstays on the internal compose network; usedocker compose psto inspect its health status, or rundocker compose exec the-brain node -e "fetch('http://127.0.0.1:3001/health',{redirect:'manual'}).then(async (response) => { process.stdout.write(await response.text()); process.exit(response.status === 200 ? 0 : 1) }).catch(() => process.exit(1))"if you want the raw JSON body.- The default compose stack uses a placeholder
OPENROUTER_API_KEYsothe-braincan boot for smoke tests. Set a real key before you expect rating jobs to succeed.
- Provision PostgreSQL and set
DATABASE_URLfor every service. - Run
db-migratebefore starting any schema-dependent service. - Seed only when bootstrapping demo data or an intentionally preloaded environment.
- Deploy
weband verify/healthz. - Deploy
the-brainand verify/health. - Deploy only the listener services you actually need.
- Validate logs, OAuth callback URLs, Telegram session persistence, and database connectivity after each rollout.
web:DATABASE_URL,BETTER_AUTH_SECRET,BETTER_AUTH_URL,NEXT_PUBLIC_APP_URL,FLAGS_SECRET,GITHUB_CLIENT_ID,GITHUB_CLIENT_SECRETthe-brain:DATABASE_URLandOPENROUTER_API_KEYdiscourse-listener:DATABASE_URL,DISCOURSE_BASE_URLtelegram-listener:DATABASE_URL,TELEGRAM_API_ID,TELEGRAM_API_HASH,TELEGRAM_CHANNELS, and eitherTELEGRAM_SESSION_DATAor a persistentTELEGRAM_SESSION_FILEnear-balance-listener:DATABASE_URL, optionalFASTNEAR_API_KEY
telegram-listener can authenticate in two ways:
- Persistent disk via
TELEGRAM_SESSION_FILE - Base64 session injection via
TELEGRAM_SESSION_DATA
For a deployment with durable storage, bootstrap the session once interactively:
docker volume create pulse-telegram-session
docker run --rm -it \
--name pulse-telegram-auth \
--network pulse-local \
-e DATABASE_URL=postgresql://postgres:postgres@pulse-db:5432/codeofconduct \
-e TELEGRAM_API_ID=... \
-e TELEGRAM_API_HASH=... \
-e TELEGRAM_CHANNELS=@nearprotocol \
-e TELEGRAM_ALLOW_INTERACTIVE_LOGIN=true \
-e TELEGRAM_SESSION_FILE=/var/lib/telegram/telegram_session.bin \
-v pulse-telegram-session:/var/lib/telegram \
pulse/telegram-listenerFor an ephemeral platform such as Railway, encode the session file and store it as TELEGRAM_SESSION_DATA:
base64 < telegram_session.bin | tr -d '\n'Then set:
TELEGRAM_SESSION_DATA=<single-line-base64-value>TELEGRAM_SESSION_FILE=/var/lib/telegram/telegram_session.bin
If you need to rotate a session, stop the worker, repeat the same bootstrap flow, then restart the long-running deployment with the updated file or base64 blob.
Pulse already includes Railway manifests for the long-running services:
apps/web/railway.tomlapps/the-brain/railway.tomlapps/discourse-listener/railway.tomlapps/telegram-listener/railway.tomlapps/near-balance-listener/railway.toml
Use one Railway service per component. Keep the full repo context so the workspace builds can access shared packages from the monorepo root.
Recommended Railway setup:
- Create or attach a PostgreSQL service.
- Deploy
webfromapps/web/railway.tomland verify/healthz. - Deploy
the-brainfromapps/the-brain/railway.tomland verify/health. - Deploy the Rust listeners from their service-specific manifests as background workers.
- Run migrations separately before shipping schema-dependent changes. The simplest path is a one-off service or command based on
migrations/Dockerfilethat runsnpx kysely migrate latest.
Railway-specific notes:
- Set
DEV_AUTH_BYPASS=falsein any public environment. webhealth check path is/healthz, not/api/health.the-brainbinds its health server on port3001.- The Rust listeners are worker processes; they should rely on restart-on-failure, logs, and metrics rather than path-based HTTP health checks.
- For Telegram on Railway, prefer
TELEGRAM_SESSION_DATAunless you attach durable storage.
webexposes a dependency-free/healthzendpoint. Treat it as a liveness check, not a full readiness probe.the-brainexposes/healthand/stats;/healthincludes database connectivity and queue backlog state.- In Docker Compose,
the-brainis intentionally not published on a host port; use container health ordocker compose execto query/health. db-seedis intentionally not part of the default rollout path.DEV_AUTH_BYPASSmust remainfalseanywhere the app is reachable from the internet.
- Compose stack:
docker-compose.yml - Migration image:
migrations/Dockerfile - Web image and Railway manifest:
apps/web/Dockerfile,apps/web/railway.toml - The Brain image and Railway manifest:
apps/the-brain/Dockerfile,apps/the-brain/railway.toml - Discourse listener image and Railway manifest:
apps/discourse-listener/Dockerfile,apps/discourse-listener/railway.toml - Telegram listener image and Railway manifest:
apps/telegram-listener/Dockerfile,apps/telegram-listener/railway.toml - NEAR balance listener image and Railway manifest:
apps/near-balance-listener/Dockerfile,apps/near-balance-listener/railway.toml
pnpm format:check
pnpm fmt:rust:check
pnpm lint
pnpm clippy
pnpm test
pnpm build- Environment variable reference:
docs/environment.md - Self-hosting notes:
docs/self-hosting.md - Repository architecture and service conventions:
CLAUDE.md
Created by Hack Humanity • Copyright © 2026 • MIT License