Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/metareflection/dafny-replay/llms.txt

Use this file to discover all available pages before exploring further.

The Cloudflare backend adapter runs the complete dafny-replay stack on Cloudflare’s edge infrastructure: D1 (SQLite) for state, a Hono-based Worker for dispatch and auth, a Durable Object for per-entity WebSocket hubs, and custom JWT authentication. Compared to the Supabase backend, this option gives you lower cost at scale and full ownership of the infrastructure, at the cost of building and operating each layer yourself.

Architecture

┌────────────────────────────────────────────────────┐
│  Cloudflare Pages (static hosting)                 │
│  - React app (dist/)                               │
└────────────────────────────────────────────────────┘


┌────────────────────────────────────────────────────┐
│  Cloudflare Workers + D1 + Durable Objects         │
│                                                    │
│  ┌─────────────┐  ┌──────────┐  ┌───────────────┐  │
│  │  Worker     │  │  D1      │  │ Durable Object│  │
│  │  (Hono)     │  │ (SQLite) │  │ (per entity)  │  │
│  │             │  │          │  │               │  │
│  │  /auth/*    │  │ projects │  │ WebSocket hub │  │
│  │  /dispatch  │  │ members  │  │ Broadcast     │  │
│  │  /projects  │  │ users    │  │ updates       │  │
│  └─────────────┘  └──────────┘  └───────────────┘  │
└────────────────────────────────────────────────────┘
The shared cloudflare/ package (@dafny-replay/cloudflare) provides auth, realtime, and helper middleware. Each app worker imports from this package and adds only app-specific routes and dispatch logic.

Shared infrastructure package

The cloudflare/ directory at the root of the repository is a shared npm package:
cloudflare/                    # @dafny-replay/cloudflare
├── src/
│   ├── index.ts               # Re-exports
│   ├── auth.ts                # createAuthRoutes(), createAuthMiddleware()
│   ├── realtime.ts            # RealtimeDurableObject, broadcastUpdate()
│   ├── helpers.ts             # corsMiddleware, checkMembership, error helpers
│   └── types.ts               # BaseEnv, AuthVariables, User, DispatchResult
Your app’s worker imports from this package instead of duplicating the infrastructure:
// worker/src/index.ts
import { createAuthRoutes, createAuthMiddleware, corsMiddleware } from '@dafny-replay/cloudflare'

Per-app worker structure

Each app that uses the Cloudflare backend has a worker/ directory alongside its frontend source:
your-app-cloud/
├── src/
│   ├── backend/
│   │   ├── types.ts
│   │   ├── supabase.ts
│   │   ├── cloudflare.ts
│   │   └── index.ts
│   └── hooks/
├── worker/
│   ├── src/
│   │   ├── index.ts           # Hono app — add app-specific routes here
│   │   ├── projects.ts        # CRUD and member management
│   │   ├── dispatch.ts        # Dispatch endpoint (calls Dafny bundle)
│   │   └── dafny-bundle.ts    # Generated — do not edit
│   ├── schema.sql
│   ├── wrangler.toml
│   └── .dev.vars              # Local secrets (not committed)
└── .env.example
Reference apps using this layout: collab-todo/, kanban-cloud/, clear-split-cloud/.

Environment variables

VITE_BACKEND=cloudflare
VITE_API_URL=https://your-worker.workers.dev
For local development, point VITE_API_URL at the local Wrangler dev server:
VITE_BACKEND=cloudflare
VITE_API_URL=http://localhost:8787

Backend implementation

The Cloudflare adapter wraps fetch with a JWT auth header and maps the Backend interface to REST endpoints on the Worker:
// src/backend/cloudflare.ts
export function createCloudflareBackend(apiUrl: string): Backend {
  const api = async (path: string, options?: RequestInit) => {
    const res = await fetch(`${apiUrl}${path}`, {
      ...options,
      headers: {
        Authorization: `Bearer ${getToken()}`,
        'Content-Type': 'application/json',
        ...options?.headers
      }
    })
    return res.json()
  }

  return {
    auth: {
      getCurrentUser: async () => api('/auth/me'),
      signIn: async (email, password) => {
        const { token, user } = await api('/auth/signin', {
          method: 'POST',
          body: JSON.stringify({ email, password })
        })
        setToken(token)
        return user
      },
      signOut: async () => {
        removeToken()
      },
      // ...
    },

    dispatch: {
      single: async (projectId, baseVersion, action) => {
        return api('/dispatch', {
          method: 'POST',
          body: JSON.stringify({ projectId, baseVersion, action })
        })
      }
    },

    realtime: {
      subscribe: (projectId, onUpdate) => {
        const wsUrl = `${apiUrl.replace('https', 'wss').replace('http', 'ws')}/realtime/${projectId}?token=${getToken()}`
        const ws = new WebSocket(wsUrl)
        ws.onmessage = (e) => {
          const { version, state } = JSON.parse(e.data)
          onUpdate(version, state)
        }
        return () => ws.close()
      }
    }
  }
}

Dispatch in the Worker

The Worker’s dispatch endpoint loads state from D1, calls the compiled Dafny bundle, and writes back with an optimistic lock — the same logic as the Supabase Edge Function, but running in the Workers runtime:
// worker/src/dispatch.ts (simplified)
import { dispatch } from './dafny-bundle'

app.post('/dispatch', authMiddleware, async (c) => {
  const { projectId, baseVersion, action } = await c.req.json()
  const user = c.get('user')

  // Authorization: check membership in D1
  const membership = await c.env.DB
    .prepare('SELECT role FROM project_members WHERE project_id = ? AND user_id = ?')
    .bind(projectId, user.id)
    .first()

  if (!membership) return c.json({ error: 'Not a member' }, 403)

  // Load current state
  const project = await c.env.DB
    .prepare('SELECT state, version, applied_log FROM projects WHERE id = ?')
    .bind(projectId)
    .first()

  // Run verified Dafny dispatch
  const result = dispatch(project.state, project.applied_log || [], baseVersion, action)

  if (result.status === 'rejected') return c.json(result)

  const newVersion = project.version + 1

  // Optimistic lock: update only if version unchanged
  const { meta } = await c.env.DB
    .prepare('UPDATE projects SET state = ?, version = ? WHERE id = ? AND version = ?')
    .bind(JSON.stringify(result.state), newVersion, projectId, project.version)
    .run()

  if (meta.changes === 0) return c.json({ status: 'conflict' }, 409)

  // Broadcast to WebSocket clients via Durable Object
  await broadcastUpdate(c.env.REALTIME, projectId, newVersion, result.state)

  return c.json({ status: 'accepted', version: newVersion, state: result.state })
})

WebSocket realtime via Durable Objects

Each entity gets its own Durable Object instance that acts as a WebSocket hub. When dispatch succeeds, the Worker calls broadcastUpdate() which forwards the new state to all connected WebSocket clients:
// Realtime subscription (client side)
realtime: {
  subscribe: (projectId, onUpdate) => {
    const ws = new WebSocket(`${wsBase}/realtime/${projectId}?token=${getToken()}`)
    ws.onmessage = (e) => {
      const { version, state } = JSON.parse(e.data)
      onUpdate(version, state)
    }
    return () => ws.close()
  }
}
WebSocket connections pass the JWT in the query string (?token=JWT) because browsers cannot set Authorization headers on WebSocket handshake requests. The Worker validates membership before upgrading the connection. Avoid enabling full URL logging in production to prevent token exposure in log data.

D1 schema

D1 uses SQLite syntax. The schema mirrors the Supabase schema but without RLS — authorization is enforced in Worker code instead:
CREATE TABLE users (
  id         TEXT PRIMARY KEY,
  email      TEXT UNIQUE NOT NULL,
  password   TEXT NOT NULL,
  created_at TEXT DEFAULT (datetime('now'))
);

CREATE TABLE projects (
  id          TEXT PRIMARY KEY,
  name        TEXT NOT NULL,
  owner_id    TEXT NOT NULL REFERENCES users(id),
  state       TEXT NOT NULL DEFAULT '{}',
  version     INTEGER NOT NULL DEFAULT 0,
  applied_log TEXT NOT NULL DEFAULT '[]',
  created_at  TEXT DEFAULT (datetime('now'))
);

CREATE TABLE project_members (
  project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
  user_id    TEXT NOT NULL REFERENCES users(id)    ON DELETE CASCADE,
  role       TEXT NOT NULL DEFAULT 'member',
  PRIMARY KEY (project_id, user_id)
);

Dafny bundle generation

The Dafny bundle for a Cloudflare Worker is generated with the --cloudflare flag:
# From the repo root
./compile.sh your-app-cloud
This generates worker/src/dafny-bundle.ts using import BigNumber from 'bignumber.js' (npm, not esm.sh) and inlines the Dafny code directly (Workers block new Function() calls).
Do not edit dafny-bundle.ts by hand. Regenerate it by running ./compile.sh after modifying domain Dafny files.

Deploying

# Step 1: compile the Dafny bundle
./compile.sh your-app-cloud

# Step 2: create the D1 database
npx wrangler d1 create your-app

# Step 3: update wrangler.toml with the database_id, then apply the schema
npx wrangler d1 execute your-app --remote --file=worker/schema.sql

# Step 4: set the JWT secret
npx wrangler secret put JWT_SECRET

# Step 5: deploy the Worker
cd worker && npm run deploy

# Step 6: build and deploy the frontend
cd ..
echo "VITE_BACKEND=cloudflare\nVITE_API_URL=https://your-worker.workers.dev" > .env.production
npm run build
npx wrangler pages deploy dist --project-name=your-app --branch=main
See DEPLOY_CLOUDFLARE.md in the repository for the complete step-by-step guide including custom domains, local development setup, and troubleshooting.

Security model

D1 has no Row-Level Security. Authorization is enforced in layers by the Worker:
LayerResponsibilityEnforced by
1. AuthenticationVerify the caller’s identityWorker (JWT validation)
2. Access controlCheck project membershipWorker (D1 query)
3. State validityValidate the action against current stateDafny
4. Invariant preservationEnsure state remains consistent after the actionDafny
If an actor bypasses layer 2 and submits an action referencing a non-existent member, Dafny (layer 3) will reject it. Layers 1 and 2 remain the application’s responsibility.
Dafny-verified membership checks (moving layer 2 into Dafny) are listed as a TODO in the repository. The Dafny spec already includes members: set<UserId> in the model and a NotAMember error type.

Trade-offs

The Cloudflare backend is the bring-your-own infrastructure option.
AdvantageDetail
Lower cost at scaleWorkers pricing is per-request; D1 and Durable Objects are inexpensive for typical workloads
Global edge deploymentWorkers run in hundreds of locations with no extra configuration
Full ownershipNo vendor auth or realtime subscription to depend on
Trade-offDetail
More to operateAuth, realtime, and database are all application concerns
No database-level RLSAccess control bugs in Worker code are not caught by the database
WebSocket token in URLJWTs appear in the WebSocket URL; avoid enabling full URL logging in production

Backend overview

Understand the Backend interface and what stays the same across providers.

Supabase backend

Compare with the Supabase option: managed auth, PostgreSQL RLS, and built-in realtime.

Build docs developers (and LLMs) love