Skip to content

Repository files navigation

Agora

Agora is a community-driven social platform API where users create and join communities, publish posts, comment, follow each other, and interact through likes. It provides full identity management (registration, email verification, password reset, sessions) and role-based access control for users, community moderators, and platform administrators.


Current Status

Implemented

  • User registration with email verification, login with session management, refresh-token rotation, password reset
  • User profile management (avatar, bio, email change with confirmation, account deactivation)
  • Community CRUD with owner/moderator/admin roles, avatar and banner media, banning, membership management
  • Posts CRUD with media attachments, full-text search (trigram), moderation (hide/restore/delete)
  • Threaded comments with media attachments, moderation, and soft-delete
  • Post and comment likes with cursor-paginated likers lists
  • User follow/unfollow with paginated followers and following lists
  • Client-side upload flow via signed Cloudinary URLs
  • Rate limiting per route group backed by Redis
  • Structured logging to file and persistent log store
  • Interactive API documentation via Swagger UI
  • Database seed scripts for bulk data generation

Intentionally Out of Scope (for now)

  • Comment likes are defined at the schema level but the controller/service is commented out
  • Real-time features (WebSockets, notifications)
  • Frontend client

Repository Structure

agora/
├── src/
│   ├── app.module.ts              Root module
│   ├── main.ts                    Bootstrap + Swagger setup
│   ├── common/                    Cross-cutting concerns
│   │   ├── api-features/          Generic query builder (filter, sort, paginate, cursor, search)
│   │   ├── constants/             Repository tokens, throttle presets, shared types
│   │   ├── decorators/            @AuthUser, @Roles, @CommunityRole
│   │   ├── dtos/                  PaginationDTO, CursorPaginationDTO
│   │   ├── filters/               GlobalExceptionFilter, AppHttpExceptions
│   │   ├── guards/                AuthenticationGuard, RolesGuard, CommunityRoleGuard
│   │   ├── interceptors/          GlobalResponseInterceptor (envelope + cookie)
│   │   ├── mapper/                AppMapperService (class-transformer)
│   │   ├── middlewares/           MorganMiddleware (HTTP request logging)
│   │   ├── services/              CommunityAccessService (membership + ban checks)
│   │   └── utils/                 AppUtilsService (token generation, hashing)
│   ├── infrastructure/            Platform-level integrations
│   │   ├── cloudinary/            Provider + service for Cloudinary
│   │   ├── database/
│   │   │   ├── data-source.ts     TypeORM DataSource configuration
│   │   │   ├── Typeorm.module.ts  Global TypeORM module
│   │   │   ├── migrations/        Incremental migration files
│   │   │   ├── scripts/           CLI helpers for migration generation
│   │   │   └── seeds/             Bulk seed scripts (users, communities, members, posts)
│   │   ├── jwt/                   Global JWT module
│   │   ├── mail/                  Global mailer module (SMTP + EJS templates)
│   │   ├── redis/                 Global Redis client module
│   │   ├── throttling/            Rate limiter (custom guard, factory, presets)
│   │   └── winston/               Logger service + config (file + MongoDB transports)
│   ├── modules/                   Domain feature modules
│   │   ├── identity/              Auth, User, UserSession
│   │   ├── community/             Community CRUD, admin, moderation, bans
│   │   ├── community-members/     Membership entity, repository, service
│   │   ├── posts/                 Post CRUD, admin moderation
│   │   ├── comments/              Comment CRUD, admin, moderation
│   │   ├── likes/                 Post likes (comment likes scaffolded)
│   │   ├── follow/                Follow/unfollow, followers/following lists
│   │   ├── mail/                  EmailEntity, MailRepository, AppEmailService
│   │   └── upload/                Signed upload flow, MediaAssetEntity
│   └── templates/                 EJS email templates
├── test/                          E2E test config
├── package.json
├── tsconfig.json
├── nest-cli.json
└── eslint.config.mjs

Architecture: Modular Monolith

Agora uses a modular monolith pattern: all features ship in a single deployable but are organized into isolated modules with clear boundaries, repository-interface abstractions, and explicit dependency injection tokens. This keeps development simple (single deployment, shared database) while enforcing module boundaries that make future extraction straightforward.

Global / Cross-Cutting Modules

These modules are imported once (at the root) and available throughout the application:

Module Responsibility
ConfigModule Loads .env variables globally via @nestjs/config
AppLoggerModule Winston logger with file + MongoDB transports
AppThrottlerModule Named rate limiters (default, auth, community, posts, comments, likes) backed by Redis storage
AppJwtModule Registers @nestjs/jwt globally with JWT_SECRET
AppTypeOrmModule PostgreSQL connection with auto-run migrations
AppRedisModule Shared ioredis client for throttling and caching
AppEmailModule SMTP mailer with EJS templates, persists sent email metadata
AppMapperModule class-transformer-based DTO mapping service
AppUtilsModule Crypto utilities (random token generation, SHA-256 hashing)

Domain Modules

Module Entities Key Responsibilities
IdentityModule UserEntity, UserSessionEntity, EmailEntity Composes AuthModule + UserModule + UserSessionModule
AuthModule Register, login, logout, refresh, email verification, password reset
UserModule Profile CRUD, avatar, email change, admin user management
UserSessionModule Session CRUD, revocation
CommunityModule CommunityEntity, CommunityBanEntity Community CRUD, join/leave, moderation (ban/unban, hide posts), admin
CommunityMembersModule CommunityMemberEntity Membership management (internal, no controller)
PostsModule PostEntity Post CRUD, search, admin moderation (hide/restore/delete)
CommentsModule CommentEntity Threaded comments, soft-delete, admin + moderator management
LikesModule LikeEntity Post likes, like status, likers list, liked posts list
FollowModule FollowEntity Follow/unfollow, followers/following lists, status check
AppUploadModule MediaAssetEntity Signed Cloudinary uploads, upload confirmation + persistence
AppCloudinaryModule Cloudinary SDK configuration + signature generation

System Design Diagrams

High-Level Architecture

flowchart LR
  Client["Client\n(Web / Mobile)"] -->|HTTPS| API["Agora API\n(:3000)"]
  API --> DB[("PostgreSQL\n(Primary DB)")]
  API --> Redis[("Redis\n(Rate Limiting)")]
  API --> Mongo[("MongoDB\n(Log Store)")]
  API --> Cloudinary["Cloudinary\n(Media Storage)"]
  API --> SMTP["SMTP Provider\n(Email)"]
Loading

Module Dependency Diagram

flowchart TB
  subgraph Global["Global Modules"]
    Config["ConfigModule"]
    Logger["AppLoggerModule"]
    Throttler["AppThrottlerModule"]
    JWT["AppJwtModule"]
    TypeORM["AppTypeOrmModule"]
    RedisM["AppRedisModule"]
    Email["AppEmailModule"]
    Mapper["AppMapperModule"]
    Utils["AppUtilsModule"]
  end

  subgraph Identity["IdentityModule"]
    Auth["AuthModule"]
    User["UserModule"]
    Session["UserSessionModule"]
  end

  subgraph Content["Content Modules"]
    Community["CommunityModule"]
    Members["CommunityMembersModule"]
    Posts["PostsModule"]
    Comments["CommentsModule"]
    Likes["LikesModule"]
    Follow["FollowModule"]
  end

  subgraph Media["Media"]
    Upload["AppUploadModule"]
    Cloud["AppCloudinaryModule"]
  end

  Auth --> Session
  Auth --> Email
  User --> Upload
  User --> Email
  Community --> Members
  Posts --> Community
  Comments --> Posts
  Comments --> Community
  Likes --> Posts
  Likes --> Community
  Upload --> Cloud
Loading

Key Flow: Register + Email Verification

sequenceDiagram
  participant U as User
  participant API as Agora API
  participant DB as PostgreSQL
  participant Mail as SMTP Provider

  U->>API: POST /api/auth/register
  API->>DB: Check email + username uniqueness
  API->>DB: Create user (unverified)
  API->>DB: Persist email record
  API->>Mail: Send verification email (EJS template)
  API-->>U: 201 { data: user }

  U->>API: POST /api/auth/verify-email?token=...
  API->>DB: Find user by hashed token + check expiry
  API->>DB: Mark isVerified = true, clear token
  API-->>U: 200 Verified
Loading

Key Flow: Login + Refresh Token Rotation

sequenceDiagram
  participant U as User
  participant API as Agora API
  participant DB as PostgreSQL

  U->>API: POST /api/auth/login {email, password}
  API->>DB: Find user, verify bcrypt hash
  API->>DB: Create session (tokenHash, userAgent, ip, expiry)
  API-->>U: 200 { token, data: user } + Set-Cookie: jwt=refreshToken

  Note over U,API: Later, access token expires...

  U->>API: POST /api/auth/refresh (Cookie: jwt=refreshToken)
  API->>DB: Verify refresh JWT, find session, check not revoked
  API->>DB: Rotate session tokenHash
  API-->>U: 200 { token } + Set-Cookie: jwt=newRefreshToken
Loading

Key Flow: Logout / Logout All

sequenceDiagram
  participant U as User
  participant API as Agora API
  participant DB as PostgreSQL

  U->>API: POST /api/auth/logout (Bearer + Cookie)
  API->>DB: Revoke current session (set revokedAt)
  API-->>U: 200 + Clear jwt cookie

  U->>API: POST /api/auth/logout-all (Bearer)
  API->>DB: Revoke all user sessions
  API-->>U: 200 + Clear jwt cookie
Loading

Key Flow: Forgot Password + Reset

sequenceDiagram
  participant U as User
  participant API as Agora API
  participant DB as PostgreSQL
  participant Mail as SMTP Provider

  U->>API: POST /api/auth/forgot-password {email}
  API->>DB: Find user, generate reset token
  API->>DB: Store hashed token + expiry on user
  API->>Mail: Send reset password email
  API-->>U: 200 Check your email

  U->>API: POST /api/auth/reset-password?token=... {password}
  API->>DB: Find user by hashed token + check expiry
  API->>DB: Update password hash, clear token
  API->>DB: Revoke all sessions
  API-->>U: 200 Password reset successful
Loading

Key Flow: Media Upload (Signed Upload)

sequenceDiagram
  participant U as User
  participant API as Agora API
  participant CDN as Cloudinary

  U->>API: POST /api/uploads/sign [{purpose, mimeType, byteSize}]
  API->>API: Validate policy (size, mime, purpose)
  API->>CDN: Generate signature (folder, timestamp, public_id)
  API-->>U: 200 [{signature, public_id, api_key, timestamp, cloud_name, folder}]

  U->>CDN: Upload file directly (using signed params)
  CDN-->>U: Upload complete

  U->>API: POST /api/uploads/confirm [{purpose, public_id, resource_type}]
  API->>CDN: Verify file exists (api.resource)
  API->>API: Persist MediaAssetEntity
  API-->>U: 200 [{id, publicId, secureUrl, ...}]
Loading

Data Design

Entity Relationship Diagram

erDiagram
  users ||--o{ user_sessions : "has sessions"
  users ||--o{ posts : "authors"
  users ||--o{ comments : "authors"
  users ||--o{ likes : "likes"
  users ||--o{ follows : "follower"
  users ||--o{ follows : "following"
  users ||--o{ community_members : "memberships"
  users ||--o{ community_bans : "bans"
  users ||--o{ communities : "owns"
  users ||--o{ mediaAssets : "owns"
  users ||--o| mediaAssets : "avatar"

  communities ||--o{ posts : "contains"
  communities ||--o{ comments : "scoped to"
  communities ||--o{ community_members : "has members"
  communities ||--o{ community_bans : "has bans"
  communities ||--o| mediaAssets : "avatar"
  communities ||--o| mediaAssets : "banner"

  posts ||--o{ comments : "has"
  posts ||--o{ likes : "receives"
  posts ||--o{ mediaAssets : "attached"

  comments ||--o{ mediaAssets : "attached"
  comments ||--o| comments : "parent (threaded)"
Loading

Major Entities

Table Purpose
users User accounts with auth fields, verification tokens, roles
user_sessions Refresh-token sessions with hash, expiry, revocation, IP/UA
emails Persisted record of every sent email (JSONB payload)
communities Named groups with owner, avatar, banner, active flag
community_members Join table: user + community + role (OWNER / MODERATOR / MEMBER)
community_bans Join table: banned user + community
posts User-authored content within a community, with isActive for moderation
comments Threaded comments on posts, with status for soft-delete
likes Polymorphic likes (POST or COMMENT target type)
follows Directed user-to-user follow relationships
mediaAssets Cloudinary-hosted media linked to users, posts, comments, or communities

Token / Session Strategy

  • Access token: Short-lived JWT signed with JWT_SECRET, payload contains { id: userId }.
  • Refresh token: Longer-lived JWT signed with JWT_REFERSH_SECRET, payload contains { id: userId, sid: sessionId }. Stored as an httpOnly cookie named jwt.
  • Rotation: On each /auth/refresh call, a new refresh token is issued and the session's tokenHash is updated (previous token becomes invalid).
  • Revocation: Logout marks the session's revokedAt; logout-all revokes every session for the user.

Migrations Strategy

  • Migrations live in src/infrastructure/database/migrations/.
  • Naming: <timestamp>-<description>.ts (auto-generated by TypeORM CLI).
  • migrationsRun: true in the data source config, so pending migrations run automatically on application startup.
  • Manual commands: npm run migration:run, npm run migration:revert.
  • Generate from schema diff: npm run migration:generate-manual -- <Name>.
  • Create empty migration: npm run migration:create -- <Name>.

Authentication & Security

Access Control Layers

Layer Mechanism
Authentication AuthenticationGuard verifies Bearer JWT, sets request['user'] = userId
Role-based RolesGuard + @Roles(ADMIN) checks user.role from DB
Community role CommunityRoleGuard + @CommunityRole(MODERATOR, OWNER) checks membership role
Community access CommunityAccessService.userCanInteract checks community active + not banned + is member
Rate limiting AppThrottlerGuard (extends ThrottlerGuard) with named throttlers per route group

Auth Endpoints

Method Route Auth Description
POST /api/auth/register No Register a new user
POST /api/auth/login No Login (returns access token + refresh cookie)
POST /api/auth/logout Bearer Logout current session
POST /api/auth/logout-all Bearer Revoke all sessions
POST /api/auth/refresh Cookie Rotate refresh token, get new access token
POST /api/auth/verify-email?token= No Verify email address
POST /api/auth/resend-email-verify No Resend verification email
POST /api/auth/forgot-password No Request password reset email
POST /api/auth/reset-password?token= No Reset password with token
GET /api/auth/sessions Bearer List active sessions

Example Requests

Register:

curl -X POST http://localhost:3000/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{"email": "user@example.com", "username": "johndoe", "password": "SecurePass123!"}'

Login:

curl -X POST http://localhost:3000/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email": "user@example.com", "password": "SecurePass123!"}' \
  -c cookies.txt

Refresh:

curl -X POST http://localhost:3000/api/auth/refresh \
  -b cookies.txt -c cookies.txt

Access a protected endpoint:

curl http://localhost:3000/api/users/me \
  -H "Authorization: Bearer <access_token>"

Observability & Logging

Request Logging Pipeline

HTTP Request
  → MorganMiddleware (combined format)
    → Winston logger.info (writes to file + MongoDB)
    → console.log (stdout)

Log Destinations

Destination Level Format Location
logs/app.log all JSON Local file
logs/error.log error JSON Local file
MongoDB collection logs info+ JSON (timestamped) LOG_DB_URI
stdout all Morgan combined Console

Exception Logging

The GlobalExceptionFilter catches all unhandled exceptions, maps TypeORM errors to appropriate HTTP statuses, and logs the structured error payload via the Winston logger. In development mode (NODE_ENV=development), stack traces and raw exception details are included in the response.

Correlation IDs

Not currently implemented. Requests are identified by path and timestamp in log entries.


Configuration

Environment Variables

Variable Required Description
PORT No Server port (default 3000)
NODE_ENV No development / production (default development)
Database
DB_HOST Yes PostgreSQL host
DB_PORT No PostgreSQL port (default 5432)
DB_USER Yes PostgreSQL username
DB_PASS Yes PostgreSQL password
DB_NAME Yes PostgreSQL database name
JWT
JWT_SECRET Yes Access token signing secret
JWT_EXPIRES_IN Yes Access token expiry (e.g. 15m)
JWT_REFERSH_SECRET Yes Refresh token signing secret
JWT_REFESH_EXPIRES_IN Yes Refresh token expiry (e.g. 30d)
JWT_REFRESH_TOKEN_EXPIRES_IN_SECONDS Yes Refresh session TTL in seconds (e.g. 2592000)
saltRounds No Bcrypt cost factor (default 12)
Redis
REDIS_HOST Yes Redis host
REDIS_PORT No Redis port (default 6379)
REDIS_USERNAME No Redis username
REDIS_PASSWORD No Redis password
REDIS_THROTTLING_URI Yes Redis URI for throttler storage (e.g. redis://...)
SMTP
SMTP_HOST Yes SMTP server host
SMTP_PORT No SMTP server port (default 587)
SMTP_USER Yes SMTP username (also used as from address)
SMTP_PASS Yes SMTP password
Email URLs
VERIFY_EMAIL_URL Yes Base URL for email verification links
RESET_PASSWORD_URL Yes Base URL for password reset links
UPDATE_EMAIL_URL Yes Base URL for email update confirmation links
Cloudinary
CLOUDINARY_NAME Yes Cloudinary cloud name
CLOUDINARY_API_KEY Yes Cloudinary API key
CLOUDINARY_API_SECRET Yes Cloudinary API secret
Logging
LOG_DB_URI No MongoDB URI for log persistence (default mongodb://localhost:27017/log_db)
Throttling (optional overrides)
THROTTLING_LIMIT No Default rate limit (default 5)
THROTTLING_TTL No Default rate limit window in seconds (default 60)
THROTTLING_AUTH_LIMIT No Auth rate limit (default 5)
THROTTLING_AUTH_TTL No Auth rate limit window (default 60)
THROTTLING_WRITE_LIMIT No Write rate limit (default 10)
THROTTLING_WRITE_TTL No Write rate limit window (default 60)
THROTTLING_COMMUNITY_LIMIT No Community rate limit (default 20)
THROTTLING_COMMUNITY_TTL No Community rate limit window (default 60)
THROTTLING_POSTS_LIMIT No Posts rate limit (default 20)
THROTTLING_POSTS_TTL No Posts rate limit window (default 60)

Running Locally

Prerequisites

  • Node.js 18+ (LTS recommended)
  • PostgreSQL 14+
  • Redis 6+
  • MongoDB (for log persistence; optional for local dev)
  • A Cloudinary account (for media uploads)
  • An SMTP provider or local mail server (e.g. Mailtrap, Mailhog)

Setup

# 1. Clone the repository
git clone <repo-url> && cd agora

# 2. Install dependencies
npm install

# 3. Create .env from the example
cp .env.example .env
# Edit .env with your database, Redis, SMTP, Cloudinary, and JWT credentials

# 4. Run database migrations (auto-runs on startup, or manually)
npm run migration:run

# 5. (Optional) Seed test data
npm run seed:users -- 100
npm run seed:communities -- 10 1
npm run seed:members -- 1 1 100
npm run seed:posts -- 50 1 1

# 6. Start the dev server
npm run start:dev

Available Commands

Command Description
npm run start:dev Start in watch mode
npm run start:debug Start in debug + watch mode
npm run start:prod Start compiled production build
npm run build Compile to dist/
npm run lint Lint and auto-fix
npm run test Run unit tests
npm run test:watch Run unit tests in watch mode
npm run test:cov Run tests with coverage
npm run test:e2e Run end-to-end tests
npm run migration:run Run pending migrations
npm run migration:revert Revert last migration
npm run migration:generate-manual -- <Name> Generate migration from schema diff
npm run migration:create -- <Name> Create empty migration file
npm run seed:users -- <count> Seed users (default password: Password123!)
npm run seed:communities -- <count> <ownerId> Seed communities
npm run seed:members -- <communityId> [startUserId] [endUserId] Seed community memberships
npm run seed:posts -- <count> <authorId> <communityId> Seed posts

Common Troubleshooting

Problem Solution
ECONNREFUSED on port 5432 Ensure PostgreSQL is running and DB_* env vars are correct
ECONNREFUSED on Redis Ensure Redis is running and REDIS_* env vars are correct
Migrations fail on startup Check DB_* credentials; run npm run migration:run manually for detailed errors
Emails not sending Verify SMTP_* vars; for local dev, use Mailtrap or Mailhog
Cloudinary signature errors Verify CLOUDINARY_* env vars match your Cloudinary dashboard

API Documentation

Swagger UI is available at:

http://localhost:3000/api/docs

The Swagger spec is auto-generated from controller decorators (@ApiTags, @ApiOperation, @ApiParam, @ApiResponse, @ApiBody) and DTO validation decorators (@ApiProperty, @ApiPropertyOptional). Authentication is configured with both Bearer token and cookie auth schemes.

API Endpoint Summary

Tag Base Path Endpoints
Auth /api/auth register, login, logout, logout-all, refresh, verify-email, resend-email-verify, forgot-password, reset-password, sessions
Users /api/users me (GET/PATCH), me/avatar, me/email, me/deactivate, me/change-password, search, confirm-update-email, :id
Admin Users /api/admin/users list, get, update-role, ban, unban, delete
Communities /api/communities create, join, leave, me, check-name, search, :id (GET/PATCH), :id/posts, :id/members, :id/avatar, :id/banner
Admin Communities /api/admin/communities list, get, ban, unban, delete
Mod Communities /api/mod/communities :id/members, :id/members/:userId/role, :id/members/:userId/kick, :id/posts/:postId/hide, :id/posts/:postId/restore, :id/posts/:postId (DELETE), :id/ban-user, :id/unban-user
Posts /api/posts create, me, search, :id (GET/PATCH/DELETE)
Admin Posts /api/admin/posts list, get, ban, unban, delete
Comments /api/comments create, list, :id (GET/PATCH/DELETE)
Admin Comments /api/admin/comments list, get, ban, unban, delete
Mod Comments /api/mod/communities :id/comments, :id/comments/:commentId, hide, restore, delete
Post Likes /api/likes/posts :postId (POST/DELETE), :postId/status, :postId/count, :postId/likers, me
Follows /api/follows :userId (POST/DELETE), followers/:userId, following/:userId, status/:userId
Uploads /api/uploads sign, confirm

Contributing Guidelines

Branch Strategy

  • main — stable, production-ready
  • development — integration branch for active work
  • Feature branches from development: feature/<name>, fix/<name>

Commit Messages

Use concise, imperative-mood messages describing the why:

fix: reject likes on inactive posts
feat: add community ban check to post-likes
refactor: deduplicate community lookups in access service

Development Workflow

  1. Create a feature branch from development
  2. Make changes, ensure linting passes: npm run lint
  3. Run tests: npm run test
  4. Open a PR against development

PR Checklist

  • Code lints without errors (npm run lint)
  • Unit tests pass (npm run test)
  • New migrations included if entities changed
  • Swagger decorators added for new/changed endpoints
  • No secrets or .env files committed

About

Agora is a Social platform built in monolith using NestJS + TypeORM, designed to scale as features grow.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages