defineForm
Every key of a form's config export, with defaults.
Each form exports its config from forms/<id>/index.tsx: its title, language,
schedule, limits, spam protection and thank-you screen. Field validation comes
from the fields in the form itself, and protection falls back to the defaults
in open-form.config.ts. Every key is optional; add
only what the form needs.
import { defineForm } from '@open-form-build/core';
export const config = defineForm({
title: 'Developer Meetup Registration',
description: 'Oct 30, Taipei. 100 seats.',
locale: 'en',
timeZone: 'Asia/Taipei',
submissions: { maxResponses: 100, closesAt: '2026-10-29', uniqueBy: 'email' },
success: { title: 'See you there!' },
});The form's look isn't config: it's the form's own theme.
open-form theme copy <theme> <id> (or open-form create <id> --theme <theme>)
copies one into forms/<id>/theme/, and the form imports it and passes it to
<Form theme>:
import theme from './theme/index.tsx';
<Form theme={theme}>…</Form>A theme without components, such as classic, is theme/index.ts instead;
open-form theme copy prints the import to use.
A form's config is bundled into the browser. Secrets — webhook URLs, API
keys — belong in open-form.config.ts, never here.
Schema
Prop
Type
submissions
Prop
Type
uniqueBy: 'device' is checked before the form shows, so a returning browser
sees the closed screen. It's a cookie — lightweight, not identity. With a
field name such as 'email', values are compared case-insensitively and
the duplicate is found on submit, so duplicateMessage appears under the
Submit button. uniqueBy must name a field in the form.
protection
Prop
Type
A Duration is a number of milliseconds or a string like '500ms', '2s',
'10m', '1h' or '7d'. Each key falls back to the project's protection,
then to the defaults above. Behind a reverse proxy, set
trustProxy so the rate limit counts visitors, not
the proxy.
success
Prop
Type
drafts
Off by default, because on a shared computer the next person would see the
answers. true keeps unfinished answers in the respondent's browser for 7
days; { expires: '1d' } sets another lifetime. Drafts never leave the
browser and are cleared on submit, when the form closes, fills up or the
respondent has already answered, and when you turn drafts off. Opening any
form also clears expired drafts of the others. A restored draft shows a notice
with a button to start over.
Dates and time zones
opensAt and closesAt accept:
'2026-10-01'— a whole day: from its start foropensAt, until its end forclosesAt.'2026-10-01T09:00'— a wall-clock time.'2026-10-31T18:00:00+09:00'or aDate— an exact instant; the time zone is ignored.
Day and wall-clock values are read in the form's timeZone, else the
project's timeZone, else UTC — never the server's own time zone, so a
deadline means the same on your laptop and in production. If you set a
deadline, set a timeZone somewhere; open-form check warns when it's
missing.
People can change opensAt, closesAt, maxResponses, closed and the
time zone in Studio. Under open-form dev that rewrites this config in the
form's code. On a deployed server it's stored in the database and wins over
the code until someone restores it in Studio — see
Submissions.
Checked at load
A config that can't work fails loudly instead of silently never applying: an
unknown locale, an invalid timeZone, a uniqueBy that isn't a field, an
unparseable date or duration, translations without a locale, or a
translation for a language Open Form doesn't support. open-form check
reports the same errors.