Skip to main content

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

TermDescription
Dashboard Firebase ProjectFirebase project for platform admins (gybc-staging) — flat user pool, no tenant feature
Custom Claimstenant_id and role set on dashboard users via Firebase Admin SDK
Tenant IDUnique 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:

  1. Auth service verifies the publishable key via Unkey
  2. Retrieves the OIDC config stored in the key's metadata
  3. Validates the user JWT against that OIDC config (issuer, audience, signature)
  4. Extracts tenant_id from the key, user_id from the JWT
  5. 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"
}
}
}'
Supported OIDC Providers

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.

caution

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"
}'
warning

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:

LayerMechanism
IdentityJWT custom claims (tenant_id, role) — dashboard users isolated via claims set by Firebase Admin SDK
API KeysUnkey externalId scopes keys to a tenant
JWT ValidationPublishable 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 GatewayKrakenD rejects JWT-authenticated requests without a tenant with 403 (except /api/v1/orgs/create)
Request ContextX-Tenant-ID header propagated to all downstream services
Actor KeysAll Restate actor keys are prefixed with tenant ID (tenantID:resourceID) for state isolation
Vector DBPinecone namespaces auto-derived from tenant ID — each tenant's vectors are isolated
MemoryMem0 app_id validated against authenticated tenant — cross-tenant memory access is rejected (403)
MCP ToolsAll tool calls require tenant context — rejected with 400 if missing
CacheAuth verification cache keys include tenant ID to prevent cross-tenant collisions
AuthorizationOpenFGA 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

ErrorCauseFix
missing_credentialsNo API key or JWT in requestAdd X-API-Key or Authorization: Bearer header
tenant membership required (403)JWT user has no tenantCreate an organization first via /api/v1/orgs/create
tenant context is required (400)MCP tool call missing tenantEnsure request includes RequestContext with tenant
app_id does not match authenticated tenant (403)Memory app_id doesn't match tenantOmit app_id (defaults to tenant) or ensure it matches
memory not owned by tenant (403)Attempting to modify another tenant's memoryOnly access memories belonging to your tenant
jwt_invalid_issuerJWT issuer doesn't match FIREBASE_PROJECT_IDEnsure the auth service and Firebase project match
publishable_key_requires_jwtPublishable key used without a user JWTAdd Authorization: Bearer <jwt> header
user_id_claim_not_founduserIdClaim in OIDC config doesn't match a JWT claimOmit userIdClaim (defaults to sub) or set it to a valid claim name like sub or email
misconfigured_publishable_keyPublishable key missing OIDC metadataRecreate the key with publishableConfig.oidc
oidc_not_configuredAuth service OIDC validator not initializedCheck auth service logs for startup errors

Roadmap

  • Org onboarding backend/api/v1/orgs/create endpoint, Firebase Admin SDK integration for SetCustomUserClaims
  • Scope enforcement — OpenFGA integration for fine-grained authorization
  • iOS app integration — update gym-hero-ios to use publishable key + JWT flow