Backend Stack
Backend Architecture
The backend is built on Cloudflare Workers, utilizing D1 for relational data and Durable Objects for real-time state.
System Overview
Technology Overview
The backend runs on Cloudflare Workers with D1 database.
Cloudflare Workers
Workers are serverless functions running on Cloudflare's edge network.
Minimal Worker Example
// index.ts - Basic Worker
export default {
async fetch(request: Request): Promise<Response> {
return new Response('Hello from Cloudflare Workers!');
},
};
With Environment Bindings
interface Env {
MY_DATABASE: D1Database;
MY_KV: KVNamespace;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// Access environment bindings
const result = await env.MY_DATABASE.prepare('SELECT 1').first();
return Response.json(result);
},
};
Cloudflare D1
D1 is SQLite at the edge.
Standalone D1 Example
// Schema
const schema = `
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE,
created_at INTEGER DEFAULT (strftime('%s', 'now'))
);
`;
// CRUD Operations
async function getUsers(db: D1Database) {
const { results } = await db.prepare('SELECT * FROM users').all();
return results;
}
async function createUser(db: D1Database, name: string, email: string) {
return await db
.prepare('INSERT INTO users (name, email) VALUES (?, ?)')
.bind(name, email)
.run();
}
async function updateUser(db: D1Database, id: number, name: string) {
return await db
.prepare('UPDATE users SET name = ? WHERE id = ?')
.bind(name, id)
.run();
}
async function deleteUser(db: D1Database, id: number) {
return await db
.prepare('DELETE FROM users WHERE id = ?')
.bind(id)
.run();
}
Hono
Hono is a lightweight web framework for Workers.
Minimal Hono Example
import { Hono } from 'hono';
interface Env {
DB: D1Database;
}
const app = new Hono<{ Bindings: Env }>();
// GET /api/users
app.get('/api/users', async (c) => {
const { results } = await c.env.DB.prepare('SELECT * FROM users').all();
return c.json(results);
});
// POST /api/users
app.post('/api/users', async (c) => {
const { name, email } = await c.req.json();
await c.env.DB
.prepare('INSERT INTO users (name, email) VALUES (?, ?)')
.bind(name, email)
.run();
return c.json({ success: true }, 201);
});
// GET /api/users/:id
app.get('/api/users/:id', async (c) => {
const id = c.req.param('id');
const user = await c.env.DB
.prepare('SELECT * FROM users WHERE id = ?')
.bind(id)
.first();
return user ? c.json(user) : c.json({ error: 'Not found' }, 404);
});
// DELETE /api/users/:id
app.delete('/api/users/:id', async (c) => {
const id = c.req.param('id');
await c.env.DB.prepare('DELETE FROM users WHERE id = ?').bind(id).run();
return c.json({ success: true });
});
export default app;
tRPC
tRPC provides type-safe APIs without code generation.
Minimal tRPC Example
// trpc.ts - Server
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
const t = initTRPC.create();
export const appRouter = t.router({
// Query (GET-like)
hello: t.procedure
.input(z.object({ name: z.string() }))
.query(({ input }) => {
return { greeting: `Hello, ${input.name}!` };
}),
// Mutation (POST-like)
createUser: t.procedure
.input(z.object({
name: z.string(),
email: z.string().email(),
}))
.mutation(({ input }) => {
console.log('Creating user:', input);
return { success: true, user: input };
}),
});
export type AppRouter = typeof appRouter;
// index.ts - Mount tRPC on Hono
import { Hono } from 'hono';
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { appRouter } from './trpc';
const app = new Hono();
// Mount tRPC at /trpc/*
app.all('/trpc/*', (c) => {
return fetchRequestHandler({
endpoint: '/trpc',
req: c.req.raw,
router: appRouter,
createContext: () => ({}),
});
});
export default app;
Client Usage (Frontend)
// trpc.ts - Client setup
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from 'cloudflare-d1-worker'; // Imported from worker!
export const trpc = createTRPCReact<AppRouter>();
// Component usage
function Greeting() {
const { data } = trpc.hello.useQuery({ name: 'World' });
return <h1>{data?.greeting}</h1>;
}
Hybrid API (REST + tRPC)
Our worker supports both REST (Hono) and tRPC endpoints.
import { Hono } from 'hono';
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { appRouter } from './trpc';
const app = new Hono();
// REST endpoints
app.get('/api/customers', async (c) => { /* ... */ });
app.post('/api/customers', async (c) => { /* ... */ });
// tRPC endpoints
app.all('/trpc/*', (c) => {
return fetchRequestHandler({
endpoint: '/trpc',
req: c.req.raw,
router: appRouter,
createContext: () => ({ env: c.env }),
});
});
export default app;
| Endpoint | Protocol | Use Case |
|---|---|---|
/api/* | REST | External integrations, simple CRUD |
/trpc/* | tRPC HTTP | Type-safe queries/mutations |
/trpc (WS) | tRPC WebSocket | Subscriptions |
/sync/subscribe | WebSocket | Real-time data change notifications |
/yjs/:roomId | WebSocket | Real-time collaborative editing |
Durable Objects
Durable Objects provide stateful, persistent connections for WebSockets.
SyncRoom (Data Change Notifications)
Broadcasts notifications when data changes, triggering client sync.
// SyncRoom.ts
export class SyncRoom {
private sessions: Set<WebSocket> = new Set();
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
// Handle broadcast request (internal)
if (url.pathname === '/broadcast') {
const body = await request.json();
this.broadcast(body);
return Response.json({ success: true });
}
// Handle WebSocket upgrade
const [client, server] = Object.values(new WebSocketPair());
this.handleSession(server);
return new Response(null, { status: 101, webSocket: client });
}
private broadcast(message: any) {
const encoded = JSON.stringify(message);
for (const ws of this.sessions) {
ws.send(encoded);
}
}
}
YjsRoom (Collaborative Editing)
Manages real-time Yjs document synchronization with D1 persistence.
// YjsRoom.ts
export class YjsRoom {
private doc: Y.Doc;
private sessions: Set<WebSocket>;
async fetch(request: Request): Promise<Response> {
const roomId = new URL(request.url).searchParams.get('room');
// Load from D1 on first connection
if (this.sessions.size === 0) {
await this.loadFromD1();
}
// Handle WebSocket
const [client, server] = Object.values(new WebSocketPair());
this.handleSession(server);
return new Response(null, { status: 101, webSocket: client });
}
private handleSession(ws: WebSocket) {
// Send current state, handle updates, broadcast to others
}
private async persistToD1() {
const state = Y.encodeStateAsUpdate(this.doc);
await this.env.prod_d1_tutorial
.prepare('INSERT OR REPLACE INTO YjsRooms (RoomId, YjsState, updated_at) VALUES (?, ?, ?)')
.bind(this.roomId, fromUint8Array(state), Date.now())
.run();
}
}
wrangler.jsonc Configuration
{
"durable_objects": {
"bindings": [
{ "name": "SYNC_ROOM", "class_name": "SyncRoom" },
{ "name": "YJS_ROOM", "class_name": "YjsRoom" }
]
},
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["SyncRoom"] },
{ "tag": "v2", "new_sqlite_classes": ["YjsRoom"] }
]
}