Documentation source
Documentation source
Documentation source
Tenant settings system for platform defaults, tenant overrides, workspace overrides, user preferences, and cached setting resolution.
# Settings
## Overview
The module lives in `features/settings/`. It defines the typed setting keys,
generic tenant/user setting writers, cached layered readers, and small
feature-specific readers such as community and onboarding status.
Settings are stored in the `tenant_settings` table. A setting row is scoped by
`tenant_id`, `workspace_id`, `user_id`, and `key`; the JSON `value` holds the
setting payload. Most consumers read through a layered resolver instead of
querying the table directly.
Settings feed core platform behavior including
[Theme Builder](/docs/features/theme-builder), agent business context,
workflow default agents, dashboard onboarding preferences, integration
visibility, community enablement, and shared knowledge graph access.
## Key Concepts
**SettingKey** - `features/settings/types.ts` defines the setting keys that the
generic settings actions accept:
```ts
export type SettingKey =
| "theme"
| "navigation"
| "ai_limits"
| "session_reaper"
| "ai_model_overrides"
| "branding"
| "agent_context"
| "workflow_agent_defaults"
| "chat_default_agent"
| "dashboard"
| "transformation"
| "allowed_redirect_hosts"
| "community"
| "shared_knowledge_graph"
| "integration_visibility"
| "integrations_self_serve"
| "feature_flags";
```
**TenantSetting** - the typed row shape in `features/settings/types.ts`:
```ts
export interface TenantSetting {
id: string;
tenantId: string;
userId: string | null;
key: SettingKey;
value: Record<string, unknown>;
createdAt: string;
updatedAt: string;
}
```
**Layered settings** - `getSettingsForKey()` returns visible setting layers
ordered from least specific to most specific:
1. Platform default: `DEFAULT_TENANT_ID`, `workspace_id IS NULL`, `user_id IS NULL`.
2. Tenant default: active tenant, `workspace_id IS NULL`, `user_id IS NULL`.
3. Workspace default: active tenant, active workspace, `user_id IS NULL`.
4. User-on-tenant override: active tenant, `workspace_id IS NULL`, current user.
5. User-on-workspace override: active tenant, active workspace, current user.
**Object merge resolver** - `resolveSettings()` in
`features/settings/lib/resolver.ts` merges ordered layers. It shallow-merges
top-level values and merges nested object values one level deep:
```ts
export function resolveSettings(
layers: Array<{ value: Record<string, unknown> }>,
): Record<string, unknown>;
```
**Single-value resolver** - `getResolvedSetting()` lives in
`features/context/server/get-resolved-setting.ts`. It returns the single most
specific `tenant_settings.value` for a `(tenantId, key, workspaceId, userId)`
tuple instead of returning all layers for object merging.
## Architecture And Data Flow
### Writes
`features/settings/server/actions.ts` exposes the authenticated Server Action
surface for generic tenant and user settings:
```ts
export async function saveTenantSetting(
key: SettingKey,
value: Record<string, unknown>,
);
export async function saveUserSetting(
key: SettingKey,
value: Record<string, unknown>,
);
export async function deleteTenantSetting(key: SettingKey);
export async function deleteUserSetting(key: SettingKey);
```
The privileged explicit-tenant writer is deliberately kept out of that
`"use server"` surface. It lives in the server-only
`features/settings/server/setting-writes.ts` module:
```ts
export async function saveTenantSettingForTenant(
tenantId: string,
key: SettingKey,
value: unknown,
opts?: { userId?: string | null },
): Promise<void>;
```
This internal function uses the admin client and performs no authorization of
its own. Only server-to-server callers that have already resolved and
authorized the tenant may call it; it must not be re-exported from a Server
Action module.
The shared internal writer looks up the existing row first and then inserts or
updates because the partial unique indexes on `tenant_settings` cannot be used
through a simple PostgREST upsert. These generic writers intentionally target
tenant-default and user-on-tenant rows with `workspace_id IS NULL`. Workspace
scoped writes use dedicated handlers such as
`features/agents/server/agent-context-actions.ts` and admin override handlers.
Every generic write calls `invalidateSettingsCache(tenantId, key)` after the
database mutation.
### Cached Reads
`features/settings/server/cached-queries.ts` implements the layered reader:
```ts
export async function getSettingsForKey(
tenantId: string,
userId: string | null,
key: SettingKey,
workspaceId?: string | null,
);
```
Tenant-default rows and workspace-default rows are cached with
`unstable_cache()` and `tenant-settings-*` cache tags. User override rows are
not cached cross-request; they are fetched per request.
When `workspaceId` is omitted, `getSettingsForKey()` calls
`getActiveWorkspaceStrict()` to detect a workspace from the request URL. Passing
`workspaceId: null` opts out of workspace overrides.
Background and tool-context callers are handled explicitly. When
`getToolContextTenantId()` is present, `getSettingsForKey()` skips
`requireAuth()` because background workers and MCP/API-key paths do not have a
browser user. It then checks that the injected tenant id matches the supplied
`tenantId` before using the admin client.
### Cache Invalidation
```ts
export async function invalidateSettingsCache(
tenantId: string,
key: SettingKey,
workspaceId?: string | null,
);
```
When `workspaceId` is present, only that workspace setting tag is invalidated.
When `workspaceId` is absent or null, the tenant-key tag is invalidated; the
workspace caches include the tenant tag, so inherited workspace reads also
refresh after tenant-default changes.
### Single-Value Resolution
Some consumers need exactly one resolved JSON value rather than merged layers.
`features/context/server/get-resolved-setting.ts` provides:
```ts
export async function getResolvedSetting<T = unknown>(
tenantId: string,
key: string,
opts?: { workspaceId?: string | null; userId?: string | null },
): Promise<T | null>;
```
It validates UUID inputs, probes for the four `tenant_settings` partial unique
indexes, applies the shared scope filter from `features/admin/lib/scope-resolver`,
and selects the most specific matching row with `pickResolvedRow()`.
## Consumers
**Theme** - `features/theme/server/cached-queries.ts` calls
`getSettingsForKey(resolvedTenantId, null, "theme", ...)`, merges layers with
`resolveSettings()`, sanitizes the result with `sanitizeThemeConfig()`, and
returns the intent shape used by theme rendering. `features/theme/server/actions.ts`
validates `ThemeConfigSchema` and saves with `saveTenantSetting("theme", ...)`.
**Agent context** - `features/agents/agent-context.ts` reads
`tenant_settings` rows with `key = "agent_context"` across platform, tenant,
workspace, and user tiers. It deliberately returns separate
`AgentContextLayers` rather than merging them, so prompt rendering can include
tenant and workspace context together. `loadAgentPromptContext()` in
`features/agents/lib/build-context.ts` calls `loadAgentContextLayers()` and
injects the rendered context into agent prompts.
**Workflow default agents** - `features/agents/workflow-defaults.ts` reads
`workflow_agent_defaults` with `getSettingsForKey()`, validates the merged
object with `WorkflowAgentDefaultsSchema`, and falls back to an enabled agent
with workflow permissions when configured agents are unavailable.
**Dashboard onboarding** - `features/settings/server/onboarding.ts` reads the
user's `dashboard` layers and uses `DashboardPreferences` to decide whether the
onboarding wizard is still needed.
**Community** - `features/settings/server/community.ts` reads the tenant-level
`community` setting with both `userId` and `workspaceId` pinned to null. The
default is disabled unless a tenant enables it.
**Admin settings tools** - `features/tools/admin/settings-tools.ts` exposes
`updateTenantSettings` for `theme`, `agent_context`,
`workflow_agent_defaults`, `integration_visibility`, and
`integrations_self_serve`; it validates known schemas before delegating to
`saveTenantSetting()`.
## Key APIs
```ts
export async function listWorkspaceSettingOverrides(
workspaceId: string,
): Promise<WorkspaceSettingOverride[]>;
```
Lists workspace-default `tenant_settings` override rows after verifying the
workspace belongs to the authenticated admin's tenant.
```ts
export async function isCommunityEnabled(): Promise<boolean>;
```
Reads the tenant-level `community` setting and returns true only when
`enabled === true`.
```ts
export async function getOnboardingStatus(): Promise<{
needsOnboarding: boolean;
preferences: DashboardPreferences;
}>;
```
Reads the current user's `dashboard` preferences and reports whether the
onboarding wizard should appear.
## Agent Instructions
When adding a setting key:
1. Add the key to `SettingKey` in `features/settings/types.ts` if it will use
the generic settings actions.
2. Add a value schema or interface next to the consuming feature or in
`features/settings/types.ts` when the shape is shared.
3. Read layered object settings through `getSettingsForKey()` plus
`resolveSettings()`.
4. Read single-value settings through `getResolvedSetting()` when only the most
specific row should win.
5. Validate before writing. Examples: `saveTheme()` uses `ThemeConfigSchema`;
`updateTenantSettings` validates agent context, workflow defaults,
integration visibility, and self-serve integration settings.
6. Invalidate the relevant cache tag after writes with
`invalidateSettingsCache(tenantId, key, workspaceId?)`.
Do not query `tenant_settings` with the admin client unless the caller has
already resolved and checked the tenant. If using a caller-supplied
`workspaceId`, verify that workspace belongs to the authenticated tenant before
reading or writing scoped rows.