Self-hosting
An fctl instance is one Cloudflare Worker (the API, the web app and two Durable Object classes) plus a D1 database, on your own Cloudflare account and domain. Hosts run on your machines and connect to it. Nothing in the code is tied to fctl.sh: everything instance-specific is configuration.
What you need
- A Cloudflare account with a domain (zone) on it. The Workers Paid plan ($5 a month) is recommended for its higher limits; a small team stays well inside what it includes.
- Rust (via rustup; the repository pins its toolchain) and Node.js 22 or newer.
- Wrangler signed in to that account: run
npx wrangler loginin the repository, or exportCLOUDFLARE_API_TOKENandCLOUDFLARE_ACCOUNT_ID. An API token needs Workers Scripts: Edit, D1: Edit, Workers KV Storage: Edit, Account Settings: Read, and, on the zone, Workers Routes: Edit and DNS: Edit (for the custom domain).
1. Describe the instance
git clone https://github.com/hyperstructure/fctl && cd fctl
cp deploy/instances/example.toml deploy/instances/acme.tomlEdit deploy/instances/acme.toml:
worker = "fctl" # the Worker's name in your account
instance_name = "Acme factory" # shown in the app and in emails
public_url = "https://fctl.acme.dev" # its host becomes the Worker's custom domain
[d1]
database_name = "fctl"
database_id = "" # filled in by --create
location = "weur" # optional primary locationNothing in this file is secret; commit it to your fork if you like.
2. Deploy
cargo xtask deploy acme --createThis creates the D1 database (recording its id in the instance file), builds
the web app and the Worker (Rust compiled to WebAssembly), renders
crates/fctl-control/wrangler.deploy.toml, applies the database migrations,
and deploys to your domain. Run it again, without --create, for every
upgrade.
3. Set the secrets and create the owner
cargo xtask secrets acmeThis sets, once:
FCTL_MASTER_KEY— 32 random bytes that encrypt stored credentials and sign state. Keep it: replacing it makes every stored Claude credential and project secret unreadable.FCTL_SETUP_TOKEN— a one-time token. The command prints the setup link,https://fctl.acme.dev/setup#token=…. Open it to create the owner account; after that, the link does nothing.--new-setup-tokenprints a fresh one.
Sign in, then allow your team under Admin → Access: exact emails (which
also creates invite links) or whole domains (*@acme.dev).
4. Connect GitHub (optional, recommended)
With a GitHub App, projects link repositories, agents get short-lived tokens for exactly one repository, and fctl can create new repositories.
Create an app under your organization: Settings → Developer settings → GitHub Apps → New GitHub App. Turn the webhook off. Repository permissions:
Permission Access Why Contents Read and write clone, commit, push Pull requests Read and write open and update pull requests Issues Read and write agents can read and comment on issues Workflows Read and write agents may edit .github/workflowsChecks, Actions, Commit statuses Read agents can see CI results Administration Read and write only for New repository Metadata Read required Install it on the organization (all repositories, or the ones agents may touch).
Generate a private key, and fill in the instance file:
[github] app_id = "123456" app_slug = "acme-fctl" client_id = "Iv23li…" default_owner = "acme" # where "New repository" creates repositoriesStore the key and redeploy:
cargo xtask secrets acme --github-key ~/Downloads/acme-fctl.private-key.pem cargo xtask deploy acme
The key is converted to PKCS#8 and stored as the GITHUB_APP_PRIVATE_KEY
secret; it never reaches a host or a sandbox.
5. Google sign-in (optional)
Create an OAuth client of type Web application in the Google Cloud
console, with the authorized redirect URI
https://fctl.acme.dev/api/v1/auth/google/callback. Then:
[google]
client_id = "1234-abc.apps.googleusercontent.com"echo "$GOOGLE_CLIENT_SECRET" | cargo xtask secrets acme --set GOOGLE_CLIENT_SECRET
cargo xtask deploy acmePeople can then link Google under Settings, and allowlisted people can sign up with it.
6. Email (optional)
Without email, admins copy invite and password-reset links from the app. To send them, use Resend with a verified domain:
[email]
from = "Acme factory <fctl@acme.dev>"echo "$RESEND_API_KEY" | cargo xtask secrets acme --set RESEND_API_KEY
cargo xtask deploy acme7. Bring hosts online
On each machine that runs agents, follow Getting started → Bring a machine online:
curl -fsSL https://fctl.sh/install | sh
fctl login fctl.acme.dev
fctl host install --build-imageHosts download their release from https://fctl.sh/releases by default; set
download_base in host.toml to mirror them elsewhere.
Operating it
- Upgrades.
git pull && cargo xtask deploy acme; migrations apply first. Hosts and agents tolerate a control plane one version ahead. Upgrade hosts by running the installer again andfctl host restart(running sessions continue). - Backups. D1 keeps 30 days of point-in-time history (Time Travel):
npx wrangler d1 time-travel restore. For a copy,npx wrangler d1 export. Session event logs live in Durable Object storage. - Logs.
npx wrangler tail fctlstreams the Worker's logs; observability is enabled in the rendered config.fctl host logsshows a host's. - Costs. The control plane is request- and storage-priced on Cloudflare; the work happens on your hardware and your Claude subscriptions. fctl never makes Anthropic API calls.
- Removing it.
npx wrangler delete fctlandnpx wrangler d1 delete fctl;fctl host uninstallon each host.