Multi-Tenant Architecture on a $15/Month Budget
When I started building my portfolio and business management platform — a system managing two separate legal entities with complete financial isolation, 5-role RBAC, compliance tracking, and invoice generation — the infrastructure budget was firm: $6-15/month. Not $6-15K. Six to fifteen dollars.
Everything runs on a single Hetzner CX32 (4 vCPU, 8GB RAM) for roughly $7/month, plus Cloudflare's free tier. Three Docker containers: Next.js + Payload CMS (1536MB), PostgreSQL 16 (shared_buffers=256MB), and Redis 7 (128MB). That's the entire production infrastructure.
The most interesting architectural decision is two-dimensional data isolation. Most SaaS platforms have one isolation dimension: tenantId. Mine has two: tenantId (SaaS-level isolation for future multi-tenant expansion) and entityId (business unit isolation — Jenga Tech vs Handisi Digital). Every financial record carries both fields. Every database query filters by both.
I'm not using PostgreSQL RLS yet — deliberately. For a single-tenant system with two entities, application-level filtering via Payload CMS access control hooks is simpler, faster, and equally secure. RLS is planned for Phase 4 when the platform supports multiple tenants. The tenantId column is already on every table — the migration will be adding policies, not restructuring data.
With 8GB total RAM, every megabyte matters. App server gets 1536MB, PostgreSQL gets ~1500MB, Redis gets 200MB, Dokploy gets ~500MB, and the OS gets ~4GB. Redis serves three roles using logical database separation: BullMQ job queue, rate limiter store, and application cache. One Redis instance, three isolated concerns.
The application uses three bounded contexts inside a single Next.js deployment: portfolio (public-facing site), business (CRM, projects, invoicing), and compliance (calendar, reminders). Cross-module communication happens through Payload's Local API which bypasses HTTP entirely server-side.
Choosing Payload CMS 3.x as the application framework was the highest-leverage decision. Auto-generated REST + GraphQL APIs from collection definitions. Collection-level access control on every operation. Versions and drafts with autosave. Admin panel that's production-ready. Local API that bypasses HTTP for server-side queries.
BullMQ runs in-process — no separate worker container. The CI/CD pipeline runs on GitHub Actions: push to main triggers lint, type-check, tests, Docker build, push to GHCR, and Dokploy API deploy. Defense-in-depth security across every layer, all within the $15 budget: Cloudflare WAF, UFW with Cloudflare-only ingress, fail2ban, SSH key-only auth, non-root Docker user, Zod validation, CSP headers, rate limiting, and RBAC at API level.
Final tally: Hetzner VPS ~$7, Storage Box ~$4, Cloudflare free, UptimeRobot free, BetterStack free, Sentry free, GitHub Actions free. Total: ~$11/month for a production platform with CDN, WAF, automated backups, CI/CD, error tracking, and structured logging. Constraints breed creativity.
Interested in working together?
Let's discuss how I can help with your next project.
Book a Discovery CallRelated Posts
Most software architecture advice assumes reliable connectivity. In East and Southern Africa, where internet penetration sits below 30% in rural areas, that assumption will kill your product.