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¶
- Request effective tenant from
getClientId(). - Read tenant override from
T_TENANT_CONFIG. - Read identity fallback from
T_CLIENTwhen applicable. - Read deployment env fallback when applicable.
- 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
COMPATIBILITYpolicy 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_TYPEmismatch isINVALIDunless 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
TOKENneed an explicit definition. For example,QUICKBOOKS_ONLINE_TOKEN_URLis 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_CONFIGrows have no publish release/source identifier. A resolver must report them asTENANT_CONFIG, not claimPUBLISHED_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_MODULEhas 35 active catalog rows. Tenant 1 has 23 activeT_TENANT_MODULErows, all enabled in this snapshot; 12 catalog modules have no tenant row and therefore currently depend on missing-row compatibility.T_TENANT_CONFIGhas 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), andT_TENANT_CONFIG(CLIENT_ID, CONFIG_KEY). OPERATION_EVENT_ROLE_POLICYis stored withVALUE_TYPE=stringwhile the current compatibility inference returnsjson.QUICKBOOKS_ONLINE_TOKEN_URLis documented as URL but the current name heuristic returnssecret; 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=NONEor 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 oneCLIENT_IDand caches them per request.tenant_config_upsert()is the shared write path. It preserves legacy schemas; when available it setsVALUE_TYPE,MODULE_ID, andUPDATED_AT.AiTaskRegistryreadsAI_TASK_PROFILESwhen callers pass a tenant DB/context. A profile can override a taskinstruction/prompt, addinput_preprocess, and provide task/vendor/document mapping context that is appended to the effective prompt.tenant_kj_table_options()readsKJ_TABLE_OPTIONSand 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.