Engine · System Design

Built for infinite scale.

OVRIX is designed around strict tenant isolation at the database layer, event-driven decoupling at the service layer, and type-safe contracts everywhere. No hacks, no leaks, no excuses.

Client
React SPA Mobile (RN) REST clients
API Gateway
NestJS HTTP JWT Guard TenantGuard SubscriptionGuard
Services
HR Module RBAC Module Billing Module Projects Module Audit Module
Event Bus
BullMQ Queues KPI aggregation Notifications Billing events
Data
PostgreSQL (schema-per-tenant) Redis (sessions + queues)

Schema-per-tenant

Every tenant receives a dedicated PostgreSQL schema named tenant_<slug>. Tables, indexes, and foreign keys are fully isolated. The shared schema holds only cross-tenant public data.

AsyncLocalStorage propagates the tenant context from the incoming JWT through every NestJS service, repository, and query builder — with zero manual context threading.

SQL
-- On tenant creation
CREATE SCHEMA tenant_acme;

-- Set search path per request
SET search_path TO tenant_acme, public;

-- Each tenant has isolated tables
CREATE TABLE tenant_acme.employees (
  id          uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id   uuid NOT NULL REFERENCES public.tenants(id),
  email       text UNIQUE NOT NULL,
  deleted_at  timestamptz  -- soft-delete
);
01
JWT Extraction

JwtStrategy validates the bearer token, extracts user ID and tenant slug, attaches both to request.user.

02
Tenant Context

TenantGuard reads the slug, resolves the tenant record from the public schema, and sets AsyncLocalStorage context before the controller runs.

03
Schema Switching

Every TypeORM query builder call reads from AsyncLocalStorage and prepends the correct schema path. No manual joins, no WHERE tenant_id = everywhere.

BullMQ queues

Critical side-effects are decoupled into separate BullMQ queues backed by Redis. Producers emit events; consumers run in isolated worker processes. Retries, dead-letter queues, and job scheduling are built in.

TypeScript
// Emit from billing service
await this.billingQueue.add('subscription.renewed', {
  tenantId: tenant.id,
  planId:   subscription.planId,
  renewedAt: new Date(),
}, {
  attempts: 3,
  backoff: { type: 'exponential', delay: 2000 },
});

// Consumer — notification worker
@Processor('billing')
export class BillingConsumer {
  @Process('subscription.renewed')
  async onRenewed(job: Job) {
    await this.mailer.sendRenewal(job.data);
    await this.kpiQueue.add('mrr.update', job.data);
  }
}
Schema isolation means a bug in any query — even a raw SQL injection — cannot accidentally return another tenant's rows. RLS requires policy correctness on every single table; schema isolation is architectural. The performance overhead is negligible at our scale.
A migration runner iterates over every active tenant schema at deploy time, applying the same TypeORM migration file to each. Migrations are transactional and reversible. Rolling deploys use additive-only changes until all tenants are migrated.
Yes. Each job carries the tenant context in its payload. Workers re-establish AsyncLocalStorage from the job data, ensuring all database calls within the consumer use the correct schema. Queue priority can be set per tenant for SLA compliance.
SubscriptionGuard checks the tenant's status on every protected request. Suspended tenants receive a 402 with a grace period expiry timestamp. Their schema remains intact — reactivation is instant on payment receipt via the Stripe webhook.