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

Design: Organizations (Phase 4)

This document describes the design for the organizations feature, enabling multi-tenant B2B use cases where end-users of an application belong to organizations with their own roles and settings.

Overview

Organizations are scoped to an Application — they are distinct from Developer Teams (which are organizations in the Dashboard application). This feature enables Aero2's customers to build B2B SaaS products where their end-users can create and manage teams/companies.

Schema

Organizations

CREATE TABLE organizations (
  id TEXT PRIMARY KEY,
  app_id TEXT NOT NULL REFERENCES applications(id) ON DELETE CASCADE,
  name TEXT NOT NULL,
  slug TEXT NOT NULL,
  logo_url TEXT,
  metadata TEXT,          -- JSON for custom metadata
  created_by TEXT NOT NULL REFERENCES users(id),
  created_at DATETIME DEFAULT (datetime('now')),
  updated_at DATETIME DEFAULT (datetime('now')),
  UNIQUE(app_id, slug)
);

Organization Members

CREATE TABLE organization_members (
  id TEXT PRIMARY KEY,
  app_id TEXT NOT NULL REFERENCES applications(id) ON DELETE CASCADE,
  org_id TEXT NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
  user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  role TEXT NOT NULL DEFAULT 'member',
  created_at DATETIME DEFAULT (datetime('now')),
  updated_at DATETIME DEFAULT (datetime('now')),
  UNIQUE(org_id, user_id)
);
 
CREATE INDEX idx_org_members_user ON organization_members(user_id);
CREATE INDEX idx_org_members_org ON organization_members(org_id);

Organization Invitations

CREATE TABLE organization_invitations (
  id TEXT PRIMARY KEY,
  app_id TEXT NOT NULL REFERENCES applications(id) ON DELETE CASCADE,
  org_id TEXT NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
  email TEXT NOT NULL,
  role TEXT NOT NULL DEFAULT 'member',
  invited_by TEXT NOT NULL REFERENCES users(id),
  token TEXT UNIQUE NOT NULL,
  expires_at DATETIME NOT NULL,
  accepted_at DATETIME,
  created_at DATETIME DEFAULT (datetime('now'))
);
 
CREATE INDEX idx_org_invitations_email ON organization_invitations(email);
CREATE INDEX idx_org_invitations_token ON organization_invitations(token);

API Endpoints

Organization CRUD

MethodEndpointDescription
POST/api/organizationsCreate org (creator becomes admin)
GET/api/organizationsList user's organizations
GET/api/organizations/:idGet org details
PUT/api/organizations/:idUpdate org (admin only)
DELETE/api/organizations/:idDelete org (admin only)

Member Management

MethodEndpointDescription
GET/api/organizations/:id/membersList members
PUT/api/organizations/:id/members/:userIdUpdate member role
DELETE/api/organizations/:id/members/:userIdRemove member

Invitations

MethodEndpointDescription
POST/api/organizations/:id/invitationsSend invitation
GET/api/organizations/:id/invitationsList invitations
DELETE/api/organizations/:id/invitations/:invIdRevoke invitation
POST/api/invitations/:token/acceptAccept invitation

Organization Roles

Default org-level roles:

RolePermissions
adminFull org management
memberView org, view members

Org-Level Permissions

org:manage          — Update org settings, delete org
org:members:read    — View member list
org:members:write   — Add/remove members, change roles
org:invitations     — Send/revoke invitations

Custom org roles can be defined per-application via the dashboard.

Invitation Flow

  1. Org admin calls POST /api/organizations/:id/invitations with { email, role }
  2. Server generates unique token, stores invitation with 7-day expiry
  3. Sends branded email: "You've been invited to join {org_name} on {app_name}"
  4. Recipient clicks link → /invitations/:token/accept
  5. If user exists → add to org with specified role
  6. If user doesn't exist → redirect to signup, then auto-add to org after verification

Verified Domains

Auto-enroll users into organizations based on verified email domain:

CREATE TABLE organization_domains (
  id TEXT PRIMARY KEY,
  app_id TEXT NOT NULL REFERENCES applications(id) ON DELETE CASCADE,
  org_id TEXT NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
  domain TEXT NOT NULL,
  verified BOOLEAN NOT NULL DEFAULT 0,
  verification_token TEXT,
  created_at DATETIME DEFAULT (datetime('now')),
  UNIQUE(app_id, domain)
);

Domain Verification

  1. Org admin adds domain (e.g., acme.com)
  2. Server generates verification token
  3. Admin adds DNS TXT record: _aero2-verification.acme.com TXT "{token}"
  4. Server verifies DNS record
  5. On successful verification, all users with matching email domain are auto-enrolled

Session Context

When a user belongs to multiple organizations, the session includes org context:

{
  "sub": "user_123",
  "org_id": "org_456",
  "org_role": "admin"
}

Org Switching

  • POST /api/auth/switch-org with { org_id } updates the session's active org
  • Frontend org switcher in the header (if user belongs to multiple orgs)

JWT Claims

When organizations are active, tokens include org claims:

{
  "sub": "user_123",
  "org_id": "org_456",
  "org_slug": "acme",
  "org_role": "admin",
  "org_permissions": ["org:manage", "org:members:read", "org:members:write"]
}

This enables downstream services to enforce org-level authorization.

Frontend Components

  • Org switcher — dropdown in header for users with multiple orgs
  • Create organization page
  • Org settings — name, logo, members, roles, domains, invitations
  • Member list — with role management
  • Invitation flow — email input + role select + send

Audit Events

  • org_created — organization created
  • org_updated — organization settings changed
  • org_deleted — organization deleted
  • member_added — user added to org
  • member_removed — user removed from org
  • member_role_changed — member role updated
  • invitation_sent — invitation email sent
  • invitation_accepted — invitation accepted

Dependencies

  • Multi-tenancy (Phase 0) — all tables require app_id
  • Email sending — for invitation emails
  • RBAC system — extended for org-level permissions