mirror of
https://github.com/VibedByKaKi/t3-code-android-nightly.git
synced 2026-10-16 23:37:44 +02:00
98 lines
3.3 KiB
Markdown
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
|
|
```
|