Skip to content

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{Has Invite Code?}

    %% Door A: Site Owner Flow
    B -- No (Site Owner) --> C[Create User Record]
    C --> D[Create Team Record 'Personal Workspace']
    D --> E[API Call: Create Virtuozzo Account]
    E --> F[Start 3-Day Trial / Billing]
    F --> G[Login Success]

    %% Door B: Invited Member Flow
    B -- Yes (Invited Member) --> H[Validate Invite Token]
    H --> I[Create User Record ONLY]
    I --> J[SKIP Team Creation]
    J --> K[SKIP Virtuozzo Account]
    K --> L[Link User to Inviter's Team]
    L --> M[Login Success (Context: Inviter's Team)]
  • Door A (Site Owner): Creates the billing entity and infrastructure account immediately.
  • Door B (Invited Member): Creates a lightweight identity. No billing, no personal infrastructure. They "live" inside the Inviter's account.

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:

{
  "sub": "user_101",
  "team_id": 50,
  "role": "manager",  // Cached role name
  "exp": 1710000000
}

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

  1. The Permissions Table (Static): Populated by developers via migration.

    • billing.view, billing.edit
    • server.create, server.restart, server.delete
    • team.invite, team.remove
  2. 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.*.
  3. The Editing Workflow:

    • Goal: Site Owner wants to stop Managers from seeing Billing.
    • Action: Frontend calls DELETE /api/teams/{id}/roles/manager/permissions with 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.

  1. Redis Key: vz_session:{team_id}
  2. 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 User model (no billing fields).
  • Implement Team model (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/login returning pre_auth_token.
  • Implement POST /auth/session exchanging token for Session Cookie.
  • Implement Middleware to decode Session Cookie -> request.state.user.

Phase 3: Granular Permissions

  • Create permissions table and seed with list (billing, server, team).
  • Create roles table (team_id, name, is_editable).
  • Create role_permissions table.
  • Create API endpoints for Owners to edit Role Permissions (PUT /teams/{id}/roles/{role_id}).

Phase 4: The Virtuozzo Proxy

  • Implement VZSessionManager class.
  • Implement Redis caching logic.
  • Implement the try/catch 401 -> re-login -> retry loop.