All docs

Bring your own Cloudflare account

Your project's object storage, public objects, deployed frontend assets, CDN-cached pages and custom domain all run on your Cloudflare account, not fn0's. You keep R2's free tier, your own purge budget and your own bill; fn0 runs the compute and holds no storage on your behalf.

This setup is required before a project can serve a frontend: fn0 Cloud has no way to meter object storage usage on an account it does not own, so bring-your-own-Cloudflare is the default premise for every project, not an opt-in. There is no fn0.dev fallback to fall back to — every project gets a hostname inside one of the owner's zones.

What you need

A Cloudflare account with a zone in it — a domain whose nameservers point at Cloudflare. Not just an account: an R2 custom domain has to live in a zone in the same account as the bucket, and that hostname is where your frontend assets get served from.

Connect the account, then initialize the project

forte cloud login --zone example.com

forte cloud init \
  --project . \
  --project-name my-app \
  --zone example.com

forte login must be run first. Then forte cloud login asks for the Cloudflare setup token and installs the account-level broker. It stores only the non-secret account ID and broker URL locally; the setup token remains in your Cloudflare account's Secrets Store.

After that, forte cloud init never asks for the setup token. It reuses the broker for this project or any other project on the same account. --setup-token-from-clipboard is available on cloud login when an AI agent creates the token for you — see AI-assisted setup.

As part of initialization, the CLI enables WebSockets for the selected zone. This is required for Forte WebSocket routes to complete their upgrade through Cloudflare's proxy. It is safe to run repeatedly because the setting is written to on each time.

--zone is the zone name, such as example.com. It is not the hexadecimal zone ID shown in Cloudflare's API details. The CLI resolves the exact zone named by the argument and never picks the first zone returned by the API.

--project-name is also a DNS label. It must contain only lowercase ASCII letters, digits, and hyphens, be 1–63 characters long, and start and end with a letter or digit. The public hostname is derived as:

<project-name>.<zone>

For example, this project answers on my-app.example.com. A separate domain argument is not needed.

Setup credential

Setup has to create buckets, point a hostname at one, write one zone rule, add one DNS record and sign a certificate. Create one reusable setup token. Cloudflare dashboard → My Profile → API Tokens → Create Token → Create Additional Tokens. Use that template and give it exactly one permission:

ScopePermission
UserAPI Tokens → Edit

Type it in once, at the masked prompt forte cloud login shows. That first run uses it to install a broker Worker — a small Cloudflare Worker that lives in your own account — and stores the token as a secret in your account's own Secrets Store, bound only to that Worker. From then on, the broker is the only thing that ever reads the token: it mints a short-lived, narrowly-scoped provisioning token for each operation setup needs, does the work, and revokes it. The CLI itself never holds the setup token beyond that first prompt, and it is never sent to fn0.

This token is powerful: a token that can create tokens can create any token allowed by the account. That's exactly why it never leaves your own Cloudflare account after the first run. If it's ever compromised or you just want a new one, forte cloud rotate replaces it in place and republishes the current broker Worker; forte cloud clear removes it without replacing it; forte cloud destroy removes the whole broker, token included. See forte cloud for all three.

AI-assisted setup

forte cloud login --setup-token-from-clipboard and forte cloud rotate --setup-token-from-clipboard take the setup token from the OS clipboard instead of a masked prompt: the command waits, you (or an AI agent driving your browser) create the token in the dashboard and click its Copy button, and the command picks it up, verifies it against Cloudflare, and wipes the clipboard. The token never becomes a command argument, an environment variable, or a file, and — when an agent does the clicking — it need not pass through the agent's own context: the agent only clicks the native Copy control. Cloudflare may copy a cURL command instead of the bare token; forte extracts its bearer value locally before verification and never prints it.

During bootstrap forte rolls the token's secret in place — Cloudflare will not re-create a token that can manage tokens, so the secret is regenerated rather than the token cloned — and stores the rolled value. The token keeps its name and permission; the string that briefly sat on the clipboard stops working the moment the roll lands.

The dashboard steps are the same whichever tool does them:

  1. https://dash.cloudflare.com/profile/api-tokens → Create Token.
  2. Scroll to the "Create Additional Tokens" template → Use template. (Not "Create Custom Token" — User -> API Tokens -> Edit is not reliably offered there.)
  3. The form is pre-filled with User | API Tokens | Edit. Continue to summary → Create Token.
  4. Click Copy. forte takes the token or the copied cURL command from there.

Signing in, two-factor auth, and any identity re-prompt are yours to do in the browser — an agent must not type credentials. Creating the token is a real change to your account, so approve it before the final click.

Claude Code users: run the cloudflare-setup-token skill, which drives this in the browser.

Codex users: invoke the $cloudflare-setup-token skill. It opens the Cloudflare dashboard with Codex's browser controls, pauses for the user's login, two-factor authentication, and final creation approval, then clicks only the dashboard's Copy control. The token value never enters the agent's context; forte consumes it from the clipboard. The repository copy lives at .codex/skills/cloudflare-setup-token/SKILL.md so every contributor can use the same procedure.

What the stored credentials can do

CredentialWhat it can do
Worker R2read and write objects in this project's two object-storage buckets. Cannot reach the frontend-asset bucket, cannot reach another project's buckets, cannot delete a bucket, cannot call the Cloudflare API at all
Frontend-asset R2read and write objects in this project's frontend-asset bucket, and nothing else. Never sent to a worker
Purgepurge this one zone's cache. Nothing else — not DNS, not cache rules, not R2, not certificates

Those limits are measured against the live API, not inferred from the permission names.

So the worst a total compromise of fn0 can do to your Cloudflare account is rewrite objects in the three buckets it created for this project, and clear your cache.

What gets created

Three buckets, all this project's alone. Nothing is shared with your other fn0 projects, so no key prefix is carrying the separation.

BucketHoldsReachable at
fn0-<project-id>-private-object-storagewhat object_storage::private writesnowhere — signed requests only
fn0-<project-id>-public-object-storagewhat object_storage::public writesfn0-<project-id>-public-object-storage.<zone>
fn0-<project-id>-frontend-assetyour deployed frontend buildfn0-<project-id>-frontend-asset.<zone>

The two public buckets answer on a hostname that is the bucket's own name in your zone, so a bucket and the address it serves from cannot drift apart. Each costs one DNS record.

fn0 adds one rule to your zone and leaves your own rules in place. It matches fn0-*-frontend-asset.<zone> and fn0-*-public-object-storage.<zone>; the cache rule also matches the custom domains registered for fn0 projects. This covers every fn0 project you add without a rule per project — a free zone allows ten of each, and a rule per project would run out at ten projects. Both halves of each pattern are required so a rule cannot swallow a hostname of your own.

The cache rule makes static HTML eligible for the CDN and respects the origin's cache headers. Your other hostnames keep the zone setting.

Smart Tiered Cache is enabled for the zone so a cache miss in an edge location can be filled by an upper tier instead of reaching the worker fleet directly.

The two buckets a browser can reach are also given a CORS allowlist holding one origin: your project's own domain. Cloudflare keys a separate cache entry per Origin value and Origin is not verified, so an allowlist of * would let any site on the web read every one of those entries and bill the misses to you. The allowlist moves with the domain, so changing the domain rewrites it.

Workers pick a connection up within about a second; no redeploy is needed.

Set the project up before it stores anything. A project has nowhere to store and cannot serve a frontend until it is connected.

Connecting is first-time only, and there is no way back. Reconnecting a project, rotating its Worker/frontend-asset/purge credentials, and moving it to a different Cloudflare account are all unsupported — not merely undocumented, but refused. If one of those three credentials is lost or revoked, the project cannot be repaired through the CLI; treat them as things you do not lose. (This is about the project's own credentials, not the setup token — that one you can rotate. See Setup credential.)

The domain

Not optional: a project answers on the hostname derived from its project name and zone, and on nothing else. There is no fn0.dev fallback.

Signing an origin certificate needs a permission fn0 deliberately does not hold. The CLI generates the key pair and the certificate request locally — the private key never leaves your machine — and has the broker submit that request to Cloudflare and sign it through your own Origin CA. The CLI then uploads the certificate and key to fn0, and the broker writes the proxied CNAME for that hostname into your zone, pointing at the fn0 origin hostname. Nothing is left for you to add by hand.

The record is written last, after fn0 holds the certificate and the zone carries the cache rule, so the hostname resolves nowhere until everything behind it is ready.

A hostname that is already taken is not silently overwritten. An existing CNAME on that name is repointed — you named the hostname on the command line, so where it resolves is what you are asking to change — but an A or AAAA record there stops the command with an error instead. Changing a project's domain removes the old record, and only that record: if the CNAME fn0 wrote has since been edited, it is left in place and reported. A record still pointing at fn0 is worth removing — the next project to register that hostname inherits whatever reaches it.

The derived hostname is written to Forte.toml, and forte deploy refuses if the stored project name, zone, and hostname disagree with the live project. Changing the zone or project hostname requires a new project connection.

The record must stay orange-clouded. An Origin CA certificate is not valid for a direct visitor connection, so switching the record to DNS-only breaks the hostname immediately and visibly.

What fn0 still holds

  • The compute and the request routing
  • Your document database (Turso), which is not part of this
  • The bundle store your deployed code is distributed from. It holds compiled WebAssembly rather than anything your app stores, and it grows with deploys rather than with traffic, so it stays on fn0's account

Removing a project

forte destroy tears the project down on both sides. fn0's control plane removes the routing, the deployed bundles and static assets, the database, and empties the three R2 buckets. The CLI waits for that asynchronous teardown to finish before the broker revokes project credentials. Through the setup broker it then removes the project's Cloudflare footprint: the app hostname's DNS record, the two public buckets' custom domains, the origin certificate, and the three minted R2/purge tokens.

The three buckets themselves are left standing — empty. fn0 holds only object-scoped credentials there by design; deleting a bucket needs a token minted from the setup token, which only the broker can do. forte destroy --delete-buckets has the broker delete them once control has emptied them; a bucket teardown that has not finished clearing is reported as pending, and re-running is safe.

An app DNS record you have edited (added an A record, turned the proxy off, or changed the CNAME target) is left in place and reported rather than deleted. The teardown request remains authorized by the broker owner's account identity after the fn0 project record has been removed, so a failed cleanup can be retried from the same project directory.

Revoking

Deleting either R2 token in your Cloudflare dashboard breaks the project at request time — there is no grace period, because every request signs against it.

There is no recovery path for a project's own credentials. A project that is already connected is refused a second connection, and nothing else can replace a stored credential, so a revoked Worker, frontend-asset, or purge token means the project has to be recreated.

The setup token is different: it isn't tied to one project, so losing or revoking it doesn't touch anything already connected. forte cloud rotate replaces it in the broker's Secrets Store and republishes the current Worker; forte cloud clear removes it without a replacement; forte cloud destroy removes the broker Worker and its Secrets Store entirely, for every project that shares this Cloudflare account.