Skip to Content
GuidesMulti-Tenant Deployment

Multi-Tenant Deployment

Bliss supports multi-tenancy out of the box — every user gets their own isolated tenant with separate accounts, transactions, and settings. This guide covers the recommended production architecture for hosting Bliss as a multi-user service.


ComponentProviderWhy
Web + APIVercel Zero-config Next.js hosting, global CDN, automatic HTTPS, preview deployments
Backend workersRailway Always-on containers for BullMQ workers, easy Redis/Postgres provisioning
PostgreSQLPrisma Postgres  with AccelerateManaged PostgreSQL with connection pooling, caching, and pgvector support
RedisRailway (managed)BullMQ job queues and caching
Error trackingSentry Structured error reports with worker context (job name, tenantId, attempt count)
AuthenticationGoogle OAuthFrictionless sign-in via NextAuth.js Google provider

Architecture Overview

Users (Browser) | Vercel CDN | +--+--+ | | Web API (Vercel — two deployments from same repo) | | | +--- INTERNAL_API_KEY ---> Backend (Railway — Express + BullMQ) | | +-------- Prisma Postgres ----------+ (Accelerate) | Redis (Railway)

Web (React SPA) and API (Next.js) deploy to Vercel from the same repository. Vercel handles TLS, CDN, and scaling automatically.

Backend deploys to Railway as a single service running in START_MODE=all (both HTTP and workers in one process). For higher traffic, split into two Railway services: one with START_MODE=web and another with START_MODE=worker — BullMQ distributes jobs across all worker instances automatically.

PostgreSQL is hosted on Prisma Postgres  with Accelerate, which provides managed connection pooling, query caching, and built-in pgvector support. Accelerate is particularly valuable when deploying on serverless platforms like Vercel, where each function invocation opens a new database connection — the connection pooler prevents exhausting PostgreSQL’s connection limit. Redis is provisioned as a Railway managed service.


Key Configuration

Environment Variables

Both Vercel and Railway read from environment variables. The critical ones for multi-tenant production:

VariableWherePurpose
DATABASE_URLAPI + BackendShared PostgreSQL connection string
REDIS_URLBackendBullMQ queue broker
INTERNAL_API_KEYAPI + BackendService-to-service auth (must match)
NEXTAUTH_URLAPIYour production domain (e.g., https://app.yoursite.com)
FRONTEND_URLAPICORS origin for the web app
BACKEND_URLAPIRailway backend URL (e.g., https://bliss-backend.up.railway.app)
COOKIE_DOMAINAPICookie scope for auth (e.g., .yoursite.com)

Google OAuth

NextAuth.js supports Google as an OAuth provider. Configure in your Vercel environment:

VariableValue
GOOGLE_CLIENT_IDFrom Google Cloud Console
GOOGLE_CLIENT_SECRETFrom Google Cloud Console

Users can sign up with Google (creates a tenant automatically) or with email/password. Both flows coexist.

Sentry

Bliss has built-in Sentry integration. Every worker failure is reported with structured context:

Worker: portfolioWorker Job: value-all-assets TenantId: clx7abc123 Attempt: 2 of 3
VariableWherePurpose
SENTRY_DSNAPI + BackendError ingestion endpoint
SENTRY_ORGCIOrganization slug (for source maps)
SENTRY_PROJECTCIProject slug

Multi-Tenancy Model

Bliss uses query-level tenant isolation — every database query includes a tenantId filter. There is no Row-Level Security (RLS); isolation is enforced at the application layer.

Each tenant gets:

  • Isolated accounts, transactions, categories, and portfolio items
  • Independent AI classification models (description cache + vector embeddings)
  • Separate analytics caches and insights
  • Configurable thresholds (auto-promote, review confidence)

Tenant data is fully isolated. A user in Tenant A cannot see or modify data belonging to Tenant B.


Scaling Considerations

ConcernApproach
More usersVercel auto-scales the web and API layers. No action needed.
Slow job processingAdd Railway worker replicas (START_MODE=worker). BullMQ distributes jobs automatically.
Database growthPrisma Postgres scales storage and compute independently. Accelerate handles connection pooling automatically — no PgBouncer needed.
Redis memoryBullMQ jobs are transient. Monitor queue depth; increase Redis memory if backlogs grow.

Next steps