feat: add Vercel deployment option (Vercel Services)#1095
feat: add Vercel deployment option (Vercel Services)#1095AmanVarshney01 wants to merge 15 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9d36baba06
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| {{#if (and (eq serverDeploy "vercel") (eq runtime "bun"))}} | ||
| "bunVersion": "1.x", |
There was a problem hiding this comment.
Move Bun runtime config into the Vercel service
When users pick Vercel for the server with the default Bun runtime, this emits bunVersion at the top level next to services. In Services mode, Vercel's docs say top-level build/runtime fields are not valid because ownership is ambiguous and should be moved into the relevant service (https://vercel.com/docs/services#top-level-configuration-in-services-mode). This makes the default Vercel server configuration invalid or leaves the server running with the wrong runtime during vercel deploy/deploy:vercel:check.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Verified against the official schema and docs — leaving as is. The Services docs section on top-level configuration enumerates the keys that are invalid at the top level (functions, installCommand, buildCommand, devCommand, ignoreCommand, outputDirectory, framework) — bunVersion is not among them. The openapi.vercel.sh/vercel.json schema defines bunVersion only at the top level; the per-service schema (root, framework, runtime, entrypoint, …) has no bunVersion field to move it into, and the service config reference confirms this. bunVersion is documented as project-scoped ("Enables Bun for the project"), which is exactly why it isn't in the ambiguous-owner list. Also verified live: a combined web+server Services deployment with top-level bunVersion deployed and served correctly.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds Vercel support across CLI validation, web builder UI, template generation, generated templates, documentation, and smoke testing. It also updates deployment-specific URL handling, env validation, and server startup behavior for Vercel-generated stacks. ChangesVercel Deployment Support
Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (5)
apps/cli/src/utils/compatibility-rules.ts (1)
336-357: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider extracting a shared helper for
validateDockerServerDeploy/validateVercelServerDeploy.Both functions are structurally identical (same backend/runtime checks, only the deploy-target string and suggested alternative differ). A small factory (e.g.
createServerDeployRuntimeValidator(target, fallbackSuggestion)) would reduce duplication and keep future targets consistent.♻️ Example refactor
-export function validateDockerServerDeploy( - serverDeploy: ServerDeploy | undefined, - backend: Backend | undefined, - runtime: Runtime | undefined, -): ValidationResult { - if (serverDeploy !== "docker") return Result.ok(undefined); - if (backend === "convex" || backend === "self") { - return validationErr( - "'--server-deploy docker' requires a separate server backend (hono, express, fastify, elysia). For a fullstack 'self' backend, use '--web-deploy docker' instead.", - ); - } - if (runtime === "workers") { - return validationErr( - "'--server-deploy docker' is not compatible with '--runtime workers'. Use '--runtime bun' or '--runtime node', or choose '--server-deploy cloudflare'.", - ); - } - return Result.ok(undefined); -} - -export function validateVercelServerDeploy( - serverDeploy: ServerDeploy | undefined, - backend: Backend | undefined, - runtime: Runtime | undefined, -): ValidationResult { - if (serverDeploy !== "vercel") return Result.ok(undefined); - if (backend === "convex" || backend === "self") { - return validationErr( - "'--server-deploy vercel' requires a separate server backend (hono, express, fastify, elysia). For a fullstack 'self' backend, use '--web-deploy vercel' instead.", - ); - } - if (runtime === "workers") { - return validationErr( - "'--server-deploy vercel' is not compatible with '--runtime workers'. Use '--runtime bun' or '--runtime node', or choose '--server-deploy cloudflare'.", - ); - } - return Result.ok(undefined); -} +function validateNonWorkersServerDeploy( + target: "docker" | "vercel", + serverDeploy: ServerDeploy | undefined, + backend: Backend | undefined, + runtime: Runtime | undefined, +): ValidationResult { + if (serverDeploy !== target) return Result.ok(undefined); + if (backend === "convex" || backend === "self") { + return validationErr( + `'--server-deploy ${target}' requires a separate server backend (hono, express, fastify, elysia). For a fullstack 'self' backend, use '--web-deploy ${target}' instead.`, + ); + } + if (runtime === "workers") { + return validationErr( + `'--server-deploy ${target}' is not compatible with '--runtime workers'. Use '--runtime bun' or '--runtime node', or choose '--server-deploy cloudflare'.`, + ); + } + return Result.ok(undefined); +} + +export const validateDockerServerDeploy = validateNonWorkersServerDeploy.bind(null, "docker"); +export const validateVercelServerDeploy = validateNonWorkersServerDeploy.bind(null, "vercel");apps/web/content/docs/cli/compatibility.mdx (1)
200-205: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winDoc omits
convexbackend exclusion for--server-deploy vercel.Per the referenced
validateVercelServerDeployimplementation,--server-deploy vercelis rejected forbackend === "convex"as well asbackend === "self", requiring a standalone backend (hono/express/fastify/elysia). This section only documents theselfexclusion; theconvexrestriction is not mentioned, which could mislead users trying--server-deploy vercel --backend convex."'--server-deploy vercel' requires a separate server backend (hono, express, fastify, elysia). For a fullstack 'self' backend, use '--web-deploy vercel' instead."
📝 Suggested doc fix
-- `--server-deploy docker` and `--server-deploy vercel` require `--runtime bun` or `--runtime node` -- `--server-deploy` is not used for `--backend self`, because fullstack backends deploy with the web app +- `--server-deploy docker` and `--server-deploy vercel` require `--runtime bun` or `--runtime node` +- `--server-deploy vercel` also requires a standalone server backend and is not compatible with `--backend convex` +- `--server-deploy` is not used for `--backend self`, because fullstack backends deploy with the web appapps/web/src/app/(home)/new/_components/utils.ts (1)
735-751: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider merging duplicate docker/vercel remap blocks.
Both blocks perform the identical transition (remap to
cloudflareon Workers runtime) with the same message. Could be merged into one condition to avoid duplicated logic if a future provider needs the same treatment.♻️ Proposed consolidation
- if (nextStack.serverDeploy === "docker" && nextStack.runtime === "workers") { - nextStack.serverDeploy = "cloudflare"; - changed = true; - changes.push({ - category: "serverDeploy", - message: "Server deploy set to 'Cloudflare' (Workers runtime deploys via Cloudflare)", - }); - } - - if (nextStack.serverDeploy === "vercel" && nextStack.runtime === "workers") { - nextStack.serverDeploy = "cloudflare"; - changed = true; - changes.push({ - category: "serverDeploy", - message: "Server deploy set to 'Cloudflare' (Workers runtime deploys via Cloudflare)", - }); - } + if ( + (nextStack.serverDeploy === "docker" || nextStack.serverDeploy === "vercel") && + nextStack.runtime === "workers" + ) { + nextStack.serverDeploy = "cloudflare"; + changed = true; + changes.push({ + category: "serverDeploy", + message: "Server deploy set to 'Cloudflare' (Workers runtime deploys via Cloudflare)", + }); + }packages/template-generator/templates/api/orpc/web/solid/src/utils/orpc.ts.hbs (1)
16-43: 📐 Maintainability & Code Quality | 🔵 TrivialVercel origin resolution logic looks correct.
Normalizes trailing slash, short-circuits for absolute URLs, uses
window.location.originin-browser, and falls back toVERCEL_URL/VERCEL_PROJECT_PRODUCTION_URL(standard Vercel system env vars) on the server, with a sane localhost fallback. No functional issues spotted.This same ~25-line helper is duplicated verbatim across Astro/Nuxt/React/Solid/Svelte templates. Given each
.hbsis an isolated per-framework file (and Svelte additionally needs it gated behind{{#unless(eq backend "self")}}), extracting a shared module isn't trivial within the template-generator's current per-template model, so this is a nice-to-have rather than a blocking concern.
[recommended_refactor_reward_low]packages/template-generator/templates/api/orpc/web/astro/src/lib/orpc.ts.hbs (1)
13-37: 📐 Maintainability & Code Quality | 🔵 TrivialDuplicate
getServerUrlimplementation across templates.This exact helper (lines 13-37) is duplicated verbatim in the React orpc template (
packages/template-generator/templates/api/orpc/web/react/base/src/utils/orpc.ts.hbs), and a similar-purpose helper exists in Nuxt/Solid/Svelte. Since these are independent per-framework.hbstemplates that must remain self-contained in generated output, some duplication is expected, but consider extracting the shared logic into a Handlebars partial (if the template-processor supports partials) to keep the Vercel-URL-resolution logic consistent as it evolves.Also applies to: 39-40
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 3115009d-0476-4641-bbf2-e1650b9035f6
📒 Files selected for processing (38)
apps/cli/README.mdapps/cli/src/prompts/server-deploy.tsapps/cli/src/prompts/web-deploy.tsapps/cli/src/utils/compatibility-rules.tsapps/cli/src/utils/config-validation.tsapps/cli/test/deployment.test.tsapps/cli/test/pnpm-workspace.test.tsapps/web/content/docs/cli/compatibility.mdxapps/web/content/docs/cli/options.mdxapps/web/content/docs/project-structure.mdxapps/web/src/app/(home)/new/_components/utils.tsapps/web/src/components/ui/kibo-ui/code-block/index.tsxapps/web/src/lib/constant.tsapps/web/test/stack-builder-compatibility.test.tspackages/template-generator/src/post-process/package-configs.tspackages/template-generator/src/processors/deploy-deps.tspackages/template-generator/src/processors/frontend-deps.tspackages/template-generator/src/processors/readme-generator.tspackages/template-generator/src/template-handlers/deploy.tspackages/template-generator/src/templates.generated.tspackages/template-generator/src/utils/add-deps.tspackages/template-generator/src/utils/generated-ignore-patterns.tspackages/template-generator/templates/api/orpc/web/astro/src/lib/orpc.ts.hbspackages/template-generator/templates/api/orpc/web/nuxt/app/plugins/orpc.ts.hbspackages/template-generator/templates/api/orpc/web/react/base/src/utils/orpc.ts.hbspackages/template-generator/templates/api/orpc/web/solid/src/utils/orpc.ts.hbspackages/template-generator/templates/api/orpc/web/svelte/src/lib/orpc.ts.hbspackages/template-generator/templates/backend/server/elysia/src/index.ts.hbspackages/template-generator/templates/backend/server/hono/src/index.ts.hbspackages/template-generator/templates/base/_gitignorepackages/template-generator/templates/deploy/vercel/scripts/sync-vercel-env.ts.hbspackages/template-generator/templates/deploy/vercel/vercel.json.hbspackages/template-generator/templates/extras/pnpm-workspace.yaml.hbspackages/template-generator/templates/frontend/astro/astro.config.mjs.hbspackages/template-generator/templates/packages/env/src/server.ts.hbspackages/template-generator/templates/packages/env/src/web.ts.hbspackages/types/src/schemas.tsplugin/skills/scaffold-project/SKILL.md
There was a problem hiding this comment.
🧹 Nitpick comments (4)
scripts/vercel-smoke.sh (4)
53-53: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueUnquoted
$flagsintentional but worth an array for robustness.Shellcheck flags
$flagsat Line 53 (SC2086) since word-splitting is required here to expand the flags string into separate CLI arguments. Functionally fine for this script's controlled hardcoded inputs, but convertingTESTSentries to a flags array (or adding a targeted# shellcheck disable=SC2086) would make the intent explicit and remove the lint noise.Source: Linters/SAST tools
76-78: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value
bash -c "$build_cmd"re-parses generated command string.Static analysis flags interpolating a variable into
bash -cas an injection pattern. Herebuild_cmdoriginates from the generatedvercel.json, which is templated from hardcoded flags in theTESTSarray, so there's no attacker-controlled input in this smoke-test context. Still, for defense-in-depth and to avoid setting a copy-paste-able unsafe pattern elsewhere, consider invoking viash -c -- "$build_cmd"with explicit--or validating the command against an allowlist before exec.Source: Linters/SAST tools
28-29: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low valuePredictable
/tmppaths flagged for TOCTOU risk.Static analysis repeatedly flags hardcoded
/tmp/vercel-smoke-*and/tmp/vercel-smoke-resp.txtpaths as susceptible to symlink/TOCTOU attacks on shared systems. Given this is local, single-user smoke-test tooling, the practical risk is low, but switching tomktemp-generated paths (or a per-run unique scratch subdirectory under$SCRATCH) would close the gap cheaply.Also applies to: 52-53, 65-66, 71-73, 77-78, 89-95, 120-122
Source: Linters/SAST tools
102-113: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low valueFixed
sleep 3before asserting no-bind could be flaky.The vercel-guard negative check waits a static 3 seconds then curls once. If a runtime is slow to initialize (e.g., first bun/tsx JIT warm-up under load), the assertion could pass even though the process would have eventually bound after the check, or intermittently fail on slower CI machines. Consider polling for a short window (similar to
wait_for_serverbut inverted, asserting no bind across a few attempts) for a more robust check.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 698a93a0-cd73-4483-b1f0-aaf3adde3e05
📒 Files selected for processing (8)
.gitignoreapps/cli/test/deployment.test.tsapps/web/content/docs/guides/docker.mdxapps/web/content/docs/guides/meta.jsonapps/web/content/docs/guides/vercel.mdxpackage.jsonpackages/template-generator/src/processors/readme-generator.tsscripts/vercel-smoke.sh
✅ Files skipped from review due to trivial changes (2)
- .gitignore
- apps/web/content/docs/guides/vercel.mdx
🚧 Files skipped from review as they are similar to previous changes (2)
- packages/template-generator/src/processors/readme-generator.ts
- apps/cli/test/deployment.test.ts
There was a problem hiding this comment.
🧹 Nitpick comments (1)
packages/template-generator/src/utils/add-deps.ts (1)
239-246: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueCustom dependency paths don't get the same dedup treatment.
The
dependencies/devDependenciesloops now dedupe between sections (Lines 220-227), butcustomDependencies/customDevDependenciesstill write directly without deleting the counterpart entry. Not currently exercised by the Vercel processors (which pass plain arrays), but could produce duplicate entries across sections if a future caller mixes custom and named deps for the same package.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 1d7251cd-0b45-4248-97ec-9b3b2a9cbfb4
📒 Files selected for processing (4)
apps/cli/src/helpers/core/post-installation.tsapps/cli/test/deployment.test.tspackages/template-generator/src/processors/deploy-deps.tspackages/template-generator/src/utils/add-deps.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- packages/template-generator/src/processors/deploy-deps.ts
- apps/cli/test/deployment.test.ts
There was a problem hiding this comment.
Actionable comments posted: 2
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: afb97d38-2e14-4952-b7ea-5f2729d16b89
📒 Files selected for processing (10)
apps/cli/test/deployment.test.tspackages/template-generator/src/templates.generated.tspackages/template-generator/templates/api/trpc/web/react/base/src/utils/trpc.ts.hbspackages/template-generator/templates/auth/better-auth/web/astro/src/lib/auth-client.ts.hbspackages/template-generator/templates/auth/better-auth/web/react/base/src/lib/auth-client.ts.hbspackages/template-generator/templates/auth/better-auth/web/solid/src/lib/auth-client.ts.hbspackages/template-generator/templates/auth/better-auth/web/svelte/src/lib/auth-client.ts.hbspackages/template-generator/templates/deploy/vercel/scripts/sync-vercel-env.ts.hbspackages/template-generator/templates/frontend/react/tanstack-start/src/router.tsx.hbsscripts/vercel-smoke.sh
🚧 Files skipped from review as they are similar to previous changes (4)
- packages/template-generator/templates/deploy/vercel/scripts/sync-vercel-env.ts.hbs
- scripts/vercel-smoke.sh
- apps/cli/test/deployment.test.ts
- packages/template-generator/src/templates.generated.ts
Add 'vercel' as a web and server deploy target across the CLI prompts, schemas, compatibility rules, config validation, docs, web stack-builder, and the scaffold-project plugin skill. Template generator: - deploy/vercel/vercel.json.hbs: Vercel Services config (web/server services + rewrites; /api request-path transform for combined web+server deploys; bunVersion for bun runtime) - deploy/vercel/scripts/sync-vercel-env.ts.hbs: tsx-based env sync (dotenv.parse, per-environment, skip/override keys for combined deploys) - Root package scripts: dev:vercel, env:vercel:preview|production, deploy:vercel[:prod]; deploy devDeps @types/node, dotenv, tsx, vercel - oRPC web clients (react/solid/svelte/astro/nuxt) normalize same-origin /api paths to absolute URLs before RPCLink - server/web env schemas derive origins from VERCEL_* and accept same-origin server URLs - hono/elysia entrypoints export the app for Vercel functions (hono node guarded by process.env.VERCEL) - .vercel ignore patterns; pnpm build approvals Known follow-ups (reviewed, not yet fixed): Astro adapter swap to @astrojs/vercel; preview env-sync interactive git-branch prompt (needs --non-interactive); SSR oRPC preferring production over preview origin; Elysia+node local-start guard.
- Astro + web-deploy vercel now ships @astrojs/vercel instead of the @astrojs/node standalone adapter, which Vercel's astro preset cannot serve (verified: build emits .vercel/output) - env sync passes --non-interactive so 'vercel env add KEY preview' with a piped stdin value no longer hangs on the interactive Git branch prompt in normal terminals - oRPC clients and server env prefer VERCEL_URL outside production so preview/branch SSR resolves the current deployment instead of the production origin - Elysia Vercel deploys keep a guarded local app.listen(3000) for bun and node; Bun does not auto-serve Elysia's default export (verified empirically) - env sync arg parsing partitions files vs flags by existence instead of position, so flags before file paths no longer drop files or leak them into vercel positionals - add deploy:vercel:check (vercel deploy --dry) and document vercel link + env-sync-before-deploy in generated READMEs
- scripts/vercel-smoke.sh (bun smoke:vercel): scaffolds 6 Vercel combos, validates vercel.json and the env-sync script, builds each web service with the exact generated buildCommand (asserting vite/next/.vercel-output artifacts), boots server entrypoints locally and probes them, and verifies guarded entrypoints do not bind under VERCEL=1. Runs fully offline; 6/6 passing. - docs: guides/vercel.mdx covering Vercel Services, vercel.json anatomy, the link->env-sync->deploy flow, the three-layer env model, and troubleshooting; guides/docker.mdx covering the compose stack, image builds, env flow, and moving to a real host. - generated READMEs now link to the matching deployment guide for Vercel and Docker (parity with Cloudflare).
- workspace-deps already adds dotenv as a root dependency for every project, so the Vercel deploy devDependencies duplicated it across sections and bun warned on install; drop it from deploy-deps and make addPackageDependency reject cross-section duplicates generically (runtime dependency wins) - post-installation next steps now include a Vercel Services section (link -> env sync -> deploy, with the guide URL), matching the Cloudflare and Docker sections
… sync runner - better-auth clients (react/svelte/solid/astro) crashed on Vercel combined deploys with 'BetterAuthError: Invalid base URL: /api' because createAuthClient requires an absolute URL; they now use the same getServerUrl normalization as the oRPC clients, as do the tRPC client and the tanstack-start router (whose SSR fetch would also reject relative URLs) - sync-vercel-env.ts drops the platform-specific local-bin resolution and runs the Vercel CLI through the stack's package runner (bunx/npx/pnpm exec), which resolves the local devDependency - vercel smoke combo 1 now scaffolds trpc + better-auth to cover this class in web builds
0faeb27 to
4e02793
Compare
Codex review finding (P1, verified against better-auth source): withPath() uses a baseURL that already has a path as-is and only auto-appends /api/auth to origin-only URLs. On combined Vercel deploys the normalized base is origin + /api, so auth requests went to /api/* instead of /api/auth/* and 404ed through the rewrite. All four web auth clients now append /api/auth explicitly, which is byte-identical to the previous resolved URL for origin-only bases and maps to /api/api/auth -> /api/auth on Vercel. Verified end-to-end against an emulation of the generated vercel.json routing (static dist + /api/(.*) rewrite with path strip): browser signup succeeds, session persists, and an authenticated tRPC call returns private data.
CodeRabbit review suggestion: the same-origin URL normalization helper was inlined 11 times across the oRPC/tRPC/better-auth client templates and the tanstack-start router, so future fixes would have to be replicated by hand. It is now a partial registered in template-processor.ts ({{> getServerUrl}}, plus a getServerUrlSpaces variant derived from the same source for 2-space templates).
Found during live deployment verification: tanstack-router/solid map to the plain vite preset, which serves static output with no history fallback, so deep links like /login returned 404 in production (the Docker path already handles this via nginx try_files). The web service now carries an in-service rewrite to /index.html for those frontends; static assets still win because rewrites apply after the filesystem check. Live-verified on four production deployments (hono/express/fastify/elysia x tanstack-router): deep links 200, /api/ OK, oRPC healthCheck OK, and a real browser signup against Neon Postgres on the hono + better-auth + tRPC stack (session persisted, authenticated tRPC returned private data).
Found by live-deploying next + self + better-auth: signup returned 403 Invalid origin for two stacked reasons. - env sync only skipped BETTER_AUTH_URL/CORS_ORIGIN/NODE_ENV for combined web+server deploys, so self-backend apps synced their localhost values to production and better-auth rejected the real origin; the skip now also applies when webDeploy is vercel with a self backend (the app runs on Vercel and derives origins from VERCEL_* at runtime) - deployments from non-git directories uploaded local .env files (nothing excluded them), and Next.js loads those at runtime, shadowing the runtime derivation; Vercel deploys now generate a root .vercelignore excluding env files and local sqlite artifacts Live-verified: browser signup on next+self+better-auth+Neon now lands on the dashboard with a session and an authenticated oRPC response.
- link:vercel script so every surface (README, docs guide, post-install) references one consistent first-run command instead of three different vercel link invocations - post-install deploy instructions now render after Clerk/Polar setup so env sync happens once real keys exist - env sync warns when values look local-only (localhost, 127.0.0.1, file:), the recurring split-deploy footgun - deployment guide pairs each env sync with its deploy target (preview and production are separate Vercel env stores)
…prefix Codex review (P2): with the sync skipping BETTER_AUTH_URL in combined deploys, the server derived a bare origin, so better-auth built callback/redirect URLs at origin/api/auth/* — which the rewrite strips to /auth/* on the server service, missing the auth handler. Email/password flows never build such URLs (which is why live signup passed), but OAuth callbacks and emailed links would 404. The combined-mode fallback is now origin + /api/api/auth, the full public path to the server's auth mount; self and server-only derivations stay origin-only, which is correct for them.
…s the only cloud target The user already chose Vercel at scaffold time, so plain names match the Cloudflare precedent (bare deploy/destroy): deploy:setup (link — a bare 'link' script would collide with the bun/npm/pnpm link builtins), env:preview, env:production, deploy, deploy:prod, deploy:check. dev:vercel keeps its name since it names the tool (vercel dev), not an action. When Vercel is mixed with Cloudflare in one repo, Alchemy owns bare deploy/destroy and the :vercel-suffixed names are generated instead; README, post-install, and docs all render whichever set applies.
When web and server deploy to different cloud platforms (Vercel + Cloudflare), the deploy scripts are named by what they deploy — deploy:web / deploy:web:prod on one side and deploy:server on the other — instead of vendor-suffixed names. Single-platform and combined deploys keep plain deploy/deploy:prod. Setup, env sync, and dry-run scripts (deploy:setup, env:preview, env:production, deploy:check) are Vercel-unique operations and keep the same names in every combination. README, post-install, and the docs guide render whichever set applies.
Mixed Vercel + Cloudflare projects keep Alchemy state under .alchemy/; without this it gets bundled into Vercel deployments.
Summary
Adds
vercelas a web and server deploy target using Vercel Services — monorepo web + server deployed as services in one Vercel project with a generatedvercel.json, same-origin/apirouting, and one-command env sync.What's generated
vercel.json— web/server services with framework presets, monorepo install commands, and combined-mode rewrites (/api/(.*)→ server with a path transform stripping the/apiprefix,/(.*)→ web)scripts/sync-vercel-env.ts— syncs local.envfiles to Vercel per environment (vercel env addper key,--force --yes --non-interactive); in combined web+server mode it overrides*_SERVER_URLto/apiand skipsCORS_ORIGIN/BETTER_AUTH_URL/NODE_ENV(derived at runtime fromVERCEL_*instead)dev:vercel,env:vercel:preview|production,deploy:vercel[:prod],deploy:vercel:check(vercel deploy --dry)/apipaths to absolute URLs (RPCLink rejects relative URLs); server env derivesCORS_ORIGIN/BETTER_AUTH_URLfrom the deployment origin (VERCEL_ENV-aware, preview usesVERCEL_URL); hono/elysia export the app for Vercel functions with guarded locallisten; Astro swaps to@astrojs/vercelDocs + smoke harness
docs/guides/vercel— Vercel Services model,vercel.jsonanatomy, thelink → env sync → deployflow, the three-layer env model, troubleshootingdocs/guides/docker— compose stack anatomy, image builds, env flow, moving to a real host (closes the docs-parity gap: every deploy target now has a guide, and generated READMEs link to the matching one)scripts/vercel-smoke.sh(bun smoke:vercel) — offline smoke harness mirroringdocker-smoke.sh: scaffolds 6 combos, validatesvercel.json+ the env-sync script, builds web services with the exact generated buildCommand, boots server entrypoints and probes them, and verifies guarded entrypoints don't bind underVERCEL=1. 6/6 passing.Review + fixes included
The
fix:commit resolves findings from an adversarial multi-agent review: Astro adapter (@astrojs/node→@astrojs/vercel), preview env-sync hanging on the interactive Git-branch prompt (--non-interactive), preview SSR resolving the production origin, Elysia local dev never starting (Bun does not auto-serve Elysia's default export — verified empirically), and order-sensitive env-sync arg parsing.Live verification
Two stacks deployed to production and verified end-to-end:
POST /api/rpc/healthCheck→{"json":"OK"}GET /api/→OK,POST /api/rpc/healthCheck→{"json":"OK"}VITE_SERVER_URL=/apibaked into bundle,CORS_ORIGINskipped + runtime-derived,DATABASE_URLsyncedAlso verified: Astro + Vercel build emits
.vercel/outputvia@astrojs/vercel; Elysia serves locally on bun and node and skipslistenunderVERCEL=1;--dryand--non-interactiveflags confirmed in vercel@54 (latest).Tests
bun test apps/cli/test/deployment.test.ts— 48 pass (new: combined services config, oRPC URL normalization, web-only self config, Elysia exports + node guard, Astro Vercel adapter, workers-runtime rejection, README guide links)bun smoke:vercel— 6/6 combos pass offlinebun run checkclean; template regen deterministic; both MDX guides compileFollow-ups (non-blocking)
localhost,file:) when syncing outside combined mode/apiround-tripSummary by CodeRabbit
vercel.json, Vercel env-sync, and deploy scripts.