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
| Method | Endpoint | Description |
|---|---|---|
POST | /api/organizations | Create org (creator becomes admin) |
GET | /api/organizations | List user's organizations |
GET | /api/organizations/:id | Get org details |
PUT | /api/organizations/:id | Update org (admin only) |
DELETE | /api/organizations/:id | Delete org (admin only) |
Member Management
| Method | Endpoint | Description |
|---|---|---|
GET | /api/organizations/:id/members | List members |
PUT | /api/organizations/:id/members/:userId | Update member role |
DELETE | /api/organizations/:id/members/:userId | Remove member |
Invitations
| Method | Endpoint | Description |
|---|---|---|
POST | /api/organizations/:id/invitations | Send invitation |
GET | /api/organizations/:id/invitations | List invitations |
DELETE | /api/organizations/:id/invitations/:invId | Revoke invitation |
POST | /api/invitations/:token/accept | Accept invitation |
Organization Roles
Default org-level roles:
| Role | Permissions |
|---|---|
admin | Full org management |
member | View 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 invitationsCustom org roles can be defined per-application via the dashboard.
Invitation Flow
- Org admin calls
POST /api/organizations/:id/invitationswith{ email, role } - Server generates unique token, stores invitation with 7-day expiry
- Sends branded email: "You've been invited to join {org_name} on {app_name}"
- Recipient clicks link →
/invitations/:token/accept - If user exists → add to org with specified role
- 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
- Org admin adds domain (e.g.,
acme.com) - Server generates verification token
- Admin adds DNS TXT record:
_aero2-verification.acme.com TXT "{token}" - Server verifies DNS record
- 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-orgwith{ 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 createdorg_updated— organization settings changedorg_deleted— organization deletedmember_added— user added to orgmember_removed— user removed from orgmember_role_changed— member role updatedinvitation_sent— invitation email sentinvitation_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