|
|
||
|---|---|---|
| .. | ||
| migrations | ||
| src | ||
| test | ||
| .gitignore | ||
| alchemy.run.ts | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
| vite.config.ts | ||
cloudflare-tanstack-rpc-drizzle
A full-stack example that wires together four pieces:
- TanStack Start (React) frontend, deployed as a Cloudflare Worker + assets via
Cloudflare.Website.Vite. - Effect RPC backend, deployed as a separate
Cloudflare.Workers.RpcWorker. - Drizzle + Neon Postgres, reached through a
Cloudflare.Hyperdrive.Connectionpool, with migrations generated byDrizzle.Schema. - Atom RPC — Effect 4's native
effect/reactivity/AtomRpcplus the React bindings from@effect/atom-react— for reactive queries and mutations in the browser.
Architecture
Browser (React)
│ useAtomValue / useAtomSet
▼
AtomRpc client (TodoRpcs) src/rpc-client.ts
│ HTTP POST /rpc (JSON)
▼
TanStack Start worker src/routes/rpc.ts (server route)
│ env.BACKEND.fetch(...) (private service binding)
▼
Backend RpcWorker src/backend/api.ts
│ Drizzle.Postgres over Hyperdrive
▼
Neon Postgres branch
The browser cannot use a Cloudflare service binding directly, so the AtomRpc
client points at a same-origin /rpc route, and the TanStack Start server
forwards that request to the private BACKEND worker over the service binding.
This keeps the backend off the public internet and avoids CORS.
Atom RPC
Effect 4 ships atom RPC in core — no third-party @effect-atom package is
needed (that one targets Effect 3). The only extra dependency is
@effect/atom-react, which provides the React hooks (useAtomValue,
useAtomSet, RegistryProvider) and is versioned in lockstep with effect.
// src/rpc-client.ts
export class TodoClient extends AtomRpc.Service<TodoClient>()("TodoClient", {
group: TodoRpcs,
protocol: RpcClient.layerProtocolHttp({ url: "/rpc" }).pipe(
Layer.provide(FetchHttpClient.layer),
Layer.provide(RpcSerialization.layerJson),
),
}) {}
export const listTodosAtom = TodoClient.query("listTodos", undefined, {
reactivityKeys: ["todos"],
});
export const createTodoAtom = TodoClient.mutation("createTodo");
// src/routes/index.tsx
const todos = AsyncResult.getOrElse(useAtomValue(listTodosAtom), () => []);
const createTodo = useAtomSet(createTodoAtom);
// ...
createTodo({ payload: { text }, reactivityKeys: ["todos"] });
reactivityKeys: ["todos"] ties the list query to the mutations: when a
mutation runs with the same key, the list query is invalidated and refetched
automatically.
The same TodoRpcs group is the single source of truth — it is served by the
backend and consumed by the browser client, so one Schema codec round-trips
every value over the wire.
Running
Requires Cloudflare and Neon credentials (same as the other Neon examples):
CLOUDFLARE_API_TOKEN(oralchemy profile edit --add Cloudflare)NEON_API_KEY
bun install
# Deploy the backend RpcWorker, Neon branch, Hyperdrive, and the TanStack site.
bun alchemy deploy
# Tear everything down.
bun alchemy destroy
Drizzle.Schema generates migration SQL into ./migrations on deploy, and
Neon.Branch applies any pending migrations transactionally before the workers
go live.
Tests
bun test # deploys, exercises /rpc end to end, then destroys
NO_DESTROY=1 bun test # keep the deployment around between runs