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.
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.
defineForm
Every key of a form's config export, with defaults: title, language, schedule, submission limits, spam protection, drafts and the success screen.
defineTheme
Every defineTheme option: color tokens, dark mode, fonts, radius and spacing, theme components, contrast checks and the --of-* CSS variables.