Tech Spec: Role-Based Access Control
1. System Architecture
1.1 High-Level Overview
RBAC is implemented as a cross-cutting concern across three layers: Firestore data model, backend middleware, and frontend UI gating.
RBAC System
├── Data Layer (Firestore)
│ ├── teams/{teamId}/members/{userId}
│ └── invitations/{inviteId}
├── Enforcement Layer
│ ├── Firestore Security Rules
│ └── API Middleware (checkPermission)
└── UI Layer
├── Role-gated components
└── Permission hooks (usePermission)
2. Data Models
2.1 Team Member Document
Stored at teams/{teamId}/members/{userId}
typescript
interface TeamMember {
userId: string;
email: string;
displayName: string;
role: 'owner' | 'admin' | 'member' | 'viewer';
joinedAt: Timestamp;
invitedBy: string;
}
2.2 Invitation Document
Stored at invitations/{inviteId}
typescript
interface Invitation {
id: string;
teamId: string;
email: string;
role: 'admin' | 'member' | 'viewer';
status: 'pending' | 'accepted' | 'expired';
createdBy: string;
createdAt: Timestamp;
expiresAt: Timestamp; // 7 days from creation
}
2.3 Permissions Map
typescript
const PERMISSIONS: Record<Role, Permission[]> = {
owner: ['view', 'create', 'edit', 'delete', 'comment', 'manage_members', 'billing', 'transfer_ownership'],
admin: ['view', 'create', 'edit', 'delete', 'comment', 'manage_members'],
member: ['view', 'create', 'edit', 'comment'],
viewer: ['view', 'comment'],
};
3. Enforcement Layer
3.1 Firestore Security Rules
match /teams/{teamId}/tickets/{ticketId} {
allow read: if isTeamMember(teamId);
allow create, update: if hasPermission(teamId, 'edit');
allow delete: if hasPermission(teamId, 'delete');
}
function isTeamMember(teamId) {
return exists(/databases/$(database)/documents/teams/$(teamId)/members/$(request.auth.uid));
}
function hasPermission(teamId, permission) {
let member = get(/databases/$(database)/documents/teams/$(teamId)/members/$(request.auth.uid));
return permission in PERMISSIONS[member.data.role];
}
3.2 Frontend Permission Hook
typescript
function usePermission(permission: Permission): boolean {
const { currentTeamMember } = useTeamContext();
if (!currentTeamMember) return false;
return PERMISSIONS[currentTeamMember.role].includes(permission);
}
// Usage in components:
function DeleteTicketButton({ ticketId }: { ticketId: string }) {
const canDelete = usePermission('delete');
if (!canDelete) return null;
return <Button onClick={() => deleteTicket(ticketId)}>Delete</Button>;
}
4. Key Workflows
4.1 Invite Flow
- •Admin calls
POST /api/teams/{teamId}/invitations - •Backend validates admin has
manage_members permission - •Creates invitation document with 7-day expiry
- •Sends email via Resend with magic link
- •On click: validates invite → creates team member → marks invite accepted
4.2 Ownership Transfer
- •Owner calls
POST /api/teams/{teamId}/transfer-ownership - •Backend runs Firestore transaction:
- ●Verify caller is current owner
- ●Set target user's role to
owner - ●Set caller's role to
admin - ●Both writes are atomic
5. API Endpoints
| Endpoint | Method | Permission Required |
|---|
/api/teams/{id}/members | GET | view |
/api/teams/{id}/invitations | POST | manage_members |
/api/teams/{id}/members/{uid}/role | PATCH | manage_members |
/api/teams/{id}/transfer-ownership | POST | transfer_ownership |
6. Migration Strategy
6.1 Existing Teams
All existing team members will be migrated to the member role. The team creator will be set as owner. This migration runs as a one-time Cloud Function.
6.2 Rollout
- ●Week 1: Deploy with feature flag, internal testing
- ●Week 2: Enable for beta teams, gather feedback
- ●Week 3: GA rollout with in-app announcement