Form Versioning in Laravel: Keep Old Submissions Readable When Fields Change | FilaForms                                 [ ![Filaforms Logo](https://filaforms.app/logo.svg)FilaForms

 ](https://filaforms.app)  [ Features ](https://filaforms.app#features) [ Pricing ](https://filaforms.app/pricing) [ Blog ](https://filaforms.app/blog) [ Documentation ](https://docs.filaforms.app)  [ Try Demo ](https://filaforms.app/login) [ Get Started ](https://filaforms.app/pricing#plans) 

 [ Features ](https://filaforms.app#features) [ Pricing ](https://filaforms.app/pricing) [ Blog ](https://filaforms.app/blog) [ Documentation ](https://docs.filaforms.app) [ Try Demo ](https://filaforms.app/login) [ Get Started ](https://filaforms.app/pricing#plans) 

   ![FilaForms](https://filaforms.app/logo.svg) FilaForms 

 TutorialsForm Versioning in Laravel: Keep Old Submissions Readable When Fields Change
============================================================================

 filaforms.app/blog

  [    Back to blog ](https://filaforms.app/blog) [ Tutorials ](https://filaforms.app/blog/category/tutorials) 

Form Versioning in Laravel: Keep Old Submissions Readable When Fields Change
============================================================================

 Manuk Minasyan ·  August 25, 2026  · 4 min read 

 Rename a dynamic field after collecting responses and a subtle question appears: should an old submission display the old label or the new one? Delete an option and the problem becomes obvious—the stored value can survive while its human meaning disappears.

The fix is to bind every submission to the immutable form revision the respondent saw.

“Version” means three different things
--------------------------------------

Separate these concepts in the data model:

1. **Schema format version:** tells code how to interpret the JSON document.
2. **Published form revision:** the exact definition served to a respondent.
3. **Application/package release:** the code deployed at that time.

A JSON field such as `"version": "1.0"` usually describes the schema dialect. It does not prove that two forms with that marker have identical labels, fields, or rules. The [JSON Schema introduction](https://json-schema.org/understanding-json-schema/reference/schema) illustrates this format-versus-instance distinction.

Compare three storage models
----------------------------

ModelStorageHistorical fidelityTrade-offOne mutable form rowLowestPoorOld answers depend on current schemaFull schema snapshot per submissionHighestExcellentSimple reads, duplicated JSONImmutable `form\_versions` tableModerateExcellentOne relation/eager loadFor most systems, a normalized revision table is the best default. Per-submission snapshots remain reasonable for low-volume, audit-heavy workflows or when definitions reference data outside the revision.

A practical revision model
--------------------------

Use an append-only table containing:

```
form_versions
  id (ULID)
  form_id
  revision (monotonic integer)
  schema_format
  schema (JSON)
  checksum
  published_at
  created_by

```

Add `forms.current_version_id` and `form_submissions.form_version_id`. A draft should carry the same version ID. If exact option wording matters and options come from mutable Eloquent rows, store the selected label snapshot too.

Keep machine field codes immutable. They prevent a label rename from changing the answer key, but they do not preserve a deleted field's label, help text, option labels, or validation behavior. Revision history supplies that context.

Publish atomically
------------------

Publishing should create an immutable revision and switch the active pointer in one database transaction:

```
DB::transaction(function () use ($form, $draftSchema): void {
    $revision = $form->versions()->create([
        'revision' => $form->versions()->max('revision') + 1,
        'schema_format' => '1.0',
        'schema' => $draftSchema,
        'checksum' => hash('sha256', json_encode($draftSchema)),
        'published_at' => now(),
        'created_by' => auth()->id(),
    ]);

    $form->update(['current_version_id' => $revision->id]);
});

```

In production, protect the revision number and active pointer against concurrent publishers with locking or a unique constraint. Dispatch cache refreshes and notifications after commit.

Never edit a published revision in place. A correction creates a new revision; rollback changes the active pointer to a known revision and records the action.

Bind at render and verify at submit
-----------------------------------

When the public page renders, resolve one active revision and include its opaque identifier in component state. On submission:

1. load that exact revision from trusted storage;
2. verify it belongs to the form and is allowed to accept responses;
3. compile validation from it;
4. store the submission with the same revision ID.

Do not validate against revision A and label the result as revision B. A delayed Livewire request or a page open during publishing can otherwise cross the boundary.

Decide how drafts survive changes
---------------------------------

An old draft can:

- finish on its original revision;
- migrate through a reviewed transformation;
- be forced to restart.

There is no universal answer. A tax or compliance form may require restart; an application may honor the original version for a grace period. Make the policy visible and test it. The upcoming save/resume design should bind the draft from its first save.

Mutable options need a display policy
-------------------------------------

Suppose a submission stores `plan_id=3`, then the plan is renamed or deleted. You can display:

- the current label, useful for operational dashboards;
- the label from the form revision;
- a label snapshot captured at submission.

Pick intentionally. Stable IDs preserve identity, not wording. If a legal acknowledgement depends on exact text, the historical snapshot matters more than current catalog data.

Retention is separate
---------------------

Schema history does not mean every attachment or personal answer must live forever. Keep enough revision metadata to interpret records while applying a documented retention policy to submissions and files. See the [GDPR form checklist](/blog/gdpr-compliant-forms-laravel-checklist) for the wider lifecycle.

Tests that prove history works
------------------------------

Create a submission, then publish new revisions that rename a field, delete a field, reorder and rename options, add a required field, and change locale copy. The old response must still render against its original revision. Also test:

- simultaneous publishers cannot create two active revisions;
- failed publishing leaves the previous pointer intact;
- a tampered revision ID is rejected;
- a retired draft follows the documented finish/migrate/restart policy;
- rollback preserves all later revisions and audit events.

The important design move is small: stop treating the mutable form row as the historical source of truth. Once published definitions are immutable and every response points to one, schema evolution becomes manageable instead of destructive.

 Related posts
-------------

 [  Tutorials   Aug 18, 2026  

 Dynamic Form Validation in Laravel: Safely Storing Rules in the Database 
--------------------------------------------------------------------------

Compile database-stored Laravel form rules safely with allowlists, typed parameters, nested validation, tenant scopes, and adversarial tests.

 ](https://filaforms.app/blog/dynamic-form-validation-in-laravel-safely-storing-rules-in-the-database) [  Tutorials   Jul 28, 2026  

 Form Performance in Laravel: Loading, Validation, and Submission Speed 
------------------------------------------------------------------------

Form page loads in 2.8 seconds. INP is 380 ms. Submit lags. Conversion drops and you feel it but can't see why. Here's the five-number checklist that finds the leak.

 ](https://filaforms.app/blog/form-performance-laravel-loading-validation-submission-speed) [  Tutorials   Jul 14, 2026  

 Signature Fields in Laravel Forms: When You Need a Real Signature, Not a Checkbox 
-----------------------------------------------------------------------------------

Checkbox-as-signature doesn't fly for NDAs, waivers, or parental consent. DocuSign is $40/user. Here's how the signature field in FilaForms works — and when it's enough.

 ](https://filaforms.app/blog/signature-fields-in-laravel-forms) 

    ![FilaForms Logo](/logo.svg) FilaForms 

 Laravel form infrastructure for Filament. Stop rebuilding forms on every project.

 ### Product

 [ Features ](https://filaforms.app#features) [ Documentation ](https://docs.filaforms.app) [ Blog ](https://filaforms.app/blog) [ Pricing ](https://filaforms.app/pricing) [ About ](https://filaforms.app/about) [ Contact ](mailto:hello@filaforms.app) 

 ### Legal

 [ Terms of Service ](https://filaforms.app/terms-of-service) [ Privacy Policy ](https://filaforms.app/privacy-policy) 

  © 2025-2026 FilaForms. All rights reserved.

 [    ](mailto:hello@filaforms.app) [    ](https://x.com/MinasyanManuk)
