Skip to content

mahmoud-bebars/fullstack-system-starter

Repository files navigation

Full Stack System Starter

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.


What's Included

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 /health and GET /status endpoints
  • 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

Stack

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)


Repository Structure

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

Getting Started

Prerequisites

  • Node.js 22+
  • Docker Desktop (for local dev database + Redis containers)

Local Development Setup

# 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:5173

Swagger docs available at http://localhost:5000/docs in development. Mailhog (email catcher) available at http://localhost:8025.


Deploying to Production

This starter is Docker-native. Both backend and frontend ship as containers. Database and Redis are always cloud-managed.

Step 1 — Choose a cloud platform

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.

Step 2 — Provision cloud database and Redis

MySQL / MariaDB (pick one):

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

Step 3 — Deploy

# 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.

Step 4 — Configure environment

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

CI/CD — GitHub Actions

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:

  1. Builds backend image (one image, shared across environments)
  2. Builds frontend image twice — once per environment, because VITE_APP_URL is baked in at build time
  3. Pushes all images to GitHub Container Registry (GHCR) — free, no extra account needed
  4. 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.

GitHub Secrets Setup

Set these in: GitHub repo → Settings → Secrets and variables → Actions

Registry (always required)

Secret Value
GITHUB_TOKEN Auto-provided by GitHub — no setup needed (used for GHCR)

Staging secrets

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)

Production secrets

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)

AWS ECS secrets (only if using the ECS alternative)

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 Setup

GitHub Environments add an approval gate before production deploys run.

  1. Go to repo Settings → Environments
  2. Create two environments: staging and production
  3. On production: enable Required reviewers and add yourself
  4. The workflow will pause and wait for approval before deploying to production

VPS Server Prerequisites

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-stdin

Environment Variables

The .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.


Services & Drivers

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.

Storage (storage.service.ts)

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.

Email (email.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.

Notifications (notification.service.ts)

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
Email 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.


Architecture

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.


Starting a New Project on This Starter

  1. Clone this repo
  2. Fill in PROJECT_UNDERSTANDING.md — business context, users, domain rules, append-only tables
  3. Write PLAN.md — phased delivery roadmap starting at Phase 1 (Phase 0 is done)
  4. Set TASKS.md for Sprint 1.1
  5. Add new domain permissions to backend/src/constants/permissions.ts
  6. Extend prisma/schema.prisma with domain models — run npm run db:migrate
  7. Build new feature modules following the existing module pattern
  8. Register new routes in server.ts

The Users module is the reference implementation for frontend module structure.


Project Docs

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

License

Private — Mahmoud Bebars. Not for redistribution without permission.

About

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.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages