for agents/llms.txtv0.12.106

Home / Articles / Cloudflare as Lego blocks: what a non-specialist can build on it in October 2026, and an sgit vault in 83 lines

Cloudflare as Lego blocks: what a non-specialist can build on it in October 2026, and an sgit vault in 83 lines

By · 2026-10-18 · article v1.0.0 · newsroom v0.12.106 · cloudflareworkersr2durable-objectsworkers-aisgitvaultsminimal-codeself-hostingarticle

Abstract: I keep my domains at Cloudflare, and Cloudflare keeps shipping good things, but I do not want to learn Cloudflare. I want to use it, the way a non-specialist would, with an agent doing the one-time setup. So this maps its developer platform, as of 18 October 2026, as the smallest blocks a person could put together, each with what it is for and what is free: static hosting with unlimited free requests, Workers at 100,000 requests a day, R2 storage with no egress fees, D1, KV and Durable Objects, 71 models running on Cloudflare's own GPUs with 10,000 free neurons a day and a promise not to train on what you send, inbound email, Zero Trust access for up to 50 users, and domains at cost. Then it tests the question I most wanted answered: can an sgit vault live on Cloudflare? A read-only vault needs no code at all. `sgit publish` writes a folder any static host can serve, and the real sgit client cloned it back identically. A read-write vault needs one Worker of 83 lines, one R2 bucket and one Durable Object per vault, all on the free plan. We wrote it and ran it in Cloudflare's own local runtime with the real client: create, push, clone, a stale push that merged, two pushes at once where one was told "nothing was overwritten", a read-only clone that could not write, and no plaintext anywhere on the server. The catch for non-specialists is setup: Cloudflare's API cannot be called from a web page, so the one-time setup is a Deploy to Cloudflare button, a temporary account, or an agent with the command line. That is fine, because it happens once.

Cloudflare's developer platform as blocks, grouped by job. Put something on the web: static assets, free and unlimited; Workers, 100,000 requests a day; DNS and domains at cost. Keep data: R2 with 10 GB and zero egress, D1, KV, Durable Objects. Think with models it hosts: Workers AI with 10,000 neurons a day on Cloudflare's own GPUs, AI Gateway, Vectorize and AI Search. Run jobs: cron, queues and workflows, Browser Run, and Containers on the paid plan. Talk and guard: inbound email routing, email sending on the paid plan, Access free to 50 users, Turnstile and Tunnel.
Less code: a series in four parts

Code is a liability: remove what each use case does not call, apply it to the browser, and build on small blocks you did not have to write.

In short

What I want

I am technical, and I could learn Cloudflare. I do not want to. I want to use it, the way I want to use a good tool: put a site on a domain I already own there, keep a vault, run a small API, call a model, receive mail, and not think about any of it again. And I want the smallest pieces that do each job, the same argument as the minimum code below: the least code and the least configuration that does exactly the thing.

The way to get there is not a course. It is a set of skills and prompts an agent can follow, and a few short code examples, so that setup happens once, correctly, and the result is something I own.

The blocks

All limits are from Cloudflare's own pages, read on 18 October 2026; most were updated in the last two months, and they change.

Put something on the web.

Keep data.

Think, with models it hosts.

Run jobs.

Talk and guard.

Which models stay inside Cloudflare

This matters for anyone who wants an assistant over private material. Workers AI's privacy page says "Cloudflare does not use Customer Content processed by Workers AI to train AI models", and the models it lists run on Cloudflare's own GPUs. So a Worker that calls a @cf/ model, on data in your own R2 bucket, keeps the whole loop inside your Cloudflare account.

One caution. Since 2026 the same call can also reach other providers' models, OpenAI's or Anthropic's, through AI Gateway. Those do not run on Cloudflare's GPUs. If staying inside is the point, the model name is the control: @cf/ stays, anything else leaves.

It also answers a gap from free models, your own key: Cloudflare had the most generous free allowance there, but a web page cannot call its API directly. A Worker in the reader's own account can, which is exactly the small server that article said would be needed.

An sgit vault on Cloudflare

medium

The question I most wanted answered was whether a vault could live on Cloudflare, and how much code that takes. The answer comes from how sgit is built. The server never sees a key or a plaintext byte: it stores encrypted objects under ids, and checks one thing on writes, the vault's write key. That is the value of constraints in practice: because the server can read nothing, it has almost nothing to do.

Read-only: no code. sgit already knows how to read a vault from any static file host. sgit publish writes a folder: a plaintext cover and a manifest, with the encrypted objects beside them at the paths the client asks for. We published a test vault, 39 files and 220 KB, served the folder from a plain file server, and cloned it with the read key and --transport static. The clone was identical. On Cloudflare that folder goes into Workers static assets or a public R2 bucket, and costs nothing at this size. There is nothing to write to, so nothing can be written.

Read-write: 83 lines. The vault API sgit talks to has seven endpoints: info, read, list, batch, write, delete and destroy. We wrote them as one Worker. Reads go straight to R2. Every write for a vault goes through that vault's Durable Object, which Cloudflare runs one request at a time, so the compare-and-swap that stops two pushes overwriting each other is atomic. The first write to a vault binds its write key, by keeping a SHA-256 of it; later writes must match.

We ran it in Cloudflare's own runtime, workerd, locally through Miniflare 4, and pointed the real sgit client at it:

  1. Create, commit, push, clone. The clone was identical to the original.
  2. A push over a stale copy. sgit merged it; both edits were kept.
  3. Two pushes at the same moment. One won. The other was told "the branch moved on the server while this push was in flight (a teammate pushed first); nothing was overwritten and nothing of yours is lost", pulled, and pushed. Both files landed.
  4. A read-only clone. It refused to write: "This vault was cloned read-only."
  5. A wrong write key. The Worker answered 403.
  6. The server's storage. We searched every file the server stored for the plaintext; none of it was there.

This is the whole Worker:

// A read-write sgit vault server on Cloudflare: one Worker, one R2 bucket, one Durable Object class.
// The server only ever holds ciphertext. Reads go straight to R2; every write for a vault goes through
// that vault's Durable Object, which runs one request at a time, so compare-and-swap is atomic.
// The write key is bound to the vault on its first write (a SHA-256 of it is kept); later writes must match.

const J = (o, s = 200) => new Response(JSON.stringify(o), { status: s, headers: { 'content-type': 'application/json', ...CORS } });
const CORS = { 'access-control-allow-origin': '*', 'access-control-allow-headers': 'content-type,x-sgraph-access-token,x-api-key,x-sgraph-vault-write-key', 'access-control-allow-methods': 'GET,PUT,POST,DELETE,OPTIONS' };
const b64 = (u8) => { let s = ''; for (let i = 0; i < u8.length; i += 0x8000) s += String.fromCharCode(...u8.subarray(i, i + 0x8000)); return btoa(s); };
const unb64 = (s) => Uint8Array.from(atob(s), (c) => c.charCodeAt(0));
const sha256 = async (s) => b64(new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(s))));
const same = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);
const VID = /^[a-z0-9]{4,24}$/;

export default {
  async fetch(req, env) {
    if (req.method === 'OPTIONS') return new Response(null, { headers: CORS });
    if (env.ACCESS_TOKEN && req.headers.get('x-sgraph-access-token') !== env.ACCESS_TOKEN) return J({ error: 'access token required' }, 401);
    const p = new URL(req.url).pathname.split('/').map(decodeURIComponent);       // ['', 'api', 'vault', op, vid, fid?]
    if (p[2] === 'info') return J({ name: 'sgit-vault-on-cloudflare', version: '0.1.0' });
    if (p[2] === 'auth') return J({ ok: true });
    const [op, vid, fid] = [p[3], p[4], p.slice(5).join('/')];
    if (p[2] !== 'vault' || !VID.test(vid || '')) return J({ error: 'not found' }, 404);
    if (await env.VAULTS.head(`${vid}/.tombstone`)) return J({ error: `vault ${vid} has been permanently deleted` }, 403);
    if (op === 'read' && req.method === 'GET') {                                  // ciphertext, straight from R2
      const o = await env.VAULTS.get(`${vid}/${fid}`);
      return o ? new Response(o.body, { headers: CORS }) : J({ error: 'not found' }, 404);
    }
    if (op === 'list') {
      const prefix = new URL(req.url).searchParams.get('prefix') || '', files = [];
      let cursor;
      do { const r = await env.VAULTS.list({ prefix: `${vid}/${prefix}`, cursor });
           r.objects.forEach((o) => { if (!o.key.endsWith('/.writekey')) files.push(o.key.slice(vid.length + 1)); });
           cursor = r.truncated ? r.cursor : null; } while (cursor);
      return J({ files });
    }
    if (op === 'batch') {
      const body = await req.json();
      if (body.operations.every((o) => o.op === 'read')) return J(await readMany(env, vid, body.operations));
      return env.WRITER.get(env.WRITER.idFromName(vid)).fetch(new Request(req.url, { method: 'POST', headers: req.headers, body: JSON.stringify(body) }));
    }
    if (['write', 'delete', 'destroy'].includes(op)) return env.WRITER.get(env.WRITER.idFromName(vid)).fetch(req);
    return J({ error: 'not found' }, 404);
  },
};

async function readMany(env, vid, ops) {
  const results = await Promise.all(ops.map(async ({ file_id }) => {
    const o = await env.VAULTS.get(`${vid}/${file_id}`);
    return o ? { status: 'ok', file_id, data: b64(new Uint8Array(await o.arrayBuffer())) } : { status: 'not_found', file_id };
  }));
  return { status: 'ok', results };
}

export class VaultWriter {                     // one per vault; Cloudflare runs its requests one at a time
  constructor(state, env) { this.env = env; }
  async authorised(vid, key) {
    if (!key) return false;
    const h = await sha256(key), k = `${vid}/.writekey`, held = await this.env.VAULTS.get(k);
    if (!held) { await this.env.VAULTS.put(k, h); return true; }               // first write binds the key
    return (await held.text()) === h;
  }
  async fetch(req) {
    const p = new URL(req.url).pathname.split('/').map(decodeURIComponent), [op, vid, fid] = [p[3], p[4], p.slice(5).join('/')];
    if (!(await this.authorised(vid, req.headers.get('x-sgraph-vault-write-key')))) return J({ error: 'write key does not match this vault' }, 403);
    const B = this.env.VAULTS;
    if (op === 'write') { await B.put(`${vid}/${fid}`, await req.arrayBuffer()); return J({ status: 'ok' }); }
    if (op === 'delete') { await B.delete(`${vid}/${fid}`); return J({ status: 'ok' }); }
    if (op === 'destroy') { await B.put(`${vid}/.tombstone`, new Date().toISOString()); return J({ status: 'ok' }); }
    const { operations } = await req.json();
    for (const o of operations) if (o.op === 'write-if-match' && o.match) {      // compare everything first: a lost race writes nothing
      const cur = await B.get(`${vid}/${o.file_id}`);
      if (!cur || !same(new Uint8Array(await cur.arrayBuffer()), unb64(o.match))) return J({ status: 'conflict', message: `CAS mismatch on ${o.file_id}` });              // inside an HTTP 200, as sgit expects
    }
    const results = [];
    for (const o of operations) {
      if (o.op === 'read') { results.push(...(await readMany(this.env, vid, [o])).results); continue; }
      if (o.op === 'delete') await B.delete(`${vid}/${o.file_id}`);
      else await B.put(`${vid}/${o.file_id}`, unb64(o.data));
      results.push({ status: 'ok', file_id: o.file_id });
    }
    return J({ status: 'ok', results });
  }
}

And its configuration, for wrangler deploy:

# An sgit vault server on Cloudflare: deploy with `npx wrangler deploy` after `npx wrangler r2 bucket create sgit-vaults`.
name = "sgit-vault"
main = "worker.js"
compatibility_date = "2026-08-01"

[[r2_buckets]]
binding = "VAULTS"
bucket_name = "sgit-vaults"

[[durable_objects.bindings]]
name = "WRITER"
class_name = "VaultWriter"

[[migrations]]
tag = "v1"
new_sqlite_classes = ["VaultWriter"]

# Optional: require an access token on every request, as the hosted server does.
# npx wrangler secret put ACCESS_TOKEN

What it does not do yet, said plainly. Files over 4 MB use presigned uploads in sgit, which this Worker does not implement. There is one optional access token, not one per user. The free plan's 10 ms of CPU per request will be tight for very large batch reads. And it has not yet run on a real Cloudflare account: everything above was in the same runtime Cloudflare uses, on this machine. Python Workers became generally available on 21 September, so a Python version is possible too; for seven endpoints, JavaScript is the smaller code.

How a non-specialist sets it up

Here is the catch. We tested Cloudflare's API from a browser's point of view: it does not answer the preflight a web page needs, so a page cannot call it directly, even with the user's own token. Setup cannot happen purely in a browser tab. That leaves four ways, and three of them suit someone who does not want to learn Cloudflare.

  1. A Deploy to Cloudflare button. A link that clones a public repository into the user's own account, creates the R2 bucket, Durable Objects and other resources it declares, builds and deploys. For a vault host, that is one click and a sign-in. It works for Workers, not Pages.
  2. A temporary account. Since June, wrangler deploy --temporary deploys with no sign-up; it stays live for an hour, with a link to claim it as a real account. Good for trying something before committing to it.
  3. An agent, once. Claude Code, or any agent with a terminal, signs in with wrangler login and its OAuth flow, runs the commands, and hands back the URL. Cloudflare also runs an MCP server for its whole API, so an agent can do it without the command line. A new command-line tool, cf, entered open beta on 28 September and will replace Wrangler over the next 18 months; a skill written today should say which it uses.
  4. The dashboard's own assistant. Agent Lee, in beta, can change settings such as DNS records with your approval, but cannot deploy applications.

The pattern I want is the first and third together: a published repository per package, with a button for people and a skill for agents, so that "host my vault on my Cloudflare account" is one sentence to an agent, done once, with a scoped token that can be revoked.

Five packages

The blocks are more useful as packages, each the smallest set for one job.

PackageBlocksOn the free plan?
A site on my domainStatic assets, DNSYes
My vault, hostedStatic assets, or a Worker with R2 and a Durable ObjectYes
An assistant over my documentsWorkers AI, R2, AI Gateway, AccessYes, within the allowances
An inbox that does thingsEmail Routing, an Email WorkerYes
A form or a small APIA Worker, D1, TurnstileYes

Each one is a repository with a button, a skill and a page saying exactly what it can reach, which is its behaviour policy.

What I would watch

What this does not claim

Write to agent@riskmandate.ai if you deploy it on a real account and something breaks.

Where this comes from

A voice memo from me on 18 October 2026. Cloudflare's developer documentation, read on 18 October 2026 by a research agent: Workers limits and pricing, R2 pricing, Durable Objects pricing, Workers AI pricing, models and privacy, Email Service pricing, Browser Run limits, Deploy to Cloudflare buttons, temporary accounts, Cloudflare's MCP servers, R2 data location, and the blog posts on Python Workers and the cf command line. The API's lack of CORS was tested from this machine. The vault tests: sgit 0.21.0 against the Worker above in workerd through Miniflare 4, and against a plain static file server, on 18 October 2026. Written up and checked by this site's agent; the editorial responsibility is mine.

Threads

Vaults & method This article as a graph →

Builds on

Continued by

All articles · All graphs

Article No. 180 · start a reading list with it

Want the next issue by email. One issue a week or so: what was published, what it adds up to, and what is worth your time. Subscribe to the SGit Newsroom →

← All articles