跳轉到

SaaS 架構設計

本頁說明 NextPDF SaaS Foundation 的多租戶架構設計,包含三種隔離模式的選擇依據、資料分區策略與典型的生產部署拓撲。


多租戶隔離模式

NextPDF SaaS Foundation 支援三種隔離級別,依您的業務需求選擇:

模式一:共享池(Shared Pool)

最低成本,適合標準 SaaS:

所有租戶共享 PHP/Prisma 程序池
租戶隔離靠 JWT tenant_id 宣告實現
資料隔離靠資料庫 WHERE tenant_id = ? 實現

適合場景:中小型 SaaS,每租戶操作量相近,無嚴格監管要求

風險:吵鬧鄰居效應(Noisy Neighbor),一個高流量租戶可能影響其他租戶

模式二:HighControl 模式(Enterprise)

Prisma Enterprise 的標準多租戶模式:

PHP 層:共享(但請求攜帶 tenant_id JWT Claims)
Prisma Worker 池:以 tenant_id 路由至隔離 Worker 子池
資料層:Cloudflare D1 per-tenant namespace 或 R2 per-tenant 前綴

適合場景:企業級 SaaS,有中等隔離需求,需要可審計的操作日誌

模式三:完全隔離(Dedicated)

最高隔離,適合監管嚴格場景:

每個大型租戶獲得獨立的:
- Prisma Enterprise 實例
- 資料庫 Schema / DB
- R2 Storage Bucket
- 專屬 Cloudflare Workers Route

適合場景:政府、金融、醫療租戶,具有資料主權需求


HighControl 模式架構

flowchart TD
    subgraph Requests["請求層"]
        T1["租戶 A 請求\nJWT: tenant_id=tenant-a"]
        T2["租戶 B 請求\nJWT: tenant_id=tenant-b"]
        T3["租戶 C 請求\nJWT: tenant_id=tenant-c"]
    end

    subgraph Gateway["Cloudflare Workers 閘道"]
        AUTH["JWT 驗證\n提取 tenant_id"]
        ROUTE["一致性雜湊路由\nhash(tenant_id) → Worker 子池"]
    end

    subgraph WorkerPools["Prisma Worker 子池"]
        POOL_A["Worker 子池 A\n(處理 tenant-a 請求)"]
        POOL_B["Worker 子池 B\n(處理 tenant-b 請求)"]
        POOL_C["Worker 子池 C\n(處理 tenant-c 請求)"]
    end

    subgraph Storage["資料層(前綴隔離)"]
        R2["R2: tenant-a/pdf/...\n      tenant-b/pdf/...\n      tenant-c/pdf/..."]
        D1["D1: audit_log WHERE tenant_id = ?"]
    end

    T1 & T2 & T3 --> AUTH --> ROUTE
    ROUTE -->|"tenant-a"| POOL_A
    ROUTE -->|"tenant-b"| POOL_B
    ROUTE -->|"tenant-c"| POOL_C
    POOL_A & POOL_B & POOL_C --> R2
    POOL_A & POOL_B & POOL_C --> D1

JWT Claims 結構

HighControl 模式要求每個請求的 JWT 必須包含以下 Claims:

{
  "iss": "your-saas-platform",
  "sub": "user-12345",
  "aud": "nextpdf-prisma",
  "iat": 1740000000,
  "exp": 1740003600,
  "jti": "unique-request-id-uuid",
  "tenant_id": "tenant-abc123",
  "plan": "enterprise",
  "data_region": "ap-east-1",
  "permissions": [
    "pdf:generate",
    "pdf:parse",
    "rag:embed",
    "rag:search"
  ],
  "rate_limits": {
    "pdf_generate_per_minute": 100,
    "rag_embed_per_day": 10000
  }
}

PHP 實作

<?php

declare(strict_types=1);

use NextPDF\Enterprise\HighControl\TenantContext;
use NextPDF\Enterprise\HighControl\HighControlClient;

final class PdfService
{
    public function __construct(
        private readonly HighControlClient $client,
    ) {}

    public function generateForTenant(
        string $tenantId,
        string $content,
        string $plan,
    ): string {
        $context = new TenantContext(
            tenantId: $tenantId,
            plan: $plan,
            dataRegion: $this->getDataRegion($tenantId),
        );

        return $this->client->withContext($context)->generate(
            content: $content,
            options: ['page_size' => 'A4'],
        );
    }

    private function getDataRegion(string $tenantId): string
    {
        // 從租戶設定讀取資料主權區域
        return TenantRepository::getDataRegion($tenantId);
    }
}

資料分區策略

PDF 文件儲存(Cloudflare R2)

物件鍵格式:{tenant_id}/{document_type}/{year}/{month}/{document_id}.pdf

範例:
  tenant-abc123/generated/2026/03/doc-uuid-001.pdf
  tenant-abc123/parsed/2026/03/source-uuid-001.pdf
  tenant-abc123/signed/2026/03/signed-uuid-001.pdf

生命週期策略: - 根據各租戶的資料保留設定自動清除過期文件 - Enterprise 租戶可設定 90 天到 7 年的保留期限

操作記錄(Cloudflare D1 / PostgreSQL)

-- 審計日誌表(不可修改,只能插入)
CREATE TABLE audit_log (
    id          TEXT PRIMARY KEY,  -- UUID v4
    tenant_id   TEXT NOT NULL,
    user_id     TEXT,
    operation   TEXT NOT NULL,     -- 'pdf.generate', 'pdf.sign', etc.
    document_id TEXT,
    metadata    JSON,
    ip_address  TEXT,
    created_at  INTEGER NOT NULL,  -- Unix timestamp (immutable)
    checksum    TEXT NOT NULL      -- SHA-256(id + tenant_id + operation + created_at)
);

CREATE INDEX idx_audit_log_tenant ON audit_log (tenant_id, created_at DESC);
CREATE INDEX idx_audit_log_operation ON audit_log (tenant_id, operation, created_at DESC);

計量資料(時序資料庫)

指標維度設計:
  nextpdf_operations_total{
    tenant_id="tenant-abc123",
    plan="enterprise",
    operation="pdf.generate",
    data_region="ap-east-1"
  } 1542

  nextpdf_pages_processed_total{
    tenant_id="tenant-abc123",
    operation="pdf.generate"
  } 12345

典型生產部署拓撲

小型 SaaS(< 100 租戶)

graph LR
    CF["Cloudflare Workers\n(API Gateway + ApiProtection)"]
    PHP["PHP 8.5 + Laravel\n(單機 / 小型 K8s)"]
    SPECTRUM["Spectrum Sidecar\n(字型 + 影像加速)"]
    DB["PostgreSQL\n(D1 或 自托管)"]
    R2["Cloudflare R2\n(PDF 儲存)"]

    CF --> PHP --> SPECTRUM
    PHP --> DB
    PHP --> R2

中型 SaaS(100-1,000 租戶)

graph LR
    CF["Cloudflare Workers\n(全球邊緣)"]
    PHP["PHP + Octane\n(K8s Deployment, 3-10 nodes)"]
    PRISMA["Prisma Pro\n(HighControl, K8s)"]
    DB["PostgreSQL HA\n(PgBouncer 連線池)"]
    R2["Cloudflare R2"]
    PROM["Prometheus + Grafana"]

    CF --> PHP --> PRISMA
    PHP --> DB
    PHP --> R2
    PRISMA --> PROM

大型企業 SaaS(1,000+ 租戶)

graph LR
    CF["Cloudflare Workers\n(多區域)"]
    PHP["PHP + Octane\n(K8s, HPA)"]
    PRISMA_ENT["Prisma Enterprise\n(叢集模式, 3+ 節點)"]
    DB["PostgreSQL\n(Citus 分片 / Aurora)"]
    R2["Cloudflare R2\n(多桶, 按資料主權分區)"]
    HSM["HSM 叢集\n(Thales / AWS CloudHSM)"]
    VDB["向量資料庫\n(Qdrant 叢集)"]

    CF --> PHP --> PRISMA_ENT
    PRISMA_ENT --> HSM
    PRISMA_ENT --> VDB
    PHP --> DB
    PHP --> R2

租戶資料生命週期

stateDiagram-v2
    [*] --> Active: 租戶訂閱開始
    Active --> Suspended: 欠費 / 違規
    Suspended --> Active: 恢復訂閱
    Suspended --> PendingDeletion: 取消訂閱
    PendingDeletion --> DataRetention: 進入保留期(30 天)
    DataRetention --> PurgeComplete: 資料清除完成(GDPR §17)
    PurgeComplete --> [*]

租戶刪除流程

// GDPR 資料刪除端點(DELETE /v1/tenants/{id}/data)
final class TenantDataPurgeService
{
    public function purge(string $tenantId, string $reason): PurgeResult
    {
        // 1. 軟刪除所有 PDF 文件(R2 標記,未立即刪除)
        // 2. 匿名化審計日誌(保留操作記錄,刪除個人識別資訊)
        // 3. 清除向量資料庫 namespace
        // 4. 記錄 GDPR 清除審計事件(不可刪除)
        // 5. 排程 R2 硬刪除(T+30 天)
    }
}

參見

Commercial License

This feature requires a commercial license. Contact our team for pricing and deployment support.

Contact Sales