From 5593eec3ced3d876181def75132e6f9fcd5318da Mon Sep 17 00:00:00 2001 From: Jasper Van Date: Wed, 22 Apr 2026 01:14:54 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20v2.5.0=20T3=20=E2=80=94=20Vercel=20depl?= =?UTF-8?q?oyment=20(entry=20+=20vercel.json=20+=20workflow=20+=20docs)=20?= =?UTF-8?q?(#328)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add Vercel deployment target (Node runtime + Turso) Adds first-class Vercel support: server/entry-vercel.ts using hono/vercel handler, deploy/vercel/vercel.json with nodejs22.x function config and SPA rewrites, build:vercel npm script producing api/entry-vercel.js + dist/, deploy-vercel GitHub Actions workflow (8-step: secrets check, tag resolve, checkout, install, migrate, build, link, deploy), and docs/deploy/vercel.md documenting secrets, quick-start, local dev, and pricing notes. Edge runtime is explicitly not used — @aws-sdk/client-s3 requires Node APIs. Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f * fix: auto-generate BETTER_AUTH_SECRET on first Vercel deploy Remove BETTER_AUTH_SECRET from the required secrets check. Add a dedicated step that detects whether the secret already exists in the Vercel project env via `vercel env ls production`, then either upserts the user-supplied GitHub secret, auto-generates one with openssl on first deploy, or skips if already present. Auto-generation case appends a backup warning to GITHUB_STEP_SUMMARY. Docs move BETTER_AUTH_SECRET to Optional Secrets with a note about the auto-gen behaviour. Matches the existing CF Workers deploy.yml pattern (step 8 contract). Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f --------- Co-authored-by: Bob --- .github/workflows/deploy-vercel.yml | 133 ++++++++++++++++++++++++++++ .gitignore | 1 + deploy/vercel/vercel.json | 26 ++++++ docs/deploy/vercel.md | 91 +++++++++++++++++++ package.json | 1 + server/entry-vercel.ts | 21 +++++ 6 files changed, 273 insertions(+) create mode 100644 .github/workflows/deploy-vercel.yml create mode 100644 deploy/vercel/vercel.json create mode 100644 docs/deploy/vercel.md create mode 100644 server/entry-vercel.ts diff --git a/.github/workflows/deploy-vercel.yml b/.github/workflows/deploy-vercel.yml new file mode 100644 index 00000000..3dbd3c6f --- /dev/null +++ b/.github/workflows/deploy-vercel.yml @@ -0,0 +1,133 @@ +name: Deploy to Vercel + +on: + push: + branches: [master] + workflow_dispatch: + inputs: + version: + description: 'Release tag to deploy (e.g. v2.5.0). Leave empty for latest.' + required: false + +# Prevent overlapping deployments. +concurrency: + group: deploy-vercel + cancel-in-progress: false + +jobs: + deploy: + name: Deploy + runs-on: ubuntu-latest + # Only run on forks — upstream repo uses Cloudflare Workers Builds. + if: github.repository != 'saltbo/zpan' + steps: + - name: Check required secrets + env: + HAS_TOKEN: ${{ secrets.VERCEL_TOKEN != '' }} + HAS_ORG: ${{ secrets.VERCEL_ORG_ID != '' }} + HAS_PROJECT: ${{ secrets.VERCEL_PROJECT_ID != '' }} + HAS_TURSO_URL: ${{ secrets.TURSO_DATABASE_URL != '' }} + run: | + missing=() + [ "$HAS_TOKEN" != "true" ] && missing+=(VERCEL_TOKEN) + [ "$HAS_ORG" != "true" ] && missing+=(VERCEL_ORG_ID) + [ "$HAS_PROJECT" != "true" ] && missing+=(VERCEL_PROJECT_ID) + [ "$HAS_TURSO_URL" != "true" ] && missing+=(TURSO_DATABASE_URL) + if [ ${#missing[@]} -gt 0 ]; then + echo "::error::Missing required secrets: ${missing[*]}. Go to Settings → Secrets and variables → Actions." + exit 1 + fi + + - name: Resolve release tag + id: release + env: + GH_TOKEN: ${{ github.token }} + INPUT_VERSION: ${{ inputs.version }} + run: | + if [ -n "$INPUT_VERSION" ]; then + TAG="$INPUT_VERSION" + else + TAG=$(gh api repos/saltbo/zpan/releases/latest --jq '.tag_name') + fi + if [ -z "$TAG" ]; then + echo "::error::No release found in saltbo/zpan" + exit 1 + fi + echo "version=$TAG" >> "$GITHUB_OUTPUT" + echo "### 🚀 Deploying $TAG to Vercel" >> "$GITHUB_STEP_SUMMARY" + + - uses: actions/checkout@v6 + with: + repository: saltbo/zpan + ref: ${{ steps.release.outputs.version }} + + - uses: actions/setup-node@v6 + with: + node-version: 24 + + - name: Install dependencies + run: npm ci + + - name: Run migrations (Turso) + env: + TURSO_DATABASE_URL: ${{ secrets.TURSO_DATABASE_URL }} + TURSO_AUTH_TOKEN: ${{ secrets.TURSO_AUTH_TOKEN }} + run: npm run db:migrate + + - name: Build + run: npm run build:vercel + + - name: Link or create Vercel project + env: + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + run: | + cp deploy/vercel/vercel.json vercel.json + npx vercel link --yes \ + --token "$VERCEL_TOKEN" \ + --scope "$VERCEL_ORG_ID" + + - name: Ensure BETTER_AUTH_SECRET (first deploy only) + env: + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + USER_SECRET: ${{ secrets.BETTER_AUTH_SECRET }} + run: | + EXISTS=$(npx vercel env ls production --token "$VERCEL_TOKEN" 2>/dev/null | grep -c "^BETTER_AUTH_SECRET" || true) + if [ -n "$USER_SECRET" ]; then + # User supplied their own secret — persist it (upsert). + echo "$USER_SECRET" | npx vercel env add BETTER_AUTH_SECRET production --token "$VERCEL_TOKEN" --force + echo "Set BETTER_AUTH_SECRET from GitHub secret." + elif [ "$EXISTS" -eq 0 ]; then + # First deploy and no user secret — auto-generate and persist. + SECRET=$(openssl rand -base64 32) + echo "$SECRET" | npx vercel env add BETTER_AUTH_SECRET production --token "$VERCEL_TOKEN" + echo "auto_generated=true" >> "$GITHUB_OUTPUT" + echo "Auto-generated BETTER_AUTH_SECRET and stored in Vercel project env." + else + echo "BETTER_AUTH_SECRET already set in Vercel project env, skipping." + fi + id: auth_secret + + - name: Deploy to Vercel + id: deploy + env: + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + TURSO_DATABASE_URL: ${{ secrets.TURSO_DATABASE_URL }} + TURSO_AUTH_TOKEN: ${{ secrets.TURSO_AUTH_TOKEN }} + BETTER_AUTH_URL: ${{ secrets.BETTER_AUTH_URL }} + TRUSTED_ORIGINS: ${{ secrets.TRUSTED_ORIGINS }} + run: | + DEPLOY_URL=$(npx vercel deploy --prod --token "$VERCEL_TOKEN" \ + --env TURSO_DATABASE_URL="$TURSO_DATABASE_URL" \ + --env TURSO_AUTH_TOKEN="$TURSO_AUTH_TOKEN" \ + --env BETTER_AUTH_URL="$BETTER_AUTH_URL" \ + --env TRUSTED_ORIGINS="$TRUSTED_ORIGINS") + echo "url=$DEPLOY_URL" >> "$GITHUB_OUTPUT" + echo "**Deployed to:** $DEPLOY_URL" >> "$GITHUB_STEP_SUMMARY" + if [ "${{ steps.auth_secret.outputs.auto_generated }}" = "true" ]; then + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "> ⚠️ **BETTER_AUTH_SECRET was auto-generated** and stored in your Vercel project env. Back it up from the Vercel dashboard before rotating." >> "$GITHUB_STEP_SUMMARY" + fi diff --git a/.gitignore b/.gitignore index b495d648..6f8e90cb 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ node_modules/ # Build output dist/ dist-server/ +api/ .wrangler/ # Environment diff --git a/deploy/vercel/vercel.json b/deploy/vercel/vercel.json new file mode 100644 index 00000000..9e3cdaf5 --- /dev/null +++ b/deploy/vercel/vercel.json @@ -0,0 +1,26 @@ +{ + "version": 2, + "buildCommand": "npm run build:vercel", + "outputDirectory": "dist", + "functions": { + "api/entry-vercel.js": { + "runtime": "nodejs22.x", + "memory": 512, + "maxDuration": 30 + } + }, + "rewrites": [ + { + "source": "/api/(.*)", + "destination": "/api/entry-vercel" + }, + { + "source": "/health", + "destination": "/api/entry-vercel" + }, + { + "source": "/((?!api/).*)", + "destination": "/index.html" + } + ] +} diff --git a/docs/deploy/vercel.md b/docs/deploy/vercel.md new file mode 100644 index 00000000..c7ec2490 --- /dev/null +++ b/docs/deploy/vercel.md @@ -0,0 +1,91 @@ +# Vercel Deployment + +ZPan supports Vercel as a first-class deploy target using Vercel Functions (Node.js runtime) + [Turso](https://turso.tech) as the database and an external S3-compatible bucket for storage. + +> **Edge runtime is not supported.** `@aws-sdk/client-s3` requires Node.js APIs (streams, crypto) that are unavailable in the Edge runtime. All Vercel Functions for ZPan run on the `nodejs22.x` runtime. + +## Prerequisites + +| Tool | Purpose | +|------|---------| +| [Vercel account](https://vercel.com) | Hosts the application | +| [Turso database](https://turso.tech) | libSQL-compatible remote database | +| S3-compatible storage | File storage (Cloudflare R2 recommended — free egress) | +| `vercel` CLI | Local development and linking | + +## Required Secrets + +Set these in your fork's GitHub repository under **Settings → Secrets and variables → Actions**: + +| Secret | Description | +|--------|-------------| +| `VERCEL_TOKEN` | Vercel API token. Create at [vercel.com/account/tokens](https://vercel.com/account/tokens) | +| `VERCEL_ORG_ID` | Your Vercel team/org ID. Found in `vercel link` output or team settings | +| `VERCEL_PROJECT_ID` | Project ID after first `vercel link`. Found in `.vercel/project.json` | +| `TURSO_DATABASE_URL` | Turso database URL, e.g. `libsql://your-db.turso.io` | +| `BETTER_AUTH_URL` | Your Vercel deployment URL, e.g. `https://your-app.vercel.app` | + +### Optional Secrets + +| Secret | Description | +|--------|-------------| +| `TURSO_AUTH_TOKEN` | Turso auth token. Create with `turso db tokens create your-db`. Required for remote Turso URLs; can be omitted for `file://` local databases. | +| `BETTER_AUTH_SECRET` | Signing secret for auth sessions. If not provided, the workflow generates one on first deploy and persists it in your Vercel project env. Back it up from the Vercel dashboard before rotating. To bring your own: `openssl rand -base64 32`. | +| `TRUSTED_ORIGINS` | Comma-separated list of additional trusted origins. | + +## Quick Start (Fork + Deploy) + +1. **Fork** the `saltbo/zpan` repository. + +2. **Create a Turso database:** + ```sh + turso db create zpan-db + turso db tokens create zpan-db + ``` + +3. **Link your Vercel project** locally (first time only): + ```sh + cp deploy/vercel/vercel.json vercel.json + npx vercel link + # Note the VERCEL_ORG_ID and VERCEL_PROJECT_ID from .vercel/project.json + ``` + +4. **Add the required secrets** to your fork (see table above). `BETTER_AUTH_SECRET` is optional — the workflow auto-generates one on first deploy and stores it in your Vercel project env. + +5. **Push to `master`** — the `deploy-vercel.yml` workflow runs automatically and deploys to production. + +## Local Development + +Install the Vercel CLI and run: + +```sh +npm install -g vercel +cp deploy/vercel/vercel.json vercel.json + +# Using a local libSQL file (no Turso token needed) +TURSO_DATABASE_URL=file:./zpan.db \ +BETTER_AUTH_SECRET=$(openssl rand -base64 32) \ +vercel dev +``` + +The app will be available at `http://localhost:3000`. + +## Build Output + +`npm run build:vercel` produces: + +| Path | Contents | +|------|---------| +| `dist/` | React SPA static assets | +| `api/entry-vercel.js` | Hono API compiled as a Vercel Function (ESM) | + +`vercel.json` routes: +- `/api/*` and `/health` → Vercel Function +- All other paths → `dist/index.html` (SPA) + +## Pricing Notes + +- **Hobby (free)** — suitable for personal and non-commercial use. 100 GB-hours of function compute per month. +- **Pro ($20/mo)** — required for commercial use per Vercel's [fair-use policy](https://vercel.com/docs/limits/fair-use-policy). Includes team features, higher limits, and SLA. +- **Turso** — free tier includes 500 databases and 9 GB of total storage. +- **Cloudflare R2** — recommended S3-compatible storage. Free egress (no bandwidth charges between Vercel and R2 if using the same region is not required — R2 has zero egress fees globally). diff --git a/package.json b/package.json index a57bd211..36b90fc7 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "dev:node": "node --env-file=.dev.vars node_modules/vite/bin/vite.js dev --mode node", "build": "[ \"$WORKERS_CI\" = \"1\" ] && [ \"$WORKERS_CI_BRANCH\" != \"master\" ] && export CLOUDFLARE_ENV=staging; vite build", "build:node": "vite build --mode node && tsup server/entry-node.ts --format esm --outDir dist-server --external better-sqlite3 --external @libsql/client", + "build:vercel": "vite build --mode node && tsup server/entry-vercel.ts --format esm --outDir api --external @libsql/client", "deploy": "npm run db:migrate:d1:prod && wrangler deploy", "db:generate": "drizzle-kit generate", "db:migrate": "drizzle-kit migrate", diff --git a/server/entry-vercel.ts b/server/entry-vercel.ts new file mode 100644 index 00000000..0510941e --- /dev/null +++ b/server/entry-vercel.ts @@ -0,0 +1,21 @@ +import { handle } from 'hono/vercel' +import { createBootstrap } from './bootstrap' +import { createLibsqlPlatform } from './platform/libsql' + +const tursoUrl = process.env.TURSO_DATABASE_URL +if (!tursoUrl) { + throw new Error('TURSO_DATABASE_URL is required for Vercel deployment.') +} + +const platform = await createLibsqlPlatform({ + TURSO_DATABASE_URL: tursoUrl, + TURSO_AUTH_TOKEN: process.env.TURSO_AUTH_TOKEN, +}) + +const app = await createBootstrap(platform) + +export const GET = handle(app) +export const POST = handle(app) +export const PUT = handle(app) +export const PATCH = handle(app) +export const DELETE = handle(app)