Skip to main content

When to Fork vs Wrap: Extending Boilerplate Functionality (2026)

·StarterPick Team
customizationforkboilerplatestrategy2026

TL;DR

Almost all boilerplate usage should be the "fork" approach — clone and own the code. True "wrapping" (using the boilerplate as a package dependency) is rare and only works for specific boilerplate architectures. The real question isn't fork vs wrap, but: how do you stay in sync with boilerplate updates without losing your customizations?

Key Takeaways

  • Fork (clone + modify): 95% of boilerplate usage
  • Wrap (npm package dependency): Only works for specifically designed boilerplates (Makerkit)
  • Rebase strategy: Keep a upstream remote to pull boilerplate updates
  • Branch strategy: Keep customizations on separate branches
  • The real risk: Not staying updated — security patches missed

Fork: The Default Approach

Most boilerplates are designed to be cloned and modified:

# Standard fork approach
git clone https://github.com/example/shipfast my-saas
cd my-saas
rm -rf .git           # Remove original git history
git init
git add .
git commit -m "Initial commit from ShipFast boilerplate"

From this point, your codebase is yours. You own it, modify it, and maintain it. No dependency on the original repository.

Advantages:

  • Full control over every line of code
  • No version conflicts
  • Can make any modification without restriction

Disadvantages:

  • You must manually apply boilerplate updates
  • Security patches require manual monitoring
  • If the boilerplate fixes a bug, you apply it yourself

Staying Updated with the Fork Approach

The key technique: keep the upstream as a separate remote

# Setup: keep track of the boilerplate's original repository
git remote add upstream https://github.com/example/shipfast.git
git fetch upstream

# When boilerplate releases an update:
git fetch upstream
git log upstream/main --oneline -5  # See what changed

# Cherry-pick specific commits you want
git cherry-pick abc1234  # Just the security patch

# Or merge selectively
git diff main upstream/main -- lib/stripe.ts  # See what changed in Stripe integration

This lets you pick and choose boilerplate updates without breaking your customizations.


Wrap: The Package Dependency Approach

A few boilerplates (notably Makerkit) are designed to work as package dependencies:

// Makerkit's plugin architecture enables true wrapping
// packages.json
{
  "dependencies": {
    "@makerkit/auth": "workspace:*",
    "@makerkit/billing": "workspace:*",
    "@makerkit/ui": "workspace:*"
  }
}

// You import features as packages — don't modify the source
import { AuthProvider } from '@makerkit/auth';
import { BillingProvider } from '@makerkit/billing';

// Your customizations live in YOUR code, not the boilerplate packages
export function AppLayout({ children }) {
  return (
    <AuthProvider config={yourAuthConfig}>
      <BillingProvider plans={yourPlans}>
        <YourCustomHeader />
        {children}
        <YourCustomFooter />
      </BillingProvider>
    </AuthProvider>
  );
}

When Makerkit releases an update: npm update @makerkit/auth — like any npm package update.

This only works when the boilerplate is designed for it. ShipFast, T3 Stack, and most others can't be used this way.


Customization Strategies Within the Fork

Once you've forked, you have several strategies for organizing customizations:

Strategy 1: Direct Modification (Most Common)

# Just modify files directly
# lib/stripe.ts — change the webhook handler
# components/auth/SignIn.tsx — customize the sign-in form

Simple, but makes boilerplate updates harder (more merge conflicts).

Strategy 2: Override Files

// Keep boilerplate code unchanged
// lib/stripe.original.ts — don't touch this

// Create your version that extends the original
// lib/stripe.ts — your customized version
import { originalWebhookHandler } from './stripe.original';

export async function webhookHandler(req: Request) {
  // Call original logic
  const result = await originalWebhookHandler(req);

  // Add your custom logic after
  await myCustomAnalytics.trackEvent('stripe_webhook', result);

  return result;
}

More verbose but makes your customizations explicit and easier to compare against updates.

Strategy 3: Feature Branches

# Keep the boilerplate on main
git checkout main  # = clean boilerplate

# Your customizations on a feature branch
git checkout -b product/saas-customizations
# All your changes here

# When updating:
git fetch upstream
git rebase upstream/main  # Rebase your customizations on latest boilerplate

Most disciplined approach — but requires good git hygiene.


The Anti-Pattern: Monolithic Customization

# Anti-pattern: Large uncommitted changes everywhere
git status
# 47 files modified, 3 files untracked

# When boilerplate releases update:
git pull upstream main
# 15 merge conflicts in 15 different files
# 4 hours resolving conflicts

If your customizations are scattered throughout the codebase without organization, updates become painful. The solution: commit frequently, in small chunks, with clear messages.


Practical Advice by Boilerplate

BoilerplateBest StrategyUpdate Method
ShipFastFork + direct modifyCherry-pick patches
T3 StackFork + extendRegenerate + merge
MakerkitFork (monorepo) OR wrap (packages)Package updates
SupastarterFork + service layer extensionsCherry-pick
Epic StackFork + extend modelsRebase

The Real Decision

The fork vs wrap question is mostly academic for most developers. The real decisions are:

  1. How often will I apply boilerplate updates? (Monthly? Security patches only?)
  2. Will I customize core infrastructure (auth, billing)? (High vs low customization)
  3. How many developers will work on the codebase? (Git strategy matters more for teams)

For solo founders: fork, modify freely, check for security patches monthly. For teams: fork, keep customizations organized, use git branches to separate boilerplate from product code.


Find boilerplates designed for easy customization on StarterPick.

Check out this boilerplate

View ShipFast on StarterPick →

Comments