Overview
Codapult provides built-in multi-tenancy through organizations. Users can create teams, invite members, assign roles, and manage settings — all with fine-grained role-based access control (RBAC).
Roles & Hierarchy
Every organization member has one of four roles, ordered from most to least privileged:
| Role | Level | Capabilities | | ---------- | ----- | ------------------------------------------------------ | | Owner | 4 | Full control — delete org, manage billing, all actions | | Admin | 3 | Manage members, settings, billing, resources | | Member | 2 | Create and edit resources, view billing | | Viewer | 1 | Read-only access to org resources and settings |
Global admins (user.role === 'admin') bypass all organization-level permission checks.
Permissions
Permissions are defined in src/lib/permissions.ts as a map of action to allowed roles:
const permissions = {
'org:update': ['owner', 'admin'],
'org:delete': ['owner'],
'member:invite': ['owner', 'admin'],
'member:remove': ['owner', 'admin'],
'member:update-role': ['owner', 'admin'],
'member:list': ['owner', 'admin', 'member', 'viewer'],
'billing:manage': ['owner', 'admin'],
'billing:view': ['owner', 'admin', 'member'],
'resource:create': ['owner', 'admin', 'member'],
'resource:read': ['owner', 'admin', 'member', 'viewer'],
'resource:update': ['owner', 'admin', 'member'],
'resource:delete': ['owner', 'admin'],
'settings:manage': ['owner', 'admin'],
'invitation:create': ['owner', 'admin'],
'invitation:revoke': ['owner', 'admin'],
} satisfies Record<string, readonly OrgRole[]>;
Use the helper functions to check permissions in your code:
import { hasPermission, isRoleAtLeast, canModifyRole } from '@/lib/permissions';
hasPermission('member', 'resource:create'); // true
hasPermission('viewer', 'resource:create'); // false
isRoleAtLeast('admin', 'member'); // true
canModifyRole('admin', 'owner'); // false (must be strictly higher)
Guards
Server actions and API routes use guards from src/lib/guards.ts to enforce access control:
| Guard | Checks |
| ------------------------ | -------------------------------------------------- |
| requireAuth() | User is logged in — throws if not |
| requireOrgMembership() | User is a member of the organization |
| requireOrgAdmin() | User is an owner or admin of the organization |
| requireOrgPermission() | User has a specific permission in the organization |
Usage in Server Actions
'use server';
import { requireOrgPermission } from '@/lib/guards';
export async function updateOrgSettings(orgId: string, data: unknown) {
const { session, role } = await requireOrgPermission(orgId, 'settings:manage');
// Only owners and admins reach this point
// ... update settings ...
}
Usage in API Routes
import { requireOrgMembership } from '@/lib/guards';
export async function GET(req: Request) {
const orgId = req.headers.get('x-org-id');
if (!orgId) return NextResponse.json({ error: 'Missing org' }, { status: 400 });
const { session } = await requireOrgMembership(orgId);
// ... return org data ...
}
Invitations
Organization owners and admins can invite new members by email:
- Admin sends an invitation from Dashboard → Team → Invite Member
- An email with a unique token is sent to the invitee
- The invitee clicks the link and lands on
/invite/[token] - After accepting, they become a member with the assigned role
Invitations have an expiration date. Expired invitations cannot be accepted — the admin must resend.
Invitation Statuses
| Status | Description |
| ---------- | ------------------------------- |
| pending | Sent, awaiting acceptance |
| accepted | Invitee joined the organization |
| expired | Token expired before acceptance |
Database Tables
Three tables power the teams module:
organization
| Column | Type | Description |
| ------------ | --------- | ---------------------------- |
| id | text (PK) | Unique ID (nanoid) |
| name | text | Display name |
| slug | text | URL-safe unique identifier |
| image | text | Avatar URL (optional) |
| branding | text | JSON-encoded theme overrides |
| created_at | timestamp | Creation date |
| updated_at | timestamp | Last modified date |
organization_member
| Column | Type | Description |
| ----------------- | --------- | ------------------------------------ |
| id | text (PK) | Unique ID |
| organization_id | text (FK) | References organization.id |
| user_id | text (FK) | References user.id |
| role | text | owner, admin, member, viewer |
| created_at | timestamp | Joined date |
organization_invitation
| Column | Type | Description |
| ----------------- | --------- | -------------------------------- |
| id | text (PK) | Unique ID |
| organization_id | text (FK) | References organization.id |
| email | text | Invitee email address |
| role | text | Assigned role on acceptance |
| token | text | Unique invitation token |
| status | text | pending, accepted, expired |
| invited_by | text (FK) | References user.id |
| expires_at | timestamp | Expiration date |
| created_at | timestamp | Invitation sent date |
Server Actions
All team mutations are in src/lib/actions/organizations.ts:
| Action | Description | Required Permission |
| ------------------- | ------------------------------ | -------------------- |
| Create organization | Creates a new team | Authenticated user |
| Update organization | Updates name, avatar, branding | org:update |
| Delete organization | Permanently deletes the team | org:delete |
| Invite member | Sends an email invitation | member:invite |
| Change member role | Promotes or demotes a member | member:update-role |
| Remove member | Removes a member from the team | member:remove |
UI Components
- TeamSwitcher — dropdown in the dashboard sidebar for switching between organizations
- Team settings page — manage name, avatar, and billing at Dashboard → Settings → Team
- Members list — view, invite, and manage members at Dashboard → Team
Module Removal
The teams module is independently removable. See the Modules documentation for step-by-step removal instructions.
Next Steps
- Database — schema conventions and query patterns
- Payments & Billing — subscription management tied to organizations
- Authentication — user accounts and sign-in methods