A production-grade, multi-tenant S3-compatible storage billing system with real-time metering, quota enforcement, and a self-service dashboard.
┌────────────────────┐ ┌────────────────────┐
│ Next.js 14 │ HTTP │ Fastify Backend │
│ Frontend :3001 │──────▶│ :3000 │
└────────────────────┘ │ ├── /auth/login │
│ ├── /portal/* │
│ ├── /billing/* │
│ ├── /admin/* │
│ └── S3 proxy │
└────────┬─────────────┘
│
┌─────────────────┼─────────────────┐
│ │ │
┌─────▼─────┐ ┌──────▼──────┐ ┌──────▼──────┐
│ PostgreSQL │ │ Redis │ │ MinIO │
│ :5432 │ │ :6379 │ │ :9000 │
└────────────┘ └─────────────┘ └─────────────┘
- Docker Desktop
- Node.js 20+
cd "Billing System"
cp .env.example .env
docker compose up -d # starts postgres, redis, minio
npm install
npm run db:push # drizzle-kit push schema
npm run dev # fastify on :3000node scripts/seed.mjsThis creates:
- Tenant:
acme— email:admin@acme.io, status:active - Plan:
starter— 10 buckets, 100 GiB storage
cd frontend
# Option A: connect to real backend
echo "NEXT_PUBLIC_API_URL=http://localhost:3000" > .env.local
echo "NEXT_PUBLIC_USE_MOCK=false" >> .env.local
# Option B: use mock data (no backend needed)
cp .env.local.mock .env.local
npm install
npm run dev # next.js on :3001| Field | Value |
|---|---|
admin@acme.io |
|
| Password | anything |
Note: Password validation is a stub (Phase 7). Any non-empty password succeeds if the tenant email exists and is
active.
| Variable | Required | Default | Description |
|---|---|---|---|
DATABASE_URL |
✅ | — | PostgreSQL connection string |
REDIS_URL |
✅ | — | Redis connection string |
MINIO_ROOT_USER |
✅ | — | MinIO access key |
MINIO_ROOT_PASSWORD |
✅ | — | MinIO secret key |
MINIO_ENDPOINT |
localhost |
MinIO hostname | |
MINIO_PORT |
9000 |
MinIO port | |
MINIO_USE_SSL |
false |
Enable TLS for MinIO | |
ADMIN_JWT_SECRET |
— | HS256 secret for admin JWT auth | |
CORS_ORIGIN |
http://localhost:3001 |
Allowed CORS origin | |
PORT |
3000 |
Fastify listen port |
| Variable | Default | Description |
|---|---|---|
NEXT_PUBLIC_API_URL |
http://localhost:3000 |
Backend API base URL |
NEXT_PUBLIC_USE_MOCK |
true |
Use mock data instead of real API |
POST /auth/login— Returns a one-time API key for the session
GET /portal/profile— Tenant profilePATCH /portal/profile— Update profileGET /portal/keys— List API keysPOST /portal/keys— Generate new API keyDELETE /portal/keys/:keyId— Revoke API keyGET /portal/buckets— List bucketsDELETE /portal/buckets/:name— Soft-delete bucketGET /portal/usage/current— Live quota usageGET /portal/usage/history— Historical usageGET /portal/alerts— Quota breach alerts
GET /billing/invoices— List invoicesGET /billing/invoices/:invoiceId— Get single invoiceGET /billing/usage— Aggregate usage metrics
GET /admin/tenants— List all tenantsPOST /admin/tenants— Create tenantPATCH /admin/tenants/:id— Update tenantPOST /admin/tenants/:id/suspend— Suspend tenantPOST /admin/tenants/:id/unsuspend— Reactivate tenantDELETE /admin/tenants/:id— Soft-delete tenantPOST /admin/tenants/:id/plan— Assign planPATCH /admin/tenants/:id/quota— Override quotasGET /admin/plans— List plansPOST /admin/plans— Create planPATCH /admin/plans/:id— Update planDELETE /admin/plans/:id— Deactivate planGET /admin/invoices— List all invoicesPOST /admin/invoices/:id/finalise— Finalise invoicePOST /admin/invoices/:id/void— Void invoiceGET /admin/system/health— Infrastructure health checkGET /admin/system/audit-log— Audit trail
| Job | Interval | Purpose |
|---|---|---|
| Quota Breach Watch | 5 min | Compares Redis counters to limits, inserts breach events |
| Storage Snapshot | 1 hour | Queries MinIO for true bucket sizes, recalibrates Redis |
| Usage Aggregator | Monthly | Rolls up usage_metrics into invoices on the 1st |
| Webhook Dispatcher | 30 sec | Polls pending webhooks, delivers with HMAC + retry |
Apache 2.0