Studio has two JSON columns. settings is a bag of feature flags, integration state and server-written stamps. branding is the trainee app’s brand skin. Neither has a full Zod schema. Each feature reads its own keys defensively and ignores the rest.
Studio.settings
How it is written
There are two write paths and they behave differently.
Other modules write their own sub-objects directly (
smartsend, agent, onboarding, demoMode). They read the current object, spread it, and write it back.
Keys validated by patchStudioSettingsBody
Defined in apps/core-api/src/modules/studios/studios.schema.ts.
Changing an anchor re-prices foods
When a patch changes any of the three anchors,patchSettings calls repriceStudioFoods in the same transaction, using the anchors stored just before the change as the “from” values. The response includes a foodRepricing summary, or null when the anchors did not change. This is why the anchors are bounded and why the generic studio update is not allowed to touch them.
onboarding
Server-written stamps about the signup wizard.
Read by
onboardingOf in billing/billing.repository.ts. Plan limits use these stamps, and never User.onboardingComplete, to decide whether a studio is still in the wizard and exempt from the seat check. See Billing models.
smartsend
State of the studio’s SmartSend WhatsApp connection. Read by readSmartSendSettings in whatsapp/whatsapp.service.ts.
One query filters on this path directly:
settings with JSON path ['smartsend', 'apiKey']. See SmartSend WhatsApp.
agent
State of the WhatsApp AI assistant for this studio. Written by apps/core-api/src/modules/agent/agent.service.ts.
See WhatsApp webhooks and the agent.
demoMode
When true, the hourly demo activity job fabricates trainee activity for the studio so a sales demo looks alive. Never set it on a real studio.
Reading settings safely
Follow the pattern every reader uses:null, and nothing guarantees a key’s type. Check the type of each value you read, and choose the default deliberately. Note that calendarEnabled defaults to on and mbpEnabled defaults to off.
Studio.branding
The brand skin of the trainee app. The web panel sends the whole object on every save. The frontend type is Branding in apps/saas/modules/shared/lib/branding.ts. Every key is nullable, and null means “use the default”.
Validation
normalizeBrandingInput in apps/core-api/src/modules/studios/branding.ts passes every key through untouched except two, so no existing blob ever fails to save:
Both fail with
ErrorCode.VALIDATION and a details entry whose path is ['branding', '<field>'].