Back to Blog
4 min read

Building a Persistent Preview State in HomeForged: How I Tamed Schema Mocking with Database-Backed Drafts

The Problem: Losing Preview Context Was Killing My Flow

If you’ve ever worked on a form-heavy app where the UI preview depends on mock data, you know the pain: you tweak a schema, refresh the page, and poof—your carefully crafted test state is gone. That was HomeForged’s reality before today.

I recently normalized my schema structure to support dynamic form layouts, which was a huge win for flexibility. But it came at a cost: every time I iterated on field types or nesting, the frontend preview would reset. Mock data lived in memory or localStorage—fine for demos, terrible for real development. I needed previews that survived refreshes, deploys, and even team handoffs.

The breaking point? Trying to debug a conditional rendering bug across nested sections. I spent 15 minutes rebuilding the mock state, only to lose it when I accidentally closed the tab. That’s when I decided: no more.

Designing a Draft Layer That Feels Invisible

The goal was simple: make preview state persistent, but without complicating the core data model or introducing race conditions. I didn’t want developers to think about "saving drafts"—it should just work, like Google Docs.

I started by adding a drafts table to my Postgres DB, with three key fields:

  • schema_id: ties the draft to a specific form configuration
  • mock_data: JSONB column storing the current field values and UI state
  • version_hash: a checksum of the schema structure to detect drift

The version hash was crucial. When a developer loads a preview, I compare the current schema’s hash with the one stored in the draft. If they don’t match, I don’t auto-apply the old data—I show a prompt: "Your schema changed. Restore previous values?" This prevents silent mismatches when fields are renamed or removed.

On the backend, I exposed a simple /drafts/:schema_id endpoint that handles GET and PATCH. On first load, if no draft exists, I generate one with empty values mapped to the current schema’s defaults. From there, every keystroke in the preview triggers a debounced update—nothing realtime, just a 500ms delay to avoid flooding the DB.

Frontend-wise, I wrapped the preview iframe in a React context that syncs with the draft API. The component tree doesn’t care where the data comes from—it just consumes the current state. This separation kept the implementation clean: the preview renderer stayed pure, while the draft logic lived in a dedicated service.

Lessons Learned: Syncing State Without the Headaches

The first version worked… sort of. I hit two issues fast.

First, race conditions during rapid edits. Two developers on the same schema could overwrite each other’s drafts. My fix? I added a last_updated timestamp and made the frontend check it on every patch response. If the server’s timestamp is newer than the client’s last known version, I trigger a merge—client wins for existing fields, server wins for new ones. It’s not perfect, but for my use case (mostly solo editing), it’s enough.

Second, I underestimated how often schema changes break old mocks. Early on, I tried to auto-migrate data—renaming fields, inferring types—but it was a mess. Instead, I embraced the reset. Now, when version hashes don’t match, I store the old draft as a snapshot and start fresh. Want to recover data? Click a button to inspect and copy-paste. Simpler, safer, more predictable.

The biggest win? Developer trust. Now when I tweak a schema, I know my preview will come back the way I left it. No more "rebuilding the universe" just to test a dropdown.

This wasn’t a flashy feature, but it removed a constant friction point. Sometimes the best engineering isn’t about building new things—it’s about making the existing ones stop fighting you. And today, HomeForged finally stopped fighting back.

Newer post

How I Scaled YAML Schema Handling in HomeForged with Dynamic API-Driven Editors

Older post

Building Fine-Grained Permissions in HomeForged: From UI to Entity-Level Control