# Self-Hosting Source: [Self-Hosting](https://typeroll.com/docs/guides/self-hosting/) > Run Typeroll on your own infrastructure — requirements, environment variables, and deployment. Typeroll 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. Note The hosted version at [app.typeroll.com](https://app.typeroll.com/) adds managed infrastructure, migration execution, operations, and support. The CMS, migration workflow, Forms, renderer, public API, and Extension protocol are part of open-source Core. ## Architecture [Section titled “Architecture”](https://typeroll.com/docs/guides/self-hosting/#architecture) ```text 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 + Firestore ``` Both 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”](https://typeroll.com/docs/guides/self-hosting/#prerequisites) * **Development:** Node.js 22.12 or later * **Production:** a GCP project with billing, `gcloud`, and `crane` * **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:** Anthropic API key for AI chat ## Quick start (local dev) [Section titled “Quick start (local dev)”](https://typeroll.com/docs/guides/self-hosting/#quick-start-local-dev) Clone the repository and install dependencies: ```bash git clone https://github.com/typeroll/typeroll cd typeroll npm ci ``` The portal includes a **fixtures backend** — a JSON file store that replaces Firestore for local development. No Firebase configuration needed to run locally: ```bash npm run dev:portal ``` Open `http://localhost:4321`. You’ll be logged in as a dev user with access to the sample content in `packages/portal/fixtures/`. ## Plan the reference profile [Section titled “Plan the reference profile”](https://typeroll.com/docs/guides/self-hosting/#plan-the-reference-profile) Clone a tagged Core release and copy the non-secret serverless configuration contract: ```bash git clone --branch core-v --depth 1 https://github.com/typeroll/typeroll cd typeroll cp config/self-host-gcp.example.json self-host.gcp.json ``` Fill 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: ```bash npm ci npm run self-host:gcp:plan -- --config self-host.gcp.json > self-host.gcp.plan.json ``` The 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. 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”](https://typeroll.com/docs/guides/self-hosting/#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: ```bash # 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-project ``` Add 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: ```bash 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-project ``` The 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: ```bash npm run self-host:gcp:doctor -- --config self-host.gcp.json npm run self-host:gcp:doctor -- --config self-host.gcp.json --json ``` The 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: ```bash curl --fail https://cms.example.com/api/healthz curl --fail https://cms.example.com/api/readyz curl --fail https://cms.example.com/api/version curl --fail https://forms.example.com/api/readyz curl --fail https://forms.example.com/api/version ``` ## Portable Compose fallback [Section titled “Portable Compose fallback”](https://typeroll.com/docs/guides/self-hosting/#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: ```bash cp .env.self-host.example .env chmod 600 .env ``` `TYPEROLL_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: ```bash 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`: ```bash 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: ```bash npm ci npm run self-host:check docker compose config --quiet ``` All 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. Bootstrap the portable data contract once before starting Core: ```bash npm run self-host:bootstrap ``` Bootstrap 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: ```bash docker compose pull docker compose up -d docker compose ps ``` Caddy requests TLS certificates after both DNS names point at the host and ports 80 and 443 are reachable. Verify every portable role: ```bash curl --fail https://cms.example.com/api/healthz curl --fail https://cms.example.com/api/readyz curl --fail https://cms.example.com/api/version curl --fail https://forms.example.com/api/readyz docker 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. 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”](https://typeroll.com/docs/guides/self-hosting/#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_VCPU` and `DEPLOY_COST_MEMORY_GIB` must 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”](https://typeroll.com/docs/guides/self-hosting/#firebase-setup) 1. Create a Firebase project at [console.firebase.google.com](https://console.firebase.google.com/). 2. Enable **Firestore** in Native mode and **Authentication → Email/Password**. 3. Add a Firebase web app and copy its four public values to the serverless config. 4. Attach the dedicated runtime service accounts from the generated plan to the Cloud Run services. Core uses `FIREBASE_PROJECT_ID` and Application Default Credentials; do not create a downloadable key. 5. 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. 6. For each user, set an `org_id` custom claim with the modular Admin API: ```javascript 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. ## Backups and disaster recovery [Section titled “Backups and disaster recovery”](https://typeroll.com/docs/guides/self-hosting/#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: ```bash docker compose stop portal forms worker npm run self-host:backup -- --output /srv/typeroll-backups/2026-09-01 npm run self-host:restore -- --backup /srv/typeroll-backups/2026-09-01 docker compose start portal forms worker ``` The 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: ```bash 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 \ --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”](https://typeroll.com/docs/guides/self-hosting/#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. 1. Stop portal, Forms, and worker so the backup and migration have no writers. 2. Create an encrypted backup and verify the copied backup directory. 3. Check out the new signed/tagged Core release, run `npm ci`, `npm run self-host:check`, and `docker compose config --quiet`. 4. Inspect the migration without changing data: ```bash npm run self-host:migrate ``` 5. If migrations are pending, apply them only with the verified pre-migration backup and exact project confirmation: ```bash npm run self-host:migrate -- \ --apply \ --backup /srv/typeroll-backups/2026-09-01 \ --confirm-project my-firebase-project ``` 6. Set `TYPEROLL_IMAGE` and `TYPEROLL_IMAGE_DIGEST` to the new published digest, then run `docker compose pull`, `docker compose up -d`, and the health, readiness, and version checks above. 7. 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”](https://typeroll.com/docs/guides/self-hosting/#direct-docker-smoke-test) For image development only, the container listens on port 8080: ```bash docker build -t typeroll-core:dev . docker run --rm -p 8080:8080 --env-file .env typeroll-core:dev ``` This 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”](https://typeroll.com/docs/guides/self-hosting/#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:///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_KEY` set (see above). Without it the `/api/mcp/oauth/token` endpoint refuses to issue JWTs. * `PORTAL_PUBLIC_URL` set to the public origin (`https://`). The OAuth `aud` claim 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. ## Extensions [Section titled “Extensions”](https://typeroll.com/docs/guides/self-hosting/#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](https://typeroll.com/docs/extensions/overview/) 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.