Putting a product on Forumulate: the quick path
A product's whole public presence — docs and an API reference, blog, community forum, status page and legal pages, on the product's own domain, with reader sign-in, consent and support chat — is one file and two commands:
npx -y @forumulate/cli@0.21.0 plan --diff # what would change; writes nothing
npx -y @forumulate/cli@0.21.0 apply # make it so; every write is read backOnly docs, a status page and a heartbeat for a Next.js app? A product's help site in an hour is the shorter path, with the health route, the worker ping and a manifest to download.
Pin the CLI version you tried (0.21.0 above; older than the API's supported minimum, it exits 6 and names the upgrade). plan and apply exist from 0.11; conflicts on studio edits and pull --only from 0.21.0. You also need a token for the organization (see Tokens and scopes) in FORUMULATE_TOKEN, and nothing else. Direct API calls you make alongside send Forumulate-Version: 2027-03-01 (see Versioning); the CLI sets its own.
Which API version? The newest, 2027-03-01, is what a new integration states; the CLI sets its own. 2026-12-01 is not frozen out or retired: every version keeps being served exactly as cut, so an integration pinned to it keeps its shape (see Versioning).
Shown-once secrets. An apply that mints a key (an executor key, a webhook secret) redacts it in the output, --json included. Keep it with forumulate apply --secrets-file ./secrets.json: JSON of every shown-once value, written mode 0600 (an existing file must already be 0600). The executor key looks like forumulate_live_exec_…; its first 16 characters are the same for every organization.
A job on the status page? forumulate heartbeat ping <monitor> is the one-line ping for a cron job (see Status monitors).
This replaces the older path of creating each space with its own call and publishing each folder with forumulate publish. Those still work, and forumulate.yaml and apply explains every key; this page is the order that works for a new product.
1. Pick the shape
One space per path of your domain, all in one site so they share a header, footer and theme:
yourproduct.com/docs → DOCS space (+ an API reference from your OpenAPI)
yourproduct.com/blog → BLOG space
yourproduct.com/community → FORUM space
yourproduct.com/status → STATUS space
yourproduct.com/legal → LEGAL space
└── all five in one site: one header, one footer, one themeEach space gets its own domain entry (hostname + basePath). The site itself gets none: a site domain with an empty path would claim your whole host, which your app needs. Pick only the spaces you will keep up, and decide your languages now (locales on each space) so every page gets its twins from the start.
2. Write forumulate.yaml
version: 2
org: yourproduct # or --org / FORUMULATE_ORG
spaces:
- slug: docs
name: Docs
type: DOCS
visibility: PUBLIC
seo: { seoTitle: Your Product docs, description: How to use Your Product. }
redirects:
# `from` and `to` are relative to the space's mount: this is yourproduct.com/docs/api-docs
- { from: /api-docs, to: /getting-started }
openapi:
url: https://api.yourproduct.com/openapi.json
collection: API reference
mode: per-tag
slugPrefix: ref
landing: true
excludeTags: [internal]
- slug: blog
name: Blog
type: BLOG
visibility: PUBLIC
posts:
- slug: hello
title: Hello
body: Our first post, in Markdown.
- slug: community
name: Community
type: FORUM
visibility: PUBLIC
categories:
- { name: Announcements }
- { name: Help, description: Questions about Your Product }
topics:
- slug: welcome
title: Welcome
body: Read this first.
category: Announcements
postAs: org # credited to the organization, not your token's owner
pin: { pinned: true, global: true }
- slug: status
name: Status
type: STATUS
visibility: PUBLIC
monitors:
- { name: Website, url: https://yourproduct.com }
- { name: API, url: https://api.yourproduct.com/health }
- { name: Worker, kind: heartbeat } # your job pings us after each run
- slug: legal
name: Legal
type: LEGAL
visibility: PUBLIC
pages: # a folder of Markdown per space
- { dir: content/docs, space: docs }
- { dir: content/legal, space: legal }
sites:
- slug: yourproduct
name: Your Product
spaces: [docs, blog, community, status, legal]
domains:
- { space: docs, hostname: yourproduct.com, basePath: /docs, alsoRedirectWww: true }
- { space: blog, hostname: yourproduct.com, basePath: /blog, alsoRedirectWww: true }
- { space: community, hostname: yourproduct.com, basePath: /community, alsoRedirectWww: true }
- { space: status, hostname: yourproduct.com, basePath: /status, alsoRedirectWww: true }
- { space: legal, hostname: yourproduct.com, basePath: /legal, alsoRedirectWww: true }
readerAuth: atlas # see step 5; leave out to manage sign-in elsewhere
executor: { ensureKey: true } # privacy requests: mint the executor key if none exists
connectors:
- providerId: chatly
space: docs
config: { publicId: your-chatly-public-id }
embedEnabled: true # the chat widget on the docs pages
knowledgeEnabled: true # the docs as the chat's knowledge baseWrite pages as Forumulate Markdown — it has a directive for every block, so no converter is needed (see Markdown in Forumulate). posts[].body is inline Markdown.
A block you leave out is not managed by this file: apply neither writes nor removes it. That is how consent and an external_jwt sign-in, whose secrets do not belong in a repository, stay with the API calls in step 5 while the file owns everything else.
3. Plan, then apply
forumulate plan --diff # read every line: + creates, ~ updates, - deletes (only with --prune)
forumulate apply # each step is printed with whether it read back
forumulate plan --check # in CI afterwards: exit 1 when the live site has drifted from the fileA second apply with nothing changed writes nothing. A wrong key is refused in the plan, by name, with nothing written. Exit codes and the plan's format are in forumulate.yaml and apply.
4. Put it on your domain
Creating a domain does not verify it. The plan and apply output list, under manual steps, exactly what to set for each domain:
a
TXTrecord for ownership (several spaces on one host means several values on the same name — that is fine);once verified, the proxy rule that sends that path of your host to us (for a whole hostname, the routing record instead).
Then, per domain, verify without switching traffic, retire the same paths in your own app, and activate:
POST /orgs/{orgId}/spaces/{spaceId}/domains/{domainId}/verify { "activate": false }
POST /orgs/{orgId}/spaces/{spaceId}/domains/{domainId}/activatePublish on your own domain has every detail, including the www twin and Cloudflare. Until a domain is live, each space is read at its platform address on forumulate.com — /p/… for docs, /b/… for the blog, /f/… for the forum, /st/… for status, /legal/<org>/… for legal — and that address is its canonical; see Our address for each space type.
5. Sign-in, consent, chat
Reader sign-in. If your product's accounts are on Atlas,
readerAuth: atlasis the whole setup (it uses the Atlas instance linked to the organization, or{method: atlas, instanceId: ins_…}). Otherwise sign readers in with a token your app mints (external_jwt) throughPATCH /orgs/{orgId}/reader-auth— see Reader sign-in with your app. When an organization that uses Atlas still signs readers in another way,GET /orgs/{orgId}/reader-authand the manifest plan (PUT /orgs/{orgId}/manifest) answerrecommendations: [{code: "reader_auth.use_atlas", …}]with the exact change and thePOST /orgs/{orgId}/members/relinkto run first, so existing members keep their accounts. Switching is then the onereaderAuth: atlasline.Consent.
PUT /orgs/{orgId}/consentpoints the organization at your consent platform, so one banner and one record cover your app and every space (see Cookie consent API). It can also be aconsent:block in the file, if you are content to keep its secret there.Privacy requests.
executor: { ensureKey: true }mints the executor key once; register it with your rights-request platform as a connected system (see The privacy executor contract).Support chat. A
connectorsentry puts a chat provider's widget on a space and gives it the space as knowledge (see Connectors).Status. Ping each heartbeat monitor from your job after every run (see Status monitors).
6. Done when
GET /orgs/{orgId}/integration-health answers ok: true for every check you configured — domains served, reader sign-in, consent, executor, sitemaps, canonicals, connectors, heartbeats, webhooks. See Integration health. Then read one page of each space on your domain in a private window, and run npx -y @forumulate/cli doctor https://yourproduct.com/docs from outside.
In the API
Every operation below is also reachable from the other surfaces without a dedicated command. forumulate api <Operation> calls it from the CLI, the JavaScript SDK has a typed method for each one (the last column names it; the Python and Go SDKs are generated from the same spec), and an agent reaches the organization-scoped ones through the MCP agent server — its api_* family tools, or call_api. Where a CLI command or a hand-made MCP tool exists too, the last column names it. Operation names link to the API reference.
Integration Health
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Check the organization’s integrations |
|