Open Form

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 -- --vercel

For an existing project, install the Postgres driver:

npm install pg

Add two files at the root of the project, next to forms/:

api/index.ts
import { createOpenFormApp } from '@open-form-build/core/vercel';

export default createOpenFormApp();
vercel.json
{
  "$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=require

Neon'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:

VariableValue
OPEN_FORM_DATABASE_URLThe 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_SECRETAt least 16 random characters, the same on every deploy. Functions can't write the generated secret file.
OPEN_FORM_ADMIN_PASSWORDTurns 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_PASSWORD

Vercel 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 --prod

The 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 status

Webhooks

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:

api/index.ts
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.

What's next

On this page