Skip to content
➜cat case-studies/gealo.md

Gealo: Multi-tenant workforce and project operations console

CLIENT

Gealo (independent SaaS)

ROLE

Founder & full-stack

DURATION

2025 to present

TECH_STACK

Nuxt 3NestJSTypeORMPostgreSQLMongoDBRedisSocket.IOS3

PROJECT_URL

view_site

Multi-tenant

Architecture

Real-time

Chat + presence

Plan-aware

Entitlements

30+

Feature modules

➜

CONTEXT

Teams that run multiple projects inside one organization end up paying for a stack of separate tools. One for tasks, one for chat, one for meetings, one for files, one for HR, one for permissions, one for billing. Each tool ships its own idea of what a workspace is and who can see what. The seams leak. A member loses access in one place but keeps it in another. A guest sees a project they should not. A plan caps a feature in the dashboard but not in the API. Gealo is the bet that an opinionated multi-tenant operations console with a single permission model is more useful than another point tool.

➜

THE CHALLENGE

Multi-tenant SaaS is easy to start and hard to finish. A handful of features become a hundred. Ten cross-tenant invariants become five hundred queries to audit. The work was setting the spine up correctly from the first migration. Every read filtered by tenant. Every cache key prefixed by tenant. Every socket room scoped by tenant. Every limit resolved from one place. Plans grant a baseline, tenant policies tighten that baseline, add-ons expand it. The effective answer is what the UI and the API both honor. No magic numbers in the frontend. No duplicated limits in two repos.

➜

ARCHITECTURE & APPROACH

Nuxt 3 handles the marketing surface, the auth shells, and the operations console. NestJS handles auth, tenancy resolution, entitlements, projects, tasks, meetings, HR, documents, integrations, AI, and billing. Postgres is the primary OLTP store with TypeORM and real migrations in production. Mongo carries chat messages where the schema needs to flex. Redis holds the cache, the rate-limit buckets, and the Socket.IO pub/sub fabric. S3 holds the file plane. The data model is multi-tenant from the first migration. The frontend never hardcodes a cap.

Tenancy is a first-class invariant

Every controller resolves a tenant before doing anything else. Every query scopes by tenant. Cache keys, queue names, and socket rooms carry a tenant prefix. The shape of the data model makes cross-tenant joins inconvenient to write. The audit becomes pattern review instead of entity-by-entity proof.

Entitlements are the single source of truth

Caps and features come from one place on the backend. Plan grants first. Then policy overrides that only tighten. Then add-ons that only expand. The frontend reads the effective answer from the API. There is no separate frontend constant for "max projects" or "max storage". The contract is enforced once, on the server, in a service the controllers and guards both read.

Real-time is scoped, not broadcast

Socket.IO joins explicit rooms keyed by tenant and project. Presence, typing indicators, message delivery, and notifications all flow through those rooms. There is no global room and no cross-tenant broadcast. The handshake validates the tenant before the connection is upgraded.

Density over decoration in the UI

The console is the surface people work in. It earns its weight by showing real counts, real permissions, and real state. Spacing varies for rhythm instead of uniformity. The light register carries elevation through shadow. The dark register carries it through lightness shift. The anti-slop rules (no gradient text, no glassmorphism by default, no hero-metric template) are written down in the design doc and reviewed against on every interface change.

➜STACK_TOPOLOGY--tiers=4 --live
data flowing

Tier_01 // edge

// where the tenant is resolved

Browser

Operations console + auth shells. Tenant resolved from host.

Tier_02 // application

// thin controllers · guards enforce tenancy

Nuxt 3 // SSR

Marketing, auth shells, console pages. Tenant context per request.

Socket.IO // realtime

Tenant + project rooms. Presence, typing, notifications.

NestJS // API

Controllers thin, services do the work. Guards enforce tenancy + entitlements.

Tier_03 // services

// business logic · 30+ modules

Auth + RBAC

JWT, refresh rotation, optional 2FA, tenant role guards.

Entitlements

Plans grant a baseline, policies tighten, add-ons expand. Resolved in one service.

Workspace services

Tasks, chat, meetings, HR, docs, integrations, AI, billing.

Background jobs

Tenant-aware queues. Retention, accrual, storage recalc.

Tier_04 // data plane

// every key, query, and prefix is tenant-scoped

PostgreSQL

Primary OLTP. Real migrations in prod. Tenant-scoped queries.

MongoDB

Chat messages and flex-schema documents.

Redis

Cache (tenant-prefixed keys), rate buckets, pub/sub fabric.

S3-compatible

File plane. Presigned URLs after permission checks. Tenant/project key paths.

➜ invariant: tenant_id on every read[no cross-tenant joins]
➜

OUTCOMES

Deployed and operational at gealo.app, with the platform surface at platform.gealo.app. Capability surface covers projects (workspaces), tasks, chat, meetings, calendar, HR + attendance + leave, documents, GitHub integration, AI assistance, billing, SSO, and audit. One permission model across all of it. No revenue numbers or user counts here. The scoreboard belongs in the product. This note covers how the shape of the system holds up under the surface area.

➜

LEARNINGS

  • Multi-tenant invariants are cheap to add at the start and expensive to retrofit. The first migration sets the shape of the next two years.
  • Entitlements in one place removes an entire class of bugs where the UI promised something the API refused. Worth more than any individual feature.
  • An opinionated permission model costs you the product asks that would violate it. The payoff is that the security review fits on a page.
  • A design system that names what it refuses does more editing than one that only names what it allows. The "no" list is the part that holds.
➜ EOF[rendered: 5 sections]