Skip to main content
Version: Next

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;
EndpointProtocolUse Case
/api/*RESTExternal integrations, simple CRUD
/trpc/*tRPC HTTPType-safe queries/mutations
/trpc (WS)tRPC WebSocketSubscriptions
/sync/subscribeWebSocketReal-time data change notifications
/yjs/:roomIdWebSocketReal-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"] }
]
}