Write a blueprint file, section by section
Each section of a blueprint with a short example, secrets, how to read a plan, and what a blueprint cannot do.
A blueprint is a text file in JSON (JavaScript Object Notation). This page explains the file one section at a time. Every field, with its allowed values, is in the file reference.
{
"$schema": "https://docs.buildbase.app/schemas/blueprint.v1.json",
"version": 1,
"app": {
"name": "Acme",
"currencies": ["usd", "eur"]
}
}How JSON is written
- An object is a list of names and values inside curly brackets
{ }. Each name is in double quotes, followed by a colon and its value. - A value is text in double quotes (
"Acme"), a number (4900),trueorfalsewithout quotes, an object, or a list. - A list is values inside square brackets
[ ], such as["usd", "eur"]. - A comma goes between two entries, and never after the last one.
- JSON has no comments. Every character in the file is part of the blueprint.
A code editor such as Visual Studio Code checks the file as you type when the first line names the schema with "$schema", as above. The schema is the description of every field the file may hold, published at https://docs.buildbase.app/schemas/blueprint.v1.json.
The top level
| Field | Required | What it holds |
|---|---|---|
version | Yes | Always 1 |
$schema | No | The schema's address, so your editor can check the file |
policy | No | Whether apply may delete, and whether it publishes new versions. See prune |
app, workspaces, auth, email, billing, rbac, webhooks, integrations, data, workflows | No | The sections below. Write only the ones you want to manage |
Inside a section, some things are one object, such as app: you write the fields you want to set. Others are maps: several entries, each under a key you choose or the thing's own slug, such as "pro" in billing.plans.pro. A slug is a short name in lowercase letters, digits and hyphens. Other entries name an entry by its key.
A field you leave out stays as it is. A blueprint can manage one section of your organization and leave the rest alone.
Sections
app
The app's name, its main domain, and the currencies it sells in. Currencies are three-letter codes in lowercase, such as usd.
{
"app": {
"name": "Acme",
"domain": "app.acme.example",
"currencies": ["usd", "eur"]
}
}workspaces
How your users group into workspaces (teams), the roles inside a workspace, and the feature flags each workspace carries. A feature flag turns a feature on or off, and your app reads it with the SDK.
{
"workspaces": {
"mode": "platform",
"roles": ["admin", "editor", "viewer", "billing"],
"defaultRole": "editor",
"features": {
"beta-reports": {
"name": "Beta reports",
"description": "The new reports page",
"enabled": true
}
}
}
}auth
Sign-in: the methods people sign in with, the auth clients (one for each app or website that signs people in), the hosted sign-in page, two-step sign-in, sign-up rules and custom sign-in domains. Sign-in setup explains what each one does.
{
"auth": {
"methods": {
"password": { "type": "password", "title": "Email and password" },
"google": {
"type": "google",
"title": "Google",
"clientId": "1234-abc.apps.googleusercontent.com",
"clientSecret": "${env:GOOGLE_CLIENT_SECRET}"
}
},
"clients": {
"web": {
"title": "Web app",
"type": "normal",
"applicationTokenUrl": "${env:APP_URL}/api/auth/token",
"applicationProfileUrl": "${env:APP_URL}/api/auth/profile",
"redirectUrls": ["${env:APP_URL}/auth/callback"],
"methods": ["password", "google"]
}
},
"mfa": { "enforcement": "optional", "allowedFactors": ["totp"] }
}
}The client web names its methods by their keys, password and google. A key that is not defined in the file is an error that names it.
Your email provider key, sending domains, senders, unsubscribe groups, templates, merge tags and notification events. A merge tag is a value such as {{appName}} that BuildBase fills in for each email. A template that BuildBase made for you, such as welcome, is changed by its key.
{
"email": {
"templates": {
"welcome": {
"subject": "Welcome to {{appName}}, {{user.name}}",
"markdown": "# Welcome\n\nThanks for joining {{appName}}."
}
},
"mergeTags": {
"supportEmail": { "title": "Support Email", "value": "[email protected]" }
}
}
}billing
What your app sells: the Stripe connection, item categories, items (features, limits and quotas your app reads by slug), credit packages, plans, pricing groups, the plan a new workspace starts on, add-ons (offers) and coupons. Money is always a whole number in the currency's smallest unit: 4900 is 49.00 US dollars.
{
"app": { "currencies": ["usd"] },
"billing": {
"categories": { "core": { "name": "Core features" } },
"items": {
"projects": { "type": "limit", "name": "Projects", "category": "core" }
},
"plans": {
"free": {
"name": "Free",
"freemium": true,
"limits": { "projects": 3 },
"pricing": { "usd": { "monthly": 0 } }
},
"pro": {
"name": "Pro",
"limits": { "projects": 50 },
"pricing": { "usd": { "monthly": 4900, "yearly": 49000 } }
}
},
"groups": {
"main": {
"name": "Main pricing",
"default": true,
"plans": ["free", "pro"]
}
},
"onboarding": { "mode": "freemium", "plan": "free" }
}
}Plans are versioned. See versioned billing.
Add-ons are sold beside a plan, in every shape: recurring (billed with the subscription, or a seat class with seatClass), one_time (it ends after validityDays), lifetime and consumable (a pack of quota units). A lifetime add-on with "includesFutureVersions": true sells every later version with the licence: a new version of it reaches every licence that includes it through a move a console admin confirms, and apply reports that move. Coupons are matched by their name and only ever created; their promotion codes are created and switched on and off with active.
{
"billing": {
"categories": { "core": { "name": "Core features" } },
"items": {
"projects": { "type": "limit", "name": "Projects", "category": "core" }
},
"plans": {
"pro": {
"name": "Pro",
"versionMoves": { "mode": "gains" },
"limits": { "projects": 50 },
"pricing": { "usd": { "monthly": 4900 } }
}
},
"offers": {
"extra-projects": {
"name": "Extra projects",
"shape": "recurring",
"items": { "projects": { "quantity": 10 } },
"prices": { "usd": { "monthly": 900, "yearly": 9000 } }
},
"weekend-pass": {
"name": "Weekend pass",
"shape": "one_time",
"validityDays": 3,
"items": { "projects": { "quantity": 100 } },
"prices": { "usd": { "oneTime": 1500 } }
},
"lifetime-projects": {
"name": "Lifetime projects",
"shape": "lifetime",
"includesFutureVersions": true,
"items": { "projects": { "quantity": 25 } },
"prices": { "usd": { "oneTime": 19900 } }
}
},
"coupons": {
"launch": {
"name": "Launch 20%",
"percentOff": 20,
"duration": "repeating",
"durationInMonths": 3,
"promotionCodes": { "LAUNCH20": { "maxRedemptions": 500 } }
}
}
}
}Add-ons and credit packages are versioned too, and versionMoves decides what a new version does to the people who hold the old one. See versioned billing and version moves.
An item is a workspace's unless it says "scope": "user": then it is a person's. An add-on a person buys ("payerType": "user") grants user-scope items only, and a workspace's add-on grants workspace-scope items only. The plan refuses an add-on whose items are not declared for its payer, before anything is written. A user-scope feature has no workspace feature flag. An item's scope never changes once it exists: a person's item gets a slug of its own.
{
"billing": {
"categories": { "core": { "name": "Core features" } },
"items": {
"devices": {
"type": "limit",
"scope": "user",
"name": "Devices",
"category": "core"
}
},
"offers": {
"more-devices": {
"name": "More devices",
"shape": "lifetime",
"payerType": "user",
"items": { "devices": { "quantity": 3 } },
"prices": { "usd": { "oneTime": 1900 } }
}
}
}
}rbac
RBAC (role-based access control) holds the console roles for your team and for API keys, and the API keys themselves. A role can grant only what the key that applies the blueprint holds.
{
"rbac": {
"roles": {
"support": {
"kind": "user",
"permissions": { "users": ["read", "update"] }
}
},
"apiKeys": { "backend": { "name": "Backend server", "role": "admin" } }
}
}A new API key is a shown-once value: you get it once, in the result of the apply that creates it.
webhooks
Addresses BuildBase sends a message to when something happens, and the events each one receives.
{
"webhooks": {
"crm": {
"name": "CRM sync",
"url": "https://hooks.acme.example/buildbase",
"events": ["user.registered", "subscription.created"]
}
}
}integrations
Slack alerts for your team, analytics and ad tags for the hosted sign-in page, web push keys, your hosting provider's deploy hooks and your monday.com app.
{
"integrations": {
"slack": {
"webhookUrl": "${env:SLACK_WEBHOOK_URL}",
"events": ["user.registered"]
},
"tracking": {
"analytics": {
"name": "Google Analytics",
"provider": "ga4",
"providerId": "G-ABCD123456"
}
}
}
}data
Your app's data model: user and audience attributes and tags, collections and the forms that write into them, form captcha, feature flags for your end users, and file storage settings.
{
"data": {
"userAttributes": {
"company-size": { "name": "Company size", "type": "string" }
},
"collections": {
"leads": {
"name": "Leads",
"fields": [
{
"slug": "email",
"title": "Email",
"type": "email",
"required": true
}
]
}
},
"forms": {
"contact": {
"name": "Contact form",
"title": "Get in touch",
"description": "Replies within a day.",
"collection": "leads",
"published": true
}
}
}
}workflows
Automations, in the shape the console exports them: a trigger, then actions and conditions joined by edges. A step that uses a template, sender, form, tag or collection names it by its key in the same file, such as { "$ref": "email.templates.welcome" }, so the workflow works in any organization. The file reference has a whole example.
Secrets and ${env:NAME}
A blueprint is meant to be shared, kept in Git and given to AI agents, so it never holds a secret. Write ${env:NAME} where a secret goes, with a name of your choice in capital letters, and give the value when you plan or apply: in the console's Values for ${env:...} fields, or as an environment variable for the command line.
{
"billing": {
"stripe": {
"publishableKey": "${env:STRIPE_PUBLISHABLE_KEY}",
"secretKey": "${env:STRIPE_SECRET_KEY}"
}
}
}- A secret field must be exactly
"${env:NAME}". Anything else there is an error. Secret fields include client secrets, the Stripe secret key, email provider keys and passwords, and the Slack webhook address. The file reference lists them under each kind. - A real secret anywhere in the file is an error, such as a Stripe secret key, a private key, or a GitHub, Slack, Mailgun, Google or SendGrid key, even in a field that is not secret.
- Any other text can contain a name, such as
"${env:APP_URL}/auth/callback". Every name used this way must have a value, or the file cannot be planned. - A secret without a value is not compared, and apply keeps the value already saved. A new thing that needs the secret is Blocked until you give it.
- Secrets cannot be read back. BuildBase keeps a fingerprint of each secret it wrote, so the plan can say a secret changed without showing either value.
- Export writes names instead of secrets, made from where each one sits, such as
${env:BILLING_STRIPE_SECRETKEY}, and lists the names to set.
Reading a plan
A plan has one line for each thing that would change: a symbol, the action, its name and its address in the file in square brackets. Changed fields follow, old value first, then any note: that says what to do. The console shows the same plan with a badge in place of the symbol. This is a plan as the command line prints it:
~ update App settings [app]
name: "My app" -> "Acme"
+ create Sign-in method "Google" (google) [auth.methods.google]
+ create Auth client "Web app" [auth.clients.web]
~ update Stripe connection [billing.stripe]
secretKey: (secret, cannot be read back, so it is written)
^ version Plan "Pro" [billing.plans.pro]
pricing.usd.monthly: 3900 -> 4900
+ create Sending domain mail.acme.example [email.domains.mail.acme.example]
note: After it is added, publish the DNS records apply lists for mail.acme.example, then verify it in Messaging > Sender accounts > Domains.
+ create Webhook "CRM sync" [webhooks.crm]
Not in the file, left alone: 1 webhooks
Plan: 4 to create, 2 to update, 1 new versions, 0 to delete, 1 unchanged.| Symbol | Action | Console badge | What it means | What apply does |
|---|---|---|---|---|
+ | create | Create | It is not in your organization yet | Creates it |
~ | update | Change | It exists and differs from the file | Changes the fields listed |
^ | version | New version | What a plan, pricing group, add-on, held credit package or collection sells changed | Adds a new version |
- | delete | Delete | This blueprint created it, the file no longer lists it, and you turned prune on | Deletes it |
! | manual | You do this step | Only a person can do this step, such as adding DNS records or a browser sign-in | Skips it. The note says what to do |
x | blocked | Blocked | BuildBase would refuse it, such as a type that cannot change or a secret with no value | Skips it. The note says why |
? | conflict | Conflict | More than one thing in your organization matches the entry | Leaves it alone |
? | unknown | Cannot check | It could not be compared: a secret has no value, or the API key cannot read that part | Skips it |
= | noop | Not shown | It already matches the file. The command line lists these with --verbose | Nothing |
. | unmanaged | Not shown | It is in your organization but not in the file. Counted under "Not in the file" | Never touches it |
A secret change reads (secret, changed), or (secret, cannot be read back, so it is written) the first time. Neither value is shown.
The last line counts each action. Apply is possible when it would create, change, version or delete something.
How a plan finds what already exists
- An entry is matched by its key when the thing has a natural one, such as a slug or a domain name. Things without one, such as a webhook, are matched by what you wrote, such as its address.
- The first apply records which thing each key stands for. Later plans match on that record, even after someone renames the thing in the console.
- Apply makes the changes in order, so a thing is created before anything that names it: methods before the clients that list them, items before the plans that use them.
- If a step fails, apply stops. The changes already made stay, the rest are Not run, and the plan made after apply shows what is left.
Prune: deleting what the file no longer lists
Removing an entry from the file never deletes anything by itself. The plan lists the thing as not in the file, and says so when this blueprint created it. It is deleted only when both are true:
- Its kind is listed in
policy.prunein the file. - You turn prune on: Delete what the file no longer lists in the console,
--pruneon the command line, or"prune": truein the API.
{
"version": 1,
"policy": { "prune": ["webhooks", "auth.clients"], "publish": "always" }
}Even then, only things this blueprint created, or matched on an earlier apply, can be deleted. Something made by hand that the file never named stays. Some kinds are archived or deactivated instead of deleted, such as feature flags, forms, merge tags and API keys. The file reference says which kinds can be pruned.
Publish: versioned resources
policy.publish decides what happens to a plan, pricing group, add-on, held credit package, collection or workflow that changed:
"always", the default, publishes the new version, so the change is live."never"leaves it as a draft for someone to publish in the console. A pricing group or a new-workspace plan that needs an unpublished plan version is then Blocked.
Versioned billing
What a subscriber bought never changes under them.
- A plan is versioned. A change to its features, limits, quotas, prices, trial or credits makes a new version. People who already pay stay on the version they bought. New subscribers get the new version.
- A pricing group pins plan versions. When a plan in a group gets a new version, the group gets a new version that uses it.
- A pricing group sells its plans to one kind of payer (
payerType), and the plan checks every plan a group lists against it, by the same rules publishing uses, before anything is written. A plan sold to people has no seats (unless the console prices it per installation), no quota counted per member, and no credits yet. A plan sold to workspaces is not priced per installation, and a quota it counts per member is a hard allowance whose shared pool, if it has one, has a size. One plan is sold to one kind of payer: a plan another group already sells to the other kind is refused. - The plan a new workspace starts on moves to the version this apply published.
- Some things are fixed once published, such as whether a plan is free (
freemium). Make a new plan to change it. - Currencies can be added, never removed. A currency your organization sells in that the file leaves out stays, and the plan says so. Every published paid plan needs a price in each currency.
- An add-on is versioned once it is published. A change to what a buyer holds (its items, validity, how many one buyer may hold or buy, what buying during a trial does, which members a seat class counts, or whether a lifetime licence includes future versions) makes a new version. Buyers keep the version they bought. A price change alone is a new price on the version on sale: new buyers pay it, and everyone who already pays keeps their price. Its name, description, badge, order, plans and visibility change in place.
- An add-on's shape and payer type never change once it is published, and neither does whether it is a seat class. The plan refuses such a change before anything is written and says what to do instead: sell a new add-on under another key, and take the old one off sale.
- A credit package is versioned once something holds it: a published plan version grants it, an auto top-up buys it, or someone bought or was granted it. Then a change to its credits, its validity or the amount of a currency it is priced in makes a new version, and what was bought keeps its terms. A package nothing holds is changed in place, and so is a new currency on any package.
- A coupon is never changed. An entry binds the coupon of the same name and the same terms, so applying twice makes one coupon. A coupon of the same name with other terms is refused before anything is written: give the entry another name, or write the terms the coupon has. A promotion code's restrictions never change either, only whether it is
active.
Collections and workflows are versioned the same way. Records stay with the collection version they were written to, and running workflows keep the version they started on.
Version moves
versionMoves on a plan, an add-on or a credit package decides what publishing a new version does to the people who hold the version it replaces:
"off", the default, leaves them on the version they have."gains"moves them only where the new version takes nothing away from them."all"moves gains at once, and moves a loss at each holder's renewal afternoticeDaysof notice (30 by default).
Setting or changing versionMoves is a plain change, not a new version. When apply publishes a new version with it on, the plan says how many holders a move would reach, with a link to the console when one is known:
^ version Plan "Pro" [billing.plans.pro]
limits.projects: 50 -> 100
note: 12 holders of v3: a move waits for confirmation in the console after this publish (https://console.buildbase.app/dashboard/admin/subscriptions/plans/<id>/moves). versionMoves is "gains" (30 days of notice before a loss); the blueprint never starts, confirms or cancels a move.
Plan: 0 to create, 0 to update, 1 new versions, 0 to delete, 4 unchanged.
Moves: 1 publish with versionMoves on reach 12 holders. Each move waits for confirmation in the console; apply never starts one.Apply publishes, and nobody moves yet: the move waits until an admin opens its preview in the console and confirms it. Apply says how many holders each waiting move has, and where to confirm it. Only a console session starts, confirms or cancels a move, never a blueprint or an API key. What was bought outright (a one-time, lifetime or consumable add-on, or credits already bought or granted) never moves.
What a blueprint cannot do
Some steps need a person, or happen outside BuildBase. The plan shows them as You do this step, and apply skips them.
- DNS records. DNS (Domain Name System) is where a domain's settings live, at the company you bought the domain from. Apply adds a custom sign-in domain or an email sending domain, and lists the records to add. Add them at your DNS provider, then verify the domain in the console: sign-in domains under User management, Authentication, Domains, and sending domains under Messaging, Sender accounts, Domains.
- Connecting a Gmail sender. It needs a sign-in to Google in a browser. In the console, open Messaging, then Sender accounts, add a sender and choose Google.
- Uploading files. The hosted sign-in page's logo, dark logo, favicon and background image are uploaded in the console. A blueprint carries no files:
logoUrlsets an image address to show until a logo is uploaded. - Creating the organization. A blueprint sets up an organization that exists. Some parts also need a BuildBase plan that includes them, such as custom sign-in domains, text message sign-in or credits. Without one, BuildBase refuses that step.
- The Stripe webhook endpoint. A blueprint can store the endpoint's signing secret (
webhookSecret), but it cannot create the endpoint in your Stripe account. In the console, open Billing, then Credentials, press Check Stripe setup, then Create the endpoint for me.
Some changes are refused by design, and the plan says why: the type of a sign-in method, auth client, item or template cannot change, and neither can an item's scope (give the new one a new key), an API key's role is fixed when it is created, a domain another organization uses cannot be added, and web push keys are fixed once made.
Next steps
- Troubleshooting - every message, what it means and what to do
- File reference - every field, with an example of each kind
- Your first blueprint in the console - download, change, plan and apply