Field Types

Complete reference for all FieldDef types — text, number, textarea, select, segmented, toggle, range, rating, color, date, tags, and linked-responses.

Field Types

Every key in fields (or within a sections[].fields block) must be a FieldDef. All field types share a common base and then add type-specific options.

Common base properties

interface SimpleFieldDef<T = any> {
  type:         FieldType      // required — determines which input component renders
  label:        string         // displayed above the input
  default?:     T              // initial value written to Y.js on first open
  placeholder?: string
  required?:    boolean
  disabled?:    boolean
  help?:        string         // small hint text below the input
  colSpan?:     1 | 2         // grid column span (sections with cols:2)
  hidden?:      boolean | ((ctx: ComputeContext) => boolean)
}

text

Single-line text input.

name: { type: 'text', label: 'Full Name', placeholder: 'e.g. Jane Smith' }

number

Numeric input. Stored as a JavaScript number.

mrr: { type: 'number', label: 'MRR (€)', default: 10000 }

textarea

Multi-line text. Use for notes, descriptions, free-form content.

notes: { type: 'textarea', label: 'Notes', placeholder: 'Key observations…' }

select

Dropdown select from a fixed list of options.

category: {
  type: 'select',
  label: 'Category',
  options: [
    { value: 'direct',      label: 'Direct Competitor' },
    { value: 'indirect',    label: 'Indirect Competitor' },
    { value: 'substitute',  label: 'Substitute' },
  ],
  default: 'direct',
}

segmented

Segmented button group (radio-style). Better UX than select for 2–5 options.

fundingPath: {
  type: 'segmented',
  label: 'Funding Path',
  options: [
    { value: 'bootstrap', label: 'Bootstrap' },
    { value: 'vc',        label: 'VC-Backed' },
  ],
  default: 'bootstrap',
}

toggle

Boolean on/off switch.

includeVat: { type: 'toggle', label: 'Include VAT', default: false }

range

Slider input. Requires min, max, and optionally step.

willingness: {
  type: 'range',
  label: 'Willingness to Pay (0–10)',
  min: 0,
  max: 10,
  step: 1,
  default: 5,
}

rating

Star rating (1–5 by default). Stored as a number.

satisfaction: {
  type: 'rating',
  label: 'Overall Satisfaction',
  max: 5,
  default: 3,
}

color

Color picker. Stored as a hex string.

brandColor: { type: 'color', label: 'Brand Color', default: '#6366f1' }

date

Date picker. Stored as an ISO date string (YYYY-MM-DD).

launchDate: { type: 'date', label: 'Target Launch Date' }

tags

Multi-value tag input. Stored as string[].

painPoints: {
  type: 'tags',
  label: 'Pain Points',
  placeholder: 'Add a pain point and press Enter…',
}

linked-responses (collections only)

A special field type for per-question textarea responses within a collection item. Used when a collection of questions exists alongside a collection of sessions/interviews, and you want to record an answer per question inside each session record.

// In a collection's fields:
responses: {
  type: 'linked-responses',
  label: 'Responses',
  sourceCollection: 'questions',  // collection key to pull questions from
  questionField: 'text',          // which field of each question to show as the label
}

When rendered in ProtoCrudModal, this shows one textarea per question from the source collection. The stored value is Record<questionId, string>.

Conditional visibility

The hidden property accepts a function receiving the current ComputeContext:

advancedOptions: {
  type: 'textarea',
  label: 'Advanced Options',
  hidden: ({ fields }) => !fields.showAdvanced,
}

Sections with columns

When using sections, set cols: 2 on the section to get a two-column grid. Individual fields can span both columns with colSpan: 2:

sections: [
  {
    title: 'Financial Inputs',
    cols: 2,
    fields: {
      revenue:  { type: 'number', label: 'Revenue',  colSpan: 1 },
      expenses: { type: 'number', label: 'Expenses', colSpan: 1 },
      notes:    { type: 'textarea', label: 'Notes',  colSpan: 2 },
    }
  }
]

Schema migration

Adding a new field requires no migration — existing stored documents automatically get the field's default value on first read.

Renaming a field, changing its type, or transforming values requires bumping version and adding an entry to migrations. Each key is the target version — the runner steps from the stored version up to current, applying each function in order:

definePrototype({
  key: 'my-tool',
  version: 3,
  migrations: {
    // v0 → v1: renamed 'revenue' → 'monthlyRevenue' (destructure out the old field)
    1: ({ revenue, ...rest }) => ({ ...rest, monthlyRevenue: revenue ?? 0 }),
    // v1 → v2: added 'currency', no transform needed — filled from default automatically
    // v2 → v3: changed 'monthlyRevenue' from raw number to cents
    3: ({ monthlyRevenue, ...rest }) => ({ ...rest, monthlyRevenue: Math.round((monthlyRevenue ?? 0) * 100) }),
  },
  fields: {
    monthlyRevenue: { type: 'number', label: 'Monthly Revenue (cents)', default: 0 },
    currency:       { type: 'select', label: 'Currency', default: 'USD', options: ['USD', 'EUR'] },
  },
})

Each step function receives the raw stored item — only the fields that were actually written to Y.js at that version. Destructure out the old field name and spread the rest to keep the shape clean. Migrations run once after the persistence layer has loaded the stored data, write the result back to Y.js in a single transaction, and are skipped on every subsequent load. Version steps with no entry (like v2 above) are skipped — the runner moves straight to the next defined step.

Need a Landing Page?

Modern landing pages with optional modules (blog, docs, forms, i18n). Let's discuss your project.

Build Your MVP

Full-stack SaaS development. Expert in database design, multi-tenancy, and scalable architecture.

Deployment Help

Dockerize your backend, set up CI/CD pipelines, deploy to Cloudflare or Hetzner. Early-stage setup.

Suggest a SaaS Tool

Missing a calculator or tool? Suggest what you'd like to see on our site.