Drafts & prefill

Persist answers across page refreshes, resume saved drafts, and pre-populate fields from URLs, props, or external data.

Draft persistence

Enable draft saving in the schema settings:

settings: {  allowDraftSave: true,  draftStorage: 'local',        // 'local' | 'server' | 'both'  draftTtlHours: 72,            // Draft expires after 72 hours (default)}

With draftStorage: 'local', drafts are saved to localStorage using the key pattern fe_draft__{schemaId}__{sessionToken}. Different forms and sessions never collide.

What's saved in a draft

type DraftData = {  schemaId: string  sessionToken: string  partialData: Record<string, unknown>    // Current field values  currentSectionId?: string               // Which section the user was on  visitedSectionIds?: string[]            // Which sections they've seen  savedAt: string                         // ISO timestamp  expiresAt: string                       // ISO timestamp (savedAt + ttlHours)}

Draft resume prompt

When a user returns to a form with a saved draft, FormEngineRenderer shows a DraftResumePrompt:

"You have a saved draft from 2 hours ago. Would you like to resume or start over?"[Resume]  [Start over]

Engine methods

await engine.saveDraft()       // Save current stateconst resumed = await engine.loadDraft()  // Load draft — returns true if foundengine.clearDraft()            // Delete the draft

Restoring drafts via onReady

When you have a custom wrapper around FormEngineRenderer (e.g. adding payment intents or analytics), use the onReady callback to restore drafts after the engine mounts:

import { useState, useCallback } from 'react'import { FormEngineRenderer } from '@squaredr/fieldcraft-react'import type { FormEngine } from '@squaredr/fieldcraft-core'function PublicForm({ schema }) {  const [draftRestored, setDraftRestored] = useState(false)  const handleReady = useCallback((engine: FormEngine) => {    engine.loadDraft().then((restored) => {      if (restored) setDraftRestored(true)    })  }, [])  return (    <>      {draftRestored && (        <div className="draft-banner">          Your previous progress has been restored.        </div>      )}      <FormEngineRenderer        schema={schema}        onReady={handleReady}        onSubmit={handleSubmit}      />    </>  )}

This pattern is useful when you need the FormEngine instance for other purposes (e.g. updating payment field props) and want draft restoration to happen alongside your own setup logic.

Server-side drafts

For drafts that persist across devices, use a DraftAdapter:

import { createSupabaseDraftAdapter } from '@squaredr/fieldcraft-adapters'const draftAdapter = createSupabaseDraftAdapter({  client: supabase,  table: 'form_drafts',          // Default: 'form_drafts'  ttlHours: 168,                 // 1 week})

Pass it to the renderer:

<FormEngineRenderer  schema={schema}  draftAdapter={draftAdapter}  onSubmit={handleSubmit}/>

Or use the Postgres adapter:

import { createPostgresDraftAdapter } from '@squaredr/fieldcraft-adapters'const draftAdapter = createPostgresDraftAdapter({  connectionString: process.env.DATABASE_URL!,  table: 'form_drafts',  ttlHours: 168,  ssl: true,})

Building a custom draft adapter

Implement the DraftAdapter interface:

import type { DraftAdapter, DraftData } from '@squaredr/fieldcraft-core'const redisDraftAdapter: DraftAdapter = {  async save(draft: DraftData) {    const key = `draft:${draft.schemaId}:${draft.sessionToken}`    await redis.set(key, JSON.stringify(draft), 'EX', draft.expiresAt)  },  async load(schemaId: string, sessionToken: string) {    const key = `draft:${schemaId}:${sessionToken}`    const data = await redis.get(key)    return data ? JSON.parse(data) : null  },  async delete(schemaId: string, sessionToken: string) {    const key = `draft:${schemaId}:${sessionToken}`    await redis.del(key)  },}

Prefill

Pre-populate field values from external data. Three sources are supported.

Props prefill

Pass values directly:

<FormEngineRenderer  schema={schema}  prefill={{    email: 'user@example.com',    company: 'Acme Inc',    plan: 'enterprise',  }}  onSubmit={handleSubmit}/>

URL prefill

Configure URL-based prefill in the schema:

settings: {  prefill: {    source: 'url',              // 'props' | 'url' | 'both'    paramPrefix: 'fc_',         // URL params prefixed with fc_  },}

Then link to the form with query parameters:

https://example.com/form?fc_email=user@example.com&fc_plan=enterprise

The paramPrefix prevents collisions with other query parameters.

Combined prefill

With source: 'both', props values take priority over URL values for the same field.

Transform

Apply transformations to prefilled values:

settings: {  prefill: {    source: 'both',    transform: {      email: (value) => String(value).toLowerCase(),      phone: (value) => String(value).replace(/\D/g, ''),    },  },}

prefillKey

Map external data keys to field IDs:

{  id: 'user_email',  type: 'email',  label: 'Email',  prefillKey: 'email',         // Maps external key 'email' → field 'user_email'}

This lets your URL parameters or API data use different key names than your field IDs.

Next steps