Multi-Tenant SaaS
How a SaaS platform serves three customers with different data protection policies using a single OGuardAI deployment
How a SaaS platform serves three customers with different data protection policies using a single OGuardAI deployment.
The Situation
A B2B SaaS platform provides AI-powered document processing to enterprise customers. Each customer operates in a different regulatory environment:
- Tenant A (EU retailer): GDPR strict mode. Block SSNs, mask emails, tokenize names.
- Tenant B (US healthcare provider): HIPAA compliance. Block PHI identifiers, tokenize patient names, preserve clinical data.
- Tenant C (Swiss bank): Financial regulations. Block IBANs from AI, full restore for internal compliance teams, masked restore for audit.
The platform needs a single OGuardAI deployment that enforces tenant-specific policies, prevents cross-tenant data access, and scales with the platform's growth.
The Solution
OGuardAI runs as a shared service in the platform's Kubernetes cluster. Each tenant has a dedicated API key, a dedicated policy configuration, and isolated session state. The sealed session encryption ensures that session blobs from one tenant cannot be used to restore another tenant's data.
Tenant Configurations
Tenant A: EU Retailer (GDPR Strict)
name: tenant-a-gdpr
version: "1.0"
tenant_id: "tenant_a"
rules:
- entity_type: "person"
action: "tokenize"
metadata: [gender, formality, language]
restore_mode: "formatted"
- entity_type: "email"
action: "tokenize"
restore_mode: "masked"
- entity_type: "phone"
action: "tokenize"
restore_mode: "masked"
- entity_type: "address"
action: "tokenize"
restore_mode: "none"
- entity_type: "ssn"
action: "block"
- entity_type: "iban"
action: "tokenize"
restore_mode: "masked"
channel_rules:
customer_facing:
person: { restore_mode: formatted }
email: { restore_mode: masked }
phone: { restore_mode: none }
internal_agent:
person: { restore_mode: full }
email: { restore_mode: full }
phone: { restore_mode: full }Tenant B: US Healthcare (HIPAA)
name: tenant-b-hipaa
version: "1.0"
tenant_id: "tenant_b"
rules:
- entity_type: "person"
action: "tokenize"
restore_mode: "full"
- entity_type: "date_of_birth"
action: "tokenize"
metadata: [age_range]
restore_mode: "masked"
- entity_type: "ssn"
action: "block"
- entity_type: "insurance_id"
action: "tokenize"
restore_mode: "masked"
- entity_type: "phone"
action: "tokenize"
restore_mode: "masked"
- entity_type: "medical_record_number"
action: "block"
channel_rules:
physician:
person: { restore_mode: full }
date_of_birth: { restore_mode: full }
insurance_id: { restore_mode: full }
phone: { restore_mode: full }
patient_portal:
person: { restore_mode: full }
date_of_birth: { restore_mode: masked }
insurance_id: { restore_mode: masked }
phone: { restore_mode: masked }Tenant C: Swiss Bank (Financial)
name: tenant-c-financial
version: "1.0"
tenant_id: "tenant_c"
rules:
- entity_type: "person"
action: "tokenize"
metadata: [country, role]
restore_mode: "full"
- entity_type: "iban"
action: "tokenize"
metadata: [country]
restore_mode: "masked"
- entity_type: "bic"
action: "tokenize"
restore_mode: "masked"
- entity_type: "tax_id"
action: "tokenize"
restore_mode: "none"
- entity_type: "passport"
action: "block"
channel_rules:
compliance_internal:
person: { restore_mode: full }
iban: { restore_mode: full }
bic: { restore_mode: full }
tax_id: { restore_mode: full }
audit_log:
person: { restore_mode: abstract }
iban: { restore_mode: masked }
bic: { restore_mode: masked }
tax_id: { restore_mode: none }Some entity types above (
insurance_id,medical_record_number,bic,tax_id) are custom types defined via policy rules, not built-in. See the Extending Entities guide for how to add custom types.
API Key Scoping
Each tenant's API key is bound to their policy and tenant ID:
# Tenant A request
curl -X POST http://guardai.internal:3000/v1/transform \
-H "Authorization: Bearer gai_tenantA_sk_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"input": "Kunden-Email von Julia Braun (julia.braun@example.de)...",
"policy": "tenant-a-gdpr"
}'
# Tenant B request
curl -X POST http://guardai.internal:3000/v1/transform \
-H "Authorization: Bearer gai_tenantB_sk_live_def456..." \
-H "Content-Type: application/json" \
-d '{
"input": "Patient: Robert Kim, DOB: 11/15/1972, SSN: 543-21-9876...",
"policy": "tenant-b-hipaa"
}'
# Tenant C request
curl -X POST http://guardai.internal:3000/v1/transform \
-H "Authorization: Bearer gai_tenantC_sk_live_ghi789..." \
-H "Content-Type: application/json" \
-d '{
"input": "Transfer from Marcel Dubois, IBAN CH93 0076 2011 6238 5295 7...",
"policy": "tenant-c-financial"
}'The server validates that the API key's tenant ID matches the requested policy. A Tenant A key cannot load Tenant B's policy.
Cross-Tenant Isolation via Sealed Sessions
Each tenant's session state is encrypted with a tenant-scoped key. The sealed blob includes the tenant ID in its authenticated data:
Session blob structure:
AES-256-GCM encrypted payload
- token_map: {token_id -> raw_value}
- tenant_id: "tenant_a"
- created_at: timestamp
- expires_at: timestamp
AAD: tenant_id + policy_nameIf Tenant B's service attempts to rehydrate using a session blob from Tenant A, the decryption fails because the authenticated additional data (AAD) includes the tenant ID. This is enforced at the cryptographic level -- not just an application check.
# This fails: Tenant B key with Tenant A session blob
curl -X POST http://guardai.internal:3000/v1/rehydrate \
-H "Authorization: Bearer gai_tenantB_sk_live_def456..." \
-d '{
"output": "Reply to `{{person:p_001}}`...",
"session_state": "<tenant-a-encrypted-blob>"
}'
# Response: 403
# {"error": "GUARDAI_SESSION_TENANT_MISMATCH",
# "message": "Session state does not belong to this tenant"}Scaling the Deployment
The platform runs OGuardAI as a Kubernetes Deployment with horizontal pod autoscaling:
apiVersion: apps/v1
kind: Deployment
metadata:
name: guardai
spec:
replicas: 3
template:
spec:
containers:
- name: guardai
image: ghcr.io/oronts/oronts-guardai/oguardai-server:latest
resources:
requests:
cpu: 500m
memory: 256Mi
limits:
cpu: 2000m
memory: 1Gi
env:
- name: GUARDAI_POLICIES_DIR
value: /etc/guardai/policies
- name: GUARDAI_SESSION_BACKEND
value: sealed
volumeMounts:
- name: policies
mountPath: /etc/guardai/policies
volumes:
- name: policies
configMap:
name: guardai-tenant-policiesAll three tenants share the same pods. Policy resolution happens per-request based on the API key's tenant binding. Session state is stateless (sealed blobs travel with the request), so any pod can handle any tenant's request.
What OGuardAI Made Possible
Single deployment, three regulatory regimes. GDPR, HIPAA, and Swiss financial regulations are enforced by policy configuration, not code changes. Adding a fourth tenant requires only a new policy YAML and API key.
Cryptographic tenant isolation. Sealed session encryption with tenant-scoped AAD prevents cross-tenant data access at the cryptographic level. A compromised tenant service cannot restore another tenant's PII.
Per-tenant, per-channel restore control. Each tenant defines which entity types are tokenized, blocked, or passed through, and each output channel within a tenant gets its own restore mode. The Swiss bank's compliance team sees full IBANs while their audit log sees masked versions.
Horizontal scaling. Stateless sealed sessions mean no shared state between pods. The platform scales OGuardAI horizontally with standard Kubernetes autoscaling, handling all tenants from the same deployment.