Installation
Stand up your own SFab factory on Cloudflare, end to end.
This guide takes you from a clone to a deployed factory on your own Cloudflare account. It is written to be followed by a human or handed to an AI agent; see Deploy with your agent for the one-prompt version.
What you're deploying
The platform is a single Cloudflare Worker (apps/platform) plus the resources
it binds: a D1 database, two R2 buckets, Durable Objects, and container images
for the agent sandboxes (built automatically from the repo's Dockerfiles on
deploy). Containers and Durable Objects require the Workers Paid plan.
Prerequisites
- Node.js 20+ and
pnpm - A Cloudflare account on the Workers
Paid plan, with
wranglerauthenticated (npx wrangler login) - A GitHub account that can create a GitHub App
- Optional: a Resend API key for outbound email
(invitations). You can skip it with
MOCK_SEND_EMAIL=true.
1. Clone and install
git clone https://github.com/sfab-oss/sfab.git
cd sfab
pnpm install2. Create the Cloudflare resources
cd apps/platform
npx wrangler d1 create sfab-platform-db
npx wrangler r2 bucket create sfab-workspace
npx wrangler r2 bucket create sfab-sandbox-backupsd1 create prints a database_id; you'll need it next.
Sandbox backups need more than the R2 binding
wrangler.jsonc wires a BACKUP_BUCKET binding to sfab-sandbox-backups
for dev-session sleep/wake snapshots (and historically sandbox-agent mid-run
resume — that ACP path was removed in ALW-469).
That binding lets the Worker read/write backups locally and is not enough in
production: the sandbox runtime uploads checkpoints via presigned URLs
from inside the container. You also need four Worker secrets (step 5) with R2
API-token credentials scoped to this bucket. Without them, checkpoints fail with InvalidBackupConfigError /
backup_failed until the secrets are set.
3. Point the config at your account
Edit apps/platform/wrangler.jsonc:
- Replace
database_idunderd1_databaseswith the id from step 2. - Replace the
routesentry (app.sfab.dev) with your own custom domain, or delete theroutesblock to serve from the default*.workers.devURL.
Everything else (Durable Objects, containers, bucket bindings) works as-is. Decide your platform URL now; the GitHub App and several secrets reference it.
4. Create the GitHub App
The factory works through a GitHub App you own: it receives PR and CI webhooks, posts the AI review as a check, and syncs tasks and documents into your repos.
On GitHub: Settings → Developer settings → GitHub Apps → New GitHub App.
- Homepage URL: your platform URL
- Callback URL:
https://<your-platform-domain>/api/auth/callback/github - Webhook URL:
https://<your-platform-domain>/webhooks/github - Webhook secret: generate one (
openssl rand -hex 32) and keep it - Repository permissions:
- Pull requests: Read & write
- Contents: Read & write
- Checks: Read & write
- Administration: Read & write — project fabrication creates the new repo
- Workflows: Read & write — fabrication seeds
.github/workflows/files, and GitHub 403s any API write touching that path without this permission - Secrets: Read & write — sealed-box write of the Cloudflare deploy token
and Worker
secret_textvalues into repo Actions secrets - Metadata: Read (default)
- Account permissions: Email addresses: Read
- Subscribe to events:
Pull request,Check suite
After creating it, collect: the App ID, the app slug (from its URL), a generated private key (.pem), the Client ID, and a generated client secret.
5. Set the secrets
From apps/platform, set each with npx wrangler secret put <NAME>:
| Secret | Value |
|---|---|
BETTER_AUTH_SECRET | Random, 32+ chars (openssl rand -base64 32) |
BETTER_AUTH_URL | Your platform URL, e.g. https://factory.example.com |
BETTER_AUTH_TRUSTED_ORIGINS | Same URL (comma-separate extras if any) |
ALLOWED_EMAILS | Comma-separated emails allowed to sign in; empty allows anyone |
ENCRYPTION_KEY | Base64 of exactly 32 bytes (openssl rand -base64 32); encrypts stored provider keys |
GITHUB_APP_SLUG | The app slug from step 4 |
GITHUB_APP_ID | The App ID |
GITHUB_APP_PRIVATE_KEY | The full private key PEM |
GITHUB_WEBHOOK_SECRET | The webhook secret from step 4 |
GITHUB_CLIENT_ID | The Client ID |
GITHUB_CLIENT_SECRET | The client secret |
RESEND_API_KEY | Resend key, or skip and set MOCK_SEND_EMAIL to true |
EMAIL_SENDER | From-address for platform email |
MOCK_SEND_EMAIL | true to log emails instead of sending |
Sandbox backup secrets (required in production). Production uploads off-container checkpoints via presigned R2 URLs;
the BACKUP_BUCKET binding alone is insufficient — set these in addition to
creating the bucket in step 2. Local dev skips them and uses
CONTRACTOR_BACKUP_LOCAL=true in .dev.vars instead (see
Local development).
Create an R2 API token in the Cloudflare Dashboard: R2 → Manage R2 API
Tokens → Create API token. Choose Object Read & Write, scope it to the
sfab-sandbox-backups bucket (or your chosen backup bucket name), and copy the
Access Key ID and Secret Access Key once shown.
| Secret | Value |
|---|---|
R2_ACCESS_KEY_ID | Access Key ID from the R2 API token above |
R2_SECRET_ACCESS_KEY | Secret Access Key from the same token |
BACKUP_BUCKET_NAME | sfab-sandbox-backups (must match the bucket from step 2 and the BACKUP_BUCKET binding in wrangler.jsonc) |
CLOUDFLARE_ACCOUNT_ID | Your Cloudflare account ID (Dashboard home sidebar, or npx wrangler whoami) |
Checkpoints expire after three days (SDK default TTL); that bounds R2 storage cost for session backups.
LLM keys for the agents (e.g. Anthropic) are not Worker secrets; you add
them in the platform UI after signing in, under settings, and they're stored
encrypted with ENCRYPTION_KEY.
6. Deploy and migrate
pnpm deploy # builds the Worker + container images, deploys
pnpm db:migrate:prod # applies the D1 migrations to the remote databaseThe first deploy takes a few minutes while Cloudflare builds the sandbox container images.
7. Sign in and connect GitHub
- Open your platform URL and sign up (the first account is yours; set
ALLOWED_EMAILSif the instance is public). - Add an LLM provider key in settings so agents can run.
- In Settings → GitHub, install your GitHub App on the org/repos you want the factory to manage, then link the installation and link each repo to a project.
Linking matters: webhooks for unlinked installations or repos are skipped by
design. If something doesn't fire, the /logs page (filter
eventType=webhook.github) shows exactly which gate skipped and why.
Local development
cp apps/platform/.dev.vars.example apps/platform/.dev.vars # fill it in
pnpm dev # platform on http://localhost:7322pnpm dev:reset:local (from apps/platform) resets and seeds the local
database. GitHub webhooks can't reach localhost; use a Cloudflare Tunnel
pointing at port 7322 and use the tunnel URL as a dev GitHub App's webhook URL.