Skip to content
Open
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
93 changes: 93 additions & 0 deletions examples/prisma-hyperdrive/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Prisma + Hyperdrive on Cloudflare Workers

A full-stack CRUD example using [Prisma](https://www.prisma.io/) with [Hyperdrive](https://developers.cloudflare.com/hyperdrive/) for accelerated PostgreSQL access on Cloudflare Workers via vinext.

Demonstrates:
- Per-request Prisma client (avoids [alternating connection failures](https://github.com/cloudflare/vinext/issues/537))
- Hyperdrive connection pooling
- Server component data fetching
- Route handler CRUD API

## Prerequisites

- A PostgreSQL database (Neon, Supabase, or any provider)
- A Cloudflare account with Hyperdrive enabled

## Setup

1. Install dependencies:

```bash
pnpm install
```

2. Set your database URL:

```bash
echo 'DATABASE_URL="postgresql://user:pass@host:5432/db"' > .env
```

3. Create the database table:

```bash
pnpm db:push
```

4. Generate the Prisma client:

```bash
pnpm db:generate
```

5. Create a Hyperdrive config:

```bash
npx wrangler hyperdrive create my-db \
--connection-string="postgresql://user:pass@host:5432/db"
```

6. Copy the Hyperdrive ID into `wrangler.jsonc`:

```jsonc
"hyperdrive": [{ "binding": "HYPERDRIVE", "id": "<paste-id-here>" }]
```

## Development

```bash
pnpm dev
```

Open http://localhost:5173 — the app fetches items from your database via Hyperdrive.

## Deploy

```bash
pnpm build
npx wrangler deploy
```

## How it works

### The per-request client pattern

Cloudflare Workers reuse isolates across requests, but each request has its own I/O context. A global `PrismaClient` singleton causes alternating failures because the connection pool from one request becomes invalid in the next.

`lib/db.ts` solves this by creating a fresh client per call:

```ts
export function getPrisma(): PrismaClient {
const pool = new Pool({ connectionString: env.HYPERDRIVE.connectionString });
return new PrismaClient({ adapter: new PrismaPg(pool) });
}
```

Each call creates a fresh client — no cross-request state leakage. PrismaClient construction is lightweight (~0.1ms); the expensive part is the query, which Hyperdrive accelerates via edge connection pooling.

### Hyperdrive

Hyperdrive pools and caches PostgreSQL connections at Cloudflare's edge. The connection string comes from the `HYPERDRIVE` binding in `wrangler.jsonc`, accessed via `import { env } from "cloudflare:workers"`.

### Route handlers use standard Request

vinext route handlers receive the Web standard `Request` object (not `NextRequest`). Use `new URL(request.url)` to access URL components like pathname or search params.
35 changes: 35 additions & 0 deletions examples/prisma-hyperdrive/app/api/items/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { getPrisma } from "@/lib/db";

export async function GET() {
const prisma = getPrisma();
const items = await prisma.item.findMany({ orderBy: { createdAt: "desc" } });
return Response.json(items);
}

export async function POST(request: Request) {
const contentType = request.headers.get("content-type") ?? "";

let title: string | null = null;

if (contentType.includes("application/json")) {
const body = await request.json();
title = body.title;
} else {
// Form submission
const formData = await request.formData();
title = formData.get("title") as string;
}

if (!title?.trim()) {
return Response.json({ error: "title is required" }, { status: 400 });
}

const prisma = getPrisma();
const item = await prisma.item.create({ data: { title: title.trim() } });

// Redirect back to home on form submit, return JSON for API calls
if (!contentType.includes("application/json")) {
return new Response(null, { status: 303, headers: { Location: "/" } });
}
return Response.json(item, { status: 201 });
}
14 changes: 14 additions & 0 deletions examples/prisma-hyperdrive/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export const metadata = {
title: "Prisma + Hyperdrive on vinext",
description: "Full-stack example with Prisma, Hyperdrive, and Cloudflare Workers",
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body style={{ fontFamily: "system-ui, sans-serif", maxWidth: 600, margin: "40px auto", padding: "0 16px" }}>
{children}
</body>
</html>
);
}
29 changes: 29 additions & 0 deletions examples/prisma-hyperdrive/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { getPrisma } from "@/lib/db";

export default async function Home() {
const prisma = getPrisma();
const items = await prisma.item.findMany({ orderBy: { createdAt: "desc" } });

return (
<main>
<h1>Items ({items.length})</h1>
<form action="/api/items" method="POST">
<input name="title" placeholder="New item..." required style={{ padding: 8, marginRight: 8 }} />
<button type="submit" style={{ padding: "8px 16px" }}>Add</button>
</form>
<ul style={{ listStyle: "none", padding: 0 }}>
{items.map((item) => (
<li key={item.id} style={{ padding: "8px 0", borderBottom: "1px solid #eee" }}>
<span style={{ textDecoration: item.completed ? "line-through" : "none" }}>
{item.title}
</span>
<span style={{ color: "#999", fontSize: 12, marginLeft: 8 }}>
{item.createdAt.toLocaleDateString()}
</span>
</li>
))}
{items.length === 0 && <li style={{ color: "#999" }}>No items yet. Add one above.</li>}
</ul>
</main>
);
}
29 changes: 29 additions & 0 deletions examples/prisma-hyperdrive/lib/db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Per-request Prisma client for Cloudflare Workers.
*
* Workers reuse isolates across requests, but each request has its own
* I/O context. A global PrismaClient singleton causes alternating
* connection failures (success, fail, success, fail...) because the
* connection from Request A is invalid in Request B.
*
* Solution: create a fresh PrismaClient for every call. PrismaClient
* construction is lightweight (~0.1ms) — the expensive part is the
* actual query, which Hyperdrive accelerates via connection pooling.
*/
import { PrismaClient } from "@prisma/client";
import { PrismaPg } from "@prisma/adapter-pg";
import { Pool } from "pg";
import { env } from "cloudflare:workers";

/**
* Get a fresh PrismaClient for the current request.
*
* Each call creates a new client to guarantee request isolation.
* Hyperdrive handles connection pooling at the edge — no need to
* cache or reuse the client across calls.
*/
export function getPrisma(): PrismaClient {
const pool = new Pool({ connectionString: env.HYPERDRIVE.connectionString });
const adapter = new PrismaPg(pool);
return new PrismaClient({ adapter });
}
33 changes: 33 additions & 0 deletions examples/prisma-hyperdrive/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "vinext-prisma-hyperdrive",
"type": "module",
"private": true,
"scripts": {
"dev": "vp dev",
"build": "vp build",
"preview": "vp preview",
"db:generate": "prisma generate",
"db:migrate": "prisma migrate dev",
"db:push": "prisma db push"
},
"dependencies": {
"@vitejs/plugin-react": "catalog:",
"react": "catalog:",
"react-dom": "catalog:",
"vite": "catalog:",
"vinext": "workspace:*",
"@vitejs/plugin-rsc": "catalog:",
"react-server-dom-webpack": "catalog:",
"@cloudflare/vite-plugin": "catalog:",
"wrangler": "catalog:",
"@prisma/client": "^7.0.0",
"@prisma/adapter-pg": "^7.0.0",
"pg": "^8.13.0"
},
"devDependencies": {
"@cloudflare/workers-types": "catalog:",
"vite-plus": "catalog:",
"prisma": "^7.0.0",
"@types/pg": "^8.11.0"
}
}
17 changes: 17 additions & 0 deletions examples/prisma-hyperdrive/prisma/schema.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
generator client {
provider = "prisma-client-js"
previewFeatures = ["driverAdapters"]
}

datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}

model Item {
id String @id @default(uuid())
title String
completed Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
16 changes: 16 additions & 0 deletions examples/prisma-hyperdrive/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"paths": {
"@/*": ["./*"]
}
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}
15 changes: 15 additions & 0 deletions examples/prisma-hyperdrive/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { defineConfig } from "vite";
import vinext from "vinext";
import { cloudflare } from "@cloudflare/vite-plugin";

export default defineConfig({
plugins: [
vinext(),
cloudflare({
viteEnvironment: {
name: "rsc",
childEnvironments: ["ssr"],
},
}),
],
});
13 changes: 13 additions & 0 deletions examples/prisma-hyperdrive/worker/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Cloudflare Worker entry point for vinext App Router.
*
* For apps without image optimization, point wrangler.jsonc main
* directly at "vinext/server/app-router-entry" instead of this file.
*/
import handler from "vinext/server/app-router-entry";

export default {
async fetch(request: Request): Promise<Response> {
return handler.fetch(request);
},
};
23 changes: 23 additions & 0 deletions examples/prisma-hyperdrive/wrangler.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "prisma-hyperdrive",
"compatibility_date": "2026-03-01",
"compatibility_flags": ["nodejs_compat"],
"main": "./worker/index.ts",
"preview_urls": true,
"assets": {
"not_found_handling": "none",
"binding": "ASSETS"
},
"images": {
"binding": "IMAGES"
},
// Hyperdrive accelerates PostgreSQL connections via connection pooling.
// Create one with: wrangler hyperdrive create my-db --connection-string="postgres://..."
"hyperdrive": [
{
"binding": "HYPERDRIVE",
"id": "<your-hyperdrive-id>"
}
]
}