Open Form

open-form.config.ts

Project-wide settings in open-form.config.ts: storage, port, locale, protection, proxies, webhooks, embedding and Studio access.

The project config lives at the workspace root. Every key is optional. Unlike a form's config, it never reaches the browser, so it's the place for webhook URLs and anything else that shouldn't be public.

open-form.config.ts
import { defineConfig } from '@open-form-build/core';

export default defineConfig({
  locale: 'zh-TW',
  timeZone: 'Asia/Taipei',
});

open-form init writes this file with timeZone set to your computer's time zone. It loads as ESM whether or not your package.json is "type": "module", so it also works at the root of an existing app.

process.env in this file reads the server's environment. Every open-form command except init first loads .env from the workspace root when there is one; variables already set in the environment win. In a Next.js app, Next.js loads .env itself.

Schema

Prop

Type

protection takes the same keys as a form's protection, merged key by key.

Checked at load

An unknown locale, an invalid timeZone or a studio.public other than 'read-only' fails loudly: open-form dev, build, check and the other commands that read this file stop with the error. Saved while open-form dev runs, a broken config is reported and the server keeps the previous one until you fix it. webhooks and embed.origins are checked when the server sets up the API; see Webhooks and Embed.

Common recipes

Deadlines in your time zone

Set timeZone once so opensAt: '2026-10-01' and closesAt: '2026-10-31' mean those days where you are, on any server:

export default defineConfig({ timeZone: 'Asia/Taipei' });

Send responses to Slack, Zapier or your API

const responsesUrl = process.env.RESPONSES_WEBHOOK_URL;

export default defineConfig({
  webhooks: responsesUrl ? [{ url: responsesUrl, forms: ['meetup'] }] : [],
});

Each new response is POSTed as submission.created. Set OPEN_FORM_WEBHOOK_SECRET to sign deliveries in the Standard Webhooks format. URLs must use https:// (http:// only for localhost, 127.0.0.1, [::1] and *.localhost). Keep the URL in an environment variable; it is read when the server starts, not copied into the build. Leave the entry out when the variable is unset, as above: a missing URL is refused like any other invalid one, so open-form start won't start, and under open-form dev or in a Next.js route every API request fails. See Webhooks.

Embed forms on another site

export default defineConfig({ embed: { origins: ['https://blog.example.com'] } });

Origins are scheme and host only — https://example.com, no path. This allows framing /f/<id> and calling the public form routes from that site; the admin API never opens to other origins. See Embed.

Behind a reverse proxy

export default defineConfig({ trustProxy: 1 });

Without it, every visitor seems to come from the proxy's address, so rate limits and sign-in attempts are counted for everyone together: one person can use up a form's rate limit or lock everyone out of Studio sign-in. Forms work either way, HTTPS included. Set it only when proxies you control sit in front of the server. open-form start also reads OPEN_FORM_TRUST_PROXY (true, false or a number) when the config doesn't set it. The Next.js route and the Vercel function default to 1 instead, since they expect Vercel or a reverse proxy in front.

Keep the database on a volume

export default defineConfig({ storage: { path: '/data/open-form.db' } });

Or leave the config alone and set OPEN_FORM_DB=/data/open-form.db on the server. The generated secret follows the database to /data/secret. To keep it off the volume, so a copy of the volume doesn't carry it, set OPEN_FORM_SECRET in your platform's secret settings instead:

OPEN_FORM_DB=/data/open-form.db
OPEN_FORM_SECRET=<at least 16 random characters>

See Storage.

Store responses in Postgres

Set OPEN_FORM_DATABASE_URL and leave the config alone, or reuse a variable your app already has:

export default defineConfig({ storage: { url: process.env.DATABASE_URL } });

This file is committed, so the connection string itself stays in the environment; open-form check fails on a URL with a password written here. See Postgres.

A public demo Studio

export default defineConfig({ studio: { public: 'read-only' } });

Under open-form start, anyone can open /studio without signing in and read every form, unpublished ones included, and every response, export included. Deleting responses and changing a form's schedule still need OPEN_FORM_ADMIN_PASSWORD: with it set, Sign in at the bottom of the sidebar unlocks them; without it, nobody can make those changes. Respondents' answers are public this way, so say so on the form and don't ask for anything personal. open-form dev ignores the setting: there, Studio stays on your machine.

Remote IDE or tunnel during development

Cloud workspaces and tunnels reach the dev server through another hostname, which the dev server blocks. Allow it — a leading dot matches every subdomain:

export default defineConfig({ allowedHosts: ['.my-workspace.example.com'] });

Without OPEN_FORM_ADMIN_PASSWORD, Studio answers only direct requests from your machine and refuses the rest, with a hint in the terminal to set it. A raw TCP tunnel (ssh -R) or a proxy that adds no X-Forwarded-For can still make a remote request look local, so set a password whenever Studio is reachable through a tunnel or proxy; see open-form dev. With it set, everyone signs in, on your own machine too.

On this page