A production-ready full-stack starter by Bob Mahmoud Bebars.
Designed for per-client ERP-style systems: one database, one deployment, one client. Ships with auth, RBAC, real-time, background jobs, file storage, email, and in-app notifications already wired together.
Backend (fully implemented):
- JWT auth with httpOnly cookies + refresh token rotation (7d)
- RBAC — roles, permissions, role-permission assignment
- Users CRUD with avatar upload (sharp WebP resize), soft delete, password reset
- In-app notifications — DB write + email + Socket.IO emit in one call
- Settings module — DB-backed key/value store, Redis flush endpoint
- BullMQ background job queue with status polling endpoint
- GitHub deploy hook (
/admin/deploy) — returns 503 if not configured - File storage abstraction — filesystem now, S3-ready via one env var flip
- Swagger docs auto-generated at
/docs(dev/staging only) GET /healthandGET /statusendpoints- Startup syncs — permissions, settings, and app version auto-upserted on boot
- Global error handler — AppError, Zod errors, Prisma P2002, all formatted consistently
Frontend (fully implemented):
- Auth flow — login, logout, refresh, session persistence
- Users page — full CRUD reference implementation
- Roles & Permissions page — role management + permission assignment UI
- Settings page with JSON editor
- Notifications center
- Dashboard shell
- Light/dark theme
- Socket.IO client — cookie auth, per-event hooks via
useSocket useDataApi,useFilterParams,useCanAccessAction, and 6 other reusable hooks- 14 shared components ready to use
Backend: Fastify 5 · TypeScript 5.7 · Prisma 7 (MariaDB adapter) · MySQL/MariaDB · Redis · BullMQ 5 · Socket.IO 4 · Zod 3 · Pino · sharp · Nodemailer
Frontend: React 19 · Vite 8 · TypeScript 5.9 · Tailwind CSS 4 · shadcn/ui · React Router 7 · Axios · Socket.io-client 4 · Recharts 3 · jsPDF · qrcode.react · html5-qrcode · Sonner · Zod 4
Infrastructure: Docker (backend + frontend containers) · Cloud MySQL/MariaDB (RDS · PlanetScale · Railway) · Cloud Redis (Upstash · Redis Cloud · Railway)
project-root/
├── backend/ # Fastify 5 backend
│ ├── src/
│ │ ├── config/ # env.ts (Zod validation), prisma.ts (singleton)
│ │ ├── plugins/ # authenticate, rbac, socket, cache
│ │ ├── modules/ # auth, users, roles, permissions, settings, notifications, queue, deploy
│ │ ├── workers/ # BullMQ file-processor worker
│ │ ├── jobs/ # BullMQ queue definitions + typed job data
│ │ ├── lib/ # errors, response, pagination, media, syncs
│ │ ├── services/ # storage, notification, email
│ │ ├── utils/ # passwords, random
│ │ ├── types/ # FastifyInstance + JWT augmentation
│ │ ├── constants/ # permissions registry, default settings
│ │ ├── enums/ # as const enum objects
│ │ └── server.ts # bootstrap + plugin registration
│ └── prisma/ # schema, migrations, seed
├── frontend/ # React 19 frontend
│ └── src/
│ ├── pages/ # Dashboard, Users, RolesPermissions, Settings, Notifications, ...
│ ├── components/ # shared/ (14 components) + module components + ui/
│ ├── hooks/ # useDataApi, useFilterParams, useSocket, useCanAccessAction, ...
│ ├── providers/ # auth-provider, theme-provider
│ ├── routes/ # route definitions
│ └── layout/ # app shell, sidebar, nav
├── .env # Single .env at root — backend runtime + frontend build
├── .env.example
├── package.json # Root script runner only
├── docker-compose.yml # Production: backend + frontend containers (cloud DB/Redis)
├── docker-compose.dev.yml # Local dev: MariaDB + Redis + Mailhog
├── Dockerfile.backend
├── Dockerfile.frontend
├── .github/workflows/
│ └── deploy.yml # CI/CD: build images + deploy to VPS or AWS (manual trigger)
├── CLAUDE.md # Stack rules, patterns, anti-patterns
├── PROJECT_UNDERSTANDING.md # Business context template — fill in per project
├── PLAN.md # Delivery roadmap template — fill in per project
└── TASKS.md # Sprint tracker template — fill in per project
- Node.js 22+
- Docker Desktop (for local dev database + Redis containers)
# 1. Copy and fill in env
cp .env.example .env
# Set DATABASE_URL, JWT_SECRET, REFRESH_SECRET, COOKIE_SECRET (all min 32 chars)
# 2. Start local MariaDB + Redis + Mailhog
docker compose -f docker-compose.dev.yml up -d
# 3. Install dependencies
npm run install:all
# 4. Run migrations and seed
npm run db:migrate
npm run db:seed
# 5. Start development servers
npm run dev:backend # http://localhost:5000
npm run dev:frontend # http://localhost:5173Swagger docs available at http://localhost:5000/docs in development.
Mailhog (email catcher) available at http://localhost:8025.
This starter is Docker-native. Both backend and frontend ship as containers. Database and Redis are always cloud-managed.
| Platform | Notes |
|---|---|
| Railway | Easiest. Deploy from GitHub, managed MySQL + Redis built-in, auto-deploys, free tier available. |
| Render | Docker support, managed PostgreSQL/Redis, easy env var management. |
| Fly.io | Excellent for containers, global edge deployment, persistent volumes for filesystem storage. |
| AWS (ECS + Fargate) | Full control, pairs naturally with RDS + ElastiCache + S3. |
| DigitalOcean App Platform | Docker support, managed DB and Redis add-ons, straightforward pricing. |
| Hostinger VPS | Docker pre-installed on many plans. Run docker compose up -d after SSH setup. |
| Any Docker host | Any VPS or server with Docker installed — copy docker-compose.yml + .env and run. |
MySQL / MariaDB (pick one):
- Railway MySQL — managed, free tier, auto-provisions
DATABASE_URL - AWS RDS MySQL — production-grade, Multi-AZ, automated backups
- PlanetScale — serverless MySQL, branching workflow, generous free tier
- DigitalOcean Managed MySQL — simple, reliable
Redis (pick one):
- Upstash — serverless Redis, free tier, pay-per-request, zero ops
- Redis Cloud — managed Redis, free 30 MB tier
- Railway Redis — one-click alongside your app
# Build and push to your registry (or let the platform build from source)
docker build -f Dockerfile.backend -t your-registry/app-backend:latest .
docker build -f Dockerfile.frontend \
--build-arg VITE_APP_URL=https://api.yourdomain.com/ \
--build-arg VITE_APP_TITLE=YourApp \
-t your-registry/app-frontend:latest .
# Or use docker compose on a VPS
scp docker-compose.yml .env user@your-server:~/app/
ssh user@your-server "cd ~/app && docker compose up -d --build"The backend container automatically runs prisma migrate deploy on startup before serving traffic.
Set the production values in your platform's env var manager (or .env on a VPS):
NODE_ENV=production
APP_URL=https://api.yourdomain.com
VITE_APP_URL=https://yourdomain.com/
DATABASE_URL=mysql://user:pass@your-cloud-db:3306/dbname
REDIS_HOST=your-upstash-host.upstash.io
REDIS_PORT=6379
REDIS_PASSWORD=your-upstash-password
JWT_SECRET=<random 32+ chars>
REFRESH_SECRET=<random 32+ chars>
COOKIE_SECRET=<random 32+ chars>
STORAGE_DRIVER=s3 # recommended for production
The workflow at .github/workflows/deploy.yml handles building Docker images and deploying to staging and production environments.
Trigger: Manual only (workflow_dispatch). Push-based auto-deploy is commented out by default — uncomment the push: block in the workflow file when you are ready to enable it.
How it works:
- Builds backend image (one image, shared across environments)
- Builds frontend image twice — once per environment, because
VITE_APP_URLis baked in at build time - Pushes all images to GitHub Container Registry (GHCR) — free, no extra account needed
- Deploys to the chosen environment via SSH (VPS) or ECS (AWS — commented-out alternative)
Designed for: Any Linux VPS with Docker + AWS EC2 + AWS ECS. Not needed for: Railway, Render, Fly.io — those platforms have native deploy pipelines.
Set these in: GitHub repo → Settings → Secrets and variables → Actions
| Secret | Value |
|---|---|
GITHUB_TOKEN |
Auto-provided by GitHub — no setup needed (used for GHCR) |
| Secret | Example value |
|---|---|
STAGING_SSH_HOST |
123.45.67.89 or staging.yourdomain.com |
STAGING_SSH_USER |
ubuntu or deploy |
STAGING_SSH_KEY |
Full PEM private key content (including -----BEGIN... headers) |
STAGING_APP_DIR |
/opt/app |
STAGING_VITE_APP_URL |
https://staging-api.yourdomain.com/ (trailing slash required) |
| Secret | Example value |
|---|---|
PROD_SSH_HOST |
98.76.54.32 or yourdomain.com |
PROD_SSH_USER |
ubuntu or deploy |
PROD_SSH_KEY |
Full PEM private key content |
PROD_APP_DIR |
/opt/app |
PROD_VITE_APP_URL |
https://api.yourdomain.com/ (trailing slash required) |
| Secret | Description |
|---|---|
AWS_ACCESS_KEY_ID |
IAM user with ECS + ECR permissions |
AWS_SECRET_ACCESS_KEY |
|
AWS_REGION |
e.g. us-east-1 |
STAGING_ECR_BACKEND_URI |
e.g. 123456789.dkr.ecr.us-east-1.amazonaws.com/app-backend |
STAGING_ECR_FRONTEND_URI |
|
STAGING_ECS_CLUSTER |
ECS cluster name |
STAGING_ECS_SERVICE_BACKEND |
ECS service name |
STAGING_ECS_SERVICE_FRONTEND |
|
PROD_ECR_* / PROD_ECS_* |
Same as staging variants |
GitHub Environments add an approval gate before production deploys run.
- Go to repo Settings → Environments
- Create two environments:
stagingandproduction - On
production: enable Required reviewers and add yourself - The workflow will pause and wait for approval before deploying to production
On every target server (run once on first setup):
# Install Docker
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
# Create app directory and copy .env
mkdir -p /opt/app
cp .env /opt/app/.env # with production values
cp docker-compose.yml /opt/app/
# Log in to GHCR so the server can pull images
echo $GITHUB_TOKEN | docker login ghcr.io -u YOUR_GITHUB_USERNAME --password-stdinThe .env lives at the project root. See .env.example for the full list. Minimum required to start:
| Group | Variable | Required | Notes |
|---|---|---|---|
| App | APP_URL |
Yes | Backend URL, no trailing slash |
| Auth | JWT_SECRET |
Yes | Min 32 chars |
| Auth | REFRESH_SECRET |
Yes | Min 32 chars, different from JWT_SECRET |
| Auth | COOKIE_SECRET |
Yes | Min 32 chars, different from both above |
| Database | DATABASE_URL |
Yes | mysql://user:pass@host:3306/dbname |
| Redis | REDIS_HOST |
Yes | Cloud host in prod, 127.0.0.1 for local dev |
| Frontend | VITE_APP_URL |
Yes | Must end with a trailing slash |
| Seed | ADMIN_EMAIL |
Yes | Email for the seeded super-admin user |
Optional vars: MAIL_* (email), NTFY_* / TWILIO_* / ONESIGNAL_* (notifications), AWS_* / CLOUDINARY_* (cloud storage), GITHUB_* (deploy hook). All are disabled when absent.
Three core services handle all external I/O. Each supports multiple providers via a DRIVER env var — the calling code never changes, only configuration does.
| Driver | Package required | When to use |
|---|---|---|
filesystem (default) |
none | Single-container deployments, local dev |
s3 |
@aws-sdk/client-s3 @aws-sdk/lib-storage |
Multi-server, scalable, CDN-backed |
cloudinary |
cloudinary |
Image-heavy apps needing on-the-fly transforms |
S3 driver works with any S3-compatible service: MinIO (local dev, add to docker-compose.yml), Cloudflare R2 (free egress), and DigitalOcean Spaces — all via AWS_ENDPOINT_URL override. To switch: set STORAGE_DRIVER=s3, install the package, uncomment the implementation block in storage.service.ts.
| Driver | Package required | When to use |
|---|---|---|
smtp (default) |
none | Any SMTP server, Mailhog (local dev) |
ses |
none (uses SES SMTP endpoint) | AWS SES — cost-effective, high deliverability |
sendgrid |
none (uses SendGrid SMTP relay) | SendGrid — built-in analytics & unsubscribes |
Both ses and sendgrid work via SMTP relay — no extra packages. Switch with MAIL_DRIVER=ses or MAIL_DRIVER=sendgrid and set the relevant credentials. SDK alternatives (for advanced features like bounce webhooks or dynamic templates) are documented in email.service.ts.
Email templates are TypeScript functions returning HTML strings. baseTemplate() provides the branded wrapper — create new templates by calling it with custom body HTML. Two built-in templates ship ready: notificationEmailTemplate and passwordResetTemplate.
Local dev: Mailhog is already included in docker-compose.dev.yml — it catches all outgoing email at http://localhost:8025 with no real sending.
The notify() function writes to DB, sends email, and emits via Socket.IO on every call. Additional channels are opt-in per call via NotifyOptions:
| Channel | How to enable | Requires |
|---|---|---|
| DB | Always | — |
Always (if MAIL_FROM set) |
Any MAIL_DRIVER configured |
|
| Socket.IO | Always | User connected |
| NTFY push | { ntfy: true } |
NTFY_URL + NTFY_CHANNEL |
| Twilio SMS | { sms: true, phone: '+1...' } |
TWILIO_ACCOUNT_SID/AUTH_TOKEN/FROM_NUMBER |
| OneSignal push | { push: true, playerId: '...' } |
ONESIGNAL_APP_ID + ONESIGNAL_API_KEY |
Twilio and OneSignal use their REST APIs directly — no SDK package required. All channels are best-effort: failures are logged and swallowed, never thrown.
The backend follows a strict module pattern: every feature lives in src/modules/{name}/ with four files — routes (schema + preHandlers only), controller (thin handlers), service (all business logic + DB), schema (Zod types). Controllers never touch Prisma directly. Services never call reply.send().
The frontend follows a page/component/hook separation: pages are thin shells that compose components, components have single responsibilities, and all data fetching goes through useDataApi. Filter state is always URL-synced via useFilterParams. Shared components in components/shared/ are never recreated inline.
Auth is JWT in httpOnly cookies with refresh token rotation — the access token expires in 15 minutes, and the refresh token (7 days) is stored in both the DB and Redis for server-side revocation. Socket.IO authenticates from the same httpOnly cookie.
File storage is abstracted behind services/storage.service.ts. Switching from local filesystem to S3 requires only updating that file and flipping STORAGE_DRIVER=s3 — nothing else in the codebase changes.
- Clone this repo
- Fill in
PROJECT_UNDERSTANDING.md— business context, users, domain rules, append-only tables - Write
PLAN.md— phased delivery roadmap starting at Phase 1 (Phase 0 is done) - Set
TASKS.mdfor Sprint 1.1 - Add new domain permissions to
backend/src/constants/permissions.ts - Extend
prisma/schema.prismawith domain models — runnpm run db:migrate - Build new feature modules following the existing module pattern
- Register new routes in
server.ts
The Users module is the reference implementation for frontend module structure.
| File | Purpose |
|---|---|
CLAUDE.md |
Stack rules, patterns, anti-patterns — Claude Code operating doc |
PROJECT_UNDERSTANDING.md |
Business context template — fill in per project before coding |
PLAN.md |
Delivery roadmap template — fill in per project |
TASKS.md |
Current sprint tracker — maintained throughout development |
Private — Mahmoud Bebars. Not for redistribution without permission.