Vercel
Deploy a whole Open Form project to Vercel, with responses in Neon or Supabase Postgres.
Vercel runs your project as static files plus a function: the built forms and Studio are served as they are, and one function answers the API and the pages. Functions have no persistent disk, so responses go to Postgres: Neon, Supabase, or any Postgres the function can reach over the internet.
This is the same project open-form dev runs: forms at /f/<id>, Studio at
/studio. To render forms inside a Next.js app you already have instead, see
Next.js.
Add the function
Starting a new project? open-form init --vercel (or answering yes to
Deploy to Vercel? after picking Postgres) writes both files below and adds
pg, so you can skip to the database.
npm create open-form@latest my-forms -- --vercelFor an existing project, install the Postgres driver:
npm install pgAdd two files at the root of the project, next to forms/:
import { createOpenFormApp } from '@open-form-build/core/vercel';
export default createOpenFormApp();{
"$schema": "https://openapi.vercel.sh/vercel.json",
"framework": null,
"buildCommand": "open-form build",
"outputDirectory": "dist/client",
"functions": {
"api/index.ts": { "includeFiles": "dist/**" }
},
"routes": [
{ "src": "/(index\\.html)?", "dest": "/api" },
{ "handle": "filesystem" },
{ "src": "/(.*)", "dest": "/api" }
]
}Vercel serves the built assets from dist/client, and every other path goes
to the function. The function reads the server bundle from
dist/server/entry.js by path, so includeFiles ships it along.
Create the database
The quickest way: add Neon from the Vercel Marketplace (Storage → Create
Database) and connect it to the project. Neon then sets POSTGRES_URL (and
DATABASE_URL) in the project, and the function uses it whenever
OPEN_FORM_DATABASE_URL isn't set, so there's nothing to copy.
Otherwise, create a project in Neon and copy the
pooled connection string (its host has -pooler in it). It looks like:
postgres://user:password@ep-cool-name-123456-pooler.us-east-2.aws.neon.tech/neondb?sslmode=requireNeon's certificates come from a public authority, so sslmode=require verifies
them as is. Change it to sslmode=verify-full to say so outright and to
silence pg's notice about require.
Set the environment variables
In the Vercel project's Settings → Environment Variables:
| Variable | Value |
|---|---|
OPEN_FORM_DATABASE_URL | The connection string from the last step. Leave it out when the Marketplace set POSTGRES_URL or DATABASE_URL for you; when it's set, it wins. |
OPEN_FORM_SECRET | At least 16 random characters, the same on every deploy. Functions can't write the generated secret file. |
OPEN_FORM_ADMIN_PASSWORD | Turns on Studio and the admin API. At least 16 characters with at least 8 different ones, not one word or pattern repeated; a passphrase of a few words works. Without it, /studio answers 404 unless studio.public is 'read-only'. |
Generate the secret with:
node -e "console.log(require('node:crypto').randomBytes(32).toString('hex'))"Or set them from the command line, after vercel link:
vercel env add OPEN_FORM_SECRET
vercel env add OPEN_FORM_ADMIN_PASSWORDVercel reads variables when it deploys, so a variable you add or change takes effect with the next deployment. Set each one for every environment you deploy to: one set only for Production is missing from Preview deployments.
You don't need OPEN_FORM_TRUST_PROXY: the function counts rate limits by the
address Vercel puts last in X-Forwarded-For.
For local development, open-form dev keeps using SQLite in .open-form/
unless .env sets OPEN_FORM_DATABASE_URL. It reads neither .env.local
(what vercel env pull writes) nor POSTGRES_URL. A separate database (or
Neon branch) for development keeps test responses out of production.
Deploy
Push to a Git repository connected to Vercel, or from the project folder:
vercel link # once; Vercel picks up vercel.json, so keep its settings
vercel --prodThe first request creates the tables. Forms are at
https://<your-app>.vercel.app/f/<id>, Studio at /studio, and / sends you
to Studio.
To check the production database from your machine, run
open-form db status with its connection string in the
environment:
OPEN_FORM_DATABASE_URL='postgres://…' npx open-form db statusWebhooks
A function stops once it has answered, so a webhook
delivery still in flight is cut off. Install @vercel/functions and pass its
waitUntil to keep it alive:
import { waitUntil } from '@vercel/functions';
import { createOpenFormApp } from '@open-form-build/core/vercel';
export default createOpenFormApp({ waitUntil });Retries that come minutes later outlast the function and are lost.
Setup errors
The function sets itself up on its first request. A missing build, a SQLite
database it can't open (no OPEN_FORM_DATABASE_URL, POSTGRES_URL or
DATABASE_URL), a secret it can't
create (no OPEN_FORM_SECRET), an admin password that breaks the rules above,
or a database it can't connect to makes that fail. Every request then answers
500, the public forms included, and the function logs (Logs in the Vercel
project) give the reason. Fix the variable and deploy again; the next request
retries.