# 01 — Architecture

## 1. System context

```mermaid
flowchart LR
    subgraph Users
      SA[Super Admin]
      AG[Agency staff]
      CL[Client users]
    end
    subgraph App["Laravel 13 app (one codebase)"]
      WEB[Web: Nginx + PHP-FPM<br/>Blade / Livewire / Alpine]
      HZ[Horizon workers<br/>per-queue supervisors]
      SCH[Scheduler<br/>schedule:run every minute]
      PROV[Provisioning worker<br/>own DB credential]
    end
    subgraph Data
      MDB[(MySQL master<br/>social_saas_master)]
      TDB[(MySQL tenant DBs<br/>social_tenant_xxxx)]
      R[(Redis<br/>queues · cache · locks · rate limits)]
      S3[(S3 / MinIO<br/>tenants/{uuid}/...)]
      SM[[Secrets Manager]]
    end
    subgraph External
      META[Meta Graph<br/>FB · IG · Threads]
      LI[LinkedIn]
      YT[YouTube]
      X[X API v2]
      TT[TikTok]
      PIN[Pinterest]
      GBP[Google Business Profile]
      PAY[Razorpay]
      AI[Claude API]
      AV[ClamAV]
    end
    Users --> WEB
    WEB --> MDB & TDB & R & S3
    HZ --> MDB & TDB & R & S3
    SCH --> R
    PROV --> MDB & TDB & SM
    WEB & HZ --> SM
    HZ --> META & LI & YT & X & TT & PIN & GBP & AI & AV
    WEB --> PAY
    META & LI & YT & TT & PAY -. webhooks .-> WEB
```

## 2. Code layout

> As built in M1: Eloquent models live in `app/Models` (master) and `app/Models/Tenant` (tenant DB); actions and services live in `app/Domain/*`. The tree below is the target shape as later milestones add domains.

```
app/
  Tenancy/                     # custom tenancy layer (D2)
    TenantManager.php          # initialize(Tenant) / end(), holds current tenant
    TenantConnectionConfigurator.php  # builds `tenant` connection from SecretStore
    WorkspaceSession.php       # current workspace (session) → tenant, membership re-checked
    Middleware/{IdentifyTenant, EnsureTenantMember, EnsureTenantActive}.php
    Jobs/TenantAware.php       # interface: tenantId()
    Jobs/Middleware/InitializeTenancy.php  # try { init } finally { end }
    Provisioning/{ProvisionTenant, Steps/*}.php
    Secrets/{SecretStore, EncryptedDbSecretStore, AwsSecretsManagerStore}.php
    Concerns/{UsesTenantConnection, UsesMasterConnection}.php
  Domain/
    Identity/      (users, MFA, sessions)              — master
    Tenants/       (tenants, domains, memberships)    — master
    Billing/       (plans, subscriptions, invoices)   — master
    Platform/      (integrations, capabilities, settings, admin audit) — master
    Clients/       (clients, brands, client_users)    — tenant
    Social/        (accounts, tokens, permissions, providers/*) — tenant
    Media/         (assets, folders, scanning, variants)  — tenant
    Publishing/    (posts, versions, targets, schedules, attempts, outbox) — tenant
    Approvals/     (requests, steps, history, comments)   — tenant
    Calendar/ Campaigns/ Teamwork/                       — tenant
    Inbox/         (conversations, messages, comments)   — tenant
    Analytics/     (metrics, snapshots, reports)          — tenant
    Ai/            (requests, usage, templates)            — tenant
    Audit/         (tenant_audit_logs, admin_audit_logs)
  Http/ Livewire/ Policies/ Jobs/ Console/
database/
  migrations/master/           # run with --database=master
  migrations/tenant/           # run per tenant via tenants:migrate
  seeders/master/ seeders/tenant/
config/rbac.php  config/social/providers.php  config/social/capabilities/*.json
```

Models declare their connection explicitly. `UsesMasterConnection` sets `$connection = 'master'` and `UsesTenantConnection` sets `$connection = 'tenant'`. A unit test fails the build if a model under `Domain/{tenant domains}` lacks the tenant trait, or the reverse.

## 3. Tenancy runtime

### 3.1 Tenant resolution (web)

1. One host (`APP_URL`) serves everything; there are no subdomains or custom domains. After `auth`, `IdentifyTenant` reads the current workspace from the session (`WorkspaceSession`), set at `/workspaces/{slug}/enter` by the picker or the top-bar switcher. A user with one workspace is placed in it; with several and no valid choice, the picker is shown.
2. `EnsureTenantActive`: status must be `active`. `suspended` routes to a read-only/billing page per gating rules. `provisioning_*` shows a "setting up" page.
3. `EnsureTenantMember`: the authenticated master `user_id` must have an `accepted` row in `tenant_users` for that tenant. **A `tenant_id` sent by the browser is never trusted.**
4. `TenantManager::initialize($tenant)`:
   - fetches the credential via `SecretStore::get($connection->secret_ref)` (cached in-process only, never in Redis)
   - `config(['database.connections.tenant' => [...]])`, `DB::purge('tenant')`, `DB::reconnect('tenant')`
   - sets the cache prefix `t:{tenant_id}:`, the filesystem root prefix `tenants/{tenant_id}/`, `Context::add('tenant_id', …)`, and Sentry/log scope
5. A terminating middleware and `app()->terminating()` call `TenantManager::end()`. It purges the connection, forgets the tenant, and resets cache prefix, disk, and context.

### 3.2 Tenant context in jobs

- Every tenant job implements `TenantAware` and carries **only** `tenant_id` plus entity IDs, never models with tenant connections and never secrets.
- Job middleware `InitializeTenancy`:
  ```php
  public function handle($job, $next) {
      $tenant = Tenant::on('master')->findOrFail($job->tenantId());
      abort_unless($tenant->isOperational(), ...);   // suspended → release/fail per rule
      try { $this->tenancy->initialize($tenant); return $next($job); }
      finally { $this->tenancy->end(); }
  }
  ```
- Belt and braces: `Queue::before()` asserts no tenant is active (and ends one if so, logging an error). `Queue::after()` and `Queue::failing()` always call `end()`.
- A test runs job A for tenant 1 and job B for tenant 2 on the same worker process. It asserts that B never sees tenant 1's connection, cache prefix, or disk.

### 3.3 Cross-tenant work (central)

- Central commands (`outbox:relay`, `tokens:refresh-due`, `metrics:sync-due`, `tenants:migrate`) iterate `Tenant::active()->cursor()`. For each tenant they call `TenantManager::run($tenant, fn () => …)`, which wraps the work in try/finally.
- No SQL ever joins across tenant databases. Central reporting reads `usage_summaries` and other aggregates that tenants push to master.

## 4. Tenant provisioning flow

```mermaid
sequenceDiagram
    participant U as User (signup)
    participant W as Web (master creds)
    participant M as Master DB
    participant Q as Redis (provisioning queue)
    participant P as Provisioner worker (DDL creds)
    participant S as SecretStore
    participant T as New tenant DB
    U->>W: Register agency (name, slug, email, password)
    W->>M: TX: users, tenants(status=provisioning_pending), tenant_users(owner, accepted), tenant_provisioning_jobs(step=queued)
    W->>Q: ProvisionTenant(tenant_id)
    W-->>U: "Setting up your workspace" (polls status)
    P->>M: lock tenant row, step=create_database
    P->>T: CREATE DATABASE social_tenant_{n} (IF NOT EXISTS)
    P->>T: CREATE USER t_{n} IDENTIFIED BY random(48); GRANT … ON social_tenant_{n}.* 
    P->>S: put(secret) → secret_ref
    P->>M: tenant_database_connections(secret_ref, host, db, status=created)
    P->>T: run database/migrations/tenant (as tenant user)
    P->>T: seed roles, settings, default approval workflow
    P->>T: verify: connect + schema version + smoke query
    P->>M: tenants.status=active, provisioned_at=now, job state=completed
    P-->>U: Welcome email / onboarding
```

- Each step is **idempotent** (`IF NOT EXISTS`, upserts, migration table). It is recorded in `tenant_provisioning_jobs` with `attempts` and `failure_reason`.
- Any failure leaves the tenant in `provisioning_failed` (not active). An admin "Retry" resumes from the last incomplete step. Acceptance test: kill the job after each step, retry, and confirm success.
- Rollback/deprovision (`tenants:deprovision`) drops the user and DB only from `deleting` status, after a final encrypted backup.

### Tenant lifecycle states

`provisioning_pending → provisioning → active ⇄ suspended → deleting → deleted`, with `provisioning → provisioning_failed → provisioning` (retry).

## 5. Request lifecycle summary

```
HTTPS → Nginx → PHP-FPM → Laravel
  → TrustProxies / security headers / session / CSRF
  → auth → IdentifyTenant (session) → EnsureTenantActive → EnsureTenantMember
  → (MFA required? RequireTwoFactor)
  → Route → FormRequest validation → Policy (tenant role + client assignment)
  → Action (DB::connection('tenant')->transaction) → audit log → response
  → terminate: TenantManager::end()
```

## 6. Key cross-cutting services

| Service | Purpose |
|---------|---------|
| `AuditLogger` | `tenant_audit_logs` (tenant) and `admin_audit_logs` (master). Actor, action, subject, before/after diff (redacted), IP, user agent, request_id |
| `EntitlementService` | Reads plan features plus overrides from master. `can($tenant, 'clients.max')`, `assertWithinLimit()`. Result cached per tenant, busted on subscription change |
| `UsageMeter` | Tenant `tenant_usage_events` (append-only) → hourly roll-up to master `usage_summaries` |
| `CapabilityResolver` | Effective capability = provider capability data ∩ integration review status ∩ granted scopes ∩ feature flag ∩ plan |
| `RateLimiterRegistry` | Redis buckets per provider app, per social account, and per tenant. Reads `Retry-After` / quota headers |
| `SignedUrlService` | Short-TTL S3 signed GET/PUT, tenant-prefixed and verified against the asset's tenant |
| `Clock` | Injected clock. Every scheduling computation goes through `CarbonImmutable` in UTC |
