defineForm
Every key of a form's config export, with defaults: title, language, schedule, submission limits, spam protection, drafts and the success screen.
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 in forms/<id>/theme/,
which open-form create <id> writes (from a theme's tokens with
--theme <theme>). The form imports it and passes it to
<Form theme>:
import theme from './theme/index.ts';
<Form theme={theme}>…</Form>It's theme/index.tsx once it adds components.
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
success.calendar
For a form that signs people up for something on a known date, the thank-you
screen can offer to save it: a Google Calendar link and an Apple /
Outlook (.ics) download, below the message or the <Success> children.
export const config = defineForm({
title: 'Taipei Developer Meetup',
timeZone: 'Asia/Taipei',
success: {
calendar: {
start: '2026-10-30T19:00',
end: '2026-10-30T21:30',
location: 'Songshan Cultural and Creative Park',
},
},
});Prop
Type
A wall-clock start needs a time zone on the event or the form; the project's
timeZone is not sent to the browser, so the form fails to load without one.
title, location and description are translated with the rest of the form.
When the event depends on the answers, such as a session the respondent picked,
render <AddToCalendar> inside
<Success> instead.
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, the
time zone and published 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.
published
published: false takes a form offline without deleting it. Its link shows
"Form not found", and the API answers 404 to both loading and submitting, the
same as for a form that doesn't exist. Studio still lists it as Unpublished,
previews it, and keeps its responses; it just drops the form's links. Remove the
line, or set it to true, to put the form back at the same URL.
export const config = defineForm({
title: 'Spring Survey',
published: false,
});This is different from submissions.closed, which keeps the link working and
shows a closed screen.
An unpublished form is still part of the build, so Studio can publish it
again on a deployed server without a redeploy. Its questions can still be
found in the built JavaScript, so don't rely on this to keep them secret: to
leave a form out of the build, move its folder out of forms/.
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.