The easiest part of forking a production SaaS is copying the repository. The dangerous part is assuming that a successful build means the new product has a clean foundation.
I learned this while turning an existing AI generation application into a second, image-first product. Authentication still worked. Uploads still worked. Credits, asynchronous tasks, provider submission, polling, and result storage were already there.
So were dozens of decisions that belonged to the first product.
The route tree described the old user journey. Navigation emphasized the old primary workflow. Shared-looking components imported page-specific data. Emails, examples, metadata, and tests quietly assumed that the first product's identity was still correct.
A fork preserves implementation and intent at the same time. Before adding features, you need to separate them.
This article presents the audit process I would use before building on any forked SaaS codebase.

A. Write the New Product Contract First
Do not begin by deciding which folders to keep. Begin by describing what the new product must mean to a user.
Write down:
- the primary task;
- the first public entry point;
- the secondary workflows;
- the inputs each workflow accepts;
- the output users expect;
- the capabilities that must remain operational;
- the claims the new product must not inherit.
For my image-first product, the smallest public contract was:
| Entry | Primary task | Accepted starting point |
| Homepage | Edit an existing image | Uploaded image plus prompt |
/image | Generate or transform an image | Text or reference image |
/video | Generate a video | Text or image |
This table did more architectural work than a folder diagram. It made the product priority explicit and gave every inherited route something to be compared against.
Without a new contract, the fork will default to the old product's choices simply because they already exist.
B. Inventory the Public Surface Before the Shared Code
Developers often start an audit in lib, services, or components. I start with the public surface because routes reveal product ownership faster.
The first pass can be simple:
rg --files src/routes
rg --files src/pages
rg --files src/components
rg --files src/data
rg --files public
Adjust the paths for the framework, but keep the purpose of the pass: identify everything that teaches a user what the application is.
For each route, record its direct dependencies:
- page blocks;
- route-specific data;
- messages and metadata;
- media assets;
- structured data;
- tests;
- navigation links;
- sitemap and discovery entries.
Treat that collection as one vertical slice. A route is rarely just one file.
This prevents a common cleanup failure: deleting the page component while leaving its datasets, translations, tests, links, and assets behind. Those leftovers keep the old product alive as dead weight and make future searches misleading.
C. Search for Identity Leaks
The next pass looks for product identity outside the obvious brand configuration.
Search for the old:
- product name;
- domain;
- email addresses;
- CDN host;
- storage prefixes;
- database names;
- route names;
- model labels;
- marketing phrases;
- default workflow values.
Example:
rg -n -i 'old-product|old-domain\.com|support@old-domain\.com' .
rg -n 'video-model|image-to-video|old-workflow' src tests messages public
Do not blindly replace every result. Classify it.
| Match type | Question | Typical action |
| Infrastructure identity | Does this select an environment or resource? | Move to the correct configuration source |
| Public product identity | Does a user see or receive it? | Rewrite for the new product |
| Historical test fixture | Does it describe a still-valid contract? | Keep only if the contract remains true |
| Provider identifier | Is it internal and required by an adapter? | Keep server-side and prevent client leakage |
| Obsolete feature reference | Does the new product still own this workflow? | Remove the entire vertical slice |
The goal is not a zero-result search at any cost. The goal is to make every remaining result intentional.
D. Delete in Vertical Slices
Once the inventory is clear, remove irrelevant product slices before designing new abstractions.
For one old route, delete or update all of its connected pieces together:
route
├── page block
├── route data
├── messages
├── metadata
├── assets
├── tests
├── navigation link
└── sitemap entry
Then run focused checks.
rg -n 'removed-route|RemovedPage|removedMessageKey' src tests messages public
git diff --check
Use the project's real typecheck and focused test commands after confirming they do not start a browser, development server, or production build unexpectedly.
Deleting vertical slices first has an architectural benefit: it reduces the number of real consumers. An abstraction that appeared necessary with eight inherited pages may become unnecessary when only two valid consumers remain.
If you generalize first, you risk preserving requirements that the new product does not have.

E. Follow Failures Inward
Deletion will break things. That is useful.
Each failure tells you something about ownership:
- A shared component fails because it imports route data.
- A global navigation test fails because it assumes an old product page exists.
- A generic generation control fails because its options come from a page-specific list.
- A server module fails because a client-facing model name leaked into provider logic.
Do not repair these failures by immediately restoring the removed file or adding an optional flag.
Ask instead:
- Was the deleted dependency a real capability?
- Which layer should own the surviving fact?
- Does more than one current consumer need it?
- Can the dependency direction be reversed?
For example, a shared generation workspace should consume a model capability contract. It should not import the model list from a particular landing page. The landing page can select from the shared contract, but the shared contract should not know that the page exists.
This is where compile errors become more than errors. They expose the direction of dependency.
F. Trace One Complete Request
After removing the old public surface, trace one real operation through the system.
For an AI generation product, the path might be:
User input
↓
Public model selection
↓
Effective parameters
↓
Credit calculation
↓
Authenticated API request
↓
Task creation
↓
Provider adapter
↓
Polling or webhook update
↓
Result persistence
↓
History and result UI
At each step, ask whether the implementation depends on a stable capability or on the old product's presentation.
Stable capabilities often include:
- access checks;
- uploads;
- credit decisions;
- task states;
- provider translation;
- result storage;
- failure recovery.
Product-owned decisions often include:
- which models are promoted;
- which controls appear;
- page defaults;
- examples and prompts;
- navigation;
- SEO copy;
- visual hierarchy.
The same effective parameters must drive the visible controls, credit estimate, and server request. If those three derive from different sources, the fork has inherited a drift problem even if each path works independently.
G. Separate Similar Lifecycles Without Erasing Domain Differences
The original application already had a structured video runner and provider adapters. The image path needed a similarly explicit lifecycle before more image models and tools were added.
That did not mean combining image and video into one giant controller.
A useful boundary can look like this:
interface TaskAdapter<Request, ProviderTask, Result> {
submit(request: Request): Promise<ProviderTask>;
getStatus(task: ProviderTask): Promise<"pending" | "complete" | "failed">;
getResult(task: ProviderTask): Promise<Result>;
}
interface TaskRunner<Request, Result> {
run(request: Request): Promise<Result>;
}
This pseudocode describes the direction, not a required implementation.
Image and video can share task lifecycle behavior while retaining separate input types, capability rules, controllers, and workspaces. Symmetry is valuable where it prevents operational drift. It is harmful when it hides meaningful domain differences.
I applied this distinction while building PicVane, the image-first product that emerged from the audit. Its image editor, image generator, and video generator can present different workflows while relying on consistent task, credit, upload, history, and result behavior underneath.
H. Require Evidence Before Building a Multi-Product Framework
Two related products do not automatically justify a multi-brand runtime.
Before adding brand factories, route registries, deployment matrices, or universal page schemas, look for evidence such as:
- repeated changes that must be ported in both directions;
- the same deployment and database;
- the same release cadence;
- the same operational ownership;
- three or more real consumers with stable shared requirements;
- measurable maintenance cost caused by separate implementations.
Without that evidence, separate repositories plus narrow, explicit capability contracts may be cheaper.
The test is not whether two codebases contain similar files. The test is whether they must make the same decision for the same reason.
I. Verify Four Different Layers
A fork audit is not complete when the typecheck passes. Record what each validation layer actually proves.
| Layer | What it can prove | What it cannot prove |
| Source | Imports, types, route registration, static contracts | Browser behavior or provider success |
| Focused tests | Specific ownership and lifecycle rules | Complete end-to-end behavior |
| Runtime | Pages load and interactions execute | Real external service outcomes unless tested |
| Product evidence | Users understand and use the new direction | Architectural correctness by itself |
Keep these claims separate.
A clean source audit does not prove that provider calls, payments, emails, analytics, or generated results work in production. A successful runtime test does not prove that users want the product. A clean architecture makes learning safer and faster; it does not replace the learning.
Final Audit Checklist
Before adding major features to a forked SaaS, confirm that you can answer yes to these questions:
- Is the new product's primary task written down?
- Has every inherited public route been accepted or rejected intentionally?
- Are old brand, domain, email, storage, and workflow references classified?
- Were obsolete features removed as complete vertical slices?
- Do shared modules avoid importing product-owned page data?
- Does one capability source drive UI options, pricing, and server requests?
- Are provider identifiers and secrets kept behind server boundaries?
- Are different domains allowed to keep different types and controllers?
- Is every new abstraction supported by current consumers rather than hypothetical ones?
- Are source, test, runtime, provider, and market claims reported separately?
If several answers are no, adding features will make the inherited assumptions harder to remove.
The best time to audit a fork is before the new product looks busy. At that stage, deletion is still cheap, boundaries are still negotiable, and a smaller route tree can reveal what the system actually knows how to do.
Reuse the operational knowledge.
Re-earn the product decisions.
Disclosure: This article is based on my own implementation experience. AI was used to help organize the structure and edit the English draft; I reviewed the technical claims and final text.