Self-Hosting
This guide is for operators installing their own CMS. Cloud users can start with Using Typeroll Cloud; shared editing and publishing guides are listed in Cloud and self-hosting.
Typeroll CMS is open-source (MIT) and can be self-hosted. The supported reference profile runs entirely in a GCP/Firebase project you own. Cloud Run hosts the portal and Forms from the same immutable Core image used by Typeroll Cloud; Cloud Tasks and Cloud Scheduler invoke authenticated internal routes when work is ready. There is no VM or always-running worker.
Architecture
Section titled “Architecture”Internet ── Cloud Run portal ───── Firebase Auth + Firestore │ │ ├── Cloud Tasks ──────────┤ deploy-worker route ├── Cloud Scheduler ──────┤ publish-sweep route ├── R2 media └── Cloudflare Pages static sites
Internet ── Cloud Run Forms ────── Firebase Auth + FirestoreBoth Cloud Run services use the same verified Core release. The public image is
copied by digest into the customer’s Artifact Registry without rebuilding it.
The portal enqueues durable Cloud Tasks; each task invokes
/api/internal/deploy-worker with an OIDC token. Cloud Scheduler invokes
/api/internal/publish-sweep with the same identity contract.
The runtime uses Application Default Credentials from dedicated Cloud Run service accounts. Do not create or mount a Firebase service-account key for the reference profile.
Prerequisites
Section titled “Prerequisites”- Development: Node.js 22.12 or later
- Production: a GCP project with billing,
gcloud, andcrane - Firebase: Firestore Native mode, Authentication, and a Firebase Web App
- Cloudflare account with R2 for media and Pages for static site output
- Two public DNS names for the portal and Forms origins
- Optional: each Organization’s own Anthropic API key for the built-in AI assistant, added in the portal (no server variable)
Quick start (local dev)
Section titled “Quick start (local dev)”Clone the repository and install dependencies:
git clone https://github.com/typeroll/typerollcd typerollnpm ciThe portal includes a fixtures backend — a JSON file store that replaces Firestore for local development. No Firebase configuration needed to run locally:
npm run dev:portalOpen http://localhost:4321. You’ll be logged in as a dev user with access to the sample content in packages/site-template/fixtures/.
Plan the reference profile
Section titled “Plan the reference profile”Clone a tagged Core release and copy the non-secret serverless configuration contract:
git clone --branch core-v<VERSION> --depth 1 https://github.com/typeroll/typerollcd typerollcp config/self-host-gcp.example.json self-host.gcp.jsonFill in the customer project, region, Firebase Web App, public origins,
resource names, Secret Manager resource names, and the release’s published
@sha256: image. The JSON contains references to secrets, never secret values.
Generate an inspectable, non-mutating plan:
npm cinpm run self-host:gcp:plan -- --config self-host.gcp.json > self-host.gcp.plan.jsonThe plan fixes the supported topology before any remote mutation: required APIs, three least-privilege identities, Artifact Registry mirror, Cloud Run portal and Forms services, Cloud Tasks queue, Cloud Scheduler job, runtime configuration, and Secret Manager bindings. It also records that the artifact is copied by digest without a rebuild and explicitly forbids VM, managed instance group, reverse proxy, and long-running-worker resources.
R2_PRIVATE_BUCKET is optional. It names a second R2 bucket in the same
account with public access disabled; the R2 access key must be able to read
and write both buckets. Uploads made before an Organization
connects its own storage, and every SVG upload on sites that use the server’s
platform bucket (R2_BUCKET), are staged there, because every object in the
public bucket is reachable as soon as it is written; SVGs are published only
after they are sanitized. Without it, those uploads return HTTP 503
(draft_storage_unavailable or svg_staging_unavailable). When the config
names a secret for it, the plan binds it to the portal only. In the Compose
fallback, set R2_PRIVATE_BUCKET in .env; npm run self-host:check warns
when it is missing and rejects the public bucket’s name.
Every required Secret Manager resource must have an enabled latest version
before deployment. Keep the actual secret values in your secret-management
workflow; do not add them to self-host.gcp.json or Git.
Apply and verify the reference profile
Section titled “Apply and verify the reference profile”The apply command is a dry run unless --apply and an exact project
confirmation are both present. Its two rerunnable phases separate resources
that contain no secret values from the runtime deployment:
# Local preview only; makes no remote calls.npm run self-host:gcp:apply -- --config self-host.gcp.json
# APIs, service accounts, IAM, Artifact Registry, Cloud Tasks, and empty# Secret Manager containers. This changes the named GCP project.npm run self-host:gcp:apply -- \ --config self-host.gcp.json \ --phase foundation \ --apply \ --confirm-project your-gcp-projectAdd one enabled version to every Secret Manager resource using your normal secret-management workflow. The apply command creates secret containers and IAM bindings, but it never accepts, reads, prints, or copies secret values. Then preview and apply the runtime phase:
npm run self-host:gcp:apply -- \ --config self-host.gcp.json \ --phase runtime
npm run self-host:gcp:apply -- \ --config self-host.gcp.json \ --phase runtime \ --apply \ --confirm-project your-gcp-projectThe runtime phase fails before copying an image or deploying Cloud Run if any
secret lacks an enabled version. It verifies the source digest, copies the
release to Artifact Registry under a deterministic digest tag without a
rebuild, deploys the portal and Forms roles, and converges the publish-sweep
Scheduler job. A new portal receives one bootstrap revision so its stable
run.app URL can be discovered; the command immediately converges the active
revision and both OIDC callers on that URL.
Run the read-only control-plane doctor at any time. It reads resource and secret-version metadata, never secret contents:
npm run self-host:gcp:doctor -- --config self-host.gcp.jsonnpm run self-host:gcp:doctor -- --config self-host.gcp.json --jsonThe operator running apply must already be allowed to enable services, manage the listed project resources and IAM bindings, and act as the three dedicated runtime service accounts. Core does not grant roles to the human or automation identity running the installer.
After deployment, verify both roles and the immutable release identity:
curl --fail https://cms.example.com/api/healthzcurl --fail https://cms.example.com/api/readyzcurl --fail https://cms.example.com/api/versioncurl --fail https://forms.example.com/api/readyzcurl --fail https://forms.example.com/api/versionPortable Compose fallback
Section titled “Portable Compose fallback”compose.yaml is retained for local evaluation and deployments outside GCP.
It is not the production-supported profile and is not the permanent self-host
E2E target. For this fallback only, copy the environment contract and keep it
readable only by the operator:
cp .env.self-host.example .envchmod 600 .envTYPEROLL_IMAGE must use the release’s published @sha256: digest, not a
mutable tag. Set TYPEROLL_IMAGE_DIGEST to the same digest.
Generate each HMAC/encryption value independently:
node -e "console.log(require('node:crypto').randomBytes(32).toString('base64url'))"Generate the Extension issuer key once and store the resulting private JWK as
single-line JSON in EXTENSION_SIGNING_PRIVATE_JWK:
node --input-type=module -e "import { generateKeyPairSync } from 'node:crypto'; const { privateKey } = generateKeyPairSync('ec', { namedCurve: 'P-256' }); console.log(JSON.stringify(privateKey.export({ format: 'jwk' })))"Validate the complete portable contract before Docker receives credentials:
npm cinpm run self-host:checkdocker compose config --quietAll self-host operation commands also accept the complete contract through
their process environment. Process-injected values override an --env-file,
and a file is optional when the injected environment is complete. Prefer that
mode with a secret-manager runner so JSON keys and recovery credentials do not
need to be materialized on disk. The 0600 check still applies whenever an
environment file is supplied.
The check prints field names and validation failures only. It never prints
secret values. Keep TYPEROLL_BACKUP_KEY in a second, off-host secret store;
losing both the host and this key makes encrypted backups unrecoverable. The
Compose profile explicitly removes this key from every Core container.
Each Core role receives only the secrets it uses. The public Forms service
keeps Firebase Admin, FORMS_HMAC_SECRET and INTEGRATIONS_SECRET_KEY; Compose
blanks its Cloudflare token, R2 keys, preview and MCP OAuth signing keys and
Extension signing key. The worker does not receive the preview or MCP OAuth
signing keys. The Core image runs as the unprivileged node user.
Bootstrap the portable data contract once before starting Core:
npm run self-host:bootstrapBootstrap is idempotent. It refuses a project or bucket containing data unless
you pass --adopt; use that flag only after verifying all existing resources
belong to this Typeroll installation. Start the installation when the checks
and bootstrap pass:
docker compose pulldocker compose up -ddocker compose psCaddy requests TLS certificates after both DNS names point at the host and ports 80 and 443 are reachable. Verify every portable role:
curl --fail https://cms.example.com/api/healthzcurl --fail https://cms.example.com/api/readyzcurl --fail https://cms.example.com/api/versioncurl --fail https://forms.example.com/api/readyzdocker compose exec worker node -e "fetch('http://127.0.0.1:8080/api/readyz').then(async r => { console.log(await r.text()); process.exit(r.ok ? 0 : 1) })"The Forms hostname exposes only Forms, Analytics, and diagnostic routes. The portable worker is not published through Caddy.
Caddy is the only client of the portal and Forms containers and appends one
X-Forwarded-For hop, so Compose sets AUTH_TRUST_PROXY_HOPS=1. Sign-in, MCP
OAuth and Forms rate limits then count each visitor separately instead of
sharing one budget for every request. If you place another proxy or CDN in front
of Caddy, configure Caddy’s trusted_proxies for it and set
AUTH_TRUST_PROXY_HOPS=2 in .env. Never raise the value without that proxy:
clients could then choose their own rate-limit address.
Outbound requests whose address a user or remote content chooses, such as
WordPress sources, webhooks, Extension endpoints and SMTP servers, only connect
to public addresses. To migrate an intranet WordPress or send through a mail
relay on your own network, set TYPEROLL_ALLOW_PRIVATE_NETWORK_FETCH=1 in
.env. It applies only to migration sources and SMTP, allowing private,
loopback and CGNAT addresses and .local/.internal names. Link-local
addresses, including cloud metadata endpoints, stay blocked. Every portal user
who can start a migration or save SMTP settings can then reach that network, so
set it only when you trust all of them. Typeroll Cloud does not offer this
setting.
The portal hostname sends Content-Security-Policy: frame-ancestors 'self' and
X-Frame-Options: SAMEORIGIN unless a response sets its own policy, so other
sites cannot frame the admin interface. Its Firestore queue is durable;
do not replace DEPLOY_QUEUE=firestore with in_process in an internet-facing
portable install.
Build-cost accounting
Section titled “Build-cost accounting”Every deploy records what the build cost in server time, on the deploy job
itself. You’ll see it in get_deploy_status responses — total, a
CPU/memory/request split, wall-clock duration, per-phase timings, and the size
of the generated site.
Nothing needs configuring to get this. The defaults above are the published rates for a request-billed container at 1 vCPU / 1 GiB, which is how the reference deployment runs.
Adjust the rates when your hosting differs:
- Different provider or region. Substitute your own per-second rates.
- Committed-use or reserved pricing. Use your effective discounted rates.
- Your own hardware. Set the rates to
0— every build then costs nothing, which is accurate: you’ve already paid for the machine.
Two things to keep in mind:
DEPLOY_COST_VCPUandDEPLOY_COST_MEMORY_GIBmust match what you actually allocate to the worker container. Change your container’s CPU or memory without changing these and every subsequent build is mispriced.- The rate card is snapshotted onto each deploy job when it runs. Changing a rate affects future builds only; history keeps the numbers it was costed with, so a rate change never silently rewrites past figures.
The numbers are estimates from that rate card, not billing records, and they’re gross — free-tier allowances and discounts aren’t deducted, so your real invoice will be lower.
Firebase setup
Section titled “Firebase setup”- Create a Firebase project at console.firebase.google.com.
- Enable Firestore in Native mode and Authentication → Email/Password.
- Add a Firebase web app and copy its four public values to the serverless config.
- Attach the dedicated runtime service accounts from the generated plan to
the Cloud Run services. Core uses
FIREBASE_PROJECT_IDand Application Default Credentials; do not create a downloadable key. - Ensure the backup identity can read the Authentication password hash
configuration (
firebaseauth.configs.getHashConfig) in addition to managing Auth users and Typeroll’s Firestore data. Backups fail closed if password hashes or their hash configuration are unavailable. - For each user, set an
org_idcustom claim with the modular Admin API:
import { applicationDefault, initializeApp } from "firebase-admin/app";import { getAuth } from "firebase-admin/auth";
initializeApp({ credential: applicationDefault(), projectId: process.env.FIREBASE_PROJECT_ID,});await getAuth().setCustomUserClaims(uid, { org_id: "your-org-id" });All content is scoped under organizations/{org_id}/ in Firestore.
The portable Compose fallback instead accepts FIREBASE_SERVICE_ACCOUNT JSON
because it does not have a GCP workload identity.
Email confirmation
Section titled “Email confirmation”In Core releases after 0.2.84, the portal accepts a signed-in person only when
Firebase reports a confirmed email address (email_verified in the ID token).
Until then the account sees only Check your email: it cannot create or join
an Organization, accept invitations, open Sites, use or create API keys,
approve MCP access or call authenticated APIs. Delegated MCP connections of an
account whose address is not confirmed stop working on the next request.
Organization-wide API keys are Organization credentials and keep working.
Firebase sends the confirmation messages; no mail service is required.
- In Authentication → Settings → Authorized domains, add the portal host
(for example
cms.your-domain.example). The confirmation link returns to the portal’s/verify-emailpage only for an authorized domain; otherwise Firebase shows its own confirmation page and the person returns manually. - Optionally adjust Authentication → Templates → Email address verification (sender name, reply-to, language).
- Set
PORTAL_PUBLIC_URLandPUBLIC_FIREBASE_API_KEY. The portal uses the public Web API key to send a new link and to refresh the session after confirmation.
Upgrading existing installations. Password accounts created before this
release usually have an unconfirmed address, because earlier releases never
sent a confirmation. Session cookies are re-checked on every request, so those
people see Check your email the next time they open the portal, even with
an active session. Their memberships are kept and work again after they confirm.
Tell your users before upgrading. Mark an address as confirmed with the Admin
SDK (getAuth().updateUser(uid, { emailVerified: true })) only when you know
the person owns it.
Sign-out and session cookies
Section titled “Sign-out and session cookies”A portal session is a Firebase session cookie that lasts up to 14 days and is renewed about once a day while the person is active. Sign out ends that browser’s sign-in on the server, not only in the browser: a copy of the cookie, and any cookie renewed from it, stops working on its next request on every portal instance. The person’s other browsers and devices stay signed in, and so do their MCP connections and API keys, because sign-out does not revoke the account’s Firebase refresh tokens. To end every session of an account, revoke its refresh tokens or disable it in Firebase; that also ends its delegated MCP connections.
Sign-out records are kept in the root Firestore collections revoked_sessions
and session_lineage. Each document holds only a hash of the user ID and
sign-in time and an expiry about 14 days ahead; the portal ignores expired
records. Enable Firestore TTL on the native timestamp field expires_at for
both collection groups so expired records are deleted; deletion is
asynchronous. For a file-backed self-host store, remove expired documents
periodically.
GitHub sign-in (optional)
Section titled “GitHub sign-in (optional)”Continue with GitHub appears only when PUBLIC_FIREBASE_GITHUB_SIGNIN=true.
It is separate from the GitHub App that an Organization connects for
publishing; a GitHub sign-in grants no repository access.
- In Firebase console → Authentication → Sign-in method, add the
GitHub provider and copy the callback URL it shows
(
https://<auth-domain>/__/auth/handler). - On GitHub, under Settings → Developer settings → OAuth Apps, create a new OAuth App (not a GitHub App). Use the portal URL as the homepage URL and paste the Firebase callback URL as the authorization callback URL. Generate a client secret.
- Paste the OAuth App’s client ID and client secret into the Firebase GitHub provider and enable it. Keep the secret in Firebase only.
- Keep One account per email address enabled in Firebase. Set
PUBLIC_FIREBASE_GITHUB_SIGNIN=truein the portal environment and restart.
The portal requests only the user:email scope and does not store the GitHub
access token. Firebase does not treat GitHub as a trusted email provider, so
GitHub identities report email_verified: false: a new GitHub account
receives a confirmation email and must confirm its address like a password
account. When the address already has a password account, Firebase reports a
conflict; the sign-in page asks for that password and links GitHub to the
existing account.
The sign-in page uses a popup and falls back to a full-page redirect when the
browser blocks popups. Browsers that partition third-party storage can break
the redirect when PUBLIC_FIREBASE_AUTH_DOMAIN is on a different site than the
portal; see Firebase’s guidance on using signInWithRedirect with blocked
third-party storage.
Cloudflare sign-in for customer publishing
Section titled “Cloudflare sign-in for customer publishing”Customer publishing connects each Organization’s Cloudflare account through
your Cloudflare OAuth client (TYPEROLL_PUBLISH_CLOUDFLARE_CLIENT_ID,
TYPEROLL_PUBLISH_CLOUDFLARE_CLIENT_SECRET, an HTTPS PORTAL_PUBLIC_URL and
INTEGRATIONS_SECRET_KEY). Cloudflare refuses the whole sign-in
(invalid_scope) when Typeroll asks for a scope the client is not allowed, so
optional scopes are requested only when you list them.
| Scope | Cloudflare name | Enables |
|---|---|---|
account-settings.write |
Account Settings Write | Create a Site’s Web Analytics site when Analytics is on |
account-analytics.read |
Account Analytics Read | Read Web Analytics reports for Insights |
dynamic-redirect.read |
Dynamic URL Redirects | Read Single Redirect rules for redirect hosts such as www |
dynamic-redirect.write |
Dynamic URL Redirects | Create Single Redirect rules for redirect hosts |
To include them in Connect Cloudflare:
-
Allow the scopes on your OAuth client in Cloudflare.
-
List the same scopes, separated by spaces or commas, in
TYPEROLL_PUBLISH_CLOUDFLARE_OPTIONAL_SCOPESand restart the portal:Terminal window TYPEROLL_PUBLISH_CLOUDFLARE_OPTIONAL_SCOPES="account-settings.write account-analytics.read dynamic-redirect.read dynamic-redirect.write"
Other values are ignored. You may list only some, for example only the two
dynamic-redirect scopes. Unset, Connect Cloudflare requests exactly what
earlier releases did; Analytics access is then approved per Site with
Approve Analytics access, which requests the two Analytics scopes and still
requires them on the client, and redirect rules are created by hand
(redirect_rule_required). Existing connections keep working and get the
listed scopes when an owner or admin selects Approve update on their
Cloudflare card. Approving a scope enables Analytics for no Site.
Backups and disaster recovery
Section titled “Backups and disaster recovery”Typeroll’s backup is one encrypted, authenticated snapshot of all three state planes:
- Firestore documents, subcollections, native value types, and installation schema metadata
- Firebase Authentication users, password hashes, password hash configuration, provider identities, custom claims, and MFA metadata
- R2 objects and their relevant HTTP metadata
The manifest exposes only source identifiers, schema version, timestamps, and counts. Data, user records, object keys, and object bodies are encrypted with AES-256-GCM. Every file and the manifest are authenticated before restore.
For a consistent manual backup of the portable Compose fallback, stop writers but leave its managed dependencies online:
docker compose stop portal forms workernpm run self-host:backup -- --output /srv/typeroll-backups/2026-09-01npm run self-host:restore -- --backup /srv/typeroll-backups/2026-09-01docker compose start portal forms workerThe restore command is verify-only unless --apply is present. Copy the
completed backup directory off-host and test that copy with the verify-only
command. Never copy a directory containing INCOMPLETE.
Restore to a completely empty Firebase project and R2 bucket with exact target and backup confirmations:
npm run self-host:restore -- \ --backup /srv/typeroll-backups/2026-09-01 \ --apply --empty-target \ --confirm-project my-disaster-recovery-project \ --confirm-bucket my-disaster-recovery-bucket \ --confirm-backup <backup-id> \ --allow-target-mismatch--allow-target-mismatch is required only when the backup source identifiers
differ from an intentional disaster-recovery target. The restore writes the
target identifiers into the recovered installation metadata.
To replace an existing installation, stop every Core role and substitute
--replace for --empty-target. Replace mode deletes Firestore documents,
Auth users, and R2 objects that are absent from the backup. It verifies the
entire backup first, requires all exact confirmations, and verifies the final
record sets, but it cannot make three external services transactional. Keep the
roles stopped until the command succeeds; if it fails, correct the cause and
rerun the same idempotent restore.
At least quarterly, restore the latest backup into isolated Firebase and R2 test resources, start the pinned Core image against them, and run the browser and agent acceptance suite. A backup that has only been created is not yet a proven recovery path.
Portable upgrade, migration, and rollback
Section titled “Portable upgrade, migration, and rollback”Core reports both its release and readable data-schema range at /api/version.
The command-level sequence below applies to the portable Compose fallback. The
serverless profile instead promotes a new digest through the generated GCP plan
and keeps the previous Cloud Run revision available for traffic rollback.
-
Stop portal, Forms, and worker so the backup and migration have no writers.
-
Create an encrypted backup and verify the copied backup directory.
-
Check out the new signed/tagged Core release, run
npm ci,npm run self-host:check, anddocker compose config --quiet. -
Inspect the migration without changing data:
Terminal window npm run self-host:migrate -
If migrations are pending, apply them only with the verified pre-migration backup and exact project confirmation:
Terminal window npm run self-host:migrate -- \--apply \--backup /srv/typeroll-backups/2026-09-01 \--confirm-project my-firebase-project -
Set
TYPEROLL_IMAGEandTYPEROLL_IMAGE_DIGESTto the new published digest, then rundocker compose pull,docker compose up -d, and the health, readiness, and version checks above. -
Run a site edit/deploy smoke test, one Forms submission, the browser visual suite, and the agent suite before ending the maintenance window.
Migration steps are ordered, idempotent, protected by a renewable installation lock, and update schema metadata after each successful step. They will not run without a matching verified backup.
If the new image fails while the previous image can read the resulting schema,
pin the previous digest again and restart Compose. If the previous image cannot
read the new schema, leave all roles stopped and restore the pre-migration
backup with --replace, then start the previous digest. Never run an image
against a schema outside the readable range reported by that image.
Direct Docker smoke test
Section titled “Direct Docker smoke test”For image development only, the container listens on port 8080:
docker build -t typeroll-core:dev .docker run --rm -p 8080:8080 --env-file .env typeroll-core:devThis single-container command does not provide the separate Forms origin or durable jobs and is not the production installation profile.
Hosted MCP connector
Section titled “Hosted MCP connector”A self-hosted portal serves the MCP Streamable HTTP transport at /api/mcp automatically — no extra deployment step. Your users add https://<your-portal-host>/api/mcp as a remote server in their MCP-compatible client, follow the consent screen, and enter a Typeroll API key (org-scoped from /app/settings/api-keys is the right default).
Requirements specific to this endpoint:
MCP_OAUTH_SIGNING_KEYset (see above). Without it the/api/mcp/oauth/tokenendpoint refuses to issue JWTs.PORTAL_PUBLIC_URLset to the public origin (https://<your-portal-host>). The OAuthaudclaim and the well-known metadata endpoints use this to bind issued tokens to your domain per RFC 8707.- TLS for remotely hosted MCP clients.
Agent clients with local process support can keep using the stdio install (npx -y @typeroll/mcp-server with TYPEROLL_API_URL pointed at your portal). Same tool surface, same API keys.
Built-in AI assistant
Section titled “Built-in AI assistant”There is no server-wide AI key. Each Organization adds its own Anthropic API key
in the portal under Account → AI assistant and chooses its Sites; see
Set up the built-in AI assistant. The portal stores the key
encrypted with INTEGRATIONS_SECRET_KEY, which the reference profile already
requires. ANTHROPIC_API_KEY is no longer read; npm run self-host:check warns
when an old environment still sets it. The optional ANTHROPIC_BASE_URL points
AI calls and key verification at a compatible endpoint, such as a gateway. MCP
and the public API do not depend on any of this.
Extensions
Section titled “Extensions”A self-hosted portal can register and install private or unlisted Extensions
without contacting Typeroll’s hosted catalog. Set
EXTENSION_SIGNING_PRIVATE_JWK to a P-256 private JWK and keep it outside
source control.
Native Extension form bindings also require FORMS_HMAC_SECRET. Set
FORMS_PUBLIC_URL when form submissions are served from a dedicated service;
otherwise components call the self-hosted PORTAL_PUBLIC_URL endpoint
directly. Customer site deployment remains static and contains no generated
Functions.
The portal publishes issuer discovery and JWKS from its own
PORTAL_PUBLIC_URL. Extension providers must pair and validate that issuer
instead of assuming the hosted Typeroll domain. During key rotation, set
EXTENSION_SIGNING_PREVIOUS_PUBLIC_JWKS to the old public JWKS for an overlap
window, issue all new tokens with the new private key, then remove the old keys
after the longest token and pairing lifetime has passed.
See Extensions for the manifest, runtime, provider, and recipient-link contracts.
The Extension protocol does not bundle premium Typeroll Apps. Those apps are sold and operated separately in Typeroll-controlled accounts even when the CMS is self-hosted. Third-party and bespoke backends likewise remain in their developer’s accounts. In both cases the browser calls the provider directly; the self-hosted portal issues identity tokens but never proxies provider API traffic.
Other platforms can run the same three image roles, but the Compose profile is the compatibility baseline. Preserve the separate origins, immutable digest, and durable queue contract when translating it to another orchestrator.
Publishing events (Core 0.2.21)
Section titled “Publishing events (Core 0.2.21)”Publishing proceeds from saved step to saved step. A completed build wakes its publication immediately; Typeroll does not restart a build because it estimates that the build should have finished. Duplicate event deliveries keep the same publication identity. A failed or disconnected build is reported through the portal and the deployment-status API used by MCP.
The existing publish-sweep endpoint now reads an index of pending events and
explicit page schedules. It does not scan all content or poll running customer
builds. Keep its recovery schedule enabled to recover failed queue submissions.
Cloud Tasks and the portable Firestore worker consume the same persisted events.
When upgrading an existing installation, drain active publications and run the one-time schedule-index preparation before starting the new workers:
node --experimental-strip-types scripts/reindex-publishing.mjs --project PROJECT_IDnode --experimental-strip-types scripts/reindex-publishing.mjs --project PROJECT_ID --apply --confirm-project PROJECT_IDUse your existing administration credentials. The first command reports a dry run; the second creates missing schedule entries. It does not change page content or start builds. It is safe to repeat. Fresh installations do not need this step.