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.
- 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
- Comment likes are defined at the schema level but the controller/service is commented out
- Real-time features (WebSockets, notifications)
- Frontend client
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
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.
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) |
| 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 |
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)"]
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
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
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
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
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
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, ...}]
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)"
| 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 |
- 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 anhttpOnlycookie namedjwt. - Rotation: On each
/auth/refreshcall, a new refresh token is issued and the session'stokenHashis updated (previous token becomes invalid). - Revocation: Logout marks the session's
revokedAt; logout-all revokes every session for the user.
- Migrations live in
src/infrastructure/database/migrations/. - Naming:
<timestamp>-<description>.ts(auto-generated by TypeORM CLI). migrationsRun: truein 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>.
| 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 |
| 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 |
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.txtRefresh:
curl -X POST http://localhost:3000/api/auth/refresh \
-b cookies.txt -c cookies.txtAccess a protected endpoint:
curl http://localhost:3000/api/users/me \
-H "Authorization: Bearer <access_token>"HTTP Request
→ MorganMiddleware (combined format)
→ Winston logger.info (writes to file + MongoDB)
→ console.log (stdout)
| 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 |
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.
Not currently implemented. Requests are identified by path and timestamp in log entries.
| 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) |
- 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)
# 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| 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 |
| 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 |
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.
| 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 |
main— stable, production-readydevelopment— integration branch for active work- Feature branches from
development:feature/<name>,fix/<name>
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
- Create a feature branch from
development - Make changes, ensure linting passes:
npm run lint - Run tests:
npm run test - Open a PR against
development
- 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
.envfiles committed