Multi-Tenancy Architecture

Overview

The Refresh App Web implements a comprehensive multi-tenancy architecture that provides complete data isolation while maintaining excellent performance. Tenant isolation is enforced at both the application level (user identity resolution, permission checking) and the database level (Row-Level Security using JWT claims).

Multi-Tenancy Model

Tenant Hierarchy

Organization (Tenant)
├── Tenant Settings
├── Tenant Users (with roles: owner, admin, manager, member, viewer)
├── Groups
│   ├── Group Settings
│   └── Group Users (with roles: admin, manager, member)
└── Tenant Data
    ├── Integrations
    ├── Employees (HRIS)
    ├── Survey Responses
    ├── Reports
    └── Resources

Key Concepts

Tenant:

  • Primary isolation boundary

  • Represents an organization/company (your customers)

  • Has its own branding, settings, and data

  • Users can belong to multiple tenants via linked accounts

Group:

  • Sub-organization within a tenant

  • Represents a team, department, or division

  • Inherits tenant context

  • Users can belong to multiple groups

Active Tenant:

  • Currently selected tenant for the request

  • Specified via URL path parameter (:tenantId in route)

  • Determines which work user_id to use for data operations

  • Encoded in Neon JWT for RLS enforcement

User-Tenant Relationship

Multiple User Records Per Person

A single person can have multiple users records, one per tenant they've worked with:

See User Management for complete details on user identity lifecycle.

Tenant Context Resolution

When a request arrives for a tenant route (e.g., /api/v1/tenants/newco/...):

Application-Level Isolation

Request Flow

Tenant and Group Context: URL Path Parameters

Tenant and group context are passed via URL path parameters (not headers):

Why URL path, not headers?

Benefit
URL Path

Self-documenting URLs

Standard REST convention

Easier to audit/log

Works with browser history/bookmarks

Shareable URLs

No hidden state

Validated by middleware

Headers are reserved for:

  • Cross-cutting concerns (Authorization, Request-Id, tracing)

  • Internal validation (bypass users may send X-Tenant-Id to validate against URL)

Example Route Structure

Middleware Implementation

Middleware validates tenant access from URL path parameters:

Group Context

Groups are always scoped within a tenant. The :groupId URL path parameter is used when an operation is group-specific:

Personal vs Tenant Routes

Route Type
Tenant Context
User ID Used
Data Access

Personal

Not in URL

Primary user_id

Personal resources only

Tenant

:tenantId in URL

Work user_id

Tenant-owned data

Personal Routes: /personal/*, /settings/account, /settings/linked-accounts

Tenant Routes: /tenants/{tenant_id}/dashboard, /tenants/{tenant_id}/surveys, etc.

For the SvelteKit frontend, activeTenantId cookie stores the last-selected tenant:

The frontend uses this cookie for navigation (e.g., redirecting to /tenants/{activeTenantId}/dashboard after login).

Database-Level Isolation

Row-Level Security (RLS)

RLS policies automatically filter database queries based on JWT claims:

Neon JWT Generation

After resolving the work user_id for a tenant, generate a Neon JWT:

Database Connection with JWT

Role-Based Access Control

Tenant Roles

Stored in tenant_users.role, mapped to permissions:

Role
Description
Example Permissions

tenant:owner

Full control

tenant:delete, billing:manage

tenant:admin

Manage tenant

member:invite, survey:delete

tenant:manager

Create content

survey:create, report:export

tenant:member

Basic access

survey:respond, survey:view:own

tenant:viewer

Read-only

survey:view, report:view

Group Roles

Stored in group_users.role:

Role
Description

group:admin

Manage group settings and members

group:manager

Add members, create content

group:member

Participate in group activities

Permission Resolution

See Authorization for complete details.

Data Model

Tenant Table

Tenant Users Table

Groups Tables

Security Considerations

Tenant Isolation Guarantees

  1. Application Level:

    • :tenantId URL path parameter required for tenant routes

    • Work user_id resolved for requested tenant (via tenant_users membership)

    • tenant_users.status = 'active' check (bypass_rls users skip this)

  2. Database Level:

    • RLS policies enforce WHERE tenant_id = jwt_claim

    • Cross-tenant queries blocked by Postgres

    • Cannot bypass RLS from application code

  3. JWT Security:

    • Neon JWT contains tenant_id (not user-controllable)

    • RS256 signing prevents tampering

    • 8-hour expiration with refresh

Attack Prevention

Attack
Prevention

Tenant ID tampering

JWT signed with private key, verified by Neon

Cross-tenant access

RLS policies enforce isolation at DB level

Unauthorized tenant access

tenant_users membership check

JWT forgery

RS256 requires private key to sign

Performance Optimizations

1. Permission Caching

2. JWT Reuse

Only regenerate Neon JWT when:

  • Token expired

  • Tenant context changes

  • User permissions change

3. Database Indexes

Best Practices

1. Always Use Work User ID for Tenant Data

2. Validate Tenant Access

3. Use Neon JWT for All Tenant Queries


Last updated: December 2025

Last updated