> ## Documentation Index
> Fetch the complete documentation index at: https://docs-terra.withunify.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Publishing

> Draft and publish workflow, scheduling, and form freezing

# Publishing

> Forms have draft and published states. Edits don't affect live forms until published.

***

## Versioning Model

```mermaid theme={null}
stateDiagram-v2
    [*] --> Draft: Create form
    Draft --> Published: Publish
    Published --> Frozen: First submission
    Published --> Draft: Edit (creates new draft)
    Draft --> Published: Re-publish
    Published --> Closed: Close form
    Closed --> Published: Reopen
```

***

## Draft vs Published

| Schema             | Who Sees It      | Purpose          |
| ------------------ | ---------------- | ---------------- |
| `draft_schema`     | Admins (preview) | Work in progress |
| `published_schema` | Applicants       | Live form        |

When you edit a published form, changes go to `draft_schema`. Applicants continue seeing `published_schema` until you re-publish.

***

## Publishing a Form

```typescript theme={null}
export async function publishForm(formId: string) {
  const form = await getForm(formId);

  await supabaseAdmin
    .from("forms")
    .update({
      published_schema: form.draft_schema,
      last_published_at: new Date().toISOString(),
      published_version: form.published_version + 1,
      status: "published",
    })
    .eq("id", formId);

  // Create version snapshot
  await recordFormVersionSnapshot(formId, form.draft_schema);
}
```

***

## Scheduled Publishing

Forms can be scheduled to go live at a future date:

```typescript theme={null}
// Set scheduled publish time
await formsRepository.schedulePublish(formId, new Date("2025-02-01T00:00:00Z"));
```

The background worker checks every minute for forms where `published_at` has passed:

```typescript theme={null}
async function processScheduledPublishes(): Promise<number> {
  const result = await formsRepository.getFormsDueForPublish();

  for (const form of result.data) {
    await supabaseAdmin
      .from("forms")
      .update({
        status: "published",
        published_schema: form.schema,
      })
      .eq("id", form.id);

    logger.info("Auto-published form", { formId: form.id });
  }

  return result.data.length;
}
```

***

## Scheduled Closing

Forms can also be scheduled to stop accepting submissions:

```typescript theme={null}
// Set scheduled close time (deadline)
await formsRepository.scheduleClose(formId, new Date("2025-03-31T23:59:59Z"));
```

The worker closes forms where `closes_at` has passed:

```typescript theme={null}
async function processScheduledCloses(): Promise<number> {
  const result = await formsRepository.getFormsDueForClose();

  for (const form of result.data) {
    await supabaseAdmin
      .from("forms")
      .update({ status: "closed" })
      .eq("id", form.id);

    logger.info("Auto-closed form", { formId: form.id });
  }

  return result.data.length;
}
```

***

## Form Freezing

After a form receives its first submission, it becomes **frozen**. Frozen forms:

* Cannot have their schema modified
* Can still be closed/reopened
* Can have settings changed (branding, etc.)

This protects data integrity—changing questions after receiving responses would break historical data.

### Freeze Fields

```typescript theme={null}
interface Form {
  frozen: boolean;
  frozen_at: string | null;
  frozen_reason: string | null;
}
```

### Auto-Freeze Behavior

The background worker automatically freezes forms:

```typescript theme={null}
async function processAutoFreeze(): Promise<number> {
  // Find published, non-frozen forms with submissions
  const { data: forms } = await supabaseAdmin
    .from("forms")
    .select("id, title")
    .eq("status", "published")
    .eq("frozen", false);

  for (const form of forms) {
    const { count } = await supabaseAdmin
      .from("submissions")
      .select("*", { count: "exact", head: true })
      .eq("form_id", form.id);

    if (count > 0) {
      await supabaseAdmin
        .from("forms")
        .update({
          frozen: true,
          frozen_at: new Date().toISOString(),
          frozen_reason: "First submission received",
        })
        .eq("id", form.id);
    }
  }
}
```

### Manual Freeze/Unfreeze

Super admins can manually freeze or unfreeze forms:

```typescript theme={null}
// Freeze a form
await formsRepository.freeze(formId, "Locked for audit");

// Unfreeze (be careful!)
await formsRepository.unfreeze(formId);
```

<Warning>
  Unfreezing a form with existing submissions can cause data integrity issues. Only do this if you're certain the schema changes won't break existing responses.
</Warning>

***

## Preview Mode

Admins can preview draft changes before publishing:

```
/f/rental-assistance?preview=draft
```

This shows `draft_schema` instead of `published_schema`. Only authenticated admins see the preview.

***

## Effective Status

The effective form status considers scheduling:

```typescript theme={null}
function getEffectiveFormStatus(
  status: "draft" | "published" | "closed",
  publishedAt: string | null,
  closesAt: string | null
): "draft" | "published" | "closed" {
  const now = new Date();

  // If closed, stay closed
  if (status === "closed") return "closed";

  // If draft and published_at hasn't passed, still draft
  if (status === "draft") {
    if (publishedAt && new Date(publishedAt) <= now) {
      return "published";  // Scheduled publish time has passed
    }
    return "draft";
  }

  // If published, check if closes_at has passed
  if (closesAt && new Date(closesAt) <= now) {
    return "closed";
  }

  return "published";
}
```

***

## Form Status Summary

| Status               | Accepting Submissions | Schema Editable | Trigger            |
| -------------------- | --------------------- | --------------- | ------------------ |
| `draft`              | No                    | Yes             | Create / Edit      |
| `published`          | Yes                   | Yes (to draft)  | Publish            |
| `published` + frozen | Yes                   | No              | First submission   |
| `closed`             | No                    | No              | Manual / Scheduled |

***

## Lifecycle Diagram

```mermaid theme={null}
flowchart TD
    subgraph Manual["Manual Actions"]
        Create[Create Form]
        Publish[Publish]
        Close[Close]
    end

    subgraph Scheduled["Automatic (Worker)"]
        AutoPub[Auto-Publish]
        AutoClose[Auto-Close]
        AutoFreeze[Auto-Freeze]
    end

    Create --> Draft
    Draft --> |published_at passes| AutoPub
    Draft --> |Manual| Publish
    AutoPub --> Published
    Publish --> Published
    Published --> |First submission| AutoFreeze
    AutoFreeze --> Frozen[Published + Frozen]
    Published --> |closes_at passes| AutoClose
    Frozen --> |closes_at passes| AutoClose
    AutoClose --> Closed
    Close --> Closed
```

***

<CardGroup cols={2}>
  <Card title="Creating Forms" icon="hammer" href="/features/form-lifecycle/creating-forms">
    Form builder guide
  </Card>

  <Card title="Public Pages" icon="globe" href="/features/form-lifecycle/public-pages">
    Applicant-facing forms
  </Card>

  <Card title="Background Jobs" icon="clock" href="/core-systems/async/background-jobs">
    Scheduled task processing
  </Card>

  <Card title="Submissions" icon="inbox" href="/features/form-lifecycle/submissions">
    How submissions work
  </Card>
</CardGroup>
