One supports multiple deployment targets for web applications. Choose the target that best fits your infrastructure needs.
Set your deployment target in vite.config.ts:
import { one } from 'one/vite'
export default { plugins: [ one({ web: { deploy: 'node' // 'node' (default), 'vercel', or 'cloudflare' }, build: { // fail the build if secrets leak into client bundles securityScan: 'error' } }) ]}We recommend setting securityScan: 'error' for production deployments to prevent accidentally shipping API keys or tokens in client-facing JavaScript. See Configuration: build.securityScan for details.
The default deployment target outputs a Node.js server using Hono. This is suitable for any Node.js hosting environment.
# Build for productionnpx one build
# Start the production servernpx one serveThe build output is in the dist directory:
dist/client - Static assets and client-side bundlesdist/server - Server-side rendering bundlesdist/api - API route handlersYou can deploy this to any platform that supports Node.js:
npx one servenpx one serveOne supports deploying to Vercel’s serverless infrastructure using the Build Output API.
import { one } from 'one/vite'
export default { plugins: [ one({ web: { deploy: 'vercel' } }) ]}{ "framework": null, "cleanUrls": true, "buildCommand": "npx one build", "installCommand": "npm install"}Setting framework: null tells Vercel to use the pre-built output from the Build Output API instead of auto-detecting a framework.
The cleanUrls: true setting is important for SSG routes - it allows URLs like /about to serve about.html without needing the .html extension. Without this, direct navigation to SSG pages will 404.
npm install -g vercelThe easiest way to deploy is connecting your Git repository to Vercel:
vercel.jsonOnce connected, every push to your main branch triggers a production deploy. Pull requests get preview deployments automatically.
For manual deploys or CI/CD pipelines:
# Build for Vercelnpx one build
# Deploy using Vercel CLIvercel deploy --prebuiltThe --prebuilt flag tells Vercel to use the pre-generated output in .vercel/output instead of running its own build process.
For production deploys:
vercel deploy --prebuilt --prodYou can do a basic sanity check of your production build locally:
# Build the appnpx one build
# Serve the production buildnpx one serveNote: This runs the Hono server from dist/, not the actual Vercel serverless functions. It’s useful for catching build errors and checking that pages render correctly, but it’s not a perfect simulation of the Vercel environment.
For true Vercel testing, use a preview deploy:
# Deploy a preview (not production)vercel deploy --prebuiltEvery push to a non-main branch also triggers a preview deployment automatically when using Git integration. This is the most reliable way to test before promoting to production.
When targeting Vercel, One generates:
.vercel/output/static - Static assets served from Vercel’s Edge Network.vercel/output/functions - Serverless functions for:
+ssr.tsx files)app/api/**)Static routes (+ssg.tsx) are pre-rendered to .vercel/output/static as HTML files.
Set environment variables in your Vercel project dashboard under Settings > Environment Variables:
# Required for SSR and API routesONE_SERVER_URL=https://your-app.vercel.appFor local development, add to .env.local:
ONE_SERVER_URL=http://localhost:8081When using vercel.json, settings are auto-configured. If you need to configure manually in the Vercel dashboard:
npx one build.vercel/outputnpm install (or bun install, pnpm install)If your site only uses SSG routes (+ssg.tsx) without SSR, loaders, or API routes, you can simplify:
one({ web: { deploy: 'vercel', defaultRenderMode: 'ssg', }})This generates only static files with no serverless functions, resulting in faster deploys and lower costs.
Build fails with “Cannot find module”: Ensure all dependencies are in dependencies, not devDependencies, if they’re used at runtime.
404 on dynamic routes: Check that your route files use the correct suffix (+ssr.tsx for server-rendered, +ssg.tsx with generateStaticParams for static).
Environment variables not available: Variables must be added in Vercel dashboard and redeployed. Local .env files are not uploaded.
Slow cold starts: Consider using +ssg.tsx for routes that don’t need real-time data. Static routes have no cold start.
If you’re only using SSG and SPA routes without loaders or API routes, you can statically serve the dist/client directory from any static hosting:
npx one build# Upload dist/client to your static hostOne supports deploying to Cloudflare Workers with full SSR, API routes, and edge performance.
import { one } from 'one/vite'
export default { plugins: [ one({ web: { deploy: 'cloudflare' } }) ]}# Build for Cloudflarenpx one build
# Deploy using Wranglerwrangler deployWhen targeting Cloudflare, One generates:
dist/worker/index.js - The Cloudflare Worker entry pointdist/worker/wrangler.json - Wrangler configuration for the generated workerdist/client - Static assets served from Cloudflare’s edgedist/server - Server-side bundles (lazy-loaded per route)dist/api - API route handlers (lazy-loaded per route)dist/middlewares - Middleware bundles used by the worker runtimeOne uses a lazy loading pattern for Cloudflare Workers. Route modules are loaded on-demand when matched, not all upfront. This improves cold start times for apps with many routes.
The generated worker config lives at dist/worker/wrangler.json and is ready to deploy with Wrangler. The worker serves static assets from ../client and loads route bundles from the generated dist/server, dist/api, and dist/middlewares outputs as requests come in.
{ "main": "index.js", "no_bundle": true, "rules": [{ "type": "ESModule", "globs": ["assets/**/*.js", "assets/**/*.mjs"] }], "assets": { "directory": "../client", "binding": "ASSETS", "run_worker_first": true }}waitUntilAPI route handlers receive the worker runtime’s env and executionCtx on their second argument as worker:
import { createAPIRoute } from 'one'
export const POST = createAPIRoute(async (request, { worker }) => { // env holds the bindings you defined in wrangler.json (KV, D1, R2, secrets, ASSETS) const db = worker?.env?.DB
// waitUntil keeps the worker alive for fire-and-forget work after the // response is sent — useful for analytics, logging, cache warming, etc. worker?.executionCtx?.waitUntil(trackAsync(request))
return Response.json({ ok: true })})worker is undefined outside the worker runtime (dev/Node), so the optional chaining keeps the handler runtime-portable. See the API Routes doc for full details.
nodejs_compat compatibility flag (auto-configured)The generated worker config is ready to use. To customize it, create your own wrangler.jsonc in your project root and One will merge it into the generated dist/worker/wrangler.json.
One preserves build-required settings like main, assets.directory, assets.binding, assets.run_worker_first, the generated worker chunk rules, and it always includes nodejs_compat in compatibility_flags.
Key settings:
{ "name": "your-app-name", "compatibility_flags": ["nodejs_compat"], "assets": { "directory": "../client" }}Edit this page on GitHub.