SaaS 架構設計¶
本頁說明 NextPDF SaaS Foundation 的多租戶架構設計,包含三種隔離模式的選擇依據、資料分區策略與典型的生產部署拓撲。
多租戶隔離模式¶
NextPDF SaaS Foundation 支援三種隔離級別,依您的業務需求選擇:
模式一:共享池(Shared Pool)¶
最低成本,適合標準 SaaS:
適合場景:中小型 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 天)
}
}
參見¶
- API Gateway — Cloudflare Workers ApiProtection 詳細設定
- 計費指標 — MeteringReporter 實作
- 合規套件 — GDPR 清除完整流程
- 平台慣例 — API 認證與 RBAC 設計
Commercial License
This feature requires a commercial license. Contact our team for pricing and deployment support.
Contact Sales