Putting a product on Forumulate: the quick path

Embed
Preview of the card
<a href="https://forumulate.com/guides/integrating-a-product"><img src="https://forumulate.com/guides/_api/public/orgs/046a9bc0-d530-43ae-9bae-af7945008cda/spaces/36aa4df0-1e95-4cef-8ae9-795169441345/pages/b8713fa7-1015-4497-b0a2-156d068ce7d3/social-card.svg?v=3e7ae33eec8b" alt="Putting a product on Forumulate: the quick path" width="600" height="315"></a>

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 back

Only 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 theme

Each 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 base

Write 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 file

A 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:

  1. a TXT record for ownership (several spaces on one host means several values on the same name — that is fine);

  2. 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}/activate

Publish 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.

  • Reader sign-in. If your product's accounts are on Atlas, readerAuth: atlas is 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) through PATCH /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-auth and the manifest plan (PUT /orgs/{orgId}/manifest) answer recommendations: [{code: "reader_auth.use_atlas", …}] with the exact change and the POST /orgs/{orgId}/members/relink to run first, so existing members keep their accounts. Switching is then the one readerAuth: atlas line.

  • Consent. PUT /orgs/{orgId}/consent points 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 a consent: 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 connectors entry 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

IntegrationHealth_get

GET /orgs/{orgId}/integration-health

Check the organization’s integrations

client.integrationHealth.get()

The Forumulate teamUpdated