0018 — Cloudflare Pages auto-provisioning is the default; manual override stays available
- Status: accepted
- Date: 2026-05-10
- Deciders: Derek
Context
After Workstream B.2, an admin could configure a Cloudflare Pages deploy hook by hand: create a project in the Cloudflare dashboard, connect it to GitHub, generate a hook URL, and paste it into ark’s Settings page. That is four manual steps for every new tenant — work that scales linearly as the platform grows.
ark’s pitch is a platform one engineer can operate across many organisations. Four manual setup steps per tenant is friction that belongs to the platform, not to the operator. The Cloudflare Pages REST API exposes the first three steps; the fourth (pasting the URL) can be replaced by the API writing it directly.
There is one manual pre-requisite that cannot be automated: the Cloudflare account must have a GitHub OAuth connection established through the Cloudflare dashboard. That is a one-time, per-account action. Once it exists, every subsequent provisioning call for every tenant goes through automatically.
Decision
ark auto-provisions Cloudflare Pages projects via the CF REST API. The admin Settings page retains a manual URL field as a fallback.
Concretely:
-
A new
@ark/cloudflare-pagesworkspace package wraps the CF API. It is a pure server-side package — no Supabase knowledge, no UI, no tenant abstractions. It takes a slug and returns{ projectName, deployHookUrl, projectUrl }. -
The ark API gains a
POST /me/orgs/:orgId/publishing/auto-provisionroute, admin-only. It resolves the org slug, calls the package, persists the hook URL, and returns the result. -
The admin Publishing settings page shows an “Auto-provision now” button when no hook URL is configured. On success it shows the new project URL and a note that the first build takes a few minutes.
-
A CLI script (
pnpm tenant:provision-cf <slug>) provides the same capability for operators who prefer the command line. -
The manual URL field remains unchanged beneath the auto-provision section. It is the recovery path for edge cases: existing projects, non-standard configurations, or accounts where the GitHub connection is not set up.
The CF API token is a single ark-platform token scoped to Pages:Edit for the configured account. It is stored as an env var on the API server — never exposed to the browser, never logged.
Consequences
Easier:
- Onboarding a new tenant to a live public site now takes one button click instead of four manual steps.
- The CLI provides a scriptable path for operators who prefer automation (
pnpm new-tenant <slug>will eventually call this). - Error handling is explicit and surfaced to the admin: a missing config shows “not configured on this instance”; a name collision shows a clear “already exists” message with next steps.
Harder:
- The GitHub OAuth connection is a one-time manual step that cannot be skipped. Documentation and onboarding must be clear about this pre-requisite.
- The CF API token must be kept secret and rotated if compromised. This is the same posture as the Supabase service-role key — a known-good pattern in ark.
- A project name collision (someone creates
<slug>-arkmanually in the same CF account) surfaces as a 409. The admin must either delete the conflicting project or paste the hook URL manually. This is acceptable for v1.
Alternatives considered
Require manual configuration only. Keeps ark simpler. Rejected because the manual four-step flow is a meaningful barrier for non-technical admins and does not match the platform-in-a-box promise.
Abstract over multiple hosting providers (Cloudflare, Vercel, Netlify). More flexible, but every provider has a different API, different auth model, and different env-var injection story. The abstraction layer would be speculative complexity — ark uses Cloudflare Pages today (ADR 0005). A provider abstraction belongs to a future ADR if a second provider is ever needed.
Custom domains and DNS automation. Out of scope for this ADR. The auto-provisioned project uses the default <project>.pages.dev subdomain. DNS configuration is manual for now.