Skip to content

Discours/publy-backend

Repository files navigation

Publy Backend

Open-source publishing ecosystem backend. Powers multi-tenant media platforms, independent blogs, and community-driven publications.

Stack

  • Framework: NestJS 11 on Node.js 20 LTS
  • Language: TypeScript 5.7 (strict mode)
  • Database: PostgreSQL 16 + Prisma ORM
  • Cache, PubSub & rate-limit: Redis 7 (via ioredis)
  • Scheduled jobs: @nestjs/schedule with a Redis distributed lock (see pattern 36)
  • Storage: S3-compatible (MinIO dev, Storj/AWS production)
  • CRDT: Hocuspocus for collaborative editing (separate apps/crdt process)
  • Real-time: SSE with O(1) EventRouter pattern (single Redis PSUBSCRIBE per instance)
  • Testing: Vitest + Supertest (integration); Playwright planned for E2E
  • Linter: Biome (shared with Publy frontend)

Note on queues: Heavy async jobs (newsletter fan-out at scale, thumbnail generation retries) will move to BullMQ in a later sprint. Current scheduled work uses @nestjs/schedule with a distributed Redis lock — simpler, sufficient for early scale.

Architecture

NestJS monorepo (apps/core, apps/crdt, libs/prisma) with escape hatches to Rust microservices at scale.

Built on architectural patterns from Discours production microservices. The docs/discours-patterns/ registry documents 25 battle-tested patterns (composable reactions, self-regulating content, O(1) EventRouter, CSV-based RBAC, CRDT two-tier auth, scheduled-publishing cron lock, …).

Quick Start

Prerequisites

  • Node.js ≥ 20.11 (use nvm use — see .nvmrc)
  • Docker & Docker Compose
  • npm ≥ 10.8

Setup

# Clone and install
git clone https://github.com/Discours/publy-backend.git
cd publy-backend
npm install

# Copy env template
cp .env.example .env

# Start infrastructure (PostgreSQL, Redis, MinIO, Mailhog)
npm run docker:dev

# Run database migrations
npm run prisma:migrate:prod  # applies existing migrations (no dev-schema-diff)

# Start dev server
npm run start:dev

Server runs on http://localhost:3000. Health check: http://localhost:3000/health/live.

Dev Services

Ports are intentionally non-default so Publy's services don't collide with a Discours stack already running on the same machine.

Service URL Purpose
API http://localhost:3000 NestJS apps/core
CRDT ws://localhost:1234 NestJS apps/crdt (Hocuspocus)
PostgreSQL localhost:5433 Primary database
Redis localhost:6380 Cache, PubSub, sessions, rate-limit
MinIO http://localhost:9001 S3-compatible storage (web UI)
MinIO S3 http://localhost:9000 S3 API endpoint
Mailhog http://localhost:8025 Email testing (SMTP on :1025)

Multi-tenancy in dev

Every HTTP request is tenant-scoped via either hostname or the X-Community-Slug header. For localhost development, set X-Community-Slug: publy (or whichever seeded community you want) on requests from your frontend/Postman. The fallback default community is DEFAULT_COMMUNITY_SLUG in .env.

Scripts

# Development
npm run start:dev          # apps/core with hot reload
npm run start:core:dev     # same as above
npm run start:crdt:dev     # apps/crdt with hot reload

# Builds
npm run build:core         # apps/core → dist/apps/core
npm run build:crdt         # apps/crdt → dist/apps/crdt

# Database
npm run prisma:generate    # regenerate Prisma client
npm run prisma:migrate     # dev migration (creates files + applies)
npm run prisma:migrate:prod # deploy-style migration (applies existing)
npm run prisma:studio      # DB browser UI
npm run prisma:seed        # seed dev data (see prisma/seed.ts)

# Quality
npm run lint               # Biome lint
npm run check:fix          # Biome lint + format + fix
npm run test               # Vitest integration tests
npm run test:cov           # with coverage report

# Docker
npm run docker:dev         # start all services
npm run docker:logs        # tail logs
npm run docker:down        # stop services

Project Structure

publy-backend/
├── apps/
│   ├── core/                       # HTTP API (NestJS)
│   │   ├── src/
│   │   │   ├── main.ts             # Bootstrap (ValidationPipe + CORS + shutdown hooks)
│   │   │   ├── app.module.ts       # Root module (wires every feature module)
│   │   │   ├── config/             # Zod-validated env config
│   │   │   ├── modules/            # Feature modules (one per entity)
│   │   │   │   ├── analytics/      # Sprint 8 — creator analytics + view tracking
│   │   │   │   ├── auth/           # Sprint 1 — JWT + lockout + sessions + OAuth (planned)
│   │   │   │   ├── community/      # Sprint 1 — multi-tenancy + CSV roles + TenantInterceptor
│   │   │   │   ├── content/        # Sprint 2 — shouts + drafts + topics
│   │   │   │   ├── email/          # Sprint 1 — Mailgun/Mailhog + Handlebars
│   │   │   │   ├── feed/           # Sprint 7 — latest/trending/following (keyset pagination)
│   │   │   │   ├── file/           # Sprint 5 — S3 + thumbnails + quotas + rate-limit
│   │   │   │   ├── follower/       # Sprint 4 — 3 follow types
│   │   │   │   ├── health/         # Terminus health checks
│   │   │   │   ├── inbox/          # Sprint 5 — DMs + groups + typing indicators
│   │   │   │   ├── moderation/     # Sprint 9 — reports + admin audit trail
│   │   │   │   ├── newsletter/     # Sprint 11 — double-opt-in + digest v1
│   │   │   │   ├── notification/   # Sprint 4 — dedup + SSE + O(1) EventRouter
│   │   │   │   ├── profile/        # Sprint 5 — public/private views + avatar
│   │   │   │   ├── reaction/       # Sprint 3 — 14 types + self-regulation
│   │   │   │   ├── scheduler/      # Sprint 12 — scheduled publishing + cron lock
│   │   │   │   ├── search/         # Sprint 7 — PG FTS (ts_rank_cd + ts_headline)
│   │   │   │   └── syndication/    # Sprint 13 — RSS + Atom + sitemap.xml
│   │   │   └── shared/             # errors (DomainError hierarchy), redis, etc.
│   │   └── test/                   # Integration tests (one file per module)
│   │
│   └── crdt/                       # Hocuspocus WebSocket server (collaborative editing)
│       └── src/
│           └── hocuspocus/         # Two-tier auth + Y.Doc persistence
│
├── libs/
│   └── prisma/                     # Shared PrismaService (imported as @libs/prisma)
│
├── prisma/
│   ├── schema.prisma               # Single source of truth for DB schema
│   └── migrations/                 # Timestamp-ordered SQL migrations
│
├── docker/                         # MinIO bucket init + misc service scripts
├── docs/
│   └── discours-patterns/          # Pattern registry (the "why" of every design decision)
│
├── docker-compose.yml              # Dev infrastructure (Postgres 5433, Redis 6380, …)
├── Dockerfile                      # Multi-stage production build
├── biome.json                      # Lint + format config (shared with Publy frontend)
├── vitest.config.ts                # Integration test runner config
├── tsconfig.json                   # Strict-mode TS config
└── .nvmrc                          # Node version pin (use `nvm use`)

Security posture

  • Global JwtAuthGuard — registered as APP_GUARD; every endpoint requires a valid JWT unless explicitly marked @Public(). New endpoints are protected by default; forgetting a decorator is an opt-in, not an opt-out.
  • Global ValidationPipe (whitelist: true, forbidNonWhitelisted: true) — strips unknown fields and rejects malformed payloads before service code runs.
  • Global DomainExceptionFilter — every error becomes a canonical { error: { code, message, details }, timestamp, path } envelope. Stack traces never leak to clients.
  • Global TenantInterceptor — resolves community_id from hostname or X-Community-Slug. Every service query is tenant-scoped via TenantContextService.getCommunityId().
  • Rate limits — IP-keyed sliding windows on auth (register/login/password-reset/verify-resend), newsletter subscribe, and file uploads.
  • Account lockout — 5 failed logins → 5-minute lockout (account-scoped, complementing the IP-scoped login rate limit).

Roadmap

Phase 1 — MVP Backend (completed through Sprint 13):

Sprint Scope Status
0 Scaffold + CI + Docker + Prisma skeleton
1 Auth (JWT+Redis+lockout+email flows) + Community (multi-tenancy+RBAC) + Email
2 Content (Shout lifecycle) + Draft (Yjs-ready) + Topic
3 Reaction (14 types) + self-regulation
4 Follower + Notification (dedup + SSE + EventRouter)
5 File (S3+Sharp+quota) + Profile + Inbox (DMs+groups+typing)
6 Monorepo refactor + CRDT (Hocuspocus two-tier auth)
7 Discovery: Search (PG FTS) + Feed (latest/trending/following)
8 Analytics (creator overview + view tracking)
9 Moderation (reports + audit log)
10 Session self-service (list/revoke/logout-others)
11 Newsletter v1 (double-opt-in + digest preview/send)
12 Scheduled publishing (cron + Redis distributed lock)
13 Syndication (RSS + Atom + sitemap.xml)

Phase 2 — Platform (planned): custom domains, paid subscriptions, admin panel, OAuth providers (Google/GitHub/…).

Phase 3 — Ecosystem (planned): P2P distribution (libp2p+IPFS), federation (ActivityPub), advanced analytics.

License

AGPL-3.0 — see LICENSE.

Inspired By

This backend is the evolution of the Discours ecosystem — a production publishing platform with 8+ microservices. We've adopted their battle-tested patterns while consolidating into a single maintainable NestJS monolith (with escape hatches to keep their Rust services usable at scale).

About

Open-source publishing ecosystem backend — NestJS + PostgreSQL + Redis. Multi-tenant, real-time, collaborative.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages