feat: migrate from CF Pages to Workers with one-click deploy button

- Replace Pages Functions with Workers entry (`workers/bootstrap.ts`)
- Add Deploy to Cloudflare button in README
- Integrate `@cloudflare/vite-plugin` for CF dev with HMR
- Integrate `@hono/vite-dev-server` for Node dev with HMR
- `npm run dev` now defaults to CF Workers mode
- Add `run_worker_first = ["/api/*"]` so static assets stay free
- Extract shared Node bootstrap (`server/bootstrap.ts`) for reuse
- Update all docs from Pages to Workers references

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
saltbo
2026-04-12 10:40:15 -04:00
co-authored by Claude Opus 4.6
parent a082a51dca
commit 3b80eff413
22 changed files with 222 additions and 116 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
# Local development environment variables for `wrangler pages dev`.
# Local development environment variables for `wrangler dev`.
# Copy this file to `.dev.vars` (gitignored) and fill in real values.
#
# For the Node entry (`npm run dev` / Docker), these same variables must be
+1 -1
View File
@@ -11,7 +11,7 @@ Complete rewrite from Go to TypeScript.
### Changed
- New tech stack: Hono + Drizzle + Better Auth
- Dual deployment: Cloudflare Pages and Docker
- Dual deployment: Cloudflare Workers and Docker
- R2 as default storage backend
- Better Auth replaces custom JWT implementation
+2 -2
View File
@@ -2,14 +2,14 @@
## Project Overview
ZPan v2 is an open-source, S3-native file hosting platform written in TypeScript. Cloudflare Pages is the primary deployment target, Node.js (Docker) is backup.
ZPan v2 is an open-source, S3-native file hosting platform written in TypeScript. Cloudflare Workers is the primary deployment target, Node.js (Docker) is backup.
Core architecture: clients upload directly to S3-compatible storage via presigned URLs, bypassing server bandwidth.
## Key Context
- Single package: `server/` (Hono API), `src/` (React SPA), `shared/` (types/schemas)
- CF Pages Functions: `functions/api/[[route]].ts` (CF Pages + D1) and `server/entry-node.ts` (Node + SQLite)
- CF Workers: `workers/bootstrap.ts` (CF Workers + D1) and `server/entry-node.ts` (Node + SQLite)
- Tests are co-located: `*.test.ts` (Node), `*.cf-test.ts` (CF Workers)
- Migrations: drizzle-kit generates SQL → wrangler manages D1 state
+11 -16
View File
@@ -16,7 +16,7 @@ npm install
```sh
npm run dev # Start server + web dev servers (reads .dev.vars)
npm run dev:pages # Build & run as Cloudflare Pages locally
npm run dev:cf # Build & run as Cloudflare Workers locally
```
## Quality Gates
@@ -43,9 +43,9 @@ npm run e2e # Playwright E2E tests
## Preview Verification
Every PR that touches UI or API behavior **must** be verified in the Cloudflare Pages preview environment before merging. The verification report **must** be posted as a PR comment — a PR without a verification comment cannot be merged.
Every PR that touches UI or API behavior **must** be verified in the Cloudflare Workers preview environment before merging. The verification report **must** be posted as a PR comment — a PR without a verification comment cannot be merged.
Cloudflare Pages automatically deploys each PR to a preview URL (posted as a PR comment, e.g. `https://<hash>.zpan.pages.dev`).
Cloudflare Workers automatically deploys each PR to a preview URL (posted as a PR comment).
Before merging, the reviewer **must** verify in the preview environment and post a PR comment with:
- **Screenshots** proving the feature works (golden path + edge cases)
@@ -68,33 +68,28 @@ Schema is defined in `server/db/schema.ts` and `server/db/auth-schema.ts`.
```sh
npm run db:generate # Generate migration SQL after schema changes
npm run db:migrate # Apply migrations (Node/SQLite)
wrangler d1 migrations apply zpan-db --local # Apply migrations (CF Pages local)
wrangler d1 migrations apply zpan-db --remote # Apply migrations (CF Pages production)
wrangler d1 migrations apply zpan-db --local # Apply migrations (D1 local)
wrangler d1 migrations apply zpan-db --remote # Apply migrations (D1 production)
```
To reset local databases with seed data (admin user + dev storage):
```sh
npm run db:reset # Reset Node database (zpan.db)
npm run db:reset:pages # Reset D1 local database (.wrangler)
npm run db:reset:d1 # Reset D1 local database (.wrangler)
```
Migration files live in `migrations/` at project root. Always commit them.
## Deployment
Primary target is Cloudflare Pages. Node.js (Docker) is the backup runtime.
Primary target is Cloudflare Workers. Node.js (Docker) is the backup runtime.
```sh
npm run build:pages # Build frontend to dist/
npm run deploy # Build + deploy to Cloudflare Pages
npm run build # Build frontend to dist/
npm run deploy # Build + deploy to Cloudflare Workers
```
The wrangler config (`wrangler.toml`) uses:
- `main` → worker entry point (bundled by wrangler)
- `[assets]` → static frontend with SPA fallback
- `run_worker_first = ["/api/*"]` → only API routes hit the worker
## Project Structure
```
@@ -102,9 +97,9 @@ zpan/
├── server/ # Hono API (routes, middleware, auth, platform abstraction)
├── src/ # React frontend (TanStack Router, shadcn/ui)
├── shared/ # Shared types, Zod schemas, constants
├── functions/ # CF Pages Functions entry
├── workers/ # Cloudflare Workers entry
├── migrations/ # D1/SQLite migrations (drizzle-kit generated, wrangler managed)
├── wrangler.toml # Cloudflare Pages + Workers config
├── wrangler.toml # Cloudflare Workers config
└── biome.json # Lint + format config
```
+3 -3
View File
@@ -18,11 +18,11 @@ ZPan is a lightweight file hosting platform built on top of S3-compatible storag
## Deploy
### Cloudflare Pages (Recommended)
### Cloudflare Workers (Recommended)
One-click deploy with zero server management. Free tier covers personal use.
<!-- TODO: Deploy button -->
[![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/saltbo/zpan)
### Docker
@@ -38,7 +38,7 @@ docker compose up -d
## Why ZPan?
- **Zero bandwidth bottleneck** — files transfer directly between client and S3
- **Zero ops option** — deploy to Cloudflare Pages for free, no server needed
- **Zero ops option** — deploy to Cloudflare Workers for free, no server needed
- **Self-host friendly** — Docker deployment with any S3-compatible storage
- **Tool ecosystem** — works with PicGo, ShareX, Flameshot out of the box
- **Open source** — free forever for self-hosted users
+1 -1
View File
@@ -40,7 +40,7 @@ Each version ships 1–2 major features. Ship small, ship often.
## Deployment Options
- **Cloudflare Pages** — Zero-ops, free tier covers personal use, one-click deploy
- **Cloudflare Workers** — Zero-ops, free tier covers personal use, one-click deploy
- **Docker** — Self-hosted, bring your own S3, full control
Same codebase, same features, two runtimes.
+1 -1
View File
@@ -11,7 +11,7 @@
"shared/**/*.ts",
"src/**/*.ts",
"src/**/*.tsx",
"functions/**/*.ts",
"workers/**/*.ts",
"e2e/**/*.ts",
"!src/routeTree.gen.ts"
],
+12 -13
View File
@@ -5,12 +5,11 @@ Technical reference for ZPan v2 development. For product roadmap, see [V2_ROADMA
## Global Architecture
```
CF Pages (wrangler) Docker (Node.js)
CF Workers (wrangler) Docker (Node.js)
┌─────────────────────┐
│ [assets] → web/dist │
│ run_worker_first = │ entry-node.ts
│ ["/api/*"] │ (serves both static + API)
│ main: entry-cf.ts │ │
│ [assets] → dist/ │
│ SPA fallback │ entry-node.ts
│ main: workers/ │ (serves both static + API)
│ exports { fetch } │ │
└──────────┬──────────┘ │
│ Single deployment │
@@ -35,8 +34,8 @@ CF Pages (wrangler) Docker (Node.js)
| Layer | Choice | Alternatives Considered | Why This One |
|-------|--------|------------------------|--------------|
| Web Framework | **Hono** | Express, Fastify, ElysiaJS | Native CF Pages Functions + Node.js dual runtime. Lightweight, fast, web-standard Request/Response API |
| ORM | **Drizzle** | Prisma, Kysely | First-class D1 support, type-safe, SQL-like syntax, lightweight bundle for CF Pages Functions |
| Web Framework | **Hono** | Express, Fastify, ElysiaJS | Native CF Workers + Node.js dual runtime. Lightweight, fast, web-standard Request/Response API |
| ORM | **Drizzle** | Prisma, Kysely | First-class D1 support, type-safe, SQL-like syntax, lightweight bundle for CF Workers |
| Auth | **Better Auth** | Lucia, Auth.js, custom JWT | Drizzle adapter, D1 support, built-in social login / OIDC / organization plugin, active development |
| S3 SDK | **@aws-sdk/client-s3** | minio-js, custom fetch | Industry standard, works with every S3-compatible provider including R2 |
| Package Manager | **npm** | pnpm, yarn, bun | Standard, no extra tooling, Volta manages Node version |
@@ -92,11 +91,11 @@ zpan/
│ ├── lib/ # API client (Hono RPC), utils
│ └── i18n/ # Translations (en.json, zh.json)
├── shared/ # Shared types, Zod schemas, constants
├── functions/ # CF Pages Functions entry
│ └── api/[[route]].ts # CF entry point
├── workers/ # CF Workers entry
│ └── app.ts # CF entry point
├── migrations/ # D1/SQLite migrations (drizzle-kit generated)
├── e2e/ # Playwright E2E tests
├── wrangler.toml # Cloudflare Pages config
├── wrangler.toml # Cloudflare Workers config
├── biome.json # Lint + format config
└── package.json # Single package, npm
```
@@ -145,7 +144,7 @@ GitHub Actions:
- `npm e2e` — Playwright tests
**On merge to master:**
- Build + deploy to CF Pages (preview / production)
- Build + deploy to CF Workers (preview / production)
- Build Docker image + push to Docker Hub (planned)
## Platform Abstraction
@@ -172,8 +171,8 @@ Note: `s3` and `cron` will be added in later versions.
### Entry Points
- `entry-cloudflare.ts` — exports `default { fetch }`, wrangler bundles it directly. Static frontend served by wrangler's `[assets]` config with SPA fallback. `run_worker_first = ["/api/*"]` routes only API requests to the worker.
- `entry-node.ts` — starts Hono via `@hono/node-server`, serves both API and static frontend from `../../dist` on the same port.
- `workers/bootstrap.ts` — exports `default { fetch }`, wrangler bundles it directly. Static frontend served by wrangler's `[assets]` config with SPA fallback.
- `server/entry-node.ts` — starts Hono via `@hono/node-server`, serves both API and static frontend from `./dist` on the same port.
## Per-Version Technical Decisions
+1 -1
View File
@@ -30,7 +30,7 @@ The minimum viable ZPan v2. Get the new stack running, ship it, validate it.
- Per-user storage quotas set by admin
- Site name and description configurable via admin panel
- Environment variable based configuration
- Cloudflare Pages one-click deploy
- Cloudflare Workers one-click deploy
- Docker image + docker-compose
### Web UI
-24
View File
@@ -1,24 +0,0 @@
import { createApp } from '../../server/app'
import { createAuth } from '../../server/auth'
import { createCloudflarePlatform } from '../../server/platform/cloudflare'
interface Env {
DB: D1Database
BETTER_AUTH_SECRET: string
BETTER_AUTH_URL?: string
TRUSTED_ORIGINS?: string
[key: string]: unknown
}
export const onRequest: PagesFunction<Env> = async (context) => {
const { BETTER_AUTH_SECRET, BETTER_AUTH_URL, TRUSTED_ORIGINS } = context.env
if (!BETTER_AUTH_SECRET) {
throw new Error('BETTER_AUTH_SECRET is not configured for this deployment.')
}
const platform = createCloudflarePlatform(context.env)
const trustedOrigins = TRUSTED_ORIGINS?.split(',')
.map((o) => o.trim())
.filter(Boolean)
const auth = createAuth(platform.db, BETTER_AUTH_SECRET, BETTER_AUTH_URL, trustedOrigins)
return createApp(platform, auth).fetch(context.request)
}
-3
View File
@@ -1,3 +0,0 @@
{
"extends": "../tsconfig.server.json"
}
+83
View File
@@ -56,8 +56,10 @@
},
"devDependencies": {
"@biomejs/biome": "^2.4.10",
"@cloudflare/vite-plugin": "^1.31.2",
"@cloudflare/vitest-pool-workers": "^0.14.0",
"@cloudflare/workers-types": "^4.20260401.1",
"@hono/vite-dev-server": "^0.25.1",
"@playwright/test": "1.58.2",
"@tailwindcss/vite": "^4.2.2",
"@tanstack/router-plugin": "^1.167.12",
@@ -1596,6 +1598,24 @@
}
}
},
"node_modules/@cloudflare/vite-plugin": {
"version": "1.31.2",
"resolved": "https://registry.npmjs.org/@cloudflare/vite-plugin/-/vite-plugin-1.31.2.tgz",
"integrity": "sha512-6RyoPhqmpuHPB+Zudt7mOUdGzB1+DQtJtPdAxUajhlS2ZUU0+bCn9Cj4g6Z2EvajBrkBTw1yVLqtt4bsUnp1Ng==",
"dev": true,
"license": "MIT",
"dependencies": {
"@cloudflare/unenv-preset": "2.16.0",
"miniflare": "4.20260409.0",
"unenv": "2.0.0-rc.24",
"wrangler": "4.81.1",
"ws": "8.18.0"
},
"peerDependencies": {
"vite": "^6.1.0 || ^7.0.0 || ^8.0.0",
"wrangler": "^4.81.1"
}
},
"node_modules/@cloudflare/vitest-pool-workers": {
"version": "0.14.3",
"resolved": "https://registry.npmjs.org/@cloudflare/vitest-pool-workers/-/vitest-pool-workers-0.14.3.tgz",
@@ -2730,6 +2750,36 @@
"hono": "^4"
}
},
"node_modules/@hono/vite-dev-server": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/@hono/vite-dev-server/-/vite-dev-server-0.25.1.tgz",
"integrity": "sha512-H6EqoPtCV+uH1fu8nKyRy5Wh3LU012QxloEYMlUcyMjjEfVFiRXPjHx/0TQiiRi140Vu8San5A4XQWyElKFKaw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@hono/node-server": "^1.19.11",
"minimatch": "^9.0.3"
},
"engines": {
"node": ">=18.14.1"
},
"peerDependencies": {
"hono": "*",
"miniflare": "*",
"wrangler": "*"
},
"peerDependenciesMeta": {
"hono": {
"optional": false
},
"miniflare": {
"optional": true
},
"wrangler": {
"optional": true
}
}
},
"node_modules/@hono/zod-validator": {
"version": "0.7.6",
"resolved": "https://registry.npmjs.org/@hono/zod-validator/-/zod-validator-0.7.6.tgz",
@@ -9641,6 +9691,13 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -9869,6 +9926,16 @@
"integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==",
"license": "MIT"
},
"node_modules/brace-expansion": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
"integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/braces": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
@@ -13413,6 +13480,22 @@
"node": ">=18.0.0"
}
},
"node_modules/minimatch": {
"version": "9.0.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.2"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+6 -4
View File
@@ -4,15 +4,15 @@
"private": true,
"type": "module",
"scripts": {
"dev": "concurrently \"tsx watch --env-file=.dev.vars server/entry-node.ts\" \"vite\"",
"dev:pages": "vite build && wrangler pages dev dist",
"dev": "vite dev",
"dev:node": "node --env-file=.dev.vars node_modules/vite/bin/vite.js dev --mode node",
"build": "vite build",
"build:server": "tsup server/entry-node.ts --format esm --outDir dist-server",
"deploy": "vite build && wrangler pages deploy dist",
"deploy": "vite build && wrangler deploy",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:reset": "tsx --env-file=.dev.vars scripts/db-reset.ts",
"db:reset:pages": "tsx --env-file=.dev.vars scripts/db-reset.ts --pages",
"db:reset:d1": "tsx --env-file=.dev.vars scripts/db-reset.ts --d1",
"typecheck": "tsc --noEmit -p tsconfig.server.json && tsc --noEmit -p tsconfig.web.json",
"test": "vitest run",
"test:cf": "vitest run --config vitest.cloudflare.config.ts",
@@ -75,8 +75,10 @@
},
"devDependencies": {
"@biomejs/biome": "^2.4.10",
"@cloudflare/vite-plugin": "^1.31.2",
"@cloudflare/vitest-pool-workers": "^0.14.0",
"@cloudflare/workers-types": "^4.20260401.1",
"@hono/vite-dev-server": "^0.25.1",
"@playwright/test": "1.58.2",
"@tailwindcss/vite": "^4.2.2",
"@tanstack/router-plugin": "^1.167.12",
+3 -3
View File
@@ -10,7 +10,7 @@ import * as authSchema from '../server/db/auth-schema'
import * as schema from '../server/db/schema'
import type { Platform } from '../server/platform/interface'
const isPages = process.argv.includes('--pages')
const isD1 = process.argv.includes('--d1')
const NODE_DB_PATH = process.env.DATABASE_URL || './zpan.db'
const D1_STATE_DIR = '.wrangler/state/v3/d1'
@@ -34,7 +34,7 @@ const storageConfig = {
}
// ── 1. reset database ──
const platform = isPages ? resetPages() : resetNode()
const platform = isD1 ? resetD1() : resetNode()
// ── 2. seed ──
const secret = process.env.BETTER_AUTH_SECRET || 'dev-secret-for-seed'
@@ -80,7 +80,7 @@ function resetNode(): Platform {
return { db, getEnv: (key) => process.env[key] }
}
function resetPages(): Platform {
function resetD1(): Platform {
// wipe D1 local state
if (fs.existsSync(D1_STATE_DIR)) {
fs.rmSync(D1_STATE_DIR, { recursive: true })
+16
View File
@@ -0,0 +1,16 @@
import { createApp } from './app'
import { createAuth } from './auth'
import { createNodePlatform } from './platform/node'
const platform = createNodePlatform()
const secret = process.env.BETTER_AUTH_SECRET
if (!secret) {
throw new Error('BETTER_AUTH_SECRET is required. Set it in the environment before starting the server.')
}
const baseURL = process.env.BETTER_AUTH_URL || 'http://localhost:5173'
const trustedOrigins = process.env.TRUSTED_ORIGINS?.split(',')
.map((o) => o.trim())
.filter(Boolean) || ['http://localhost:5173']
const auth = createAuth(platform.db, secret, baseURL, trustedOrigins)
export default createApp(platform, auth)
+6 -21
View File
@@ -1,28 +1,13 @@
import { serve } from '@hono/node-server'
import { serveStatic } from '@hono/node-server/serve-static'
import { Hono } from 'hono'
import { createApp } from './app'
import { createAuth } from './auth'
import { createNodePlatform } from './platform/node'
import app from './bootstrap'
const platform = createNodePlatform()
const secret = process.env.BETTER_AUTH_SECRET
if (!secret) {
throw new Error('BETTER_AUTH_SECRET is required. Set it in the environment before starting the server.')
}
const baseURL = process.env.BETTER_AUTH_URL || 'http://localhost:8222'
const trustedOrigins = process.env.TRUSTED_ORIGINS?.split(',')
.map((o) => o.trim())
.filter(Boolean) || ['http://localhost:5173']
const auth = createAuth(platform.db, secret, baseURL, trustedOrigins)
const api = createApp(platform, auth)
const app = new Hono()
app.route('/', api)
app.use('/*', serveStatic({ root: './dist' }))
app.get('/*', serveStatic({ root: './dist', path: 'index.html' }))
const server = new Hono()
server.route('/', app)
server.use('/*', serveStatic({ root: './dist' }))
server.get('/*', serveStatic({ root: './dist', path: 'index.html' }))
const port = Number(process.env.PORT) || 8222
console.log(`ZPan server running on http://localhost:${port}`)
serve({ fetch: app.fetch, port })
serve({ fetch: server.fetch, port })
+2 -2
View File
@@ -5,6 +5,6 @@
"declaration": false,
"declarationMap": false
},
"include": ["server", "shared", "functions"],
"exclude": ["server/**/*.cf-test.ts", "server/test/apply-migrations.ts"]
"include": ["server", "shared", "workers"],
"exclude": ["server/**/*.cf-test.ts", "workers/**/*.cf-test.ts", "server/test/apply-migrations.ts"]
}
+7 -5
View File
@@ -1,10 +1,12 @@
import path from 'node:path'
import { cloudflare } from '@cloudflare/vite-plugin'
import tailwindcss from '@tailwindcss/vite'
import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
import react from '@vitejs/plugin-react-swc'
import devServer from '@hono/vite-dev-server'
import { defineConfig } from 'vite'
export default defineConfig({
export default defineConfig(({ mode }) => ({
build: {
outDir: 'dist',
emptyOutDir: true,
@@ -16,6 +18,9 @@ export default defineConfig({
}),
react(),
tailwindcss(),
...(mode === 'node'
? [devServer({ entry: './server/bootstrap.ts', injectClientScript: false, exclude: [/^(?!\/api\/).*/] })]
: [cloudflare()]),
],
resolve: {
alias: {
@@ -26,8 +31,5 @@ export default defineConfig({
},
server: {
port: 5173,
proxy: {
'/api': 'http://localhost:8222',
},
},
})
}))
+1 -1
View File
@@ -21,7 +21,7 @@ export default defineConfig({
test: {
globals: true,
testTimeout: 15000,
include: ['server/**/*.cf-test.ts'],
include: ['server/**/*.cf-test.ts', 'workers/**/*.cf-test.ts'],
setupFiles: ['./server/test/apply-migrations.ts'],
},
})
+27
View File
@@ -0,0 +1,27 @@
import { env } from 'cloudflare:workers'
import { describe, expect, it } from 'vitest'
import worker from './bootstrap'
describe('[CF] Worker fetch handler', () => {
it('throws when BETTER_AUTH_SECRET is missing', async () => {
const request = new Request('http://localhost/api/health')
const envWithoutSecret = { ...env, BETTER_AUTH_SECRET: '' }
await expect(worker.fetch(request, envWithoutSecret)).rejects.toThrow(
'BETTER_AUTH_SECRET is not configured for this deployment.',
)
})
it('returns a response for a valid request', async () => {
const request = new Request('http://localhost/api/health')
const res = await worker.fetch(request, env)
expect(res.status).toBe(200)
expect(await res.json()).toEqual({ status: 'ok' })
})
it('splits and trims TRUSTED_ORIGINS when provided', async () => {
const request = new Request('http://localhost/api/health')
const envWithOrigins = { ...env, TRUSTED_ORIGINS: ' https://a.example.com , https://b.example.com ' }
const res = await worker.fetch(request, envWithOrigins)
expect(res.status).toBe(200)
})
})
+26
View File
@@ -0,0 +1,26 @@
import { createApp } from '../server/app'
import { createAuth } from '../server/auth'
import { createCloudflarePlatform } from '../server/platform/cloudflare'
interface Env {
DB: D1Database
BETTER_AUTH_SECRET: string
BETTER_AUTH_URL?: string
TRUSTED_ORIGINS?: string
[key: string]: unknown
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const { BETTER_AUTH_SECRET, BETTER_AUTH_URL, TRUSTED_ORIGINS } = env
if (!BETTER_AUTH_SECRET) {
throw new Error('BETTER_AUTH_SECRET is not configured for this deployment.')
}
const platform = createCloudflarePlatform(env)
const trustedOrigins = TRUSTED_ORIGINS?.split(',')
.map((o) => o.trim())
.filter(Boolean)
const auth = createAuth(platform.db, BETTER_AUTH_SECRET, BETTER_AUTH_URL, trustedOrigins)
return createApp(platform, auth).fetch(request)
},
}
+12 -14
View File
@@ -1,10 +1,13 @@
name = "zpan"
main = "./workers/bootstrap.ts"
compatibility_date = "2024-12-01"
compatibility_flags = ["nodejs_compat"]
pages_build_output_dir = "./dist"
[assets]
not_found_handling = "single-page-application"
run_worker_first = ["/api/*"]
# Local development D1 binding (used by `wrangler pages dev`)
# Local development D1 binding (used by `wrangler dev`)
[[d1_databases]]
binding = "DB"
database_name = "zpan-db-local"
@@ -14,18 +17,16 @@ migrations_dir = "./migrations"
# ----------------------------------------------------------------------------
# Environment-specific config.
#
# CF Pages has exactly two environments: production and preview. Top-level
# bindings in wrangler.toml apply to BOTH of them unless an [env.<name>] block
# overrides — so to isolate preview from production we must declare both.
# Workers has two environments: production and preview. Top-level bindings
# apply to BOTH unless an [env.<name>] block overrides.
#
# Secrets (BETTER_AUTH_SECRET etc.) cannot live in this file. Set them via:
# wrangler pages secret put BETTER_AUTH_SECRET --project-name=zpan
# For preview, the secret is managed directly via the Cloudflare API (there
# is no per-env flag on `wrangler pages secret put` for Pages). See
# scripts/setup-preview.sh (or the project runbook) for the PATCH command.
# wrangler secret put BETTER_AUTH_SECRET
# ----------------------------------------------------------------------------
# --- production ---
# TODO: Update these URLs after creating the Workers project in CF Dashboard.
# Workers subdomain will be https://zpan.<account>.workers.dev or your custom domain.
[env.production.vars]
BETTER_AUTH_URL = "https://zpan.pages.dev"
TRUSTED_ORIGINS = "https://zpan.pages.dev"
@@ -48,8 +49,5 @@ database_name = "zpan-db-preview"
database_id = "a9197d7a-6524-42f0-b646-e714dcb30b3b"
migrations_dir = "./migrations"
# Observability note: Pages Functions do not support [observability] in
# wrangler.toml or via the deployment_configs API. Logs are real-time only:
# npx wrangler pages deployment tail <deployment-id> --project-name=zpan
# For persistent Workers Logs, the project would need to migrate from
# Pages Functions to a standalone Worker.
[observability]
enabled = true