1# cloudflare-tanstack-rpc-drizzle
2
3A full-stack example that wires together four pieces:
4
5- **TanStack Start** (React) frontend, deployed as a Cloudflare Worker + assets via `Cloudflare.Website.Vite`.
6- **Effect RPC** backend, deployed as a separate `Cloudflare.Workers.RpcWorker`.
7- **Drizzle + Neon Postgres**, reached through a `Cloudflare.Hyperdrive.Connection` pool, with migrations generated by `Drizzle.Schema`.
8- **Atom RPC** — Effect 4's native `effect/reactivity/AtomRpc` plus the React bindings from `@effect/atom-react` — for reactive queries and mutations in the browser.
9
10## Architecture
11
12```
13Browser (React)
14 │ useAtomValue / useAtomSet
15 ▼
16AtomRpc client (TodoRpcs) src/rpc-client.ts
17 │ HTTP POST /rpc (JSON)
18 ▼
19TanStack Start worker src/routes/rpc.ts (server route)
20 │ env.BACKEND.fetch(...) (private service binding)
21 ▼
22Backend RpcWorker src/backend/api.ts
23 │ Drizzle.Postgres over Hyperdrive
24 ▼
25Neon Postgres branch
26```
27
28The browser cannot use a Cloudflare service binding directly, so the `AtomRpc`
29client points at a same-origin `/rpc` route, and the TanStack Start server
30forwards that request to the private `BACKEND` worker over the service binding.
31This keeps the backend off the public internet and avoids CORS.
32
33## Atom RPC
34
35Effect 4 ships atom RPC in core — no third-party `@effect-atom` package is
36needed (that one targets Effect 3). The only extra dependency is
37`@effect/atom-react`, which provides the React hooks (`useAtomValue`,
38`useAtomSet`, `RegistryProvider`) and is versioned in lockstep with `effect`.
39
40```ts
41// src/rpc-client.ts
42export class TodoClient extends AtomRpc.Service<TodoClient>()("TodoClient", {
43 group: TodoRpcs,
44 protocol: RpcClient.layerProtocolHttp({ url: "/rpc" }).pipe(
45 Layer.provide(FetchHttpClient.layer),
46 Layer.provide(RpcSerialization.layerJson),
47 ),
48}) {}
49
50export const listTodosAtom = TodoClient.query("listTodos", undefined, {
51 reactivityKeys: ["todos"],
52});
53export const createTodoAtom = TodoClient.mutation("createTodo");
54```
55
56```tsx
57// src/routes/index.tsx
58const todos = AsyncResult.getOrElse(useAtomValue(listTodosAtom), () => []);
59const createTodo = useAtomSet(createTodoAtom);
60// ...
61createTodo({ payload: { text }, reactivityKeys: ["todos"] });
62```
63
64`reactivityKeys: ["todos"]` ties the list query to the mutations: when a
65mutation runs with the same key, the list query is invalidated and refetched
66automatically.
67
68The same `TodoRpcs` group is the single source of truth — it is served by the
69backend and consumed by the browser client, so one `Schema` codec round-trips
70every value over the wire.
71
72## Running
73
74Requires Cloudflare and Neon credentials (same as the other Neon examples):
75
76- `CLOUDFLARE_API_TOKEN` (or `alchemy profile edit --add Cloudflare`)
77- `NEON_API_KEY`
78
79```sh
80bun install
81
82# Deploy the backend RpcWorker, Neon branch, Hyperdrive, and the TanStack site.
83bun alchemy deploy
84
85# Tear everything down.
86bun alchemy destroy
87```
88
89`Drizzle.Schema` generates migration SQL into `./migrations` on deploy, and
90`Neon.Branch` applies any pending migrations transactionally before the workers
91go live.
92
93## Tests
94
95```sh
96bun test # deploys, exercises /rpc end to end, then destroys
97NO_DESTROY=1 bun test # keep the deployment around between runs
98```