This is the Master Technical Design Document (TDD). It consolidates every architectural decision we have made regarding the Forked Registration, the Backend Proxy, and specifically the Editable Granular Permission System.
This document is ready for your stakeholders and serves as the source of truth for your engineering team.
📘 Technical Design Document: MBPanel Core Architecture¶
Project: MBPanel (Multi-Tenant SaaS Platform) Version: 2.0 (Finalized) Scope: Authentication, Team Management, RBAC/PBAC, Virtuozzo Proxy.
1. Executive System Overview¶
MBPanel uses a Team-Centric Architecture where identity (User) is decoupled from billing and resources (Team). This ensures that user accounts are portable across multiple teams and prevents access lockouts due to subscription expiry.
The system employs a Backend-for-Frontend (BFF) security model. The frontend never interacts directly with infrastructure providers (Virtuozzo); instead, the FastAPI backend acts as a secure, self-healing proxy.
1.1 High-Level Architecture¶
- Frontend (Next.js): Stateless UI. authentication via HttpOnly Cookies.
- Backend (FastAPI): Enforces RBAC/PBAC, manages sessions, and proxies infrastructure calls.
- State (Redis): Caches the shared "Singleton" Virtuozzo session keys.
- Infrastructure (Virtuozzo): Accessed ONLY via the Backend Proxy.
2. The "Forked" Registration Architecture¶
To prevent database bloat and "Ghost Accounts" in Virtuozzo, we implement two distinct entry paths.
2.1 Logical Flow Diagram¶
graph TD
A[User Entry] --> B{Registration Type?}
%% Door A1: New Customer Flow
B -- New Customer --> C1[Create User + Team]
C1 --> D1[Send Email Verification]
D1 --> E1[User Verifies Email]
E1 --> F1[Backend Creates VZ Account]
F1 --> G1[Store VZ Credentials Encrypted]
G1 --> H1[Login Success]
%% Door A2: Migrating Customer Flow
B -- Migrating from Legacy --> C2[Validate VZ Credentials]
C2 --> D2[Create User + Team]
D2 --> E2[Store VZ Credentials Encrypted]
E2 --> F2[Send Email Verification]
F2 --> G2[Login Success]
%% Door B: Invited Member Flow
B -- Has Invite Token --> C3[Validate Invite Token]
C3 --> D3[Create User Record ONLY]
D3 --> E3[SKIP Team Creation]
E3 --> F3[SKIP Virtuozzo Account]
F3 --> G3[Link to Inviter's Team]
G3 --> H3[Login Success]
- Door A1 (New Customer): Creates user + team. VZ account created after email verification.
- Door A2 (Migrating Customer): Validates existing VZ credentials, creates user + team with VZ data.
- Door B (Invited Member): Creates lightweight identity only. No billing, no personal infrastructure.
3. Authentication & Session Strategy¶
We utilize a Two-Step "Pre-Auth" Flow to secure the transition between verifying credentials and selecting a team context.
3.1 The Login Flow Diagram¶
sequenceDiagram
participant Client as Next.js
participant Auth as FastAPI Auth
participant DB as Database
%% Step 1
Client->>Auth: POST /auth/login {email, password}
Auth->>DB: Validate Creds & Fetch Active Teams
DB-->>Auth: User Valid, Teams List
Auth-->>Client: 200 OK { pre_auth_token, available_teams }
Note right of Client: pre_auth_token scope: 'team_select_only'<br/>Expires in 5 mins.
%% Step 2
Client->>Auth: POST /auth/session { team_id: 5 }
Note right of Client: Header: Bearer <pre_auth_token>
Auth->>DB: Verify User Membership in Team 5
Auth->>DB: Verify Team 5 is ACTIVE (Not Expired)
DB-->>Auth: Allowed
Auth-->>Client: 200 OK (Set HttpOnly Cookie: Access Token)
3.2 Access Token Structure (JWT)¶
The final access_token contains the context required for RBAC:
4. Granular Permission System (PBAC)¶
This section addresses the requirement for Pre-Defined Roles with Granular Editing.
We do not check "Roles" in our code. We check "Permissions". Roles are simply database containers that group permissions together.
4.1 Database Schema (ERD)¶
erDiagram
TEAMS ||--o{ ROLES : defines
ROLES ||--o{ ROLE_PERMISSIONS : has
PERMISSIONS ||--o{ ROLE_PERMISSIONS : links
TEAM_MEMBERS }o--|| ROLES : assigned
TEAMS {
int id
string name
}
ROLES {
int id
int team_id
string name
boolean is_editable
string description
}
PERMISSIONS {
string slug "PK (e.g. billing.view)"
string description
}
ROLE_PERMISSIONS {
int role_id
string permission_slug
}
4.2 The Logic: Editable vs. Static¶
-
The Permissions Table (Static): Populated by developers via migration.
billing.view,billing.editserver.create,server.restart,server.deleteteam.invite,team.remove
-
The Roles Table (Dynamic per Team):
- When a Team is created, we seed it with default roles.
- Owner (is_editable=False): Has
*(All permissions). Cannot be changed. - Manager (is_editable=True): Seeded with
billing.*,server.*,team.*. - Developer (is_editable=True): Seeded with
server.*.
-
The Editing Workflow:
- Goal: Site Owner wants to stop Managers from seeing Billing.
- Action: Frontend calls
DELETE /api/teams/{id}/roles/manager/permissionswith body['billing.view', 'billing.edit']. - Backend: Removes rows from
ROLE_PERMISSIONS. - Result: Any user with the "Manager" role instantly loses access to billing endpoints.
4.3 Code Implementation (The Enforcer)¶
We use a FastAPI Dependency to enforce permissions, not roles.
# backend/core/security.py
def require_permission(required_perm: str):
def dependency(request: Request, db: Session = Depends(get_db)):
user = request.state.user
team_id = request.state.team_id
# 1. Check DB: Does the user's current Role have this Permission?
has_perm = db.query(RolePermission).join(TeamMember).filter(
TeamMember.user_id == user.id,
TeamMember.team_id == team_id,
RolePermission.permission_slug == required_perm
).exists()
if not has_perm:
raise HTTPException(403, "Permission Denied")
return dependency
# backend/api/billing.py
@router.get("/invoices")
def get_invoices(_ = Depends(require_permission("billing.view"))):
return {"invoices": [...]}
5. The Infrastructure Proxy (Virtuozzo Integration)¶
This acts as the bridge between our secure internal logic and the external infrastructure.
5.1 The "Lazy Singleton" Pattern¶
We treat the Virtuozzo Session Key as a Team Resource, not a User Resource.
- Redis Key:
vz_session:{team_id} - TTL: 25 Minutes (VZ expires in 30 mins).
5.2 Self-Healing Workflow Diagram¶
sequenceDiagram
participant API as FastAPI Endpoint
participant Proxy as VZ Manager Service
participant Redis as Redis Cache
participant VZ as Virtuozzo API
API->>Proxy: execute_request(restart_server)
rect rgb(240, 248, 255)
Note over Proxy, Redis: 1. Try Cache
Proxy->>Redis: GET vz_session:{team_id}
Redis-->>Proxy: Return SessionKey "ABC"
end
rect rgb(255, 240, 240)
Note over Proxy, VZ: 2. Execute with Key
Proxy->>VZ: POST /restart (Session="ABC")
VZ-->>Proxy: 401 Unauthorized (Session Expired)
end
rect rgb(240, 255, 240)
Note over Proxy, VZ: 3. Self-Healing Triggered
Proxy->>Proxy: Fetch DB Creds & Decrypt
Proxy->>VZ: Login(User, Pass)
VZ-->>Proxy: 200 OK (NewSession="XYZ")
Proxy->>Redis: SET vz_session:{team_id} = "XYZ"
end
rect rgb(240, 248, 255)
Note over Proxy, VZ: 4. Retry Original Request
Proxy->>VZ: POST /restart (Session="XYZ")
VZ-->>Proxy: 200 OK (Success)
end
Proxy-->>API: Return Data
6. Implementation Task Checklist¶
Phase 1: Core Identity (Door A / Door B)¶
- Implement
Usermodel (no billing fields). - Implement
Teammodel (status: active/trial/archived). - Create
POST /register(Owner flow - creates Team + User). - Create
POST /invite/accept(Member flow - creates User only).
Phase 2: Authentication¶
- Implement
POST /auth/loginreturningpre_auth_token. - Implement
POST /auth/sessionexchanging token for Session Cookie. - Implement Middleware to decode Session Cookie ->
request.state.user.
Phase 3: Granular Permissions¶
- Create
permissionstable and seed with list (billing, server, team). - Create
rolestable (team_id, name, is_editable). - Create
role_permissionstable. - Create API endpoints for Owners to edit Role Permissions (
PUT /teams/{id}/roles/{role_id}). - Create frontend RoleManagement component with permission tooltips.
- Create custom role creation dialog with permission checkboxes.
Phase 4: The Virtuozzo Proxy¶
- Implement
VZSessionManagerclass. - Implement Redis caching logic.
- Implement the
try/catch 401 -> re-login -> retryloop.
7. Permissions System Implementation Guide¶
7.1 Default Roles and Permissions¶
When a new team is created, the system automatically seeds three default roles:
Owner Role¶
- Permissions:
*(wildcard - all permissions) - Editable: No (system-protected)
- Description: Owner role with full control
Manager Role¶
- Permissions:
team.manage- Manage team settings and rolesteam.invite- Invite team membersevents:read- Read SSE eventsbilling.view- View billing informationbilling.edit- Edit billing settingsserver.create- Create new serversserver.restart- Restart serversserver.delete- Delete servers- Editable: Yes
- Description: Manager role with team and infrastructure management
Developer Role¶
- Permissions:
events:read- Read SSE eventsserver.create- Create new serversserver.restart- Restart serversserver.delete- Delete servers- Editable: Yes
- Description: Developer role with server management only
7.2 Permission Catalog¶
All available permissions are seeded in the database via Alembic migration:
# Migration: 2025_12_17_0002_seed_permissions_catalog.py
_PERMISSIONS = [
{"slug": "*", "description": "All permissions (wildcard)"},
{"slug": "team.manage", "description": "Manage team (ownership/roles/settings)"},
{"slug": "team.invite", "description": "Invite team members"},
{"slug": "events:read", "description": "Read SSE events"},
{"slug": "billing.view", "description": "View billing"},
{"slug": "billing.edit", "description": "Edit billing"},
{"slug": "server.create", "description": "Create server"},
{"slug": "server.restart", "description": "Restart server"},
{"slug": "server.delete", "description": "Delete server"},
]
7.3 Frontend Implementation¶
Using the RoleManagement Component¶
import { RoleManagement } from '@/features/teams/components';
function TeamSettingsPage() {
const user = useCurrentUser();
const teamId = user.team_id;
return (
<RoleManagement
teamId={teamId}
currentUserPermissions={user.permissions}
/>
);
}
Component Features¶
- Role Display with Hover Tooltips
- Shows role name, description, and editable status
- Displays first 3 permissions as badges
-
On hover, shows complete list of permissions with descriptions in a tooltip
-
Custom Role Creation
- Dialog with form for role name and description
- Grouped permission checkboxes by category (team, billing, server, events)
- Select All / Deselect All functionality
-
Real-time permission count display
-
Permission Editing
- Edit existing role permissions (for editable roles only)
- Same checkbox interface as creation dialog
-
Preserves role name and description
-
Role Deletion
- Delete custom roles (with confirmation)
- Protected system roles cannot be deleted
- Prevents deletion of roles currently assigned to members
Available Service Methods¶
// Get all permissions
await teamService.getPermissions();
// Get roles for a team
await teamService.getRoles(teamId);
// Create custom role
await teamService.createRole(
teamId,
'Support Team',
'Customer support role',
['billing.view', 'server.restart']
);
// Update role permissions
await teamService.updateRolePermissions(
teamId,
roleId,
['billing.view', 'billing.edit', 'server.restart']
);
// Delete role
await teamService.deleteRole(teamId, roleId);
// Assign role to member
await teamService.assignMemberRole(teamId, userId, roleId);
7.4 Security Considerations¶
-
Session Revocation: When role permissions change, all user sessions for members with that role are automatically revoked to ensure immediate permission updates.
-
Owner Protection: The Owner role with wildcard permissions cannot be edited or deleted.
-
In-Use Protection: Roles currently assigned to team members cannot be deleted.
-
Permission Caching: Permissions are cached in JWT tokens. The backend automatically revokes tokens when permissions change.