saas-automation-framework
v1.0.0
Published
Production-ready SaaS starter framework for Node.js, PostgreSQL, Docker, and CI/CD.
Maintainers
Readme
SAAS Automation Framework
A production-ready SaaS starter framework built on Node.js + PostgreSQL. Clone it, add your modules, and ship — user accounts, auth, roles, rate limiting, migrations, and CI/CD are already wired up.
Stack: Express.js + PostgreSQL + JWT + Zod + Docker
How It Works
Request Lifecycle
Every HTTP request flows through this chain:
Request → Helmet → CORS → Body parser → Rate limiter
→ Router → [authMiddleware] → [requireRole] → [validate]
→ Controller → Service → Repository → PostgreSQL
→ Response (or → errorHandler if anything throws)The Module Pattern
To add a new feature (e.g. "posts"), create 5 files in src/modules/posts/:
| File | Responsibility |
|------|---------------|
| posts.schema.js | Zod validation shapes for request bodies |
| posts.repository.js | Extends BaseRepository — SQL queries |
| posts.service.js | Business logic (auth checks, transforms) |
| posts.controller.js | HTTP handlers (req/res), calls service |
| posts.routes.js | Express router, applies middleware |
Then register the router in src/routes/index.js.
Key Building Blocks
BaseRepository— Generic CRUD (findById,findMany,create,update,delete) for any table. Extend it, don't modify it.HttpErrorsubclasses — Thrownew NotFoundError()anywhere; the global error handler catches it and sends the right HTTP status.asyncHandler— Wraps every route handler so rejected promises get forwarded to the error handler automatically.validate(schema)— Middleware factory: pass a Zod schema, it validatesreq.bodyand returns 422 with field errors on failure.authMiddleware— Verifies JWT, injectsreq.userfor downstream handlers.requireRole('admin')— Guards a route to specific roles; must come afterauthMiddleware.
Stack
- Runtime: Node.js 22 (ESM)
- Framework: Express.js
- Database: PostgreSQL 15 (raw SQL via
pg) - Validation: Zod
- Auth: JWT (jsonwebtoken) + bcrypt
- Logging: Pino (structured)
- Rate Limiting: express-rate-limit
- Security: Helmet (HTTP headers)
- Testing: Jest + Supertest
- Containers: Docker + Docker Compose
- CI/CD: GitHub Actions → GHCR → deploy webhook
Project Structure
├── src/
│ ├── index.js # Entry point, graceful shutdown
│ ├── app.js # Express app factory + middleware stack
│ ├── config/
│ │ ├── database.js # pg Pool, query helper
│ │ └── env.js # Zod env validation (fails fast on startup)
│ ├── lib/
│ │ ├── BaseRepository.js # Generic CRUD base class
│ │ ├── HttpError.js # Typed HTTP error hierarchy
│ │ ├── asyncHandler.js # Wraps async route handlers
│ │ └── validate.js # Zod request body validation middleware
│ ├── middleware/
│ │ ├── auth.js # JWT Bearer auth → req.user
│ │ ├── errorHandler.js # 404 + global error handler
│ │ ├── rateLimiter.js # Global + auth-specific rate limits
│ │ └── requireRole.js # Role-based access control
│ ├── modules/
│ │ ├── auth/ # register, login, /me
│ │ └── users/ # CRUD with owner/admin authorization
│ ├── routes/
│ │ ├── index.js # Route aggregator
│ │ └── health.js # GET /api/health
│ └── utils/
│ └── logger.js # Pino logger
├── scripts/
│ ├── migrate.js # SQL migration runner
│ └── migrations/
│ └── 001_initial.sql # users table (UUID, role, timestamps)
├── tests/
│ ├── setup.js # Loads .env before test suite
│ ├── health.test.js # Health + 404 sanity checks
│ └── auth.test.js # Full auth flow + edge cases
├── Dockerfile
├── docker-compose.yml # app + postgres + redis
└── .github/workflows/ci.ymlQuick Start
# 1. Copy env
cp .env.example .env
# 2. Start services
docker compose up --build
# 3. Run migrations (first time only)
docker compose exec app npm run migrateApp will be available at http://localhost:3000/api/health.
Development (local, no Docker)
npm install
cp .env.example .env # edit DATABASE_URL to point at your local Postgres
npm run migrate
npm run devAuthentication
Flow: Register → Login → Use Protected Routes
POST /api/auth/register → 201 + { user, token }
POST /api/auth/login → 200 + { user, token }
GET /api/auth/me → 200 + { user } (requires Bearer token)Both /register and /login are protected by the auth rate limiter (10 req/15 min).
Token Usage
Authorization: Bearer <token>JWT payload: { userId, email, role }. Default expiry: 7d.
Users API
All routes require a valid JWT.
GET /api/users/ → list all users [admin only]
GET /api/users/:id → get user by ID (own record or admin)
PATCH /api/users/:id → update email (own record or admin)
DELETE /api/users/:id → delete user [admin only]Tests
Tests use Jest + Supertest (real HTTP requests against the live app + database):
tests/health.test.js— 200 on health, 404 on unknown routestests/auth.test.js— register/login/me happy paths + edge cases (duplicate email, bad password, tampered token)
Each test creates a unique email (test-${Date.now()}@example.com) and cleans up with afterAll.
npm testDatabase Migrations
Migration files live in scripts/migrations/ and are named NNN_description.sql. The runner tracks applied migrations in a _migrations table, wraps each in a transaction, and rolls back on error.
npm run migrate
# or in Docker:
docker compose exec app npm run migrateCI/CD
On every push to main:
- Test — Spins up a Postgres service container, runs migrations and tests.
- Build — Builds and pushes a Docker image to GitHub Container Registry tagged
:latestand:<sha>. - Deploy — Triggers your deployment webhook (configure
DEPLOY_WEBHOOK_URLin GitHub secrets).
Environment Variables
See .env.example for all variables. Two are required at startup (validated by Zod — server won't start without them):
| Variable | Required | Default | Notes |
|----------|----------|---------|-------|
| DATABASE_URL | ✅ | — | PostgreSQL connection string |
| JWT_SECRET | ✅ | — | Must be ≥ 32 characters |
| NODE_ENV | — | development | development | test | production |
| PORT | — | 3000 | |
| JWT_EXPIRES_IN | — | 7d | |
| ALLOWED_ORIGINS | — | * | CORS origins |
| LOG_LEVEL | — | info | fatal…trace |
Security
- Helmet (HTTP security headers)
- CORS (configurable via
ALLOWED_ORIGINS) - Rate limiting (global 100 req/15 min, auth routes 10 req/15 min)
- JWT verification on protected routes
- Bcrypt password hashing (12 rounds) + constant-time comparison
- Parameterized queries (SQL injection protection)
- Role-based access control (
user/admin) - Non-root Docker user
- Environment validated at startup via Zod
Adding a Feature
- Add a migration in
scripts/migrations/NNN_name.sql - Create
src/modules/your-feature/with the 5-file module pattern - Mount the router in
src/routes/index.js - Write tests in
tests/your-feature.test.js
Roadmap
The core is intentionally lean. These are the natural next modules for a full SaaS product:
| Module | What it covers | |--------|----------------| | Billing | Stripe webhook handling, subscription status, plan tiers stored on the user record | | Email | Transactional emails — verification on register, password reset flow | | API Keys | Alternative to JWT for programmatic/server-to-server access | | Organizations | Multi-tenancy — users belong to an org, resources are scoped to it | | Audit Log | Append-only log of user actions for compliance and debugging |
