Open Form

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.

forms/meetup/index.tsx
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 for opensAt, until its end for closesAt.
  • '2026-10-01T09:00' — a wall-clock time.
  • '2026-10-31T18:00:00+09:00' or a Date — 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.

On this page