t3-code-android-nightly/.repos/alchemy-effect/examples/cloudflare-tanstack-rpc-drizzle/README.md
Julius Marminge 6f9cea00ae
chore(refs): sync Effect and Alchemy references to 4.0.1 and beta.80 (#16170)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 13:22:30 -07:00

98 lines
3.3 KiB
Markdown

# 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.Connection` pool, with migrations generated by `Drizzle.Schema`.
- **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.
## 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`.
```ts
// 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");
```
```tsx
// 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` (or `alchemy profile edit --add Cloudflare`)
- `NEON_API_KEY`
```sh
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
```sh
bun test # deploys, exercises /rpc end to end, then destroys
NO_DESTROY=1 bun test # keep the deployment around between runs
```