Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Multi-Tenancy Design

Aero2 is a multi-tenant platform where a single deployment serves many applications. This page documents the entity hierarchy, isolation model, and data scoping strategy.

Hierarchy

Platform (single Aero2 deployment)
|
+-- Dashboard Application (built-in, slug: "dashboard")
|   +-- Developers (Users of the dashboard app)
|   |   +-- Developer Teams (Organizations in the dashboard app)
|   |       +-- owns Applications
|   |
|   +-- Uses Aero2's own auth (dogfooding)
|
+-- Application "TaskFlow" (slug: "swift-maple", auto-generated)
|   +-- App Users (end-users of TaskFlow)
|   +-- App Organizations (customer teams within TaskFlow)
|   +-- App IdPs, Branding, Settings
|   +-- Served at swift-maple.aero2.dev
|
+-- Application "ShopEasy" (slug: "brave-falcon", auto-generated)
    +-- App Users (completely separate from TaskFlow)
    +-- App Organizations
    +-- Served at brave-falcon.aero2.dev

Entity Relationships

"Dashboard" Application (built-in)
|
+-- Developer (User of dashboard app)
|   +-- Developer Team (Organization in dashboard app)
|       +-- owns ---+
                    v
            +------------------+
            |  Application     |
            |------------------|
            |  slug (auto)     | -> swift-maple.aero2.dev
            |  custom_domain   | -> auth.myapp.com (optional)
            |  owner_org_id    | -> Developer Team
            |  settings (JSON) |
            |  branding (JSON) |
            +--------+---------+
         +-----------+-----------+--------------+
         v           v           v              v
  +----------+ +--------+ +----------+ +--------------+
  | App User | |App IdP | |App OAuth | | App          |
  |----------| |--------| |Client    | |Organization  |
  | app_id   | | app_id | |----------| |--------------|
  | email    | | type   | | app_id   | | app_id       |
  | password | | config | | scopes   | | name         |
  +----------+ +--------+ +----------+ | members      |
                                       +--------------+

Key Principles

  • Application slugs are auto-generated as adjective-noun pairs (e.g., swift-maple, brave-falcon). Developers choose the app name, but the subdomain slug is assigned by the system. On collision, a 3-character suffix is appended (e.g., swift-maple-k3f).
  • A Platform Operator is seeded at first boot (from BOOTSTRAP_ADMIN_EMAIL environment variable).
  • The Dashboard is a built-in Application created at first boot (slug: dashboard).
  • A Developer is a User of the Dashboard Application.
  • A Developer signs up at dashboard.{domain} via OAuth login (self-service) and receives the developer role.
  • A Developer Team is an Organization within the Dashboard Application.
  • A Developer Team owns many Applications.
  • An Application belongs to exactly one Developer Team.
  • Multiple Developers in a Team can manage the same Application.
  • An Application has its own Users, IdPs, OAuth Clients, Settings, and Branding.
  • An Application User belongs to exactly one Application.
  • An Application User can belong to many Organizations within that Application.
  • Dashboard Users (Developers) cannot see Application Users and vice versa.
  • The same email can exist in multiple Applications as separate user accounts.

Data Isolation via app_id

Every tenant-scoped table includes an app_id column that references the applications table. This column enforces data isolation at the database level.

Tables with app_id Scoping

  • users -- each user belongs to one application
  • user_identity_links -- inherits from user, explicit for query safety
  • oauth_clients -- each client belongs to one application
  • identity_providers -- each IdP config belongs to one application
  • authorization_codes -- scoped to app
  • refresh_tokens -- scoped to app
  • oauth_state -- scoped to app
  • user_sessions -- scoped to app
  • audit_logs -- scoped to app
  • roles -- each app has its own roles
  • permissions -- each app has its own permissions
  • role_permissions -- scoped to app
  • user_roles -- scoped to app

Composite Unique Constraints

Because the same email can exist in different applications, unique constraints must be composite:

  • UNIQUE(app_id, email) on users
  • UNIQUE(app_id, name) on roles
  • UNIQUE(app_id, client_id) on oauth_clients
  • UNIQUE(app_id, name) on identity_providers

Query Pattern

Every database query touching tenant-scoped tables must include the app_id filter:

-- Correct: scoped to the current application
SELECT * FROM users WHERE app_id = ? AND email = ?
 
-- WRONG: missing app_id filter, leaks data across applications
SELECT * FROM users WHERE email = ?

RBAC scoping rule: All RBAC joins must include app_id across user_roles, roles, role_permissions, and permissions. Role and permission IDs are not globally unique, so missing app_id filters can leak access across applications.

Per-Application Isolation

Each application is isolated across multiple dimensions:

DimensionIsolation Mechanism
Dataapp_id column on all tenant-scoped tables
CookiesCookie Domain set to app's subdomain
CORSEach subdomain is its own origin
OIDC Issuerhttps://{slug}.{PLATFORM_DOMAIN} per app
BrandingPer-app logo, colors, name applied to auth pages
SettingsPer-app signup mode, MFA policy, session TTL
UsersSame email in different apps = different user accounts

Dashboard as an Application

The Dashboard itself is an Application with is_dashboard = true. This enables dogfooding: developers authenticate using the same auth system they configure for their own applications. Developer Teams are simply Organizations within the dashboard app.

Dashboard Access Control

The Dashboard uses a two-layer authorization model that separates what actions a user can perform (RBAC) from which resources they can access (ownership).

Dashboard Roles

RoleDescriptionHow Assigned
operatorPlatform operator with full system accessSeeded from BOOTSTRAP_ADMIN_EMAIL; manually granted
developerCan create and manage own applicationsAuto-assigned on first OAuth login (default)
memberRead-only access to team applicationsManually assigned by team admin

Two-Layer Authorization

Every authorized action passes through two checks:

  1. RBAC Layer: Does the user's role include the required permission?
  2. Ownership Layer: Does the user's team own the target resource?

The operator role bypasses the ownership layer and can access any resource (for platform support). The developer role must pass both layers.

Dashboard vs Per-App Permissions

The Dashboard has its own permission set, separate from the permissions within each application:

Dashboard permissions control platform-level actions:

  • Platform operations (operator only): platform:manage, developers:read, developers:write, apps:read:all, apps:write:all
  • Developer operations: apps:create, apps:read, apps:write, apps:delete, apps:transfer, teams:read, teams:write
  • Everyone: profile:read, profile:write

Per-app permissions control end-user actions within an application:

  • The default set (clients:read, clients:write, users:read, users:write, idps:read, etc.) is seeded when creating each new application
  • Each application can customize its own roles and permissions independently
  • When a developer manages their app's users/clients/roles via /api/applications/:slug/*, the system checks dashboard permissions (RBAC + ownership), not per-app permissions

Dashboard vs App Context: Dashboard requests are authenticated against the dashboard app but may operate on a target application if RBAC + ownership checks pass. This is the only supported cross-app access path.

Token and Session Scoping

  • Sessions and refresh tokens are stored with app_id and must be validated against the current request's app_id.
  • Tokens should carry app context via iss (per-app issuer) and/or an app_id claim so verifiers can reject cross-app tokens.

Sign-up Flows

Developer Sign-up (Dashboard)

Developer navigates to dashboard.aero2.dev/login
        |
        v
Clicks "Sign in with GitHub" (or Google, etc.)
        |
        v
OAuth callback → User record created in dashboard app
        |
        v
Auto-assigned "developer" role
(Exception: BOOTSTRAP_ADMIN_EMAIL gets "operator" role)
        |
        v
Lands on Dashboard → Can create applications

Developers sign up through the Dashboard's own OAuth login. There is no separate sign-up form -- the first OAuth login implicitly creates the developer account. This is the same auth flow used by all applications, but pointed at the Dashboard's own configured identity providers.

End-User Sign-up (Per-Application)

End-user navigates to swift-maple.aero2.dev/signup
        |
        v
Signs up via app's configured auth methods
(email/password, OAuth, magic link, passkey)
        |
        v
User record created in the application
(completely separate from dashboard users)
        |
        v
Assigned app's default role → Access app resources

End-users sign up on the application's subdomain using whatever authentication methods the developer has enabled. Their user record exists only within that application's app_id partition.

Key Distinction

A developer's identity in the Dashboard is completely separate from any end-user identity in an application. Even if the same person uses the same email address for both, they are different user records in different app_id partitions. The same email can exist in as many applications as needed.