Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 55 additions & 20 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions examples/cloudflare-tanstack-rpc-drizzle/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules
.tanstack
.nitro
.output
dist
98 changes: 98 additions & 0 deletions examples/cloudflare-tanstack-rpc-drizzle/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# 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.Vite`.
- **Effect RPC** backend, deployed as a separate `Cloudflare.RpcWorker`.
- **Drizzle + Neon Postgres**, reached through a `Cloudflare.Hyperdrive` pool, with migrations generated by `Drizzle.Schema`.
- **Atom RPC** — Effect 4's native `effect/unstable/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.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 login`)
- `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
```
Loading
Loading