# Blueprint file reference

Every section and kind a buildbase.blueprint.json can set, with its key, its fields, its secrets and an example of each.

Every section and kind a blueprint can set, generated from the same declarations the blueprint engine runs on, so this page always lists exactly what the API accepts. For what each section is for, with short examples, read [the file format](/blueprints/file-format). If BuildBase reports a problem with your file, [troubleshooting](/blueprints/troubleshooting) explains each message and what to do.

The full JSON Schema, with every nested field and its allowed values, is served at [https://docs.buildbase.app/schemas/blueprint.v1.json](https://docs.buildbase.app/schemas/blueprint.v1.json). Point `$schema` at it and your editor completes and checks the file as you type:

```json title="buildbase.blueprint.json"
{
  "$schema": "https://docs.buildbase.app/schemas/blueprint.v1.json",
  "version": 1
}
```

`npx buildbase blueprint schema --out blueprint.schema.json` and `GET /api/blueprint/schema` return the same document.

How to read an entry below:

- **One object** kinds (`app`, `auth.security`) are set once. Only the fields you write are changed.
- **Map** kinds (`auth.methods`, `billing.plans`) hold entries keyed by a name or slug. Other entries refer to an entry by that key.
- **Secrets** must be written as `${env:NAME}` and supplied when you apply. See [secrets](/blueprints/file-format#secrets-and-env-name).
- **Can be pruned** means a resource of this kind that the blueprint created can be deleted when it leaves the file, if the kind is listed in `policy.prune` and you apply with prune switched on. See [prune](/blueprints/file-format#prune-deleting-what-the-file-no-longer-lists).
- **Applied after** names the kinds apply handles first, so a reference always points at something that exists.



## Top level

- `$schema` (string): Editor hint: the URL of this schema.
- `version` (`1`, required): Blueprint format version. Always 1.
- `policy` (object): How apply treats things the file does not cover.

`policy` holds:

- `prune` (array of string): Kinds (e.g. "webhooks", "auth.clients") whose resources this blueprint created or adopted may be DELETED when removed from the file. Empty by default: removing an entry never deletes anything unless its kind is listed here AND apply is run with prune switched on.
- `publish` (one of `always` or `never`): Versioned resources (plans, pricing groups, add-ons, held credit packages, collections): "always" (default) publishes the new version so the change is live; "never" leaves it as a draft to publish in the console. Existing subscribers keep the version they bought either way, and a publish with versionMoves on never moves them by itself: the move waits for confirmation in the console.

## App

**App settings.** One object at `app`.

The app itself: its name, main domain, the currencies it sells in, and organization-wide switches for notifications and workflow sending. Settings > General in the console.

- Never deleted by a blueprint.

Fields:

- `name` (string): The app name. The console, the default email sender name and the hosted sign-in page title use it.
- `domain` (string): The app's main website domain, e.g. app.example.com. Informational: sign-in uses auth.domains and each auth client's redirect URLs.
- `currencies` (array, each one of 21 values): Currencies plans are sold in. Every published paid plan must have a price in each one. Currencies can be added but never removed: a currency listed live and missing here stays, and the plan says so.
- `workflowLimits` (object): Frequency caps for messages sent by workflows. Workflows > Frequency cap in the console.
- `notifications` (object): Organization-wide kill switches for the notification events in email.notificationEvents.

```json
{
  "app": {
    "name": "Acme",
    "domain": "app.acme.example.com",
    "currencies": ["usd", "eur"],
    "workflowLimits": { "maxEmailsPerDay": 3, "maxPushPerDay": 5 },
    "notifications": { "email": true, "push": true }
  }
}
```

## Workspaces

**Workspace settings.** One object at `workspaces`.

How end users group into workspaces (teams), who may create and invite, workspace roles and what each may do, and the feature flags workspaces carry.

- Never deleted by a blueprint.

Fields:

- `mode` (one of `personal` or `platform`): "personal": every user gets exactly one workspace of their own, no invites, no roles. "platform": shared team workspaces with invites, roles and a switcher (a paid BuildBase plan feature). Switching mode resets the behaviour flags below to that mode's defaults unless you set them too.
- `canCreateWorkspace` (boolean): Users may create workspaces themselves. Must be false in personal mode.
- `canInviteMembers` (boolean or `admin-only`): Who may invite people into a workspace: true (any member with the invite permission), "admin-only" (workspace admins), or false (nobody). Must be false in personal mode.
- `showSwitcher` (boolean): Show the workspace switcher in the app. Must be false in personal mode.
- `maxWorkspacesPerUser` (integer): Most workspaces one person may own (create). 0 means no cap. At most 1 in personal mode.
- `autoCreateFirst` (boolean): Create a first workspace for every new user at sign-up. Must be true in personal mode.
- `maxWorkspaces` (integer): Most workspaces one person may belong to, owned or joined. 1 in personal mode.
- `maxWorkspaceUsers` (integer): Most members in one workspace. 1 in personal mode.
- `roles` (array of string): Workspace roles, in display order. Must include the built-in roles admin, editor and viewer. Platform mode only.
- `defaultRole` (string): Role new members get. Any role except admin. Platform mode only.
- `permissions` (object): What each workspace role may do: role name to permission keys. Roles left out keep their current permissions; the built-in defaults apply until you set a role.
- `customPermissions` (object): Permissions your own app checks (with the SDK), keyed by permission key, e.g. "reports:export". Removing one also removes it from every role.

```json
{
  "workspaces": {
    "mode": "platform",
    "canInviteMembers": "admin-only",
    "maxWorkspaceUsers": 25,
    "roles": ["admin", "editor", "viewer", "billing"],
    "defaultRole": "editor",
    "permissions": {
      "billing": ["workspace:billing:view", "workspace:billing:manage"]
    }
  }
}
```

### `workspaces.features`

**Workspace feature flag.** A map at `workspaces.features`: each key is the feature slug your app checks with the SDK (cannot change after creation). Keys match `^[a-z0-9][a-z0-9_-]{1,99}$`.

Feature flags your app checks per workspace with the SDK. Flags that a plan grants are declared as billing items of type "feature" instead.

- Applied after `workspaces`.
- Can be pruned: list `workspaces.features` in `policy.prune`.

Fields:

- `name` (string, required): Name shown in the console and to workspace admins.
- `description` (string, required): What the flag turns on.
- `enabled` (boolean): The flag is live. Off, it reads as off everywhere whatever its value. Default false.
- `defaultValue` (boolean): Value a workspace gets until someone changes it. Default false.
- `userManaged` (boolean): Workspace admins may switch it themselves in the app; otherwise only the console can. Default false.
- `allowAll` (boolean): Available to every workspace rather than only ones you select. Default false.
- `category` (string): Grouping label in the console. Default "general".
- `archived` (boolean): Archived flags are hidden and read as off. Removing a flag from the file never archives it.

```json
{
  "workspaces": {
    "features": {
      "beta-reports": {
        "name": "Beta reports",
        "description": "The new reports page",
        "enabled": true,
        "userManaged": true
      }
    }
  }
}
```

## Auth

How end users sign in to the app: login methods, auth clients (one per front end), the hosted sign-in page, session and MFA policy, sign-up rules, custom auth domains, bot protection, SMS and agent access.

### `auth.methods`

**Sign-in method.** A map at `auth.methods`: each key is a name you choose for the method (it only lives in this file); auth.clients name methods by it. Keys match `^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$`.

Sign-in methods (User management > Authentication > Methods): email and password, magic link or email code, passkeys, phone codes, guests, wallets, the social providers and one custom OIDC provider. A method does nothing until an auth client lists it in its methods. Social secrets are write-only: give them as `${env:NAME}`.

- Secrets, written as `${env:NAME}`: `clientSecret` and `privateKey`.
- Can be pruned: list `auth.methods` in `policy.prune`.

Fields:

- `type` (one of 12 values, required): Which sign-in method this is. password: email and password. magic-link: a link or 6-digit code by email. passkey: device passkeys (WebAuthn). phone: a code by text message, WhatsApp or voice call through the app's own SMS provider (auth.sms). guest: "Continue as guest", an account-less session that upgrades in place. wallet: an Ethereum or Solana wallet signature. google, linkedin, github, microsoft, apple: those providers' OAuth sign-in (Google, LinkedIn, GitHub, Microsoft, Apple, Custom OIDC provider). custom-oidc: one button for any other OpenID Connect or OAuth 2.0 provider (one per app). The type of an existing method cannot change.
- `title` (string, required): The method's name in the console and, for most types, on the hosted sign-in page's button.
- `delivery` (one of `link`, `code` or `both`): Only for magic-link: what the email sends. link (default): a sign-in link. code: a 6-digit code typed on the page. both: either.
- `ethereumChainIds` (array of integer): Only for wallet: the Ethereum networks a wallet may sign in from. Default [1] (mainnet). Empty allows no Ethereum wallet, which needs solana: true.
- `solana` (boolean): Only for wallet: allow Solana wallets too. Default false.
- `clientId` (string): Social providers only: the OAuth client ID registered with the provider (for apple, the Services ID, e.g. com.example.signin).
- `clientSecret` (string): Social providers except apple: the OAuth client secret registered with the provider. Write-only: put `${env:NAME}` here and supply the value when you apply. It is never shown again.
- `privateKey` (string): Only for apple: the contents of the .p8 Sign in with Apple key (an EC P-256 private key), from -----BEGIN PRIVATE KEY----- to the end. Write-only: put `${env:NAME}` here and supply the value when you apply. It is never shown again.
- `scopes` (array of string): Social providers only: the scopes to ask for. Empty (the default) asks for the provider's standard sign-in scopes (email and profile).
- `oneTap` (boolean): Only for google: offer Google One Tap on the sign-in page. Default false. Register the sign-in page's origin as an authorized JavaScript origin on the Google client.
- `tenant` (string): Only for microsoft: the directory tenant. "common" (the default) accepts work, school and personal accounts; a directory ID or domain accepts that directory only.
- `teamId` (string): Only for apple: the Apple Developer team ID (Membership details), e.g. ABCDE12345.
- `keyId` (string): Only for apple: the ID of the Sign in with Apple key, e.g. XYZ987WVU6.
- `label` (string): Only for custom-oidc: the name on its sign-in button, e.g. "Acme ID".
- `issuer` (string or null): The OIDC issuer URL; BuildBase reads the endpoints from its /.well-known/openid-configuration when saving (it must answer). Give this, or all three of authorizeUrl, tokenUrl and userInfoUrl. Only for custom-oidc. Use null to clear it.
- `authorizeUrl` (string or null): For a provider without discovery: its authorization endpoint. Only for custom-oidc. Use null to clear it.
- `tokenUrl` (string or null): For a provider without discovery: its token endpoint. Only for custom-oidc. Use null to clear it.
- `userInfoUrl` (string or null): For a provider without discovery: its user info endpoint. Only for custom-oidc. Use null to clear it.
- `iconUrl` (string or null): Only for custom-oidc: a square image for its button, over HTTPS. Without one the button shows a generic key. Use null to clear it.
- `emailTrust` (one of `verified-claim`, `always-verified` or `never`): Only for custom-oidc: when an email address from it may sign into an existing account with the same email. never (default): it cannot. verified-claim: when it sends `email_verified`: true. always-verified: always. Trust only a provider that verifies addresses itself.

```json
{
  "auth": {
    "methods": {
      "google": {
        "type": "google",
        "title": "Google",
        "clientId": "1234-abc.apps.googleusercontent.com",
        "clientSecret": "${env:GOOGLE_CLIENT_SECRET}",
        "oneTap": true
      }
    }
  }
}
```

### `auth.clients`

**Auth client.** A map at `auth.clients`: each key is a name you choose for the client (it only lives in this file). Keys match `^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$`.

Auth clients (User management > Authentication > Settings), one per app or front end that signs people in. A normal client is your own app using the hosted sign-in page; an oauth2 client makes this organization the OAuth 2.0 provider another application signs in with. The client ID and secret are generated by BuildBase and returned once, in the apply result of the run that creates the client.

- Refers to `auth.methods` (`methods`) and `integrations.tracking` (`trackingScripts`) by key.
- Applied after `auth.methods` and `integrations.tracking`.
- Can be pruned: list `auth.clients` in `policy.prune`.

Fields:

- `title` (string, required): The client's name in the console, on the consent screen and in emails.
- `type` (one of `normal` or `oauth2`, required): normal: your own app signs people in through the hosted page (SDK or redirect). oauth2: a standard OAuth 2.0 / OIDC client, for an application that uses this organization as its identity provider. Cannot change once created.
- `applicationTokenUrl` (string, required): Your app's endpoint the hosted page posts the signed-in session to (normal clients), e.g. https://app.example.com/api/auth/token.
- `applicationProfileUrl` (string, required): Your app's endpoint for the signed-in person's profile, e.g. https://app.example.com/api/auth/profile.
- `applicationRevokeUrl` (string): Optional: an http(s) URL BuildBase POSTs to when a session is revoked, so the app can drop its own token. Empty clears it.
- `requirePkce` (boolean): Require PKCE on the authorization code flow. Defaults to true for a new oauth2 client and false for a normal one.
- `clientKind` (one of `app` or `agent`): app (default): a first-party app; tokens are issued straight after sign-in. agent: a third-party client such as an AI agent; the person approves access on a consent screen first.
- `deviceGrant` (boolean): Allow the OAuth device code grant (RFC 8628), for CLIs and TVs. Default false.
- `redirectUrls` (array of string): Where the hosted page may send people back to after sign-in. A redirect to anything else is refused. Order does not matter.
- `methods` (array of string): The sign-in methods this client's hosted page offers, by their auth.methods key. An empty list leaves the page with nothing to sign in with. Order does not matter.
- `trackingScripts` (array of string): The tracking scripts this client's pages load (once consent allows), by their integrations.tracking key. Default none.
- `branding` (object): How this app appears on the hosted sign-in page; anything unset falls back to auth.hostedPage. Logos and the favicon are uploaded in the console (a blueprint carries no files).
- `trackingSettings` (object): Consent and conversion settings for the tracking scripts this client loads.
- `copy` (object): This app's own words on the hosted sign-in page and their translations.

```json
{
  "auth": {
    "clients": {
      "web": {
        "title": "Web app",
        "type": "normal",
        "applicationTokenUrl": "https://app.example.com/api/auth/token",
        "applicationProfileUrl": "https://app.example.com/api/auth/profile",
        "redirectUrls": ["https://app.example.com/auth/callback"],
        "methods": ["password", "google"],
        "branding": { "displayName": "Acme" }
      }
    }
  }
}
```

### `auth.hostedPage`

**Hosted sign-in page.** One object at `auth.hostedPage`.

The hosted sign-in page every auth client shares (User management > Access > Branding): its name, links, the words on each screen and their translations, brand colours, font and layout. An auth client can override the name, logos, colour and words for its own app, on the same page.

- Never deleted by a blueprint.

Fields:

- `name` (string): The app's name on the page and in the browser tab. The old default "BuildBase Auth" reads as unset, and the organization's name is shown.
- `shortName` (string): A short form of the name, where space is tight.
- `description` (string): A one-line description of the app, for the page metadata.
- `logoUrl` (string): An image URL shown as the logo when no logo is uploaded in the console.
- `privacyPolicyUrl` (string or null): Your privacy policy, linked from the page. Null shows no link. An http(s) link. Use null to clear it.
- `termsOfServiceUrl` (string or null): Your terms of service, linked from the page. Null shows no link. An http(s) link. Use null to clear it.
- `supportUrl` (string or null): Where people get help, linked from the page. Null shows no link. An http(s) link. Use null to clear it.
- `websiteUrl` (string or null): Your app's website, linked from the page. Null shows no link. An http(s) link. Use null to clear it.
- `hideBranding` (boolean): Hide the "Secured by BuildBase" badge. Turning it on needs a plan with white-label. Default false.
- `screens` (object): The title and line on each screen. A screen left out keeps what it says now; BuildBase's wording is the default.
- `branding` (object): Brand colours, font and layout of the hosted page. The logo, dark logo, favicon and background image are uploaded in the console (a blueprint carries no files).
- `defaultLocale` (one of `en`, `es`, `fr`, `de`, `ja`, `zh`, `hi` or `ar`): The page's language when the visitor's browser asks for none it has. Default en.
- `consentLabel` (string or null): The sign-up consent checkbox's sentence for every app (an auth client's own comes first). Null uses ours. Use null to clear it.
- `untranslatedCopy` (one of `as-written` or `default`): Where a language has no translation of your words: as-written (default) shows your words as written; default shows ours in the visitor's language.
- `translations` (object): Your words in other languages, by locale, place and field. Null removes one. The default language needs none.

```json
{
  "auth": {
    "hostedPage": {
      "name": "Acme",
      "privacyPolicyUrl": "https://acme.example.com/privacy",
      "screens": {
        "login": {
          "title": "Welcome back to {app}",
          "description": "Sign in to continue."
        }
      },
      "branding": {
        "primaryColor": "#4F46E5",
        "radius": "medium",
        "layout": { "cardStyle": "bordered" }
      },
      "translations": {
        "es": { "login": { "title": "Bienvenido de nuevo a {app}" } }
      }
    }
  }
}
```

### `auth.security`

**Sign-in security.** One object at `auth.security`.

Session lifetime, "remember me", disposable email blocking and the account deletion grace period (User management > Authentication > Security).

- Never deleted by a blueprint.

Fields:

- `sessionTtlDays` (integer): How long a sign-in lasts without "remember me", in days. Default 30.
- `allowRememberMe` (boolean): Offer "remember me" on the sign-in page. Default true.
- `rememberMeDays` (integer): How long a sign-in with "remember me" lasts, in days. Default 90.
- `blockDisposableEmail` (boolean): Refuse sign-up, and a change of email, to a throwaway inbox (mailinator and the like). Default false. auth.signup.blockDisposableSignups does the same: either switch on refuses.
- `accountDeletionGraceDays` (integer): Days between someone asking to delete their account and the data being removed; they can cancel until then. 0 deletes at once. Default 7.

```json
{
  "auth": {
    "security": {
      "sessionTtlDays": 14,
      "allowRememberMe": true,
      "rememberMeDays": 60,
      "blockDisposableEmail": true
    }
  }
}
```

### `auth.mfa`

**Two-step sign-in policy.** One object at `auth.mfa`.

Two-step sign-in (MFA) for end users: whether it is off, optional or required, and which second steps are allowed (User management > Authentication > Security).

- Never deleted by a blueprint.

Fields:

- `enforcement` (one of `off`, `optional` or `required`): off: nobody is prompted and nobody can enroll. optional (default): people may add a second step, and whoever has one is asked for it. required: everyone needs one, and sets it up at their next sign-in.
- `allowedFactors` (array, each `totp` or `sms`): Second steps people may use. Default ["totp"]. sms cannot be the only one.
- `requireForSocial` (boolean): Ask for the second step after a social sign-in too. Default false (the provider ran its own checks).

```json
{
  "auth": {
    "mfa": { "enforcement": "optional", "allowedFactors": ["totp", "sms"] }
  }
}
```

### `auth.phone`

**Phone sign-in settings.** One object at `auth.phone`.

How the phone sign-in method asks for a number and sends its code. Codes go through the app's own SMS provider (auth.sms); BuildBase sends no text messages of its own.

- Never deleted by a blueprint.

Fields:

- `defaultCountry` (string): The ISO 3166 alpha-2 country a number without a country code is read in, e.g. US. Default US.
- `channels` (array, each `sms`, `whatsapp` or `voice`): How the page offers to send the code, narrowed to what the SMS provider can send (a webhook provider can send all three; Twilio without Verify sends sms only). Default ["sms"].

```json
{
  "auth": {
    "phone": { "defaultCountry": "GB", "channels": ["sms", "whatsapp"] }
  }
}
```

### `auth.username`

**Username settings.** One object at `auth.username`.

Usernames beside the email address: people may then sign in with either (User management > Authentication > Sign-up).

- Never deleted by a blueprint.

Fields:

- `enabled` (boolean): Let people choose a username and sign in with it. Default false.
- `required` (boolean): Ask every sign-up for a username. Needs enabled. Default false.

```json
{ "auth": { "username": { "enabled": true, "required": false } } }
```

### `auth.guests`

**Guest settings.** One object at `auth.guests`.

Guest sessions, from the guest sign-in method: how long an idle guest is kept.

- Never deleted by a blueprint.

Fields:

- `idleDays` (integer): Days a guest who has not upgraded to an account is kept after their last visit, then removed. Default 30.

```json
{ "auth": { "guests": { "idleDays": 14 } } }
```

### `auth.signup`

**Sign-up rules.** One object at `auth.signup`.

Who may create an account in the app (User management > Authentication > Sign-up): public sign-up, disposable email blocking, a sign-up captcha, and the access mode with its allow and block lists.

- Never deleted by a blueprint.

Fields:

- `allowPublicSignup` (boolean): Anyone may create an account: through the hosted register page, a social sign-in or an integration. Off makes sign-up invite-only (a workspace invitation still lets its recipient sign up). Default true.
- `blockDisposableSignups` (boolean): Refuse sign-ups from throwaway inboxes (mailinator and the like). Default false. auth.security.blockDisposableEmail does the same: either switch on refuses.
- `captcha` (boolean): Ask for a captcha on sign-up (the hosted register page and /api/v1/auth/register), using the organization's form captcha settings. Default false.
- `access` (object): Sign-up access: the mode and its lists. The lists are private to the console.

```json
{
  "auth": {
    "signup": {
      "allowPublicSignup": true,
      "blockDisposableSignups": true,
      "access": {
        "mode": "allowlist",
        "allow": ["@acme.example.com"],
        "block": []
      }
    }
  }
}
```

### `auth.botProtection`

**Bot protection.** One object at `auth.botProtection`.

Bot protection on the hosted sign-in page, with Cloudflare Turnstile. On BuildBase's shared auth domain it works with no keys; a custom auth domain needs your own site key and secret key (a key pair lists the hostnames it may run on).

- Secrets, written as `${env:NAME}`: `secretKey`.
- Never deleted by a blueprint.

Fields:

- `mode` (one of `off`, `always` or `suspicious`): off (default): no challenge. always: sign-up, magic link and email code sends and password sign-in each need a solved challenge. suspicious: the sends and sign-up always do; password sign-in only after the visitor's IP failed three sign-ins in 15 minutes.
- `siteKey` (string or null): Your Turnstile site key, for a custom auth domain. Public by design. Null removes your key pair (site and secret). Use null to clear it.
- `secretKey` (string): Your Turnstile secret key, which goes with siteKey. Checked with Cloudflare when saved, and refused if Cloudflare does not recognise it. Write-only: put `${env:NAME}` here and supply the value when you apply. It is never shown again.

```json
{ "auth": { "botProtection": { "mode": "suspicious" } } }
```

### `auth.sms`

**SMS provider.** One object at `auth.sms`.

The app's own SMS provider for phone sign-in codes and SMS second steps: Twilio (Verify or Messaging), or a webhook of yours that BuildBase POSTs each code to, signed with a secret it generates and returns once in the apply result. Needs a plan with SMS sign-in.

- Secrets, written as `${env:NAME}`: `twilioAuthToken`.
- Can be pruned: list `auth.sms` in `policy.prune`.

Fields:

- `mode` (one of `twilio` or `webhook`, required): twilio: codes go through your Twilio account. webhook: BuildBase POSTs each code to webhookUrl, signed, and your service sends it.
- `accountSid` (string): twilio: your Twilio account SID (AC...).
- `twilioAuthToken` (string): twilio: your Twilio auth token. Checked with Twilio when saved. Write-only: put `${env:NAME}` here and supply the value when you apply. It is never shown again.
- `verifyServiceSid` (string): twilio: a Twilio Verify service SID (VA...). With one, codes can go by SMS, WhatsApp and voice.
- `messagingServiceSid` (string): twilio, without Verify: a Messaging service SID (MG...) to send SMS from.
- `fromNumber` (string): twilio, without Verify: the number to send SMS from, in E.164 form (+14155550123).
- `webhookUrl` (string): webhook: where BuildBase POSTs each code. Must be reachable from BuildBase (a public https URL in production).
- `dailyCap` (integer): Most codes sent in a day (UTC); sends stop beyond it, so a bot cannot run up your bill. Default 1000.
- `alertAt` (integer): Percent of the daily cap at which the organization is warned. Default 80.

```json
{
  "auth": {
    "sms": {
      "mode": "webhook",
      "webhookUrl": "https://sms.example.com/buildbase",
      "dailyCap": 500
    }
  }
}
```

### `auth.agentReadiness`

**Agent readiness.** One object at `auth.agentReadiness`.

How AI agents reach the app (User management > Agent readiness): the public discovery documents, whether agents may sign up, and dynamic client registration (RFC 7591), where an agent registers its own OAuth client modelled on one of the organization's oauth2 clients. Needs a plan with agent readiness to switch any of it on.

- Refers to `auth.clients` (`dynamicClientRegistration.baseClient`) by key.
- Applied after `auth.clients`.
- Never deleted by a blueprint.

Fields:

- `enabled` (boolean): Publish the agent discovery documents (the organization's OAuth metadata and agent instructions), so agents can find how to sign in. Default false.
- `allowAgentSignup` (boolean): Let autonomous agents create accounts on the app themselves. Default false.
- `dynamicClientRegistration` (object): Dynamic client registration.

```json
{
  "auth": {
    "agentReadiness": {
      "enabled": true,
      "allowAgentSignup": false,
      "dynamicClientRegistration": {
        "enabled": true,
        "baseClient": "agents",
        "maxClients": 100
      }
    }
  }
}
```

### `auth.domains`

**Custom auth domain.** A map at `auth.domains`: each key is the hostname, lowercase, e.g. login.example.com. Keys match `^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$`.

Custom auth domains: the hosted sign-in page on a hostname of your own (login.example.com). Each entry is the hostname and nothing else (`{}`). Apply attaches it; the DNS records to add are listed as manual steps until it is verified in the console. A hostname another organization holds is refused. Needs a plan with custom auth domains.

- Can be pruned: list `auth.domains` in `policy.prune`.

```json
{ "auth": { "domains": { "login.example.com": {} } } }
```

### `auth.waitlist`

**Waitlist page.** One object at `auth.waitlist`.

The hosted waitlist page and the emails it sends (Users > Beta users > Settings). Turn the waitlist on or off with auth.signup.access.mode ("waitlist" turns it on). The page words are stored here, not in auth.hostedPage.

- Refers to `email.templates` (`emails.registered.template`), `email.templates` (`emails.approved.template`) and `email.templates` (`emails.rejected.template`) by key.
- Applied after `email.templates`.
- Never deleted by a blueprint.

Fields:

- `name` (string): Name of the program, shown on the page.
- `description` (string): What people are joining, shown on the page.
- `logoUrl` (string): An image URL shown as the logo when none is uploaded.
- `privacyPolicyUrl` (string): Your privacy policy, linked from the page. Empty shows the hosted sign-in page's link, or none.
- `termsOfServiceUrl` (string): Your terms of service, linked from the page. Empty shows the hosted sign-in page's link, or none.
- `screens` (object): The words on the two waitlist screens.
- `emails` (object): The waitlist emails: which template each sends, and whether it is sent.

```json
{
  "auth": {
    "waitlist": {
      "name": "Acme early access",
      "screens": { "register": { "title": "Join the Acme beta" } },
      "emails": {
        "registered": { "template": "waitlist-welcome", "send": true }
      }
    }
  }
}
```

## Email

Transactional email: sending domains, senders, unsubscribe groups, templates, custom merge tags and the notification events that send them.

### `email.mailgun`

**Mailgun credentials.** One object at `email.mailgun`.

Your Mailgun account, used to add sending domains (email.domains) and Mailgun senders. Set once; to replace the key later, use the console.

- Secrets, written as `${env:NAME}`: `apiKey` and `webhookSigningKey`.
- Never deleted by a blueprint.

Fields:

- `apiKey` (string, required): Mailgun private API key. Write-only: put `${env:NAME}` here and supply the value when you apply. It is never shown again.
- `webhookSigningKey` (string): Mailgun HTTP webhook signing key, so bounce and complaint events are trusted. Write-only: put `${env:NAME}` here and supply the value when you apply. It is never shown again.

### `email.domains`

**Sending domain.** A map at `email.domains`: each key is the domain name, e.g. mail.example.com. Keys match `^(?=.{1,253}$)([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$`.

Domains email is sent from through Mailgun. Needs email.mailgun. After apply, add the DNS records the result lists at your DNS provider, then verify the domain in the console (Messaging > Sender accounts > Domains).

- Applied after `email.mailgun`.
- Never deleted by a blueprint.

```json
{ "email": { "domains": { "mail.example.com": {} } } }
```

### `email.senders`

**Email sender.** A map at `email.senders`: each key is a name you choose for the sender. Keys match `^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$`.

Who email is sent from. "smtp": your own SMTP server (checked with a live connection before it is saved). "ses": Amazon SES SMTP credentials. "mailgun": an address on one of your email.domains. "managed": BuildBase-managed sending, where the server offers it. "google": a Gmail account, which needs a browser sign-in in the console. Set "default": true on the one that sends system email.

- Secrets, written as `${env:NAME}`: `password`.
- Refers to `email.domains` (`domain`) by key.
- Applied after `email.domains`.
- Can be pruned: list `email.senders` in `policy.prune`.

Fields:

- `type` (one of `smtp`, `ses`, `mailgun`, `managed` or `google`, required): How mail leaves.
- `title` (string): Name shown in the console (smtp, ses).
- `description` (string): What the sender is for (smtp, ses).
- `from` (string): The From header, e.g. `Acme <hello@example.com>`. The address must be one the server is allowed to send as.
- `replyTo` (string): Reply-To address for mail from this sender.
- `default` (boolean): This sender sends system email (sign-in codes, invitations, notifications). One sender at a time.
- `host` (string): SMTP host (smtp).
- `port` (integer): SMTP port (smtp), e.g. 587 or 465.
- `secure` (boolean): Connect with TLS from the start (port 465); false uses STARTTLS when offered (smtp).
- `username` (string): SMTP or SES SMTP user name (smtp, ses).
- `password` (string): SMTP or SES SMTP password (smtp, ses). Write-only: put `${env:NAME}` here and supply the value when you apply. It is never shown again.
- `region` (string): AWS region of the SES endpoint, e.g. us-east-1 (ses). Fixed once created.
- `configurationSet` (string): SES configuration set to send with (ses).
- `snsTopicArn` (string): SNS topic ARN that receives SES bounce and complaint events (ses).
- `domain` (string): The email.domains key the address belongs to (mailgun).
- `senderName` (string): Display name (mailgun).
- `senderEmail` (string): Address on the domain, e.g. hello@mail.example.com (mailgun).

```json
{
  "email": {
    "senders": {
      "transactional": {
        "type": "smtp",
        "title": "Transactional",
        "description": "Sign-in codes and receipts",
        "host": "smtp.example.com",
        "port": 587,
        "secure": false,
        "username": "apikey",
        "password": "${env:SMTP_PASSWORD}",
        "from": "Acme <hello@example.com>",
        "default": true
      }
    }
  }
}
```

### `email.unsubscribeGroups`

**Unsubscribe group.** A map at `email.unsubscribeGroups`: each key is a name you choose for the group (templates refer to it by this key). Keys match `^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$`.

Groups a recipient can unsubscribe from one by one (newsletters, product updates...). The seeded groups "system-workspace-notifications" and "system-billing-notifications" exist in every organization, can be referenced by templates, and cannot be changed.

- Never deleted by a blueprint.

Fields:

- `title` (string, required): Name recipients see on the preference page.
- `description` (string): What the group sends, shown on the preference page.
- `isDefault` (boolean): New templates join this group unless they name another. Default false.
- `isPublic` (boolean): Listed on the recipient preference page. Default false.

```json
{
  "email": {
    "unsubscribeGroups": {
      "newsletter": {
        "title": "Newsletter",
        "description": "Monthly product news",
        "isPublic": true
      }
    }
  }
}
```

### `email.templates`

**Email template.** A map at `email.templates`: each key is a seeded template id (welcome, magic-link, sign-in-code, password-reset-code, email-verification-code, workspace-invite, subscription-created, payment-failed, ...) to customize it, or a name you choose for a new template. Keys match `^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$`.

Email templates. Merge tags such as `{{user.name}}`, `{{appName}}` and your own email.mergeTags work in the subject and body. A seeded template keeps its type (markdown); a new template is markdown, html or text, and its type cannot change later.

- Refers to `email.unsubscribeGroups` (`unsubscribeGroup`) by key.
- Applied after `email.unsubscribeGroups`.
- Can be pruned: list `email.templates` in `policy.prune`.

Fields:

- `title` (string): Name shown in the console.
- `description` (string): What the template is for. Required for a new template.
- `type` (one of `text`, `html` or `markdown`): Body format of a new template: "markdown" (recommended), "html" or "text". Fixed once created.
- `category` (one of `user`, `contact`, `audience`, `auth`, `default`, `beta-users`, `custom` or `notification`): Which merge tags the editor offers: user, contact, audience, auth, notification, beta-users, custom or default. Default "default".
- `subject` (string): Subject line. Merge tags allowed.
- `unsubscribeGroup` (string): The email.unsubscribeGroups key (or a system group) recipients can opt out of. New templates only; seeded templates keep theirs.
- `markdown` (string): Body of a markdown template.
- `html` (string): Body of an html template, as complete HTML. It opens in the code editor rather than the drag-and-drop designer.
- `text` (string): Body of a plain text template.

```json
{
  "email": {
    "templates": {
      "welcome": {
        "subject": "Welcome to {{appName}}, {{user.name}}",
        "markdown": "# Welcome\n\nThanks for joining {{appName}}."
      }
    }
  }
}
```

### `email.mergeTags`

**Merge tag.** A map at `email.mergeTags`: each key is the tag name used in templates as `{{key}}`. Keys match `^[a-zA-Z][a-zA-Z0-9_-]{1,63}$`.

Organization-wide merge tags. Every organization has appName, supportEmail, privacyPolicyUrl, termsUrl and companyAddress with empty values: set them here. Tags are archived, never deleted.

- Can be pruned: list `email.mergeTags` in `policy.prune`.

Fields:

- `title` (string, required): Label in the template editor.
- `value` (string, required): What `{{key}}` renders as.
- `archived` (boolean): Hidden from the editor; templates that still use it render it empty.

```json
{
  "email": {
    "mergeTags": {
      "supportEmail": { "title": "Support Email", "value": "help@example.com" }
    }
  }
}
```

### `email.notificationEvents`

**Notification event.** A map at `email.notificationEvents`: each key is the event slug your app triggers (or a platform event slug, to switch its channels). Keys match `^[a-z0-9][a-z0-9._-]{1,99}$`.

Notification events. A new event gets its own email template (`custom-notification-<slug>`) unless you point "template" at one of yours. Platform events accept only "channels".

- Refers to `email.templates` (`template`) by key.
- Applied after `email.templates`.
- Can be pruned: list `email.notificationEvents` in `policy.prune`.

Fields:

- `name` (string): Name in the console and in the user preference center.
- `description` (string): What the event tells the user.
- `category` (one of `workspace`, `billing`, `trial`, `payment`, `account` or `general`): Preference center grouping: workspace, billing, trial, payment, account or general. Default "general".
- `enabled` (boolean): Send at all. Default true.
- `userManaged` (boolean): Users may switch it off in their notification preferences. Default false.
- `channels` (object): Which channels carry this event.
- `template` (string): The email.templates key used for the email.

```json
{
  "email": {
    "notificationEvents": {
      "report-ready": {
        "name": "Report ready",
        "description": "Your weekly report is ready",
        "channels": { "email": true, "push": false }
      }
    }
  }
}
```

## Billing

What the app sells: the Stripe connection, product categories, items (features, limits, quotas), credit packages, plans with versioned prices, pricing groups, the plan new workspaces start on, add-ons sold beside the plans, and coupons with their promotion codes.

### `billing.stripe`

**Stripe connection.** One object at `billing.stripe`.

The app's own Stripe account, which plan prices, checkout, invoices and credit packs are created in. The keys are stored as given; nothing is checked against Stripe until Billing > Credentials > Check Stripe setup runs or a published price syncs.

- Secrets, written as `${env:NAME}`: `secretKey` and `webhookSecret`.
- Can be pruned: list `billing.stripe` in `policy.prune`.

Fields:

- `publishableKey` (string, required): Stripe publishable key (`pk_test_...` or `pk_live_...`). Not a secret: the SDK sends it to browsers for checkout. `${env:NAME}` works too.
- `secretKey` (string, required): Stripe secret key (`sk_test_...`, `sk_live_...`, or a restricted `rk_` key) of the same account and mode as the publishable key. Write-only: put `${env:NAME}` here and supply the value when you apply. It is never shown again.
- `webhookSecret` (string): Signing secret (`whsec_...`) of the Stripe webhook endpoint that points at this organization. Without it Stripe events are refused, so subscriptions never update. In the console, Billing > Credentials > "Check Stripe setup", then "Create the endpoint for me", creates the endpoint and stores this for you instead. Write-only: put `${env:NAME}` here and supply the value when you apply. It is never shown again.

```json
{
  "billing": {
    "stripe": {
      "publishableKey": "pk_test_51Hxxxxxxxxxxxxxxxx",
      "secretKey": "${env:STRIPE_SECRET_KEY}",
      "webhookSecret": "${env:STRIPE_WEBHOOK_SECRET}"
    }
  }
}
```

### `billing.categories`

**Item category.** A map at `billing.categories`: each key is the category slug (cannot change after creation). Keys match `^[a-z0-9][a-z0-9_-]{1,99}$`.

Groups that billing items are filed under. Pricing tables and plan comparisons show items section by section in category order. Every item belongs to exactly one category.

- Can be pruned: list `billing.categories` in `policy.prune`.

Fields:

- `name` (string, required): Section heading shown on pricing tables and in the console. No leading or trailing spaces.
- `description` (string): What the items in this category have in common. Shown in the console. Empty clears it.
- `sortOrder` (integer): Position of this section on pricing tables, ascending. Default 0; ties fall back to creation order.

```json
{
  "billing": {
    "categories": { "core": { "name": "Core features", "sortOrder": 10 } }
  }
}
```

### `billing.items`

**Billing item.** A map at `billing.items`: each key is the item slug your app checks with the SDK (useEntitlement or useLimit from SDK 1.0.0; WhenWorkspaceFeatureEnabled and useQuotaUsageStatus on 0.0.90 and earlier) and plans list it under. Keys match `^[a-z0-9][a-z0-9_-]{1,99}$`.

The catalog plans are built from. A "feature" item is a capability a plan switches on (BuildBase creates and owns its workspace feature flag, so do not declare it under workspaces.features). A "limit" is a count a plan caps, -1 meaning unlimited. A "quota" is a usage allowance per billing period, optionally billed as overage. Your app reads each one by its slug. An item is a workspace's unless its scope is "user": a person's, which is what an add-on a person buys (payerType "user") grants.

- Refers to `billing.categories` (`category`) by key.
- Applied after `billing.categories`.
- Can be pruned: list `billing.items` in `policy.prune`.

Fields:

- `type` (one of `feature`, `limit` or `quota`, required): What kind of entitlement: "feature" (on or off), "limit" (a ceiling such as projects or seats) or "quota" (usage per billing period such as API calls). Cannot change after creation; use a new slug instead.
- `scope` (one of `workspace` or `user`): Who the item is declared for: "workspace" (the default) or "user", a person. An add-on a person buys (payerType "user") grants user-scope items only, and a workspace's add-on workspace-scope items only. A user-scope feature has no workspace feature flag: it is read for the person. Cannot change after creation; use a new slug instead. Counting a user-scope limit by claims (linkedTo, claimPolicy) is set in the console: the blueprint leaves it as it is.
- `name` (string, required): Name shown on pricing tables and in the console. No leading or trailing spaces.
- `description` (string): One line shown under the name on pricing tables. Empty clears it.
- `category` (string, required): Key of the billing.categories entry this item is listed under.
- `sortOrder` (integer): Position within its category on pricing tables, ascending. Default 0.
- `highlight` (boolean): A selling point: pricing tables list highlighted items first on each plan card. Default false.
- `badge` (string): Short label shown next to the item on pricing tables, such as "New" or "Beta". Empty clears it.
- `internalOnly` (boolean): Gate a capability without ever showing it on a public pricing table. Default false.

```json
{
  "billing": {
    "items": {
      "api-calls": {
        "type": "quota",
        "name": "API calls",
        "category": "usage",
        "description": "Requests to the public API"
      }
    }
  }
}
```

### `billing.creditPackages`

**Credit package.** A map at `billing.creditPackages`: each key is the package slug the SDK buys it by (lowercase letters, digits and -; cannot change after creation). Keys match `^[a-z0-9][a-z0-9-]{1,99}$`.

Bundles of credits sold as a one-time purchase, or granted by a plan (`billing.plans.*.credits`). Each price is a one-time charge in the currency's smallest unit. The organization's plan must include credits (402 otherwise). A package nothing holds is edited in place. Once something holds it (a published plan version grants it, an auto top-up buys it, or it was bought or granted), a change to its credits, its validity or the amount of a currency it is priced in is a new version of the package, published unless policy.publish is "never": what was bought keeps its terms. Its name, description, order, whether it is on sale and a currency it is not priced in yet are still edited in place.

- Can be pruned: list `billing.creditPackages` in `policy.prune`.

Fields:

- `name` (string, required): Name shown at checkout and in the console. No leading or trailing spaces.
- `description` (string): What the buyer gets. Shown at checkout. Empty clears it.
- `credits` (integer, required): Credits added to the workspace balance per purchase or grant. At least 1. A sold term: once the package is held, a change is a new version.
- `prices` (object, required): Price per currency (lowercase ISO 4217 code). Exactly these currencies are offered: a currency left out is removed. At least one. Once the package is held, a new amount or a removed currency is a new version; a new currency is added in place.
- `validityDays` (integer or null): Days the credits stay usable after purchase or grant. Leave it out or use null for credits that never expire (refused when your BuildBase plan caps credit expiry). A sold term: once the package is held, a change is a new version. Use null to clear it.
- `active` (boolean): On sale. Inactive packages cannot be bought, but plans may still grant them. Default true.
- `sortOrder` (integer): Position in package lists, ascending. Default 0.
- `metadata` (object): Free-form string pairs stored with the package and returned by the API, for your own use. Keys left out keep their stored value.
- `versionMoves` (object): Version moves: what publishing a new version of the package does to the holders of the version it takes off sale. Credits already bought or granted never move: a move reaches the auto top-ups that buy the package. A publish made by apply never moves anyone by itself: the move waits as "awaiting confirmation" until an admin confirms its preview in the console, and the plan says how many holders it would reach.

```json
{
  "billing": {
    "creditPackages": {
      "starter-pack": {
        "name": "Starter pack",
        "credits": 1000,
        "prices": { "usd": 900, "eur": 800 },
        "validityDays": 365
      }
    }
  }
}
```

### `billing.plans`

**Plan.** A map at `billing.plans`: each key is the plan slug (cannot change after creation). Keys match `^[a-z0-9][a-z0-9_-]{1,99}$`.

Plans and their prices. A plan is versioned: what a subscriber bought never changes under them. Any change to features, limits, quotas, pricing, trial or credits creates a new version (filled from the current one plus this file) and publishes it unless policy.publish is "never"; existing subscribers stay on the version they have. The file is compared against the published version. Money is always an integer in the currency's smallest unit.

- Versioned: a change creates a new version instead of editing the live one.
- Refers to `billing.items` (`features`), `billing.items` (`limits`), `billing.items` (`quotas`) and `billing.creditPackages` (`credits.package`) by key.
- Applied after `app`, `billing.items` and `billing.creditPackages`.
- Can be pruned: list `billing.plans` in `policy.prune`.

Fields:

- `name` (string, required): Plan name shown on pricing tables, checkout and invoices. No leading or trailing spaces.
- `description` (string): One line under the plan name on pricing tables. Empty clears it.
- `freemium` (boolean): A free plan: prices may be 0, and the SDK assigns it without checkout. Cannot change once a version is published. Default false.
- `versionMoves` (object): Version moves: what publishing a new version of the plan does to the holders of the version it takes off sale. A setting on the plan, not a term of a version: changing it makes no new version. A publish made by apply never moves anyone by itself: the move waits as "awaiting confirmation" until an admin confirms its preview in the console, and the plan says how many holders it would reach.
- `features` (object): Feature items (billing.items of type "feature") on this plan, by slug. Exactly these: a feature left out is removed from the next version. Leave the whole field out to keep the features as they are.
- `limits` (object): Limit items (type "limit") on this plan, by slug. Exactly these: a limit left out is removed from the next version.
- `quotas` (object): Quota items (type "quota") on this plan, by slug. Exactly these: a quota left out is removed from the next version.
- `pricing` (object): Prices per currency (lowercase ISO 4217 code). Every currency the organization sells in (app.currencies) needs one. Fields left out keep their current value; null removes an interval or seat pricing.
- `trial` (object or null): Free trial before the first charge. Use null to clear it.
- `credits` (object or null): Credits the plan grants its subscribers. Use null to clear it.

```json
{
  "billing": {
    "plans": {
      "pro": {
        "name": "Pro",
        "features": { "sso": true },
        "limits": { "projects": 50, "members": -1 },
        "quotas": {
          "api-calls": {
            "allowOverage": true,
            "monthly": { "included": 100000, "unitSize": 1000 }
          }
        },
        "pricing": {
          "usd": {
            "monthly": 4900,
            "yearly": 49000,
            "overage": { "api-calls": { "monthly": 50 } }
          }
        },
        "trial": { "days": 14, "requireCard": false }
      }
    }
  }
}
```

### `billing.groups`

**Pricing group.** A map at `billing.groups`: each key is the group slug a pricing page asks for (not "default", which is reserved). Keys match `^[a-z0-9][a-z0-9_-]{1,99}$`.

Pricing pages. A group lists plans in display order, and each published group version pins the exact plan versions it sells. When a plan in a group publishes a new version, the group gets a new version pinning it, and subscribers on the old group version move with it where their plan is still listed. A group sells to workspaces or to people (payerType), and each payer type has one default group, served when a pricing page names none.

- Versioned: a change creates a new version instead of editing the live one.
- Refers to `billing.plans` (`plans`) and `billing.items` (`requiredItems`) by key.
- Applied after `billing.plans` and `billing.items`.
- Can be pruned: list `billing.groups` in `policy.prune`.

Fields:

- `name` (string, required): Group name, shown in the console and returned to pricing pages. No leading or trailing spaces.
- `description` (string): What this pricing page is for, such as "Self-serve plans". Empty clears it.
- `default` (boolean): The pricing page served when none is named (GET /api/subscriptions/public/groups/default). One group per payer type is the default; setting it here unsets it on the other groups of the same payerType.
- `payerType` (one of `workspace` or `user`): Who the plans in this group are for: "workspace" (a workspace pays, the default) or "user" (a person pays). Locked once a version of the group is published.
- `plans` (array of string, required): Plans on this page, in display order. Each is pinned at its current published version.
- `requiredItems` (array of string): Items recorded on the group version as required (stored as requiredItems and returned with the group). No server rule acts on it today.
- `versionDescription` (string): Note stored on each new group version, such as what changed. Empty clears it.

```json
{
  "billing": {
    "groups": {
      "main": {
        "name": "Main pricing",
        "default": true,
        "plans": ["free", "pro", "team"]
      }
    }
  }
}
```

### `billing.onboarding`

**Workspace onboarding.** One object at `billing.onboarding`.

The plan a new workspace starts on, stored on the organization settings (workspaceOnboarding). The plan is pinned at its current published version, and re-pinned whenever this apply publishes a new one.

- Refers to `billing.plans` (`plan`) by key.
- Applied after `billing.plans`.
- Never deleted by a blueprint.

Fields:

- `mode` (one of `none`, `trial` or `freemium`): "none": new workspaces start with no plan. "trial": they start a trial of `plan`. "freemium": they start on `plan`, which must be a freemium plan. Default "none".
- `plan` (string or null): Key of the billing.plans entry new workspaces start on. Use null to clear it.

```json
{ "billing": { "onboarding": { "mode": "freemium", "plan": "free" } } }
```

### `billing.customerBilling`

**Customer billing.** One object at `billing.customerBilling`.

What a customer may do to their own subscription, and what Stripe ending one for non-payment leads to. Stored on the organization settings (cancellation); every field left out keeps the org's value, and an org that never set one behaves as billing always has.

- Never deleted by a blueprint.

Fields:

- `cancel` (one of `period_end` or `immediately`): "`period_end`": a customer's cancel runs to the end of the period paid for (the default). "immediately": customers may also end it now.
- `refundOnCancelNow` (one of `none`, `prorated_credit` or `prorated_refund`): What ending a subscription now gives back for the unused time: "none" (the default), "`prorated_credit`" (a credit on the customer's next invoice) or "`prorated_refund`" (a refund to the card).
- `intervalSwitch` (boolean): Whether customers may move their plan to another billing interval themselves. Default true.
- `afterFailedPayments` (object): What Stripe ending a subscription after its failed payments leads to.

```json
{
  "billing": {
    "customerBilling": {
      "cancel": "immediately",
      "refundOnCancelNow": "prorated_credit",
      "intervalSwitch": true,
      "afterFailedPayments": { "then": "lapse_policy", "freeAfterDays": 30 }
    }
  }
}
```

### `billing.offers`

**Add-on.** A map at `billing.offers`: each key is the add-on slug the SDK buys it by (lowercase letters, digits and -; cannot change after creation). Keys match `^[a-z0-9][a-z0-9-]{1,99}$`.

Add-ons sold beside a plan, in every shape: "recurring" (a second item on the subscription, or a seat class), "`one_time`" (paid once, ends after validityDays), "lifetime" and "consumable" (a quota pack that counts down). An add-on is saved, then published (its Stripe prices minted) unless policy.publish is "never". Once published, a change to what a buyer holds (items, validity, limits on how many, trial rule, seat counting, whether a lifetime licence includes future versions) is a new version of the add-on, published per policy.publish, and the version it replaces stays with its buyers; a price change alone is a new price generation that new buyers pay. Its shape and payer type never change once published. Its name, description, badge, order, plans and visibility are edited in place. Money is always an integer in the currency's smallest unit. The organization's plan must include billing add-ons (402 otherwise), and publishing needs Stripe connected.

- Refers to `billing.items` (`items`) and `billing.plans` (`appliesToPlans`) by key.
- Applied after `app`, `billing.items` and `billing.plans`.
- Can be pruned: list `billing.offers` in `policy.prune`.

Fields:

- `name` (string, required): Name shown at checkout and on the pricing page. No leading or trailing spaces.
- `description` (string): What the buyer gets. Empty clears it.
- `shape` (one of `recurring`, `one_time`, `lifetime` or `consumable`, required): "recurring" bills with the subscription on its interval; "`one_time`" is paid once and ends after validityDays; "lifetime" is paid once and kept; "consumable" is a pack of quota units that counts down. Fixed once published.
- `payerType` (one of `workspace` or `user`): Who buys it: "workspace" (the default) or "user" (a person). A person's add-on grants user-scope items (billing.items with "scope": "user"), and a workspace's add-on workspace-scope items. Fixed once published.
- `items` (object): Items (keys of billing.items) one unit grants. Exactly these: an item left out is no longer granted by the next version. A consumable carries quota items only; a quota is granted by a recurring or consumable add-on only. At least one, except on a seat class.
- `prices` (object, required): Price per currency (lowercase ISO 4217 code). Exactly these currencies: a currency left out is no longer sold. To publish, every currency in app.currencies needs one.
- `validityDays` (integer or null): Days a `one_time` add-on lasts after purchase. Required on `one_time`, refused on the other shapes. Use null to clear it.
- `consumeOrder` (one of `before_plan` or `after_plan` or null): A consumable only: spend these units before the plan's allowance, or after it (the default). Use null to clear it.
- `rolloverCapUnits` (integer or null): A consumable only: at each usage reset, units above this many expire. Left out, nothing expires. Use null to clear it.
- `maxPerSubject` (integer or null): How many unexpired purchases one buyer may hold. A lifetime add-on defaults to 1. Use null to clear it.
- `maxQuantity` (integer or null): Most units one purchase may buy. Use null to clear it.
- `duringTrial` (one of `join_trial`, `refuse` or `convert_first` or null): A recurring add-on only: what buying it during a trial does. "`join_trial`" adds it to the trial, "refuse" refuses, "`convert_first`" ends the trial first. Left out or null, the organization's add-on setting decides. Use null to clear it.
- `seatClass` (object or null): A seat class: a recurring workspace add-on whose quantity follows the members this rule counts, as the plan's own seats do. It may grant no item. An add-on is a seat class or not for good once published. Use null to clear it.
- `includesFutureVersions` (boolean): A lifetime add-on only: each licence includes every later version of the add-on. Publishing a new version gives it to those licences through a move an admin confirms in the console, and apply reports that move. What a buyer holds: on a published add-on a change is a new version, and the licences already sold keep what they bought. Default false.
- `appliesToPlans` (array of string): Plans a buyer must be on to buy it. Empty or left out: every plan of its payer type.
- `sortOrder` (integer): Position in add-on lists, ascending.
- `badge` (string): A short label shown on the add-on, such as "Popular". Empty clears it.
- `internalOnly` (boolean): Hidden from the public add-on list: sold or granted by your own backend only.
- `versionMoves` (object): Version moves: what publishing a new version of the add-on does to the holders of the version it takes off sale. Only a recurring add-on (and a seat class) has holders a move can reach: what was bought outright (`one_time`, lifetime, consumable) never moves. A publish made by apply never moves anyone by itself: the move waits as "awaiting confirmation" until an admin confirms its preview in the console, and the plan says how many holders it would reach.

```json
{
  "billing": {
    "offers": {
      "extra-projects": {
        "name": "Extra projects",
        "shape": "recurring",
        "items": { "projects": { "quantity": 10 } },
        "prices": { "usd": { "monthly": 900, "yearly": 9000 } },
        "versionMoves": { "mode": "gains" }
      }
    }
  }
}
```

### `billing.coupons`

**Coupon.** A map at `billing.coupons`: each key is a name for the entry in this file (letters, digits, - and `_`); the coupon itself is found by its name. Keys match `^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$`.

Discounts in the organization's Stripe account, and the promotion codes buyers type to get them. Create-only: a coupon's terms (what it takes off, for how long, how often) never change once it exists. An entry is matched by its name: a coupon of the same name and terms is bound to it, not created again, and one of the same name with other terms is refused before anything is written. Promotion codes are created and switched on and off (active); a code's restrictions never change either. Codes the file leaves out are left as they are. Needs Stripe connected (billing.stripe).

- Applied after `billing.stripe`.
- Can be pruned: list `billing.coupons` in `policy.prune`.

Fields:

- `name` (string, required): The coupon name buyers see on checkout and invoices, and what this entry finds it by. Up to 40 characters.
- `percentOff` (number): Percent taken off, above 0 and at most 100. Give this or amountOff.
- `amountOff` (integer): Amount taken off. Give this or percentOff. An integer in the currency's smallest unit (cents for USD and EUR, yen for JPY, which has none): 2900 is $29.00, 2900 in JPY is 2900 yen.
- `currency` (one of 21 values): The currency of amountOff (lowercase ISO 4217 code). Only with amountOff.
- `duration` (one of `once`, `repeating` or `forever`, required): "once" takes it off the first invoice; "repeating" for durationInMonths months; "forever" on every invoice.
- `durationInMonths` (integer): Months a "repeating" coupon applies. Only with "repeating".
- `appliesToProducts` (array of string): Stripe products it is limited to. Left out, it applies to everything. Product ids belong to one Stripe account and mode: write `${env:NAME}` to vary them.
- `maxRedemptions` (integer): How many times it may be redeemed in all.
- `redeemBy` (string): The last moment it may be redeemed, as a date and time (2026-12-31T23:59:59Z). Must be in the future when the coupon is created.
- `promotionCodes` (object): Promotion codes on this coupon, by the code a buyer types (letters, digits, - and `_`, up to 64). Codes left out are left as they are.

```json
{
  "billing": {
    "coupons": {
      "launch": {
        "name": "Launch 20%",
        "percentOff": 20,
        "duration": "repeating",
        "durationInMonths": 3,
        "promotionCodes": { "LAUNCH20": { "maxRedemptions": 500 } }
      }
    }
  }
}
```

## RBAC

Console roles for team members and API keys.

### `rbac.roles`

**Role.** A map at `rbac.roles`: each key is the role name (lowercase). Keys match `^[a-z][a-z0-9_-]{1,49}$`.

Custom roles. "user" roles are given to team members in the console; "api" roles are given to API keys. The built-in owner, admin and user roles cannot be changed. A role can only grant what the key applying the blueprint itself holds.

- Can be pruned: list `rbac.roles` in `policy.prune`.

Fields:

- `kind` (one of `user` or `api`): Who the role is for: team members ("user", the default) or API keys ("api"). Fixed once created.
- `permissions` (object, required): Console resource to allowed actions, e.g. `{ "subscription_plans": ["read", "update"] }`. Resources left out are not granted.

```json
{
  "rbac": {
    "roles": {
      "support": {
        "kind": "user",
        "permissions": {
          "users": ["read", "update"],
          "emails_templates": ["read"]
        }
      }
    }
  }
}
```

### `rbac.apiKeys`

**API key.** A map at `rbac.apiKeys`: each key is a name you choose for the key. Keys match `^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$`.

Org API keys (orgId:secret) for your servers, CI and agents. The secret appears once, in the result of the apply that creates the key. A key's role is fixed at creation: to change it, add a new key and deactivate the old one.

- Refers to `rbac.roles` (`role`) by key.
- Applied after `rbac.roles`.
- Can be pruned: list `rbac.apiKeys` in `policy.prune`.

Fields:

- `name` (string, required): Name shown in the console.
- `description` (string): What the key is for.
- `role` (string): An "api" role from rbac.roles, or a built-in role (admin, content-reader). Default: the role of whoever applies.
- `expiresAt` (string): Expiry as an ISO date-time. Fixed at creation.
- `active` (boolean): Accepted by the API. false switches the key off without deleting it.

```json
{
  "rbac": {
    "apiKeys": { "backend": { "name": "Backend server", "role": "admin" } }
  }
}
```

## Webhooks

**Webhook endpoint.** A map at `webhooks`: each key is a name you choose for the endpoint (it only lives in this file). Keys match `^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$`.

Outbound webhook endpoints. Each receives a signed HTTPS POST for every event it subscribes to. The signing secret is generated by BuildBase and returned once, in the apply result of the run that creates the endpoint.

- Can be pruned: list `webhooks` in `policy.prune`.

Fields:

- `name` (string, required): Name shown in the console.
- `description` (string): What the endpoint is for.
- `url` (string, required): HTTPS URL that receives the events. Private and internal addresses are refused.
- `events` (array, each one of 200 values, required): Events to send. The webhook event catalog lists them (user.registered, subscription.created, ...).
- `enabled` (boolean): Deliver events to this endpoint. Default true.

```json
{
  "webhooks": {
    "crm": {
      "name": "CRM sync",
      "url": "https://hooks.example.com/buildbase",
      "events": ["user.registered", "subscription.created"]
    }
  }
}
```

## Integrations

Outside services the console connects to: Slack admin alerts, tracking scripts, web push keys, deploy hooks and the monday.com app.

### `integrations.slack`

**Slack alerts.** One object at `integrations.slack`.

Post admin alerts (new sign-ups, subscriptions, failed payments...) to a Slack channel through an incoming webhook.

- Secrets, written as `${env:NAME}`: `webhookUrl`.
- Never deleted by a blueprint.

Fields:

- `webhookUrl` (string): Slack incoming-webhook URL (https://hooks.slack.com/services/...). Write-only: put `${env:NAME}` here and supply the value when you apply. It is never shown again.
- `events` (array, each one of 69 values): Events to post. An empty list posts nothing.

```json
{
  "integrations": {
    "slack": {
      "webhookUrl": "${env:SLACK_WEBHOOK_URL}",
      "events": ["user.registered", "subscription.created"]
    }
  }
}
```

### `integrations.tracking`

**Tracking script.** A map at `integrations.tracking`: each key is a name you choose for the script (auth clients refer to it by this key). Keys match `^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$`.

Analytics and ad tags for the hosted sign-in page, gated on visitor consent. Known providers: gtm, ga4, `google_ads`, `meta_pixel`, `x_pixel`, linkedin, tiktok, reddit, `bing_uet`, clarity, hotjar, posthog, plausible, ahrefs; anything else is "external" with its script URL.

- Secrets, written as `${env:NAME}`: `serverSide.secret`.
- Can be pruned: list `integrations.tracking` in `policy.prune`.

Fields:

- `name` (string, required): Name shown in the console.
- `provider` (one of 15 values, required): Which tag this is. "external" loads any https script by URL.
- `providerId` (string): The provider's id for your property, e.g. GTM-ABC1234 (gtm), G-ABCD123456 (ga4), AW-123456789 (`google_ads`). Not used by "external".
- `host` (string): Self-hosted analytics host, for providers that allow one (posthog, plausible).
- `src` (string): Script URL of an "external" script: https, no query string.
- `dataAttributes` (object): `data-*` attributes on an "external" script tag.
- `consentCategory` (one of `analytics` or `marketing`): Which consent the visitor must give before it loads. Defaults to the provider's usual category.
- `conversionIds` (object): Event name to the provider conversion id, for ad providers that need one per event.
- `vendorName` (string): Vendor named in the consent banner ("external").
- `privacyUrl` (string): Vendor privacy policy linked from the consent banner ("external").
- `enabled` (boolean): Load it. Default true.
- `serverSide` (object): Server-side conversions (meta and ga4 only).

```json
{
  "integrations": {
    "tracking": {
      "analytics": {
        "name": "Google Analytics",
        "provider": "ga4",
        "providerId": "G-ABCD123456"
      }
    }
  }
}
```

### `integrations.push`

**Web push.** One object at `integrations.push`.

Web push notifications. enabled: true generates the organization's VAPID key pair once (a plan with push notifications is needed); the SDK reads the public key itself. The keys and their subject cannot be changed or removed afterwards.

- Never deleted by a blueprint.

Fields:

- `enabled` (`true`, required): Generate the VAPID keys if the organization has none. Always true: keys, once made, stay.
- `subject` (string): Contact push services may use about your sends: mailto:you@example.com or an https URL. Fixed when the keys are made. Default: the platform contact.

```json
{
  "integrations": {
    "push": { "enabled": true, "subject": "mailto:ops@acme.example.com" }
  }
}
```

### `integrations.deployHooks`

**Deploy hook.** A map at `integrations.deployHooks`: each key is a name you choose for the hook (it only lives in this file). Keys match `^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$`.

Build hooks of your hosting provider (Vercel, Netlify, Render, Cloudflare Pages) that BuildBase calls when content is published, so your site rebuilds. A hook URL lets anyone who holds it start a build: write it as `${env:NAME}` to keep it out of the file. Only the secret can be edited in place; a changed title, description or URL replaces the hook.

- Secrets, written as `${env:NAME}`: `webhookSecret`.
- Can be pruned: list `integrations.deployHooks` in `policy.prune`.

Fields:

- `title` (string, required): Name shown in the console.
- `description` (string): What the hook rebuilds. Empty, or at least 2 characters.
- `url` (string, required): The provider's deploy hook URL (Vercel: https://api.vercel.com/v1/integrations/deploy/..., Netlify: `https://api.netlify.com/build_hooks/...`). Private and internal addresses are refused.
- `webhookSecret` (string): Vercel only: the secret Vercel shows when you add its deployment webhook, so BuildBase can trust the build status events it sends. Write-only: put `${env:NAME}` here and supply the value when you apply. It is never shown again.

```json
{
  "integrations": {
    "deployHooks": {
      "site": {
        "title": "Marketing site",
        "url": "https://api.vercel.com/v1/integrations/deploy/prj_abc123/hook456"
      }
    }
  }
}
```

### `integrations.monday`

**monday.com app.** One object at `integrations.monday`.

Your monday.com app (Developer Center > your app > Basic information), so BuildBase can run its install flow and verify the events monday sends. Connecting needs a plan with the monday.com integration.

- Secrets, written as `${env:NAME}`: `clientSecret` and `signingSecret`.
- Can be pruned: list `integrations.monday` in `policy.prune`.

Fields:

- `clientId` (string, required): The app's Client ID.
- `clientSecret` (string, required): The app's Client Secret. Write-only: put `${env:NAME}` here and supply the value when you apply. It is never shown again.
- `signingSecret` (string, required): The app's Signing Secret, which signs the events monday sends. Write-only: put `${env:NAME}` here and supply the value when you apply. It is never shown again.
- `appId` (integer, required): The app's numeric App ID.
- `appVersionId` (integer or null): An App version ID, only for letting collaborators authorize a draft version. Use null to clear it.
- `applicationEventWebhook` (string): Your own URL that BuildBase forwards install and uninstall events to. Empty means not set.
- `applicationAuthenticationUrl` (string): Your own URL installers are sent to after authorizing. Empty means not set.
- `requiredScopes` (array of string): Scopes the app needs; an installer whose token lacks one goes through consent again. Order does not matter.

```json
{
  "integrations": {
    "monday": {
      "clientId": "abc123",
      "clientSecret": "${env:MONDAY_CLIENT_SECRET}",
      "signingSecret": "${env:MONDAY_SIGNING_SECRET}",
      "appId": 10123456,
      "requiredScopes": ["boards:read", "me:read"]
    }
  }
}
```

## Data

The app data model: user and audience attributes and tags, collections, forms and their captcha, end-user feature flags and storage settings.

### `data.userAttributes`

**User attribute.** A map at `data.userAttributes`: each key is the attribute key values are stored under (lowercase letters and digits joined by single hyphens, at least 2 characters). Fixed once created: a new key is a new attribute. Keys match `^(?=.{2,100}$)[a-z0-9]+(?:-[a-z0-9]+)*$`.

Custom fields on every end user (Users > Attributes): plan tier, company size, a birthday. Your app reads and writes them with the SDK; workflows and segments use them. Deleting one (prune) deletes its values too.

- Can be pruned: list `data.userAttributes` in `policy.prune`.

Fields:

- `name` (string, required): Label shown in the console and the SDK profile forms.
- `type` (one of `string`, `boolean`, `number`, `dropdown` or `date`, required): Value type: string, number, boolean, date (YYYY-MM-DD or a full ISO date-time in UTC) or dropdown (one of options).
- `description` (string): Help text for the attribute. Empty, or at least 2 characters. Default empty.
- `options` (array of object): The choices of a dropdown attribute, in display order. Required (at least one) for type dropdown, not allowed for other types. Replaced whole when stated.
- `defaultValue` (string or number or boolean): Value a record gets until one is set. Must match the type: a number for number, true or false for boolean, a date string for date, one of the option values for dropdown, text for string. Leaving it out keeps what is set; it cannot be cleared through the API.
- `allowPublicEdit` (boolean): Users may change the value themselves through the SDK. Default false.
- `allowPublicView` (boolean): Users may read the value through the SDK. Default false.

```json
{
  "data": {
    "userAttributes": {
      "company-size": {
        "name": "Company size",
        "type": "dropdown",
        "options": [
          { "label": "1-10", "value": "small" },
          { "label": "11-200", "value": "medium" }
        ],
        "defaultValue": "small",
        "allowPublicEdit": true,
        "allowPublicView": true
      }
    }
  }
}
```

### `data.audienceAttributes`

**Audience attribute.** A map at `data.audienceAttributes`: each key is the attribute key values are stored under (lowercase letters and digits joined by single hyphens, at least 2 characters). Fixed once created: a new key is a new attribute. Keys match `^(?=.{2,100}$)[a-z0-9]+(?:-[a-z0-9]+)*$`.

Custom fields on every audience contact (Audience > Attributes), for email campaigns, forms and workflows. Deleting one (prune) deletes its values too.

- Can be pruned: list `data.audienceAttributes` in `policy.prune`.

Fields:

- `name` (string, required): Label shown in the console and the SDK profile forms.
- `type` (one of `string`, `boolean`, `number`, `dropdown` or `date`, required): Value type: string, number, boolean, date (YYYY-MM-DD or a full ISO date-time in UTC) or dropdown (one of options).
- `description` (string): Help text for the attribute. Empty, or at least 2 characters. Default empty.
- `options` (array of object): The choices of a dropdown attribute, in display order. Required (at least one) for type dropdown, not allowed for other types. Replaced whole when stated.
- `defaultValue` (string or number or boolean): Value a record gets until one is set. Must match the type: a number for number, true or false for boolean, a date string for date, one of the option values for dropdown, text for string. Leaving it out keeps what is set; it cannot be cleared through the API.
- `allowPublicEdit` (boolean): Contacts may change the value themselves through the SDK. Default false.
- `allowPublicView` (boolean): Contacts may read the value through the SDK. Default false.

```json
{
  "data": {
    "audienceAttributes": {
      "lead-score": {
        "name": "Lead score",
        "type": "number",
        "defaultValue": 0
      }
    }
  }
}
```

### `data.userTags`

**User tag.** A map at `data.userTags`: each key is the tag slug (lowercase letters and digits joined by single hyphens, at least 2 characters), which the SDK and workflows use. Keys match `^(?=.{2,100}$)[a-z0-9]+(?:-[a-z0-9]+)*$`.

Labels on end users (Users > Tags), for segments, feature flag targeting and workflows. Tags cannot be deleted through the API: one removed from the file is left as it is.

- Never deleted by a blueprint.

Fields:

- `title` (string, required): Name shown in the console. Unique: two tags cannot share a title.

```json
{ "data": { "userTags": { "vip": { "title": "VIP customer" } } } }
```

### `data.audienceTags`

**Audience tag.** A map at `data.audienceTags`: each key is the tag slug (lowercase letters and digits joined by single hyphens, at least 2 characters), which the SDK and workflows use. Keys match `^(?=.{2,100}$)[a-z0-9]+(?:-[a-z0-9]+)*$`.

Labels on audience contacts (Audience > Tags), for campaigns, lists and workflows (the add-tag action refers to one). Tags cannot be deleted through the API: one removed from the file is left as it is.

- Never deleted by a blueprint.

Fields:

- `title` (string, required): Name shown in the console. Unique: two tags cannot share a title.

```json
{ "data": { "audienceTags": { "newsletter": { "title": "Newsletter" } } } }
```

### `data.collections`

**Collection.** A map at `data.collections`: each key is the collection slug, which apps read records by (GET `/api/collections/data/<slug>`). Fixed once created. Keys match `^[a-z0-9][a-z0-9_-]{1,99}$`.

Structured content and form data (Content > Collections). The fields describe the live version: changing them creates a new version and publishes it unless policy.publish is "never" (then it stays a draft, and later plans compare against that draft). Records stay with the version they were written to. A collection stated without fields gets no version.

- Versioned: a change creates a new version instead of editing the live one.
- Can be pruned: list `data.collections` in `policy.prune`.

Fields:

- `name` (string, required): Name shown in the console.
- `fields` (array of object): The fields of the live version, in display order. Replaced whole when stated: a field left out is not in the new version.

```json
{
  "data": {
    "collections": {
      "leads": {
        "name": "Leads",
        "fields": [
          {
            "slug": "email",
            "title": "Email",
            "type": "email",
            "required": true
          },
          { "slug": "company", "title": "Company", "type": "text" }
        ]
      }
    }
  }
}
```

### `data.formProtection`

**Form captcha.** One object at `data.formProtection`.

Which captcha forms use when protection.captcha is on. "managed" (default) is BuildBase's own (Cloudflare Turnstile when the platform holds keys, else a built-in proof-of-work challenge); "builtin" is always the proof-of-work challenge; turnstile, hcaptcha and recaptcha use your own keys, checked with the provider when saved.

- Secrets, written as `${env:NAME}`: `secretKey`.
- Never deleted by a blueprint.

Fields:

- `mode` (one of `managed`, `builtin`, `turnstile`, `hcaptcha` or `recaptcha`, required): managed (default), builtin, or your own turnstile, hcaptcha or recaptcha keys.
- `siteKey` (string): Your site key, for turnstile, hcaptcha or recaptcha. Public by design: it is rendered into the form page.
- `secretKey` (string): Your secret key, which goes with siteKey. Checked with the provider when saved. Write-only: put `${env:NAME}` here and supply the value when you apply. It is never shown again.

```json
{ "data": { "formProtection": { "mode": "builtin" } } }
```

### `data.forms`

**Form.** A map at `data.forms`: each key is a name you choose for the form (workflows refer to it by this key). Keys match `^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$`.

Public forms whose submissions become records of a collection (Content > Forms). A form writes into its collection's live version and follows it: when this apply publishes a new version of the collection, the form moves to it. Forms cannot be deleted, only archived.

- Refers to `data.collections` (`collection`) by key.
- Applied after `data.collections`.
- Can be pruned: list `data.forms` in `policy.prune`.

Fields:

- `name` (string, required): Internal name, shown in the console. Unique across the organization.
- `title` (string, required): Heading on the hosted form.
- `description` (string, required): Text under the heading on the hosted form.
- `collection` (string, required): The data.collections key whose records submissions become. The collection needs a live version (state its fields).
- `published` (boolean): The form accepts submissions at its public URL. Default false.
- `archived` (boolean): Archived forms are hidden and refuse submissions; restoring one brings it back unpublished. Removing a form from the file never archives it unless you prune.
- `protection` (object): Spam protection for this form. Fields left out keep their value.
- `subscribe` (object): Collect email subscribers from this form. Fields left out keep their value.

```json
{
  "data": {
    "forms": {
      "contact": {
        "name": "Contact us",
        "title": "Get in touch",
        "description": "We answer within a day.",
        "collection": "leads",
        "published": true,
        "protection": { "captcha": true, "maxLinks": 2 }
      }
    }
  }
}
```

### `data.featureFlags`

**Feature flag.** A map at `data.featureFlags`: each key is the feature id your app checks with the SDK (useUserFeatures().isFeatureEnabled). Fixed once created. Keys match `^[A-Za-z0-9][A-Za-z0-9_.-]{1,99}$`.

Feature flags for end users (Users > Feature flags), checked per user with the SDK. Workspace flags are workspaces.features. Targeting individual users, lists or tags stays in the console. Flags cannot be deleted, only archived.

- Can be pruned: list `data.featureFlags` in `policy.prune`.

Fields:

- `name` (string, required): Name shown in the console.
- `description` (string, required): What the flag turns on.
- `enabled` (boolean): The flag is live. Off, it is off for everyone whatever it targets. Default false.
- `allowAll` (boolean): On for every user while enabled, rather than only the users it targets. Default false.
- `archived` (boolean): Archived flags are off for everyone and hidden. Default false.

```json
{
  "data": {
    "featureFlags": {
      "new-editor": {
        "name": "New editor",
        "description": "The rebuilt document editor",
        "enabled": true,
        "allowAll": true
      }
    }
  }
}
```

### `data.storage`

**Storage settings.** One object at `data.storage`.

File storage settings (Content > Assets): stop uploads at the plan allowance instead of billing overage, and what end users may upload through the SDK (workspace images, profile pictures).

- Never deleted by a blueprint.

Fields:

- `hardStop` (boolean): Stop uploads at the plan's included storage instead of billing overage beyond it. Default false.
- `endUserUploads` (object): What end users may upload through the SDK. Fields left out keep their value.

```json
{
  "data": {
    "storage": {
      "hardStop": true,
      "endUserUploads": { "maxFileBytes": 2097152, "perUserBytes": 52428800 }
    }
  }
}
```

## Workflows

**Workflow.** A map at `workflows`: each key is a name you choose for the workflow (it only lives in this file). Keys match `^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$`.

Automations: a trigger node, then actions and conditions joined by edges, in the shape the console exports (Automations > Workflows > Export). Apply saves the flow as the draft and publishes it as a new version unless policy.publish is "never" or status is "draft"; running instances keep the version they started on. Node configs may refer to email templates, senders, forms, tags and collections of this file with `{ "$ref": "<kind>.<key>" }`.

- Applied after `email.templates`, `email.senders`, `data.forms`, `data.userTags`, `data.audienceTags` and `data.collections`.
- Can be pruned: list `workflows` in `policy.prune`.

Fields:

- `name` (string, required): Name shown in the console. A workflow is matched by it the first time.
- `description` (string): What the workflow does. Empty, or at least 2 characters. Default empty.
- `status` (one of `active`, `paused` or `draft`): active (default): published and running. paused: published, new triggers are held and running instances wait. draft: saved, not running (a published workflow is paused and moved back to draft).
- `trigger` (object): The trigger, as the console export writes it. Optional: it is always taken from the flow's trigger node on publish, so when stated it must match that node.
- `flow` (object, required): The flow graph. Compared whole against the saved draft, node by node and edge by edge.
- `reEntry` (object): Whether the same person can enter the workflow again.
- `frequencyCap` (object): Per-person send caps. Stated fields are merged over the current caps.
- `executionLimits` (object): Safety limits on how much the workflow runs. Stated fields are merged over the current limits.
- `exitConditions` (array of object): Conditions checked before every action; when met, the instance stops. Replaced whole when stated.
- `exitConditionsCombinator` (one of `AND` or `OR`): OR (default): stop when any condition is met. AND: only when all are.

```json
{
  "workflows": {
    "welcome": {
      "name": "Welcome email",
      "flow": {
        "nodes": [
          {
            "id": "trigger",
            "type": "triggers.user.registered",
            "kind": "trigger"
          },
          {
            "id": "send",
            "type": "actions.send_email",
            "kind": "action",
            "data": {
              "config": {
                "mode": "template",
                "templateId": { "$ref": "email.templates.welcome" },
                "senderId": { "$ref": "email.senders.main" },
                "to": "{{trigger.email}}"
              }
            }
          }
        ],
        "edges": [{ "id": "e1", "source": "trigger", "target": "send" }]
      }
    }
  }
}
```



## Next steps

- [File format](/blueprints/file-format) - each section with a short example, secrets, and how to read a plan
- [Troubleshooting](/blueprints/troubleshooting) - every message, what it means and what to do
- [Sign-in setup guides](/sign-in-setup/overview) - what each `auth` setting does in the console
- [Admin API](/reference/admin-api) - the API keys a blueprint runs with
