Runtime Fallback Adapter
The Runtime Fallback Adapter provides runtime fallback functionality from a control plane tenant to child tenants. This allows you to set up default configurations that child tenants can inherit while still allowing tenant-specific customizations.
Complementary Approaches
This adapter provides runtime value fallback (sensitive data stays in control plane), while Entity Sync (see Entity Synchronization) copies entities to child tenants (needed for foreign key references).
Overview
In multi-tenant applications, you often want to provide common defaults across all tenants while allowing individual tenants to override specific settings. The Runtime Fallback Adapter solves this by:
- Providing Fallbacks at Query Time: Values are merged when data is retrieved
- Protecting Sensitive Data: Connection secrets, API keys remain in control plane only
- Allowing Overrides: Tenant-specific values always take precedence
- Transparent Integration: Works seamlessly with existing data adapters
Runtime Fallback vs Entity Sync
| Feature | Runtime Fallback | Entity Sync |
|---|---|---|
| When applied | At query time | At creation/update time |
| Sensitive data | Stays in control plane | Stripped before copying |
| Use case | Connection secrets, SMTP keys, default URLs | Resource servers, roles (foreign keys) |
| Storage | Single copy in control plane | Copied to each tenant |
| Updates | Immediate (next query) | Requires sync operation |
Features
Client Fallbacks
- Merges
web_origins,allowed_logout_urls, andcallbacksarrays - Inherits tenant properties from control plane client
- Preserves tenant-specific overrides
Connection Fallbacks
- Merges connection options from control plane connections
- Matches connections by strategy (e.g., "google", "email", "facebook")
- Supports deep merging of connection options objects
- Enables shared social connections without exposing client secrets to individual tenants
Strategy-Based Matching
Connections are matched by strategy rather than by name. This means a tenant can create a connection with strategy "google" and leave the OAuth keys blank - the system will automatically inherit credentials from the control plane's Google connection.
Shared Social Authentication
One of the most powerful features is the ability to provide social authentication (Google, Facebook, Apple, etc.) to all tenants without sharing sensitive client secrets:
- Centralized OAuth Apps: Configure OAuth applications once in the control plane
- Secret Protection: Client secrets remain secure in the control plane only
- Automatic Availability: Social connections become available to all tenants automatically
- Tenant Customization: Individual tenants can still override settings like scopes or branding
Usage
Basic Setup
import { withRuntimeFallback } from "@authhero/multi-tenancy";
import createAdapters from "@authhero/kysely-adapter";
// Create your base data adapters
const db = // ... your database connection
const baseAdapters = createAdapters(db);
// Wrap with runtime fallback functionality
const adapters = withRuntimeFallback(baseAdapters, {
controlPlaneTenantId: "main", // Control plane tenant for defaults
controlPlaneClientId: "main-client" // Control plane client for defaults
});
// Use with AuthHero init
import { init } from "authhero";
const { app } = init({
dataAdapter: adapters
});Environment-Based Configuration
You can configure the runtime fallback adapter using environment variables:
import { withRuntimeFallback } from "@authhero/multi-tenancy";
const adapters = withRuntimeFallback(baseAdapters, {
controlPlaneTenantId: process.env.CONTROL_PLANE_TENANT_ID,
controlPlaneClientId: process.env.CONTROL_PLANE_CLIENT_ID,
});Combined with Multi-Tenancy Features
import { init, withRuntimeFallback } from "@authhero/multi-tenancy";
// First, wrap adapters with runtime fallback
const baseAdapters = createAdapters(db);
const fallbackAdapters = withRuntimeFallback(baseAdapters, {
controlPlaneTenantId: "control_plane",
controlPlaneClientId: "control_plane_client",
});
// Then use with multi-tenancy init for entity sync
const { app } = init({
dataAdapter: fallbackAdapters,
controlPlaneTenantId: "control_plane",
sync: {
resourceServers: true,
roles: true,
},
});How It Works
Client Retrieval Flow
When a client is retrieved:
- Fetches the client from the database
- Fetches control plane client (if
controlPlaneClientIdis configured) - Merges arrays (
web_origins,allowed_logout_urls,callbacks) - Merges tenant properties with control plane fallbacks
- Fetches and merges connections for the client
Connection Fallback Flow
When connections are retrieved:
- Fetches tenant's connections from the database
- Fetches control plane connections (if
controlPlaneTenantIdis configured) - Matches connections by strategy (e.g., "google", "email")
- Merges connection options - tenant values override control plane values
- Returns merged connections
Fallback Priority
Settings are merged with tenant-specific values always taking precedence:
// Control plane connection
{
name: "email",
strategy: "email", // Connections matched by strategy
options: {
from: "noreply@example.com",
smtp_host: "smtp.example.com",
smtp_user: "noreply@example.com",
smtp_password: "secret123" // Sensitive!
}
}
// Tenant's connection (override from address only)
{
name: "my-email", // Name can be different
strategy: "email", // Same strategy = will inherit
options: {
from: "hello@tenant.com"
// smtp_password is inherited from control plane
}
}
// Merged result (returned to tenant)
{
name: "my-email", // Tenant's name preserved
strategy: "email",
options: {
from: "hello@tenant.com", // Tenant override
smtp_host: "smtp.example.com", // Fallback
smtp_user: "noreply@example.com", // Fallback
smtp_password: "secret123" // Fallback (stays secure)
}
}Common Patterns
Shared Social Connections
Set up social authentication once in the control plane:
// In control plane tenant
POST /api/v2/connections
{
"name": "google-oauth2",
"strategy": "google-oauth2",
"options": {
"client_id": "your-google-client-id",
"client_secret": "your-google-client-secret",
"scope": ["openid", "email", "profile"]
}
}
// Child tenants just need to define the strategy - keys are inherited!
POST /api/v2/connections // In child tenant
{
"name": "google", // Name can be anything
"strategy": "google-oauth2", // Same strategy = inherits credentials
"display_name": "Sign in with Google for Acme Corp",
"options": {
"scope": ["openid", "email", "profile", "calendar"] // Optional overrides
// client_id and client_secret automatically inherited from control plane!
}
}Minimal Configuration
Tenants can simply create a connection with the desired strategy and leave sensitive fields blank. The runtime fallback will automatically provide the OAuth credentials from the control plane.
Shared Email Provider
Set up email provider once in control plane:
// In control plane
POST /api/v2/email-providers
{
"name": "smtp",
"credentials": {
"smtp_host": "smtp.sendgrid.net",
"smtp_port": 587,
"smtp_user": "apikey",
"smtp_password": "SG.xxx..."
}
}
// Child tenants inherit SMTP settings
// They can override from address per tenant
PATCH /api/v2/tenants/{id}
{
"sender_email": "noreply@tenant.com",
"sender_name": "Acme Corp"
}Default Client URLs
Provide default callback URLs from control plane:
// Control plane client
{
"client_id": "control_plane_client",
"callbacks": [
"http://localhost:3000/callback",
"https://dev.example.com/callback"
]
}
// Tenant client inherits and extends
{
"client_id": "tenant_client",
"callbacks": [
"https://tenant.com/callback"
]
}
// Merged result includes both
{
"client_id": "tenant_client",
"callbacks": [
"http://localhost:3000/callback", // From control plane
"https://dev.example.com/callback", // From control plane
"https://tenant.com/callback" // From tenant
]
}API Reference
createRuntimeFallbackAdapter(baseAdapters, config)
Creates a wrapped adapter with runtime fallback functionality.
Parameters:
baseAdapters: DataAdapters- The base data adapters to wrapconfig: RuntimeFallbackConfig- Configuration object
Returns: DataAdapters - Wrapped adapters with fallback functionality
withRuntimeFallback(baseAdapters, config)
Convenience helper for createRuntimeFallbackAdapter.
Parameters:
baseAdapters: DataAdapters- The base data adapters to wrapconfig: RuntimeFallbackConfig- Configuration object
Returns: DataAdapters - Wrapped adapters with fallback functionality
RuntimeFallbackConfig
Configuration interface for runtime fallback:
interface RuntimeFallbackConfig {
controlPlaneTenantId?: string; // Control plane tenant ID for connection fallbacks
controlPlaneClientId?: string; // Control plane client ID for client fallbacks
}Migration from @authhero/authhero
If you were using the deprecated withMainTenantFallback from @authhero/authhero:
Before (Deprecated)
import { withMainTenantFallback } from "@authhero/authhero";
const adapters = withMainTenantFallback(baseAdapters, {
mainTenantId: "main",
mainClientId: "main-client",
});After (Current)
import { withRuntimeFallback } from "@authhero/multi-tenancy";
const adapters = withRuntimeFallback(baseAdapters, {
controlPlaneTenantId: "main", // renamed from mainTenantId
controlPlaneClientId: "main-client", // renamed from mainClientId
});The functionality is identical, only the naming has changed to better reflect the control plane/child tenant model.
Best Practices
- Use for Sensitive Data: Perfect for connection secrets, SMTP credentials, OAuth keys
- Combine with Entity Sync: Use sync for resource servers/roles, fallback for secrets
- Document Overrides: Clearly document which settings can be overridden by tenants
- Test Fallbacks: Verify fallback behavior works as expected in development
- Monitor Usage: Track which tenants use fallback vs custom configs
Troubleshooting
Fallbacks Not Working
Check that:
controlPlaneTenantIdandcontrolPlaneClientIdare set correctly- Control plane tenant/client exists in database
- Connection strategies match between control plane and child tenant (e.g., both use "google-oauth2")
- Adapter is properly wrapped before use
Wrong Values Being Used
Remember:
- Tenant values always override control plane values
- Arrays are merged (both control plane and tenant values included)
- Null/empty tenant values don't trigger fallback - only missing properties do
See Also
- Entity Synchronization - Sync resource servers and roles to child tenants
- Control Plane - Understanding the control plane model