Monorepo layout:
| Package | Description |
|---|---|
apps/api |
NestJS REST API — single source of truth, PostgreSQL + Redis |
apps/bot |
discord.js v14 bot — talks only to the API |
apps/web |
Next.js dashboard — Discord OAuth, admin views |
packages/shared |
Shared Zod schemas and types |
On a fresh Ubuntu server with Docker:
chmod +x setup.sh
./setup.shThat creates .env, starts Postgres, Redis, API, worker, bot, and web (docker-compose.yml). The API container runs prisma migrate deploy before boot. See docs/runbooks/production-deploy.md.
From the server, in your git clone (same directory as docker-compose.yml). This works without chmod (uses bash):
DEPLOY_BRANCH=main bash scripts/vps-update.shAfter git pull, the script is also stored as executable in git, so this works too:
DEPLOY_BRANCH=main ./scripts/vps-update.shThe project includes .cursor/hooks.json: when the Cursor agent finishes a response, it runs node .cursor/hooks/auto-git-push.mjs, which commits all pending changes and git pushes the current branch (if any). Requires non-interactive git push (SSH agent or credential helper). To disable, create an empty file .cursor/no-autopush or set environment variable CURSOR_AUTO_PUSH=0. The hook prints short VPS follow-up steps to the Hooks output when a push succeeds.
After a reboot or deploy, verify the stack:
chmod +x validate-deployment.sh && ./validate-deployment.shOperational debugging: docs/runbooks/operations-and-recovery.md.
To bring the stack up manually (after configuring .env for Docker hostnames postgres / redis / api):
docker compose up -d --build- Node.js 20+
- pnpm (
npx pnpm@9.15.0works if pnpm is not installed globally) - PostgreSQL 16 and Redis 7 (Docker Compose file included)
-
Copy environment template:
copy .env.example .env
Use the same
BOT_API_KEYfor the API and bot, and the sameDASHBOARD_JWT_SECRETfor the API and web app. AssignADMIN_DISCORD_IDSor rely onplatform_accounts(see seed) for dashboardADMINRBAC. -
Start infrastructure only (Postgres + Redis):
docker compose up -d postgres redis
-
Install dependencies and migrate:
pnpm install pnpm --filter @protect/api exec prisma migrate deploy -
Seed sample data (optional; creates
PlatformAccountADMIN + trusted user):pnpm db:seed
-
Run core services (four terminals for the full report/outbox/event loop — web dashboard optional):
pnpm --filter @protect/api dev pnpm --filter @protect/worker dev pnpm --filter @protect/bot dev pnpm --filter @protect/web dev
The worker claims
outbox_eventsand publishes domain envelopes to Redis (pub/sub + stream). Without it, events stay queued and downstream consumers never see them. -
Integration tests (real Postgres + Redis; see env vars in docs/e2e-local.md):
pnpm --filter @protect/api test:integration
Use
SKIP_INTEGRATION=truewhenDATABASE_URL/REDIS_URLare not available. For read-only API behavior without Redis, seeREDIS_OPTIONALin docs/e2e-local.md.
Business APIs live under /v1 (e.g. GET /v1/user/:id). GET /health and GET /ready stay at the root for load balancers.
- API OpenAPI: http://localhost:3001/docs
- Integrations stub:
GET /v1/integrations/status(future FiveM / Minecraft clients) - Web app resolves
API_BASE_URLto.../v1automatically if the path is omitted.
- Bot: Enable Server Members Intent in the Discord Developer Portal for
GuildMemberAddand member lookups. - OAuth (web): Redirect URL:
NEXTAUTH_URL+/api/auth/callback/discord. - Slash commands register on bot startup.
Optional: set REDIS_URL for the bot to subscribe to Redis pub/sub (protect:user.*, protect:server.config.updated) and invalidate local server-config cache.
- BOT —
x-api-keymatchingBOT_API_KEY. - Dashboard users —
Authorization: BearerJWT (sub= Discord snowflake), signed withDASHBOARD_JWT_SECRET. - Effective roles: ADMIN (
platform_accounts.roleor legacyADMIN_DISCORD_IDS), TRUSTED (trusted_users), USER (default), BOT (API key).
Guards: BotOrJwtGuard (identity) + RbacGuard + @RequireRoles(...) per route.
The API publishes JSON envelopes on:
protect:user.flaggedprotect:user.reportedprotect:user.updatedprotect:server.config.updated
The bot can subscribe (when REDIS_URL is set) to refresh cached guild config. Delivery is at-most-once.
Community reports insert a low-weight Flag (COMMUNITY_REPORT) after Redis/database dedupe, per-reporter cooldown, and daily caps (see REPORT_* env vars in .env.example).
- Only
apps/apiuses Prisma /DATABASE_URL. - Bot and web call the versioned JSON API under
/v1. - User profiles use Redis cache-aside (
user:{discordId}) plus optional short negative caching for unknown CLEAN profiles. BotGET /v1/user/:idmay sendx-protect-skip-user-cache(bot key only) so/checkreads through to Postgres.
GET /internal/cache/user/:discordId/validate— compare DB flags/scores, optional Redis public JSON (admin JWT).POST /internal/cache/user/:discordId/repair—UsersService.refreshPublicCachefor that user (admin JWT).
- Worker logs JSON lines on successful dispatch and on dispatch failure (
eventId,type,correlationId,outboxStatus,instanceId). - Bot logs one JSON info line per event (
eventId,type,guildId);user.*events withoutguildIdlog a debug line (expected — no server config invalidation). - API: set
LOG_OUTBOX_DEBUG=truefor debug logs when outbox rows are enqueued (includesoutboxEventIdfor single enqueue).
-
Worker/outbox tuning and backpressure: docs/runbooks/worker-recovery.md (
API_OUTBOX_REJECT_THRESHOLD, batch sizes, idempotent dispatch). -
API exposure: With root
docker-compose.yml, the API publishes127.0.0.1:$API_PUBLISH_PORTonly (not on0.0.0.0). Reach it from other containers viahttp://api:3001; avoid exposinghttps://api.sentra.gg(or any public API host) without a locked-down reverse proxy and auth./integrations/statusand/metricsrequirex-api-key(bot) or dashboard JWT (BOT_API_KEY,DASHBOARD_JWT_SECRET). InNODE_ENV=production, CORS defaults to off unless you setCORS_ORIGINS(comma list); the dashboard normally calls the API server-side, so browsers do not need CORS./docs(Swagger) is disabled in production unless you setSWAGGER_ENABLED=true. -
Run
pnpm run buildthennode dist/main.js(API/bot) orpnpm startinapps/web. -
Use strong secrets and terminate TLS at your edge for user-facing apps (dashboard).
-
api_clientstable is reserved for future scoped API keys (game servers, resellers).