--- title: Architecture description: Technical architecture of the core-tenant multi-tenancy package updated: 2026-01-29 --- # core-tenant Architecture This document describes the technical architecture of the core-tenant package, which provides multi-tenancy, user management, and entitlement systems for the Host UK platform. ## Overview core-tenant is the foundational tenancy layer that enables: - **Workspaces** - The primary tenant boundary (organisations, teams) - **Namespaces** - Product-level isolation within or across workspaces - **Entitlements** - Feature access control, usage limits, and billing integration - **User Management** - Authentication, 2FA, and workspace membership ## Core Concepts ### Tenant Hierarchy ``` User ├── owns Workspaces (can own multiple) │ ├── has WorkspacePackages (entitlements) │ ├── has Boosts (temporary limit increases) │ ├── has Members (users with roles/permissions) │ ├── has Teams (permission groups) │ └── owns Namespaces (product boundaries) └── owns Namespaces (personal, not workspace-linked) ``` ### Workspace The `Workspace` model is the primary tenant boundary. All tenant-scoped data references a workspace_id. **Key Properties:** - `slug` - URL-safe unique identifier - `domain` - Optional custom domain - `settings` - JSON configuration blob - `stripe_customer_id` / `btcpay_customer_id` - Billing integration **Relationships:** - `users()` - Members via pivot table - `workspacePackages()` - Active entitlement packages - `boosts()` - Temporary limit increases - `namespaces()` - Owned namespaces (polymorphic) ### Namespace The `Namespace_` model provides a universal product boundary. Products belong to namespaces rather than directly to users/workspaces. **Ownership Patterns:** 1. **User-owned**: Individual creator with personal namespace 2. **Workspace-owned**: Agency managing client namespaces 3. **User with workspace billing**: Personal namespace but billed to workspace **Entitlement Cascade:** 1. Check namespace-level packages first 2. Fall back to workspace pool (if namespace has workspace_id) 3. Fall back to user tier (for user-owned namespaces) ### BelongsToWorkspace Trait Models that are workspace-scoped should use the `BelongsToWorkspace` trait: ```php class Account extends Model { use BelongsToWorkspace; } ``` **Security Features:** - Auto-assigns `workspace_id` on create (or throws exception) - Provides `ownedByCurrentWorkspace()` scope - Auto-invalidates workspace cache on model changes **Strict Mode:** When `WorkspaceScope::isStrictModeEnabled()` is true: - Creating models without workspace context throws `MissingWorkspaceContextException` - Querying without context throws exception - This prevents accidental cross-tenant data access ## Entitlement System ### Feature Types Features (`entitlement_features` table) have three types: | Type | Description | Example | |------|-------------|---------| | `boolean` | On/off access | Beta features | | `limit` | Numeric limit with usage tracking | 100 AI credits/month | | `unlimited` | No limit | Unlimited social accounts | ### Reset Types | Type | Description | |------|-------------| | `none` | No reset (cumulative) | | `monthly` | Resets at billing cycle start | | `rolling` | Rolling window (e.g., last 30 days) | ### Package Model Packages bundle features with specific limits: ``` Package (creator) ├── Feature: ai.credits (limit: 100) ├── Feature: social.accounts (limit: 5) └── Feature: tier.apollo (boolean) ``` ### Boost Model Boosts provide temporary limit increases: | Boost Type | Description | |------------|-------------| | `add_limit` | Adds to existing limit | | `enable` | Enables a boolean feature | | `unlimited` | Makes feature unlimited | | Duration Type | Description | |---------------|-------------| | `cycle_bound` | Expires at billing cycle end | | `duration` | Expires after set period | | `permanent` | Never expires | ### Entitlement Check Flow ``` EntitlementService::can($workspace, 'ai.credits', quantity: 5) │ ├─> Get Feature by code │ └─> Get pool feature code (for hierarchical features) │ ├─> Calculate total limit │ ├─> Sum limits from active WorkspacePackages │ └─> Add remaining limits from active Boosts │ ├─> Get current usage │ ├─> Check reset type (monthly/rolling/none) │ └─> Sum UsageRecords in window │ └─> Return EntitlementResult ├─> allowed: bool ├─> limit: int|null ├─> used: int ├─> remaining: int|null └─> reason: string (if denied) ``` ### Caching Strategy Entitlement data is cached with 5-minute TTL: - `entitlement:{workspace_id}:limit:{feature_code}` - `entitlement:{workspace_id}:usage:{feature_code}` Cache invalidation occurs on: - Package provision/suspend/cancel - Boost provision/expire - Usage recording ## Service Layer ### WorkspaceManager Manages workspace context and basic CRUD: ```php $manager = app(WorkspaceManager::class); $manager->setCurrent($workspace); // Set context $manager->loadBySlug('acme'); // Load by slug $manager->create($user, $attrs); // Create workspace $manager->addUser($workspace, $user); // Add member ``` ### EntitlementService Core API for entitlement checks and management: ```php $service = app(EntitlementService::class); // Check feature access $result = $service->can($workspace, 'ai.credits', quantity: 5); if ($result->isAllowed()) { // Record usage after action $service->recordUsage($workspace, 'ai.credits', quantity: 5); } // Provision packages $service->provisionPackage($workspace, 'creator', [ 'source' => 'blesta', 'billing_cycle_anchor' => now(), ]); // Suspend/reactivate $service->suspendWorkspace($workspace); $service->reactivateWorkspace($workspace); ``` ### WorkspaceTeamService Manages teams and permissions: ```php $teamService = app(WorkspaceTeamService::class); $teamService->forWorkspace($workspace); // Check permissions if ($teamService->hasPermission($user, 'social.write')) { // User can write social content } // Team management $team = $teamService->createTeam([ 'name' => 'Content Creators', 'permissions' => ['social.read', 'social.write'], ]); $teamService->addMemberToTeam($user, $team); ``` ### WorkspaceCacheManager Workspace-scoped caching with tag support: ```php $cache = app(WorkspaceCacheManager::class); // Cache workspace data $data = $cache->remember($workspace, 'expensive-query', 300, function () { return ExpensiveModel::forWorkspace($workspace)->get(); }); // Flush workspace cache $cache->flush($workspace); ``` ## Middleware ### RequireWorkspaceContext Ensures workspace context before processing: ```php Route::middleware('workspace.required')->group(function () { // Routes here require workspace context }); // With user access validation Route::middleware('workspace.required:validate')->group(function () { // Also validates user has access to workspace }); ``` Workspace resolved from (in order): 1. Request attribute `workspace_model` 2. `Workspace::current()` (session/auth) 3. Request input `workspace_id` 4. Header `X-Workspace-ID` 5. Query param `workspace` ### CheckWorkspacePermission Checks user has specific permissions: ```php Route::middleware('workspace.permission:social.write')->group(function () { // Requires social.write permission }); // Multiple permissions (OR logic) Route::middleware('workspace.permission:admin,owner')->group(function () { // Requires admin OR owner role }); ``` ## Event System ### Lifecycle Events The module uses event-driven lazy loading: ```php class Boot extends ServiceProvider { public static array $listens = [ AdminPanelBooting::class => 'onAdminPanel', ApiRoutesRegistering::class => 'onApiRoutes', WebRoutesRegistering::class => 'onWebRoutes', ConsoleBooting::class => 'onConsole', ]; } ``` ### Entitlement Webhooks External systems can subscribe to entitlement events: | Event | Trigger | |-------|---------| | `limit_warning` | Usage at 80% or 90% | | `limit_reached` | Usage at 100% | | `package_changed` | Package add/change/remove | | `boost_activated` | Boost provisioned | | `boost_expired` | Boost expired | Webhooks include: - HMAC-SHA256 signature verification - Automatic retry with exponential backoff - Circuit breaker after consecutive failures ## Two-Factor Authentication ### TotpService RFC 6238 compliant TOTP implementation: ```php $totp = app(TwoFactorAuthenticationProvider::class); // Generate secret $secret = $totp->generateSecretKey(); // 160-bit base32 // Generate QR code URL $url = $totp->qrCodeUrl('AppName', $user->email, $secret); // Verify code if ($totp->verify($secret, $userCode)) { // Valid } ``` ### TwoFactorAuthenticatable Trait Add to User model: ```php class User extends Authenticatable { use TwoFactorAuthenticatable; } // Enable 2FA $secret = $user->enableTwoFactorAuth(); // User scans QR, enters code if ($user->verifyTwoFactorCode($code)) { $recoveryCodes = $user->confirmTwoFactorAuth(); } // Disable $user->disableTwoFactorAuth(); ``` ## Database Schema ### Core Tables | Table | Purpose | |-------|---------| | `users` | User accounts | | `workspaces` | Tenant organisations | | `user_workspace` | User-workspace pivot | | `namespaces` | Product boundaries | ### Entitlement Tables | Table | Purpose | |-------|---------| | `entitlement_features` | Feature definitions | | `entitlement_packages` | Package definitions | | `entitlement_package_features` | Package-feature pivot | | `entitlement_workspace_packages` | Workspace package assignments | | `entitlement_namespace_packages` | Namespace package assignments | | `entitlement_boosts` | Active boosts | | `entitlement_usage_records` | Usage tracking | | `entitlement_logs` | Audit log | ### Team Tables | Table | Purpose | |-------|---------| | `workspace_teams` | Team definitions | | `workspace_invitations` | Pending invitations | ## Configuration The package uses these config keys: ```php // config/core.php return [ 'workspace_cache' => [ 'enabled' => true, 'ttl' => 300, 'prefix' => 'workspace_cache', 'use_tags' => true, ], ]; ``` ## Testing Tests are in `tests/Feature/` using Pest: ```bash composer test # All tests vendor/bin/pest tests/Feature/EntitlementServiceTest.php # Single file vendor/bin/pest --filter="can method" # Filter by name ``` Key test files: - `EntitlementServiceTest.php` - Core entitlement logic - `WorkspaceSecurityTest.php` - Tenant isolation - `WorkspaceCacheTest.php` - Caching behaviour - `TwoFactorAuthenticatableTest.php` - 2FA flows