A production-quality backend portfolio project demonstrating multi-tenancy, secure authentication, and enterprise-grade authorization patterns using NestJS + TypeScript, PostgreSQL, and JWT.
This project is intentionally backend-only (no frontend) and focuses on:
- Clean modular architecture and maintainable code
- IAM concepts: users, organizations (tenants), roles, permissions boundaries
- Secure defaults: validation, error handling, least privilege
- Cloud & DevOps readiness: Docker, CI, and Vercel deployment
- Goals
- Core features
- Architecture
- Domain model
- Auth & RBAC flow
- Multi-tenancy model
- Database schema
- API endpoints (high level)
- Configuration
- How to run locally (Docker)
- How to run locally (non-Docker)
- Migrations
- Tests
- CI (GitHub Actions)
- Deploy to Vercel
- Security considerations
- Project roadmap (modules)
- Provide a multi-tenant backend where all business data is isolated by organization.
- Demonstrate authentication (register/login) and authorization (RBAC with org roles).
- Use a Controller → Service → Repository pattern with DTOs and centralized error handling.
- Be deployable with minimal changes to Vercel and compatible with Supabase or AWS Free Tier Postgres.
-
Organizations (tenants)
- An Organization is the tenant boundary.
-
Users belong to organizations
- Users authenticate globally, but data access is scoped to one organization.
-
RBAC
- Roles:
ORG_ADMIN,ORG_USER,READ_ONLY.
- Roles:
-
Secure authentication
- Register, login, JWT access token.
-
Protected business domain: Projects
- Standard CRUD, scoped to organization.
-
Tenant isolation enforcement
- Users can only access data within their organization.
-
Validation and error handling
- DTO validation, consistent error responses, centralized exception filtering.
-
Presentation layer (HTTP)
- NestJS controllers
- DTOs (request/response)
- Guards (JWT auth + RBAC)
- Pipes (validation)
-
Application layer
- Services (use-cases)
- Orchestration and business rules
-
Infrastructure layer
- Repositories (data access)
- Database module (Postgres connection)
- Migrations
AuthModule— register/login, password hashing, JWT signingOrganizationsModule— tenant entities and admin operationsUsersModule— user management within tenant scopeProjectsModule— protected domainConfigModule— strongly-validated environment configDatabaseModule— Postgres + migrations
-
Controllers:
- validate incoming requests
- map HTTP inputs to DTOs
- return stable response DTOs
-
Services:
- enforce business invariants
- apply authorization decisions (in cooperation with guards)
- call repositories
-
Repositories:
- own persistence logic
- ensure queries are tenant-scoped
- A global exception filter provides:
- consistent error response shape
- safe messages (no leaking internals)
- traceability via request correlation IDs (planned)
-
Organization
- Represents a tenant.
-
User
- Global identity that authenticates via email/password.
-
OrganizationMembership
- Connects a user to an organization.
- Stores role for that user inside that organization.
-
Project
- A business entity owned by an organization.
- Client calls
POST /auth/registerwith email, password, org name (or orgId depending on endpoint choice). - Service:
- hashes password (bcrypt/argon2; recommended argon2)
- creates user
- creates organization and membership (first user becomes
ORG_ADMIN)
- Client calls
POST /auth/loginwith email/password. - Service verifies password and issues JWT:
sub: userIdorgId: active organization contextroles: derived from membership for that org
-
JwtAuthGuard:- verifies JWT signature
- attaches
request.user(subject, orgId, roles)
-
RolesGuard:- reads required roles from
@Roles(...) - checks whether request user has sufficient role
- reads required roles from
- Client sends
Authorization: Bearer <token>. - JWT guard authenticates and sets
request.user. - Roles guard authorizes.
- Service executes use-case.
- Repository executes tenant-scoped query using
orgId.
This project uses organization-based multi-tenancy (single database, shared schema), enforced by:
- JWT includes
orgIdrepresenting the active tenant context. - All domain queries include
orgIdpredicates. - Guard/service checks prevent cross-tenant access.
- Database constraints/indexes support tenant-scoped access patterns.
This design maps cleanly to Supabase (managed Postgres) and AWS RDS Free Tier.
Exact SQL migrations will be provided in the migrations deliverable.
id(uuid, pk)name(text, unique per tenant naming rules)created_at(timestamptz)updated_at(timestamptz)
id(uuid, pk)email(citext/text, unique)password_hash(text)is_active(boolean)created_at(timestamptz)updated_at(timestamptz)
id(uuid, pk)organization_id(uuid, fk -> organizations.id)user_id(uuid, fk -> users.id)role(enum/text: ORG_ADMIN | ORG_USER | READ_ONLY)created_at(timestamptz)
Constraints:
- unique
(organization_id, user_id)
id(uuid, pk)organization_id(uuid, fk -> organizations.id)name(text)description(text, nullable)created_by_user_id(uuid, fk -> users.id)created_at(timestamptz)updated_at(timestamptz)
Indexes:
(organization_id, id)(organization_id, created_at)
POST /auth/register— create org + admin user (or join flow if enabled later)POST /auth/login— returns JWT access tokenGET /auth/me— returns current user + active org + role
GET /organizations— list organizations for current user (membership-based)POST /organizations— create organization (admin only)
POST /projects— create project (ORG_ADMIN/ORG_USER)GET /projects— list projects within org (all roles)GET /projects/:id— read a project within org (all roles)PATCH /projects/:id— update (ORG_ADMIN/ORG_USER)DELETE /projects/:id— delete (ORG_ADMIN)
The application uses a dedicated config module to:
- load env vars
- validate at startup
- provide strongly typed access to settings
NODE_ENV—development | test | productionPORT— default 3000DATABASE_URL— Postgres connection stringJWT_ACCESS_SECRET— strong secretJWT_ACCESS_TTL_SECONDS— e.g. 900
A .env.example file will be provided.
A
docker-compose.ymlwill run the API and a Postgres instance.
- Copy env file:
cp .env.example .env
- Start services:
docker compose up --build
- Run migrations (command provided in repository scripts):
npm run db:migrate
- API will be available at:
http://localhost:3000
- Install dependencies:
npm ci
- Start Postgres (local or Supabase) and set
DATABASE_URL. - Run migrations:
npm run db:migrate
- Start dev server:
npm run start:dev
- Database schema changes are managed via migrations.
- Migrations are designed to be deterministic and safe to run in CI and production.
Commands (to be implemented):
npm run db:migrate— applies migrationsnpm run db:rollback— rolls back last migration (where supported)
Basic automated tests will include:
- unit tests for services (auth and RBAC checks)
- integration tests for controllers with a test database
Commands:
npm testnpm run test:e2e
CI pipeline will:
- install dependencies (
npm ci) - run lint/typecheck
- run tests
- optionally run migrations against a CI Postgres service
This ensures changes are safe and keeps the repository portfolio-grade.
This API is designed to be deployable to Vercel with these constraints:
-
Ensure environment variables are configured in Vercel:
DATABASE_URLJWT_ACCESS_SECRETJWT_ACCESS_TTL_SECONDS
-
Use a managed Postgres provider:
- Supabase Postgres (recommended)
- AWS RDS Free Tier Postgres
-
Migrations strategy:
- run migrations as part of CI/CD before deployment, or as a controlled release step
Notes:
- Vercel serverless environments can be sensitive to connection pooling; using a provider/pooler is recommended.
This project aims for strong security defaults:
- Passwords: stored as salted hashes using a modern KDF (argon2 recommended).
- JWT secrets: must be long and random; rotated via environment management.
- Least privilege: role checks enforced via guards and service-level invariants.
- Tenant isolation: every query is scoped by
organization_id. - Input validation: DTO validation rejects unknown/invalid fields.
- Error handling: centralized filter prevents leaking stack traces.
- Auditability (planned within scope): record
created_by_user_idfor domain entities. - Rate limiting (optional if time permits): protect login endpoints.