Skip to content

Tenant Config

Purpose

Tenant-specific runtime values should be resolved by effective CLIENT_ID, not by server-level constants. Legacy constants and env values remain fallback defaults until each caller is migrated.

Long term, Control Tower owns tenant config drafts, releases, approvals, publish history, and the canonical tenant UUID. That value must equal runtime T_CLIENT.UUID and tenant-owned TENANT_ID. Runtime T_TENANT_CONFIG remains the applied state used by the app. During migration, Control Tower publishes by canonical UUID and may resolve to deployment-local legacy CLIENT_ID during QA apply only as a compatibility projection.

Storage Model

Storage Use
T_CLIENT Tenant identity such as canonical UUID, client display name, address, and contact email. Numeric ID is compatibility during migration.
T_MODULE Module and integration catalog. Gmail/Gemini/QuickBooks/Telegram usage is represented as add-on modules.
T_TENANT_MODULE Tenant-level module/integration enablement.
T_TENANT_CONFIG Current tenant runtime values and integration credentials keyed by CLIENT_ID and CONFIG_KEY; target tenant scope uses canonical UUID-valued TENANT_ID. VALUE_TYPE='secret' means the value must be masked.
Env / encrypted secret store Deployment-wide fallback secrets while each integration is migrated to tenant config.

Control Tower runtime boundary rules are documented in .agents/rules/CONTROL_TOWER.md. Detailed Control Tower planning, publish, inventory, and deployment docs live in the sibling ctc-control-tower/docs/ checkout.

T_TENANT_CONFIG stays as a single table. Do not add a separate field-definition table unless runtime validation or admin form generation actually needs database-owned metadata.

Control Tower owns the editable source configuration for tenant menus, modules, screens, documents, workflow toggles, and integration settings. Runtime apps read the published snapshot from local runtime tables; they must not query Control Tower on every request.

Resolution Order

  1. Request effective tenant from getClientId().
  2. Read tenant override from T_TENANT_CONFIG.
  3. Read identity fallback from T_CLIENT when applicable.
  4. Read deployment env fallback when applicable.
  5. Use legacy Config::* defaults.

This order describes common value fallback only. It is not a module, role, menu, page/API, process-capability, or integration authorization decision. Current callers do not all use every step and must retain their existing order until a caller-specific cutover.

The numbered resolution order is the current compatibility baseline. P4-004 converts each caller and table from numeric CLIENT_ID to canonical UUID-valued TENANT_ID; no config caller may infer completion merely because a Control Tower UUID is present.

Capability And Configuration Concern Model

The target model keeps independently owned questions separate. One positive result never implies another.

Concern Current / target owner Question answered Must not imply
platform core explicit route/page classification Is this baseline platform surface independent of tenant module subscription? role access, tenant target, config presence
module enablement T_MODULE + T_TENANT_MODULE Is a catalog module enabled for this tenant? menu visibility, role permission, process capability, configured credential
named process capability registered capability definition plus module/config prerequisites May this named business or integration behavior run? generic page access or every behavior in the owning module
menu visibility / role access T_MENU, effective role, page access helpers May this user see or enter this tenant menu/page? module enablement or config validity
page/API module access page mapping or explicit API moduleKey plus module decision Is the routed surface available for the effective tenant? authentication, role permission, process capability
config value T_TENANT_CONFIG plus an explicit typed definition and allowed fallback sources What typed non-secret value and provenance apply? enablement or authorization
secret resolution credential owner reference plus allowlisted tenant/env/external-secret source Is a credential available for this tenant execution? module enablement, role permission, permission to call a provider
workflow variation named capability or typed config consumed by a process owner Which declared workflow variant applies? hard-coded tenant identity or a new module

Integration execution therefore requires separate positive evidence for the effective tenant, owning module, named capability when one exists, valid config, and matching credential ownership. Menu visibility alone is never sufficient.

Current Compatibility Baseline

These are observed current behaviors, not the target semantics for new callers.

Current surface Missing row/key Explicit disabled / empty DB or source error Invalid typed value Current fallback / visibility
isModuleEnabled() allow IS_ENABLED=0 denies allow an existing null/false-like value denies after boolean cast missing/error are indistinguishable from enabled to callers; reason is enabled_or_unconfigured
tenant_module_enabled(..., $defaultEnabled=true) caller default, normally allow false denies caller default boolean cast additive helper; only the QuickBooks Time caller currently uses it
page/API module guards inherit module result, so missing allows deny with page unavailable or API/endpoint 403 inherit current fail-open module result inherit module result authentication and role/menu checks happen separately
page config gate in moduleGuardConfigDecision() deny deny underlying config read normally becomes false and is reported as config_disabled; construction-level failure is config_error deny uses tenant_config_get_bool(..., false)
tenant_config_get() caller default stored empty becomes caller default caller default not typed missing, empty, error, and fallback are not distinguishable in the return value
tenant_config_get_many() empty string per requested key empty string empty string per key not typed no caller-supplied default; callers layer their own fallback
tenant_config_get_int() / tenant_config_get_bool() caller default caller default for empty caller default caller default return type is normalized but provenance/validity is lost
tenant_config_get_env() env, then caller default env, then caller default env, then caller default not typed non-empty tenant value wins; helper does not check module or credential owner
menu/role access surface-specific: unmapped pages are generally admin-only role mismatch denies current page helpers contain compatibility fail-open branches; explicit API access helpers can fail closed or surface an error n/a remains outside capability resolution
representative workflow toggles definition-specific false or normalized caller fallback definition-specific fallback definition-specific fallback MK document workflow preserves its existing tenant-2 default; export brokerage defaults off; transload mode normalizes unknown input to manual
integration callers caller-specific caller-specific caller-specific caller-specific Gmail paths commonly use tenant -> env -> legacy default; current QuickBooks Online paths read tenant config directly

Compatibility requirements:

  • WP01-06 does not change isModuleEnabled(), module guards, tenant_config_get*(), menus, pages, API routes, integration callers, or config rows.
  • Existing missing-row and DB-error fail-open behavior remains only a reported COMPATIBILITY policy result. It is not the default for a new strict caller.
  • Current divergence is preserved as evidence until each caller opts into a registered target definition. Do not globally reinterpret existing helper returns.

Target Capability Resolution Contract

CapabilityResolution is an additive immutable-style result for WP01-06 and later caller migration. The resolver consumes explicit inputs and injected observations; the value/result contract itself does not call getClientId(), read globals, open a database, contact Control Tower, load a secret store, or authorize a route.

The inactive foundation is implemented by app/platform/tenant/CapabilityResolution.php and app/platform/tenant/capability_resolver.php. It is intentionally not included by any active helper or caller. scripts/tests/unit/capability_resolution_test.php is the no-I/O decision-table fixture; caller opt-in and source loading remain separate P02 work.

Required Inputs

Input Rule
tenant context One strict TenantExecutionContext; platform-global core classification is a separate explicit path.
concern PLATFORM_CORE, MODULE_ENABLEMENT, PROCESS_CAPABILITY, CONFIG_VALUE, or SECRET.
subject key Registered module key, capability key, or config key; never inferred from an arbitrary page payload.
definition Expected value type, owning module, allowed sources, default policy, secret flag, and compatibility policy.
observations Explicit module row state, tenant-config row/value metadata, env/external reference availability, source error, and optional legacy default. Missing observations remain missing.

MENU_VISIBILITY and PAGE_API_ACCESS are deliberately not resolver concerns. Their owners may consume a module or named-capability result, but the resolver cannot grant authentication or role/menu access.

Version 1 Result Fields

Field Values / rule
schemaVersion literal capability-resolution/v1
concern one supported concern enum
subjectKey normalized non-secret key
moduleKey owning module or null
decision ENABLED, DISABLED, VALUE, or UNAVAILABLE
valueType registered string, bool, int, url, uuid, datetime, json, file_id, or secret
value typed runtime value or null; secret values are runtime-only and never enter safe serialization
hasValue safe boolean indicating a resolved value without exposing it
source PLATFORM_CORE, TENANT_MODULE, TENANT_CONFIG, PUBLISHED_SNAPSHOT, DEPLOYMENT_ENV, EXTERNAL_SECRET_REF, LEGACY_DEFAULT, CALLER_DEFAULT, or NONE
validity VALID, MISSING, INVALID, or ERROR
policyState STRICT or COMPATIBILITY
compatibilityFallback true only when the decision/value exists because a declared compatibility fallback was used
reason stable non-secret reason code explaining the selected or rejected candidate
credentialOwnerRef matching non-secret owner reference for SECRET, otherwise null

Safe serialization includes no secret value, raw config value, env value, exception text containing credentials, or untrusted payload. For a secret it may expose only hasValue, source, validity, module key, reason, and the safe credential owner reference.

Target Decision Table

Concern Missing Explicit disabled / empty Source error Invalid value Allowed fallback
platform core registered key -> ENABLED/VALID; unknown key -> UNAVAILABLE/INVALID n/a n/a UNAVAILABLE/INVALID none
module, STRICT DISABLED/MISSING DISABLED/VALID UNAVAILABLE/ERROR UNAVAILABLE/INVALID none
module, COMPATIBILITY definition permits unconfigured -> ENABLED/MISSING with fallback; otherwise DISABLED/MISSING DISABLED/VALID definition permits current fail-open -> ENABLED/ERROR with fallback; otherwise UNAVAILABLE/ERROR UNAVAILABLE/INVALID only registered current-behavior fallback
named process capability definition default off -> DISABLED/MISSING; an explicit enabled compatibility default -> ENABLED/MISSING with fallback DISABLED/VALID UNAVAILABLE/ERROR UNAVAILABLE/INVALID only an explicit definition default; never inferred from module enablement
non-secret config typed default -> VALUE/MISSING with fallback; no default -> UNAVAILABLE/MISSING empty follows the missing rule unless explicitly valid typed default -> VALUE/ERROR with fallback; otherwise UNAVAILABLE/ERROR typed default -> VALUE/INVALID with fallback; otherwise UNAVAILABLE/INVALID source and fallback flag remain visible
secret UNAVAILABLE/MISSING UNAVAILABLE/MISSING UNAVAILABLE/ERROR UNAVAILABLE/INVALID allowlisted deployment env/external reference may produce VALUE/VALID; label as deployment fallback and require matching credential owner
workflow variation use the registered process-capability or non-secret-config rule use the owning rule use the owning rule use the owning rule must be named and typed; no new CLIENT_ID branch

Stable target reasons include platform_core, explicit_enabled, explicit_disabled, tenant_override, published_snapshot, deployment_fallback, legacy_default, caller_default, unconfigured_compatibility, missing, invalid, source_error, module_required, and credential_owner_mismatch. A caller must branch on decision/validity, not parse human-readable messages.

Value Type And Definition Ownership

  • A registered definition is authoritative for type and secret classification. tenant_config_value_type() remains a compatibility heuristic, not the target registry.
  • A stored VALUE_TYPE mismatch is INVALID unless a caller-specific compatibility definition explicitly records how it is normalized. It is not silently promoted into a trusted type.
  • URL/key names containing words such as TOKEN need an explicit definition. For example, QUICKBOOKS_ONLINE_TOKEN_URL is a URL even though the current heuristic classifies it as secret.
  • JSON is valid only after decoding to the definition's expected shape. A non-empty string is not sufficient proof.
  • Config presence does not activate an owning module. Module binding metadata groups ownership; it does not authorize use.

Published Snapshot Boundary

  • Control Tower owns drafts, approval, release history, and publish orchestration. CTC runtime consumes only applied local state or an injected trusted snapshot; it does not query the Control Tower service/database during a request or job.
  • Current T_TENANT_CONFIG rows have no publish release/source identifier. A resolver must report them as TENANT_CONFIG, not claim PUBLISHED_SNAPSHOT, unless trusted publish metadata is supplied by a later applied-state contract.
  • A deployment env fallback is always DEPLOYMENT_ENV / DEPLOYMENT_FALLBACK, never tenant-owned or Control Tower-published.
  • Publish success is verified at apply time by rereading the local runtime target. Runtime resolution does not turn a missing local row into a request-time Control Tower lookup.

WP01-05 Read-only Baseline Evidence

Verified 2026-07-15 against the local MySQL host kyle-mba:3306, database name prod, with CLIENT_ID=1. The host identity confirms this is the local DB despite the compatibility database name. No config values, credentials, writes, or tenant 2 data were read.

  • T_MODULE has 35 active catalog rows. Tenant 1 has 23 active T_TENANT_MODULE rows, all enabled in this snapshot; 12 catalog modules have no tenant row and therefore currently depend on missing-row compatibility.
  • T_TENANT_CONFIG has 19 active tenant 1 rows: 17 module-bound and 2 unbound. Four rows are empty; values were not selected or recorded.
  • Unique keys exist on T_MODULE.MODULE_KEY, T_TENANT_MODULE(CLIENT_ID, MODULE_ID), and T_TENANT_CONFIG(CLIENT_ID, CONFIG_KEY).
  • OPERATION_EVENT_ROLE_POLICY is stored with VALUE_TYPE=string while the current compatibility inference returns json.
  • QUICKBOOKS_ONLINE_TOKEN_URL is documented as URL but the current name heuristic returns secret; the admin surface therefore masks it. This proves a registered definition must outrank heuristic inference.
  • The snapshot proves current local coverage only. Production-wide and tenant 2 coverage remain unverified.

Credential Owner Reference

TenantExecutionContext stores only non-secret credential ownership metadata. It never stores or resolves a credential value. A target credential reference has this logical shape:

Field Values / rule
scope TENANT, DEPLOYMENT_FALLBACK, or NONE
sourceType TENANT_CONFIG, ENV, or EXTERNAL_SECRET_REF
moduleKey Owning integration module such as addon.email or addon.quickbooks; required unless scope=NONE
referenceKey Non-secret config/env key or opaque external secret identifier; never the credential value
ownerLocalTenantId Required positive CLIENT_ID when scope=TENANT; must equal the execution context tenant
ownerGlobalTenantId Published UUID or null during LEGACY_LOCAL_ONLY; when present it must match the execution context UUID

Rules:

  • An execution that uses no integration credential carries scope=NONE or no credential reference.
  • Tenant-owned credentials must match both available tenant identities before resolution. A mismatch fails before the config/env/secret value is loaded.
  • A deployment env fallback is labeled DEPLOYMENT_FALLBACK; it must not be reported as tenant-owned. Existing fallbacks remain compatible until their caller-specific cutover.
  • An all-tenant job control context cannot bind one credential for every tenant. It creates a single-tenant child context and resolves that child's credential reference before each provider call.
  • Safe logs may include scope, source type, module key, reference key, and owner IDs. They must not include config values, tokens, passwords, OAuth JSON, authorization headers, or secret file contents.
  • Capability/module enablement and credential ownership are separate checks. A valid credential reference does not enable a disabled module or grant a role permission.

Brand Fields

Config key Value type Fallback
brand.display_name string T_CLIENT.CLIENT_NAME, Config::COMPANY_NAME
brand.logo_url url Config::COMPANY_LOGO_URL
brand.favicon_url url empty
brand.primary_color string #0f172a
brand.support_email string T_CLIENT.EMAIL

Runtime Fields

Config key Value type Notes
app.public_base_url url Canonical tenant app URL. Falls back to existing CTC_APP_BASE_URL.
TIMEZONE string Existing key used by tenant_timezone().
DEFAULT_COMPANY_UUID uuid Default company pointer. Prefer this over legacy DEFAULT_COMPANY_ID.
DEFAULT_COMPANY_ID int Legacy fallback while callers migrate to company UUID.
SCALE_TICKET_PREFIX string Existing scale-ticket prefix.
INVOICE_PREFIX string Existing invoice prefix.
AI_TASK_PROFILES json Tenant/task AI extraction profile overrides. The runtime uses this for task-specific prompt/profile context while preserving code/file prompt fallback.
COMMERCIAL_ORDER_COMPANY_TEMPLATES json Existing commercial order defaults, including optional commercial order signature presets.
OPERATION_EVENT_ROLE_POLICY json Tenant config key stored as T_TENANT_CONFIG.CONFIG_KEY; its JSON lives in CONFIG_VALUE. Role/event-level display policy for persisted operation notifications. Controls visible, toast, bell, history, ack, sound, and optional browser notifications.
EXPORT_YARD_EQUIPMENT_STATE_READY bool Operational gate set to 1 only after a tenant's T_EQUIPMENT yard-state mirror backfill completes.
MK_DOCUMENT_WORKFLOW_ENABLED bool Enables MK-specific booking confirmation, equipment-scoped packing list, local invoice, and sales weight ticket print workflows. tenant_mk_document_workflow_enabled() preserves the existing client 2 default until tenant config rows are seeded.
EXPORT_BROKERAGE_WORKFLOW_ENABLED bool Enables the one-step export brokerage workflow page/API for tenants that use purchase inbound, outbound sales, and allocation as one non-yard process. Defaults to off; expose through tenant config, not CLIENT_ID checks.
KJ_TABLE_OPTIONS json Tenant/page/table UI behavior overrides for kj-table, such as primaryRowActionTrigger and selectRowOnClick. Defaults must remain page-owned so tenants without this config keep existing behavior.
TMS_TRANSLOAD_INSTRUCTION_RULES json Tenant appointment/reload instruction rules. Runtime stores version and rules; each rule can match destination, loading types, commodity, and commodity match mode before returning instruction lines. Missing config falls back to built-in defaults; saves require at least one valid rule.
TMS_TRANSLOAD_INSTRUCTION_ENABLED bool Enables Transload instruction rules for the tenant. When disabled, the instruction-rules API returns no rules and rejects saves; clients must not fall back to built-in defaults.

OPERATION_EVENT_ROLE_POLICY is not a database column. It is a named JSON tenant config entry: T_TENANT_CONFIG.CONFIG_KEY='OPERATION_EVENT_ROLE_POLICY', CONFIG_VALUE='<json>'. The policy uses a compact JSON structure. default applies first, roles overrides by role id, then events.<EVENT_TYPE>.default and events.<EVENT_TYPE>.roles override for a specific event type. Missing config denies persisted operation event visibility, toast, bell, history, ack, sound, and browser notification for regular roles; super-admin remains allowed for admin/testing flows. Screen-facing event responses must expose only public display fields, not raw PAYLOAD_JSON. TMS transload auto-planning emits AUTO_PLANNED; explicit system-marked auto-assignment paths can emit AUTO_ASSIGNED; driver one-time location pings emit DRIVER_LOCATION_PING. Acknowledge means the notification was seen, not that the plan, assignment, or driver location has been workflow-confirmed.

The shared shell notification feed is intentionally bounded for screen use. /api/platform/operation-events defaults to recent candidates only (recentDays=14, scanLimit=300, unreadScanLimit=1000) and narrows SQL candidates by configured visible event types when the role has no broad visibility grant, then applies role/event policy in PHP. It is not a full long-term audit export; build a separate filtered admin/history surface if a screen needs older persisted event review.

{
  "default": { "visible": true, "toast": true, "bell": true, "history": true, "ack": true },
  "roles": {
    "2": { "sound": true },
    "5": { "visible": false }
  },
  "events": {
    "AUTO_ASSIGNED": {
      "default": { "toast": true, "bell": true, "sound": true },
      "roles": {
        "2": { "visible": true },
        "3": { "visible": true, "browser": true }
      }
    },
    "DRIVER_LOCATION_PING": {
      "default": { "toast": true, "bell": true, "history": true },
      "roles": {
        "1": { "visible": true, "ack": true },
        "2": { "visible": true, "ack": true },
        "3": { "visible": true, "ack": true }
      }
    }
  }
}

Integration Enablement

Tenant-level integration usage is not a config key. It belongs to module enablement:

Integration Module key
Gmail / email addon.email
Gemini / AI addon.ai
QuickBooks / QuickBooks Time addon.quickbooks
Telegram addon.telegram

Runtime code should check module enablement before using integration config once the integration is fully migrated. Legacy callers may continue using env fallback until cutover.

Do not add tenant integration toggles as granular ff.* config keys. Add or update the corresponding T_MODULE row and control tenant access through T_TENANT_MODULE.

Integration Config Fields

Config key Value type Module
GMAIL_APPOINTMENT_ACCOUNT string addon.email
GMAIL_APPOINTMENT_QUEUE_LABEL string addon.email
GMAIL_APPOINTMENT_PUBSUB_TOPIC string addon.email
GMAIL_APPOINTMENT_PUBSUB_TOKEN secret addon.email
GMAIL_APPOINTMENT_THREAD_CONTEXT_LIMIT int addon.email
GMAIL_APPOINTMENT_MANUAL_CONTACT_USERNAMES string addon.email
GMAIL_APPOINTMENT_MANUAL_CONTACT_USERNAME string addon.email
GMAIL_ADDON_TOKEN secret addon.email
GMAIL_ADDON_SYSTEM_BASE_URL url addon.email
GMAIL_ADDON_REVIEW_PATH string addon.email
GMAIL_CREDENTIALS_PATH file path addon.email
GOOGLE_APPLICATION_CREDENTIALS file path addon.email
GMAIL_CONTROLLER_TOKEN secret addon.email
GMAIL_OPERATION_TOKEN secret addon.email
GMAIL_OAUTH_STATE_SECRET secret addon.email
GEMINI_KEY secret addon.ai
GEMINI_API_KEY secret addon.ai
GEMINI_MODEL string addon.ai
GEMINI_FALLBACK_MODELS string addon.ai
GEMINI_API_BASE_URL url addon.ai
CHATGPT_KEY secret addon.ai
MK_DOCUMENT_WORKFLOW_ENABLED bool tms.booking
EXPORT_BROKERAGE_WORKFLOW_ENABLED bool oms.outbound-order
TELEGRAM_TOKEN secret addon.telegram
TELEGRAM_BOT_TOKEN secret addon.telegram
TELEGRAM_WEBHOOK_SECRET secret addon.telegram
QUICKBOOKS_TIME_BASE_URL url addon.quickbooks
QUICKBOOKS_TIME_PUBLIC_BASE_URL url addon.quickbooks
QUICKBOOKS_TIME_REDIRECT_URI url addon.quickbooks
QUICKBOOKS_TIME_TIMEOUT_SECONDS int addon.quickbooks
QUICKBOOKS_TIME_WEEK_START_DAY string addon.quickbooks
QUICKBOOKS_TIME_ACCESS_TOKEN secret addon.quickbooks
QUICKBOOKS_TIME_REFRESH_TOKEN secret addon.quickbooks
QUICKBOOKS_TIME_ACCESS_TOKEN_EXPIRES_AT datetime addon.quickbooks
QUICKBOOKS_TIME_CLIENT_ID string addon.quickbooks
QUICKBOOKS_TIME_CLIENT_SECRET secret addon.quickbooks
QUICKBOOKS_TIME_SETUP_TOKEN secret addon.quickbooks
QUICKBOOKS_TIME_REGULAR_JOBCODE_ID int addon.quickbooks
QUICKBOOKS_TIME_BREAK_10_JOBCODE_ID int addon.quickbooks
QUICKBOOKS_TIME_LUNCH_JOBCODE_ID int addon.quickbooks
QUICKBOOKS_TIME_OAUTH_USER_ID string addon.quickbooks
QUICKBOOKS_TIME_OAUTH_CLIENT_ID string addon.quickbooks
QUICKBOOKS_ONLINE_REALM_ID string addon.quickbooks
QUICKBOOKS_ONLINE_CLIENT_ID string addon.quickbooks
QUICKBOOKS_ONLINE_CLIENT_SECRET secret addon.quickbooks
QUICKBOOKS_ONLINE_ACCESS_TOKEN secret addon.quickbooks
QUICKBOOKS_ONLINE_ACCESS_TOKEN_EXPIRES_AT datetime addon.quickbooks
QUICKBOOKS_ONLINE_REFRESH_TOKEN secret addon.quickbooks
QUICKBOOKS_ONLINE_REFRESH_TOKEN_EXPIRES_AT datetime addon.quickbooks
QUICKBOOKS_ONLINE_BASE_URL url addon.quickbooks
QUICKBOOKS_ONLINE_TOKEN_URL url addon.quickbooks
QUICKBOOKS_ONLINE_TIMEOUT_SECONDS int addon.quickbooks
QUICKBOOKS_ONLINE_MINOR_VERSION int addon.quickbooks

Helper Contract

  • tenant_config_get() reads a single key with legacy fallback.
  • tenant_config_get_many() reads several keys for one CLIENT_ID and caches them per request.
  • tenant_config_upsert() is the shared write path. It preserves legacy schemas; when available it sets VALUE_TYPE, MODULE_ID, and UPDATED_AT.
  • AiTaskRegistry reads AI_TASK_PROFILES when callers pass a tenant DB/context. A profile can override a task instruction/prompt, add input_preprocess, and provide task/vendor/document mapping context that is appended to the effective prompt.
  • tenant_kj_table_options() reads KJ_TABLE_OPTIONS and returns a safe allowlisted option set for one page/table. Page defaults should be passed in by the caller.
  • tenant_config_module_key() maps integration config prefixes to add-on module keys for grouping only. It is not a feature-flag registry.
  • tenant_brand() resolves public brand data using config, T_CLIENT, and legacy fallbacks.
  • tenant_brand_public_config() returns safe browser-exposed brand fields only.

Migration Rule

Do not move a secret-like value into tenant config unless admin surfaces mask VALUE_TYPE='secret' values and the storage path has an explicitly documented encryption or temporary plaintext exception.

Gmail OAuth client JSON files are deployment files, not tenant config values. Store only an external path such as /etc/ctc/gmail/credentials.json; do not store the JSON body in T_TENANT_CONFIG or under the Git worktree.

If a Gmail OAuth JSON file was ever committed or left under the web root, treat it as exposed. Rotate the OAuth client secret and install the new JSON outside the worktree instead of copying the exposed file into the external path.

---

Source: .agents/rules/TENANT_CONFIG.md — ctc repository · This page is an English translation of the source document. Identifiers, file paths and code symbols are unchanged.