Multi-Tenancy
The GYBC platform uses JWT custom claims and API keys to isolate tenants. Dashboard users get tenant_id and role custom claims, while tenant end-users authenticate via publishable keys with per-key OIDC config.
Concepts
| Term | Description |
|---|---|
| Dashboard Firebase Project | Firebase project for platform admins (gybc-staging) — flat user pool, no tenant feature |
| Custom Claims | tenant_id and role set on dashboard users via Firebase Admin SDK |
| Tenant ID | Unique identifier (e.g., socayo-a1b2c3) — stored in JWT custom claims and Unkey externalId |
Secret Key (sk_) | Backend-to-backend API key, scoped to a tenant |
Publishable Key (pk_) | Frontend-safe API key, requires a user JWT alongside it |
Architecture
Authentication Methods
1. Publishable Key + User JWT (Client Apps)
For end-user facing applications (iOS, web). Requires both a publishable key and a user JWT.
curl -X POST https://api.yocaso.dev/api/v1/llm/gateway/list-threads \
-H "X-API-Key: pk_your_key_here" \
-H "Authorization: Bearer <firebase-user-jwt>" \
-H "Content-Type: application/json" \
-d '{}'
How it works:
- Auth service verifies the publishable key via Unkey
- Retrieves the OIDC config stored in the key's metadata
- Validates the user JWT against that OIDC config (issuer, audience, signature)
- Extracts
tenant_idfrom the key,user_idfrom the JWT - Both must be valid — the key alone or JWT alone is not enough
2. Secret Key (Backend-to-Backend)
For server-to-server calls from the tenant's own backend. No user JWT needed.
curl -X POST https://api.yocaso.dev/api/v1/llm/gateway/list-threads \
-H "X-API-Key: sk_your_key_here" \
-H "Content-Type: application/json" \
-d '{}'
3. JWT Only (Dashboard)
For tenant admins using the GYBC dashboard directly.
curl -X POST https://api.yocaso.dev/api/v1/llm/gateway/list-threads \
-H "Authorization: Bearer <firebase-jwt>" \
-H "Content-Type: application/json" \
-d '{}'
Tenant Setup Guide
Step 1: Dashboard User Setup
Dashboard users authenticate via the gybc-staging Firebase project (flat user pool, no tenants). After creating an organization, the backend sets custom claims via Firebase Admin SDK:
{
"tenant_id": "socayo-a1b2c3",
"role": "admin"
}
New users start without tenant_id — they get it after creating an organization via the dashboard.
Step 2: Verify JWT Contains Custom Claims
After org creation, the dashboard forces a token refresh. The JWT should include:
{
"sub": "user-uid",
"email": "admin@socayo.com",
"tenant_id": "socayo-a1b2c3",
"role": "admin",
"iss": "https://securetoken.google.com/gybc-staging",
"aud": "gybc-staging"
}
Step 4: Create API Keys for the Tenant
Authenticate as an admin (JWT or existing secret key), then create keys for the tenant.
Create a publishable key (for client apps):
The OIDC configuration tells the platform how to validate user JWTs. The platform supports multiple OIDC providers — configure the one your tenant's end-users authenticate with.
Firebase:
curl -X POST https://api.yocaso.dev/api/v1/api-keys/create \
-H "Authorization: Bearer <admin-jwt>" \
-H "Content-Type: application/json" \
-d '{
"name": "socayo-ios",
"description": "Socayo iOS app",
"keyType": "KEY_TYPE_PUBLISHABLE",
"publishableConfig": {
"oidc": {
"issuer": "https://securetoken.google.com/gym-hero-staging",
"jwksUrl": "https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com",
"audience": "gym-hero-staging"
}
}
}'
Auth0:
curl -X POST https://api.yocaso.dev/api/v1/api-keys/create \
-H "Authorization: Bearer <admin-jwt>" \
-H "Content-Type: application/json" \
-d '{
"name": "socayo-web",
"description": "Socayo web app (Auth0)",
"keyType": "KEY_TYPE_PUBLISHABLE",
"publishableConfig": {
"oidc": {
"issuer": "https://your-tenant.auth0.com/",
"jwksUrl": "https://your-tenant.auth0.com/.well-known/jwks.json",
"audience": "https://api.your-tenant.com"
}
}
}'
Okta:
curl -X POST https://api.yocaso.dev/api/v1/api-keys/create \
-H "Authorization: Bearer <admin-jwt>" \
-H "Content-Type: application/json" \
-d '{
"name": "socayo-web",
"description": "Socayo web app (Okta)",
"keyType": "KEY_TYPE_PUBLISHABLE",
"publishableConfig": {
"oidc": {
"issuer": "https://your-org.okta.com/oauth2/default",
"jwksUrl": "https://your-org.okta.com/oauth2/default/v1/keys",
"audience": "api://default"
}
}
}'
The platform validates JWTs from any OIDC-compliant provider. Common providers include Firebase, Auth0, Okta, AWS Cognito, and Keycloak. All issuer and JWKS URLs must use HTTPS.
The userIdClaim field in the OIDC config specifies which JWT claim name to extract the user ID from (e.g., "sub", "email"). It is not the user ID value itself. If omitted, it defaults to "sub", which is correct for most providers.
Create a secret key (for backend-to-backend):
curl -X POST https://api.yocaso.dev/api/v1/api-keys/create \
-H "Authorization: Bearer <admin-jwt>" \
-H "Content-Type: application/json" \
-d '{
"name": "socayo-backend",
"description": "Socayo backend server"
}'
The full key value is only returned once at creation time. Store it securely.
Step 5: Integrate Client Apps
iOS (Swift):
import FirebaseAuth
// Sign in via the tenant's own Firebase project (no tenantId needed — flat user pool)
Auth.auth().signIn(with: credential) { result, error in
result?.user.getIDToken { token, error in
// Use token + publishable key for API calls
}
}
Web (JavaScript):
import { getAuth, signInWithPopup, GoogleAuthProvider } from "firebase/auth";
const auth = getAuth();
const result = await signInWithPopup(auth, new GoogleAuthProvider());
const token = await result.user.getIdToken();
const response = await fetch("https://api.yocaso.dev/api/v1/llm/gateway/send-message", {
method: "POST",
headers: {
"X-API-Key": "pk_your_key_here",
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
conversationKey: "conv_abc",
userMessage: { role: "user", content: "Hello" },
}),
});
Tenant Isolation
Tenant boundaries are enforced at multiple layers:
| Layer | Mechanism |
|---|---|
| Identity | JWT custom claims (tenant_id, role) — dashboard users isolated via claims set by Firebase Admin SDK |
| API Keys | Unkey externalId scopes keys to a tenant |
| JWT Validation | Publishable keys validate user JWTs against tenant-specific OIDC config (supports Firebase, Auth0, Okta, and other OIDC providers); optional required_claims for additional claim enforcement |
| API Gateway | KrakenD rejects JWT-authenticated requests without a tenant with 403 (except /api/v1/orgs/create) |
| Request Context | X-Tenant-ID header propagated to all downstream services |
| Actor Keys | All Restate actor keys are prefixed with tenant ID (tenantID:resourceID) for state isolation |
| Vector DB | Pinecone namespaces auto-derived from tenant ID — each tenant's vectors are isolated |
| Memory | Mem0 app_id validated against authenticated tenant — cross-tenant memory access is rejected (403) |
| MCP Tools | All tool calls require tenant context — rejected with 400 if missing |
| Cache | Auth verification cache keys include tenant ID to prevent cross-tenant collisions |
| Authorization | OpenFGA stores per tenant (planned) |
JWT Tenant Requirement
All Firebase JWTs must include a firebase.tenant claim. JWTs without this claim are rejected at the auth layer.
At the API gateway level, KrakenD enforces that JWT-authenticated users have a tenant. Requests without a tenant receive 403 Forbidden with the message "tenant membership required". The only exception is /api/v1/orgs/create, which allows org-less JWTs so new users can create their first organization.
API key authentication is unaffected — the tenant is derived from the key's Unkey metadata.
Actor Key Isolation
All Restate actor keys include a tenant prefix in the format tenantID:resourceID. This means:
- Conversation keys:
socayo:conv_abc - User keys:
socayo:user_123 - Storage keys:
socayo:user_123
Gateways automatically construct tenant-prefixed keys from the X-Tenant-ID header. Tenants cannot access each other's actor state.
Vector DB & Memory Isolation
Pinecone: Each tenant's vectors are stored in a separate namespace (derived from tenant ID). Cross-tenant vector access is not possible.
Memory (Mem0): The app_id field defaults to the authenticated tenant. If an explicit app_id is provided, it must match the tenant — mismatches are rejected with 403 Forbidden. Single-memory mutations (update, delete) pre-validate tenant ownership before execution.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
missing_credentials | No API key or JWT in request | Add X-API-Key or Authorization: Bearer header |
tenant membership required (403) | JWT user has no tenant | Create an organization first via /api/v1/orgs/create |
tenant context is required (400) | MCP tool call missing tenant | Ensure request includes RequestContext with tenant |
app_id does not match authenticated tenant (403) | Memory app_id doesn't match tenant | Omit app_id (defaults to tenant) or ensure it matches |
memory not owned by tenant (403) | Attempting to modify another tenant's memory | Only access memories belonging to your tenant |
jwt_invalid_issuer | JWT issuer doesn't match FIREBASE_PROJECT_ID | Ensure the auth service and Firebase project match |
publishable_key_requires_jwt | Publishable key used without a user JWT | Add Authorization: Bearer <jwt> header |
user_id_claim_not_found | userIdClaim in OIDC config doesn't match a JWT claim | Omit userIdClaim (defaults to sub) or set it to a valid claim name like sub or email |
misconfigured_publishable_key | Publishable key missing OIDC metadata | Recreate the key with publishableConfig.oidc |
oidc_not_configured | Auth service OIDC validator not initialized | Check auth service logs for startup errors |
Roadmap
- Org onboarding backend —
/api/v1/orgs/createendpoint, Firebase Admin SDK integration forSetCustomUserClaims - Scope enforcement — OpenFGA integration for fine-grained authorization
- iOS app integration — update
gym-hero-iosto use publishable key + JWT flow