How We Built Project Forking and Versioning for AI-Generated Apps

SA

By Suraj Ahmed

18th Oct 2026

Last updated: 18th Oct 2026

How We Built Project Forking and Versioning for AI-Generated Apps

When a user prompts an AI builder to "add a dark mode toggle," two different kinds of history start pulling in opposite directions. One is temporal — the user needs to roll back if the model mangles their styles. The other is relational — someone else, three weeks from now, might fork that exact project and build a completely different app from it. If you only build the first, you ship a Google Doc. If you only build the second, you ship GitHub without a commit log. AI app versioning has to solve both at once, and the two systems hate each other.

This post is the architecture behind how we solved that at RapidNative — our AI mobile app builder that generates production-ready React Native and Expo apps from natural language. We'll walk through the three copy paths (remix, duplicate, admin duplicate), the lineage columns that quietly hold everything together, the one ON DELETE clause that would destroy user work if we got it wrong, and why we deliberately chose to make our version history implicit rather than explicit.

Developer branching codebase on laptop screen Branching code is a solved problem for engineers; branching AI-generated projects is not. — Photo by Markus Spiske on Unsplash

Why AI-generated apps don't fit Git's model

Git versioning works on a simple assumption: the person editing the code is also the one writing it. Each commit has a clear author, a diff, a message. When somebody forks the repo, they get a snapshot plus a backlink. Easy.

AI app versioning breaks every one of those assumptions. The author isn't writing the code — they're writing prompts, and the AI writes the code. A single turn can touch 15 files. The user doesn't think in commits; they think in intents ("make the header blue," "move the login flow"). And when they want to fork a public project someone else built, they don't want the diff — they want a living copy that stays in sync with the editor's AI generation pipeline, the chat history, the preview sandbox, and the binary assets.

The quick summary: in our system, every AI chat turn is an implicit version, every project copy is a lineage edge, and the two live in different tables with different lifecycles.

The three copy paths

Early on we tried to build "copy a project" as one feature. That lasted about a month before three distinct use cases forced us apart. Today, RapidNative has three separate API routes, three distinct permissions models, and three different sets of what-gets-copied semantics.

PathRouteWho uses itCopies filesCopies binary assetsCopies chat history
RemixPOST /api/user/projects/remixAny logged-in user, from a public project✅ (except tasks.json)❌❌
DuplicatePOST /api/user/projects/[id]/duplicateProject owner or shared user✅✅ (6-way parallel)❌ (explicit includeMessages: false)
Admin DuplicatePOST /api/admin/duplicate-projectAdmins only✅—configurable

The reason three paths exist, instead of one with flags:

  • Remix is a public, viral action. An unauthenticated visitor on a /project/public/<id> page clicks "Remix," gets bounced to signup, and then the server copies the project. It has to be cheap (no binary bloat), fast, and must reset anything that would make the copy feel haunted — notably tasks.json, which stores in-progress AI tasks from the original owner's session.
  • Duplicate is private workflow. You want a full copy, including the images and PDFs you uploaded, so you can experiment without touching your main project. It has to be high-fidelity.
  • Admin Duplicate exists for a very different reason: debugging. When a paying customer reports a bug, support needs an exact copy of their project to reproduce it, without that copy counting as user activity. It's tagged distinctly so business intelligence queries can filter it out.

Collapsing these into one route with boolean flags sounded clean on a whiteboard. In practice it meant that a single refactor in the "copy helper" could silently change the behavior of all three — and did, twice. Separating them cost us ~200 lines of duplicated code and bought us back a class of regression bugs that was impossible to prevent.

The two columns that hold the entire genealogy

Every forked project in RapidNative carries exactly two columns of lineage metadata:

ALTER TABLE public.projects
  ADD COLUMN parent_project_id UUID,
  ADD COLUMN copy_origin TEXT;

ALTER TABLE public.projects
  ADD CONSTRAINT fk_projects_parent_project
  FOREIGN KEY (parent_project_id) REFERENCES public.projects(id)
  ON DELETE SET NULL;

CREATE INDEX idx_projects_parent_project_id
  ON public.projects (parent_project_id)
  WHERE parent_project_id IS NOT NULL;

parent_project_id is a self-referential foreign key. copy_origin is a check-constrained enum: 'remix', 'admin_duplicate', or NULL for original projects. The partial index — only indexing rows where parent_project_id IS NOT NULL — exists because the overwhelming majority of projects are originals, and a full index would waste space on a column that's NULL 90%+ of the time.

That's it. No graph table, no project_ancestors closure, no materialized path. Two columns is enough to answer every lineage question we ask:

  • "Who did this user fork?" — a single select.
  • "How many descendants does this project have?" — one scan against the partial index.
  • "Which public projects get remixed most?" — group by parent_project_id where copy_origin = 'remix'.

Database schema visualization with foreign key relationships Two columns, one foreign key, one partial index — everything else is computable. — Photo by Caspar Camille Rubin on Unsplash

The one line that would destroy user work

The ON DELETE SET NULL clause above is the single most consequential piece of this architecture. The default for a foreign key would be NO ACTION — refuse the delete. The intuitive upgrade is ON DELETE CASCADE — delete the children too. Both would be catastrophic.

Here's why. RapidNative has public projects with hundreds of descendants. One of our most-remixed templates has over 800 downstream forks, built by 800 different users, over several months. Those users have poured prompts, screens, and iterations into their copies. The forks are their work, not a derivative.

If the original author ever decides to delete their project — their account, their experiment, their mind change — CASCADE would delete every single fork in the same transaction. 800 users would wake up to find their apps gone, with no error anywhere, because the delete succeeded exactly as the schema prescribed.

SET NULL is the only humane option. If the source goes away, the child survives — it just stops knowing where it came from. We lose the lineage pointer. We don't lose the project.

This decision was easy in retrospect and completely non-obvious when the migration was written. The migration file carries a comment that says, in effect, "never change this to CASCADE." That comment was written because somebody, during review, suggested CASCADE.

What gets copied (and the painful lessons about what doesn't)

Every project in RapidNative has three distinct physical storage layers:

  1. The files table — text files only, stored directly in Postgres content columns. Source code, JSON configs, markdown.
  2. Supabase Storage, projects bucket — binary assets. Images, PDFs, fonts, anything the AI editor shouldn't be reading as text.
  3. The messages table — the chat turn log that drove every modification.

Each copy path treats these three layers differently.

Remix copies only layer 1. Not binary assets, not messages. The theory: a remix is a starting point for a new project. The binary assets of a stranger's project aren't your assets — their logo, their PDF, their stock photos. Copying them would be a quiet copyright time bomb. And copying the chat history would make the new project feel haunted; the user would open the editor to see 40 messages they never wrote.

Duplicate copies layers 1 and 2 but not 3. Binary assets copy because they're yours — you're duplicating your own project. The copy runs with a concurrency ceiling of six parallel transfers. We tried unbounded first; Supabase Storage didn't love it. We tried serial; a project with 40 images took 90 seconds. Six is the number where we stopped seeing failures on the long tail. Chat history still doesn't copy, because a duplicate should start with a clean slate — "this is my experiment," not "this is my experiment plus the ghost of my previous experiment's conversation."

Admin Duplicate does whatever the support engineer asks it to. In practice, support usually wants chat history too, because the bug they're reproducing happened partway through a conversation.

Mobile phone showing an app being edited on a developer's laptop A duplicated project needs its binary assets to be visually identical; a remixed project doesn't want them at all. — Photo by Tim Mossholder on Unsplash

Message-driven versioning: why chat turns ARE the history

Inside a single project, RapidNative doesn't use Git either. There's no commit table, no diffs, no project_versions row-per-generation. Instead, we treat the messages table as an append-only ledger, and every AI turn is an implicit version.

The mechanics: when a user sends a prompt, the AI reads the current file tree (from the Lifo virtual file system that backs the preview sandbox), responds with modifications, and those modifications get written back to the files table in place. The chat message that preceded the write is kept forever. The file state is current-only — it doesn't preserve old content.

Rolling back is a replay. To revert N turns, we take the template base (or, for imported projects like Lovable conversions, the base snapshot stored in the internal-project-zips bucket) and replay the first total_messages - N turns against it. The AI model isn't re-invoked; we're replaying the file writes that each turn produced, not regenerating them.

This design has one huge win: it's cheap. Every project pays O(1) storage for versioning — one messages row per turn, no file-level diff log. For a 50-turn project with 200 files, Git-style storage would be hundreds of megabytes of history. Message-driven storage is a few kilobytes of chat.

It also has one real cost: you can't randomly access a version. Jumping to "the state at turn 23" requires replaying turns 1–23 from the base. For editor undo, this is fine; users almost always want to go back one or two turns, not twenty. For deployment safety, it would be a disaster — which is why we built an entirely separate system for that.

Snapshots and backups: the three safety nets

Message-driven history is great for the editor and terrifying for production. If you deployed your project last Tuesday, and today the AI rewrites something, the deployment you shipped shouldn't change under you. Three separate systems guarantee that it doesn't.

Checkpoints are preview bundles. Every ~30 seconds while the AI is generating, the editor packages the current VFS into a bundle and pushes it to Supabase Storage. The upload goes directly to Storage via a signed URL, bypassing Vercel's 4.5 MB body limit — important, because a React Native bundle with native modules exceeds that trivially. These exist so a reload mid-generation lands you back on the last stable preview, not a blank screen.

Deployment snapshots are frozen zips. The moment a user finalizes a deployment, the entire project is zipped and stored in the project-snapshot-zips bucket, with its path recorded on the deployments row as snapshot_path and the moment stamped as snapshot_taken_at. On every subsequent rebuild of that deployment, the pipeline fetches the frozen zip — not the live project files. Future AI edits can't retroactively touch what you shipped.

Migration backups are our pre-upgrade escape hatch. When we migrate projects from older template generations (v1 → v3, nativewind-themed → fullstack-supabase), the file set is zipped first and recorded in project_migration_backups with the component_base_before and version_before metadata. An admin_restore_project_files SQL function atomically restores the entire snapshot in a single transaction, resets the template version, and clears the preview bundle. If a migration ever corrupts a project, the undo is one function call.

The template is the ultimate root

Every project in RapidNative starts from a template — fullstack-supabase, nativewind-themed, fullstack-v2. The template directory under tools/project-templates/<name>/scaffold/ is the real "parent of parents." When a new project is created, we unzip its scaffold into the files table and the generation chat begins against that base.

This is why message replay works for revert even on projects that were never forked. The base is deterministic. Replaying turn 1 against the fullstack-supabase scaffold today produces the same result it produced three months ago, because the scaffold hasn't moved. For imported projects — Lovable conversions, uploaded zips — the base is instead the frozen zip in the internal-project-zips bucket. Same mechanic, different physical root.

Modern mobile app development workspace with multiple screens The template scaffold is the deterministic root of every project — forks branch from projects, projects branch from templates. — Photo by Carlos Muza on Unsplash

What we got wrong

Three things we'd do differently if we were starting over:

  1. We should have backfilled parent_project_id on day one. Projects forked before 2026-08-29 have NULL lineage. For BI, we now regex on " Copy" in project names to count historical forks — brittle, and anyone who renamed their copy escapes the count. If you're adding genealogy to an existing table, backfill from day one. The historical data is gone the moment the migration lands.

  2. We should have separated the editor VFS from the stored files sooner. For a while, the Lifo preview sandbox and the files table were updated by two slightly different code paths. When a copy fired mid-generation, the sandbox and the DB would momentarily disagree. The fix was to make the DB write the single source of truth and the sandbox hydrate from it. Easy to describe, two sprints to actually ship.

  3. We should have made copy_origin a Postgres enum, not a text column. The CHECK constraint catches invalid values at write time, but a real enum would catch them at compile time. We've already needed to add new copy origins once (admin_duplicate), and each addition is a migration plus a check constraint rewrite. Lesson learned for the next lineage-shaped column.

Why this matters for building with AI

AI code generation changes what versioning means. The useful unit of history isn't a diff — it's an intent. The useful unit of ownership isn't a repo — it's a project tree with explicit copy semantics. And the useful default for genealogy isn't CASCADE — it's SET NULL, because a forked project is somebody else's work the moment the fork completes.

If you're building AI-generated apps on RapidNative, the net effect of all of this is simple: every chat turn is undoable, every public project is forkable, every deployment is frozen, every upgrade is reversible. The architecture holding that up is three tables, two columns, and one ON DELETE clause that would have destroyed user work if we'd defaulted it the obvious way. AI app versioning is a schema problem dressed up as a UX problem — get the schema right, and the UX follows.

Want to see it in action? Start a new project, or browse the public project showcase and remix one. You'll get your own copy, your own chat history, and your own lineage pointer, all backed by exactly the architecture above.

FAQ

What does it mean to fork an AI-generated app? Forking an AI-generated app means creating an independent copy of someone else's project that you can edit, re-prompt, and build on without affecting the original. In RapidNative, forking (we call it "remixing") copies the project files but not the original owner's chat history or binary assets, giving you a clean starting point tied back to the source via a parent_project_id lineage pointer.

How does AI app versioning differ from Git version control? AI app versioning tracks intents (chat turns) instead of diffs. Every AI turn is an append-only message, and the current file state is recomputed by replaying messages against a deterministic template base. Git stores every file state; AI app versioning stores only the current state plus the conversation that produced it. The trade-off is cheaper storage but no random-access version jumps.

Can you roll back a project after the AI generates something you don't want? Yes. RapidNative lets you revert to any previous chat turn, and the file tree is reconstructed by replaying turns from the template base. Deployed versions are frozen separately as snapshot zips, so rollback in the editor never touches your production app. See our earlier post on in-project version history for the message replay mechanics.

What happens to forked projects if the original is deleted? The forks survive. RapidNative uses ON DELETE SET NULL on the parent_project_id foreign key, which means deleting a source project only clears the lineage pointer on its descendants — it never deletes the descendant projects. This matters especially for popular public templates with hundreds of forks, where cascading deletes would destroy work owned by hundreds of different users.

Start now

Ready to build your app?

Turn your idea into a production-ready React Native app in minutes.

Free tools to get you started

Questions

Frequently asked questions

What is RapidNative?

RapidNative is an AI-powered mobile app builder. Describe the app you want in plain English and RapidNative generates real, production-ready React Native screens you can preview, edit, and publish to the App Store or Google Play.

Can I export the code?

Yes. RapidNative generates clean React Native and Expo code that you can export at any time. No lock-in, no proprietary format. Hand it to your developers or keep building inside RapidNative.

Is RapidNative free to use?

Yes. You can build apps on the free plan with no credit card required. Paid plans unlock unlimited AI generations, code export, and direct publishing to the App Store and Google Play.

Do I need to know how to code?

No. Most users build apps by describing what they want in plain English. Developers can drop into the code whenever they want more control, but coding is optional.

How long does it take to build an app?

Most users have a working first screen in under a minute. A full MVP usually takes a few hours instead of the weeks or months traditional development requires.