open-form.config.ts
Project-wide configuration with defineConfig.
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 or an invalid timeZone 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 defaults to 1 instead, since it expects 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. See Storage.
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.