Infrastructure
Regulus uses a composable, cloud-agnostic infrastructure designed to run at ~$5–30/month during Alpha and scale to $500/month at 50k MAU without architecture changes.
Service Map
| Service | Provider | Purpose | Notes |
|---|---|---|---|
| API | Single VPS (regulus, 204.168.160.114) | NestJS modular monolith under PM2 | nginx → api.rgls.uk |
| Real-time | Same VPS | Centrifugo self-hosted | ws.rgls.uk |
| Database | Postgres on same VPS (Docker, port 5433) | Primary DB + pgvector | Local, no PgBouncer yet |
| Auth | Custom JWT via NestJS | Invite-only register flow | Issued by apps/api, no Supabase Auth |
| Storage | Cloudflare R2 | Media uploads | Zero egress for video |
| Admin panel | Same VPS, nginx-served Vite SPA | /var/www/regulus-app/admin/dist | admin.rgls.uk. No Vercel. |
| Mobile | Expo EAS | iOS + Android OTA updates | EAS Build + EAS Submit |
| Push | Expo Push API | Mobile push notifications | 600 notif/sec capacity |
| Resend (or SMTP) | Transactional email | Welcome, invite-used, digest | |
| Observability | PostHog + Sentry | Analytics + error tracking | Free tiers |
API — VPS + PM2
Repository: apps/api/Host: regulus (204.168.160.114), root, SSH key in ~/.ssh/regulus_deploy.
Layout on the box:
/opt/regulus-app/
repo/ # rsync target (full monorepo, no .git)
apps/api/dist/ # compiled NestJS bundle (entry: dist/main.js)
apps/api/prisma/migrations # for `prisma migrate deploy`
node_modules/ # installed once, reused across deploys
ecosystem.config.cjs # PM2 spec for regulus-api
logs/ # api.out.log, api.err.log
admin-dist/ # admin bundle, kept as a stage copyDeploy (from your laptop):
# 1. Build everything locally
pnpm build
# 2. Sync compiled bundle + prisma migrations + domain dist
rsync -avz --delete apps/api/dist/ regulus:/opt/regulus-app/repo/apps/api/dist/
rsync -avz --delete apps/api/prisma/ regulus:/opt/regulus-app/repo/apps/api/prisma/ --exclude='migration_lock.toml'
rsync -avz --delete packages/domain/dist/ regulus:/opt/regulus-app/repo/packages/domain/dist/
# 3. Migrate + regenerate Prisma client + restart on server
# NOTE: `prisma generate` is mandatory whenever schema.prisma changed —
# otherwise the existing @prisma/client in node_modules will reject any
# new fields ("Unknown field X on UserCountOutputType" / 500).
ssh regulus 'cd /opt/regulus-app/repo/apps/api \
&& pnpm prisma migrate deploy \
&& pnpm prisma generate \
&& pm2 restart regulus-api'Health check: GET /health → { status: "ok", uptime, version } (outside /api/v1 prefix). Public: https://api.rgls.uk/health. PM2 binds the process to localhost:4101; nginx terminates TLS.
Centrifugo — same VPS
Self-hosted real-time server. Users subscribe to:
user:{userId}— personal notifications channelfeed:public— new public post IDsconversation:{conversationId}— message delivery (planned)
Runs as a separate PM2 / systemd unit; nginx vhost regulus-app-ws proxies wss://ws.rgls.uk/connection/websocket. Config via env — see Environment.
Supabase
Project: regulus-alpha
Database
- Postgres 15 with
pgvectorextension (for future AI embeddings). - Row-Level Security (RLS) policies on public-facing tables.
- Connection via
DATABASE_URL(pooled PgBouncer URL for API, direct for migrations).
Storage
Two buckets:
media— user-uploaded photos/videos (private, presigned URLs).avatars— profile pictures (public CDN).
Upload flow:
- Client requests presigned PUT URL:
POST /media/upload→{ uploadUrl, key }. - Client uploads directly to R2/Supabase Storage.
- Client commits:
POST /media/commit { key }→ createsMediaAssetrow.
Cloudflare R2
Used for large media (videos). Zero egress fees. Served via Cloudflare CDN.
- Bucket:
regulus-media - CORS: allow
rgls.uk+*.regulus.app - Lifecycle: delete
pendingmedia assets older than 24h (server cron).
Admin Panel — same VPS, nginx-served Vite SPA
Repository: apps/admin/ (Vite + React 19 + Ant Design — not Next.js). Public URL: https://admin.rgls.ukPath on server: /var/www/regulus-app/admin/dist
API base URL is hard-coded to https://api.rgls.uk in apps/admin/src/lib/api.ts (no build-time env needed for prod).
Deploy (from your laptop):
pnpm --filter @regulus/admin build
rsync -avz --delete apps/admin/dist/ regulus:/opt/regulus-app/admin-dist/
ssh regulus 'rsync -a --delete /opt/regulus-app/admin-dist/ /var/www/regulus-app/admin/dist/'No restart needed — nginx (/etc/nginx/sites-enabled/regulus-app-admin) serves the static SPA directly. Cloudflare sits in front of nginx for TLS + real-IP.
Expo EAS — Mobile
Repository: apps/mobile/
eas build --platform all --profile production
eas submit --platform all
eas update --branch production --message "v1.2.3"OTA updates via expo-updates. New JS bundle pushed without App Store review.
Cost Breakdown (Alpha)
| Service | Monthly Cost |
|---|---|
| VPS (API + admin + Centrifugo + Postgres) | ~$10–25 |
| Cloudflare R2 | ~$0 (free 10 GB) |
| Cloudflare DNS/TLS | $0 |
| Expo EAS | $0 (free tier) |
| Resend (email) | $0–5 |
| PostHog + Sentry | $0 (free tiers) |
| Total | ~$5–40 |