Library · Architecture that lasts

Portfolio detachability: projects you can unplug

Engineer65 minUpdated: October 2026
99 of 105 in the library

Time: about 25 min of theory + 40 min of practice


The gist

Most founders make one quiet mistake early on: they build a "platform" where all their projects are tangled together. On paper it looks like a saving: one repository, one database, one config. In practice, a year later it turns into a rat's nest of wires where you can't pull a single cable without shutting off power to the whole house.

Then the moment comes: project X needs to be sold. Or shut down. Or handed over to a partner's team. And it turns out you can't, because project X's Worker reads a secret that belongs to project Y, the brand voice lives in the platform's IDENTITY.md, and half of project X's production tables are a shared schema with everything else.

Portfolio detachability is an engineering principle that protects you from this trap from day one. The idea is simple: the platform is separate, the projects are separate. The platform is long-lived (10-50 years). Projects come and go. Removing one project should not break the platform, the other projects, or the project being removed (its new owner should get something that works).

In this lesson we'll look at how to design for this, how to test detachability on an ongoing basis, and the 4 red lines you should never cross.

🎨 Picture this: an apartment building vs. single-family houses on the same street. In the apartment building, your bathroom shares a riser with your neighbor's: someone moves out and everyone floods. In a house, the bathroom is yours, with your own sewer line and your own wiring. From the outside the difference is small: the pipe is a little longer. Over ten years, it's the difference between "I sold the house" and "I can't sell until the whole street is replanned."


Key concepts

  • Portfolio Pattern: an architectural model where the platform is the infrastructure layer (engineering charter + shared services) and the projects are independent startups (their own brand, their own audience, their own economics)
  • Detachability: a property of a project: it can be cut out of the platform in a reasonable amount of time (hours, not weeks) without breaking the platform and without the project itself losing the ability to run
  • Namespace: an isolated space for data and code with its own access rules. In the Portfolio Pattern there are usually 4: CORE / SHARED / PROJECT-OWNED / PRIVATE
  • Coupling: how much one part of a system depends on another. For projects: hard coupling (you can't remove it without breaking something) vs. loose coupling (you can remove it and the rest keeps working)
  • Anti-coupling rules: formal restrictions that keep coupling from appearing (for example: project-specific data is not allowed in CORE)
  • Strangler Fig pattern: a pattern for breaking coupling gradually: new code is written the right way, and old code moves into the right namespace one component at a time
  • Detachment artifact: the set of files and instructions handed to a new owner when a project is sold or archived (code, brand, customer base, runbook, secrets handoff)
  • Sale-readiness: a formal state of a project: detachable + documented + secrets isolated + an independent build passes

Theory

Where the problem comes from

Coupling rarely shows up as a single decision. It's dozens of small "this is faster" choices that pile up.

A typical story:

  • Month 1. We launch the platform. There's one project: a real estate agency. We write the brand voice into the platform's IDENTITY.md. "We'll move it out later, this is fine for now."
  • Month 4. A second project appears: an online school. We create shared/brand-voice.ts. But half the lines in it are specific to the real estate agency ("a calm advisor, not a salesperson"). The school inherits it and overrides things. It works.
  • Month 9. A third project: a subscription service. It has its own audience. But it uses the same shared/auth.ts, which reads the Cloudflare KV namespace REALTY_USERS. All the users sit in one table. "We'll split it later."
  • Month 14. A buyer shows up for the real estate agency, ready to pay serious money. They ask: what am I buying, a folder or the whole platform? You realize there's no way to separate it in a week. The buyer walks away.

Each individual decision was reasonable. Added together, they're a disaster.

🎨 Picture this: two kinds of trees with intertwined roots. Each little root on its own is thin. All together, you can only dig up one tree by pulling out the other one with it.


Portfolio Pattern: a startup studio and independent startups

The cleanest model is a startup studio (a new-generation incubator).

🎨 Picture this: a startup studio is a company that launches several businesses at once. The studio provides shared infrastructure: legal, DevOps, accounting, methodology. Each startup in the portfolio is an independent business: its own brand, its own audience, its own P&L. A startup can be sold to a strategic investor, shut down, or scaled by a separate team, and the studio keeps running and launches the next one.

Level Analogy In an AI startup platform What lives there
Platform Core The studio's charter and rules (for everyone) CORE CLAUDE.md, IDENTITY.md, VISION.md, context separation rules, engineering charter
Platform Services The studio's shared infrastructure (DevOps, auth, legal) SHARED departments/security/, departments/finance/, tools/
Portfolio Project An independent startup in the portfolio PROJECT-OWNED projects/realty/, projects/school/
Founder's Vault The founder's personal safe PRIVATE the owner's personal folder, secrets, keys

Rules of the Portfolio Pattern:

  1. The platform doesn't change for the sake of one startup. When YC backed Airbnb, YC didn't rewrite its charter around Airbnb. When your platform launches a new product, the platform's IDENTITY.md stays the same.

  2. Startups are autonomous. Each one has its own brand, its own customer base, its own CLAUDE.md, its own database (or at least its own namespace in a shared one). A startup makes its own product decisions within the platform's architectural boundaries.

  3. Startups can leave the portfolio. A sale, a shutdown, a handoff to a team: the architecture has to survive any exit. If one startup is sold, the platform and the other startups keep working without a single change.

  4. Platform services are shared. Auth, monitoring, security apply to every startup. That's normal; that's the whole point of a platform. But services must not contain project-specific logic. They're universal by design.


The 4 namespaces in detail

CORE (federal level, the base rules)

What lives here: CLAUDE.md, IDENTITY.md, VISION.md, MEMORY.md, context separation rules, security rules, the skills catalog, strategy/engineering-charter.md.

Rules:

  • Changes only through a formal amendment to the charter (with a cooling-off period, for example 7 days)
  • Contains nothing project-specific
  • Contains no project names in business logic (names in examples are OK)
  • Universal principles, vetoes, the platform's identity

CORE cleanliness test: open IDENTITY.md and search for project names (for example, "realty", "school"). If you find one outside a "sample projects" section, that's a leak. Fix it.

SHARED (federal services)

What lives here: departments/, tools/, infrastructure/ (if used by several projects), .claude/agents/ (shared subagents), shared skills.

Rules:

  • Used by at least 2 projects at the same time
  • Universal by design (interface-first, not "here's code built for the real estate agency, everyone else adapts")
  • A change requires an impact assessment: which projects will it affect?

Test: if the code is used by only one project, it doesn't belong in SHARED. Move it to projects/<X>/.

PROJECT-OWNED (state level)

What lives here: the entire projects/<name>/ folder.

Usually inside:

  • CLAUDE.md (project context, references CORE via @~/platform/CLAUDE.md)
  • BRAND-VOICE.md (the brand of this project only)
  • code/ or app/ (source code)
  • customers/ or db/ (isolated data)
  • deployments/ (its own wrangler.toml, its own Cloudflare secrets with the project prefix)
  • decisions/ (the project's decision log)
  • HANDOFF.md (what a new owner needs to know)

Rules:

  • Everything specific to the project lives here
  • Doesn't depend on other projects/<Y>/ folders
  • Can depend on CORE and SHARED, but through documented interfaces

PRIVATE (personal)

What lives here: the owner's personal folder, an encrypted file with keys, hardware key settings, a letter to a successor.

Rules:

  • Never handed over when a project is sold
  • Never overlaps with PROJECT-OWNED
  • Access is for the platform owner only, with a hardware key

The detachability test (the main tool)

One question tells you how healthy the architecture is:

If I delete the whole projects/X/ folder tomorrow, what breaks?

Possible answers and what they mean:

What broke Diagnosis What to do
Nothing. The platform works, the other projects work, the other projects deploy ✅ Detachable. The project is ready to be sold Document the handoff
The platform build failed (build error in shared code) ❌ Hard coupling in SHARED Move project-specific code from SHARED to PROJECT-OWNED
Another project Y failed because it imports from projects/X/ ❌ Cross-project coupling Move the common code to SHARED or duplicate it
Other projects' Cloudflare Workers broke ❌ Shared infrastructure without boundaries Isolate namespaces, separate KV / D1 per project
The test runner passes, but production fails an hour later ❌ Hidden runtime coupling (cron, queue, webhook) Audit all async dependencies
The platform's IDENTITY.md or CLAUDE.md now has broken links to the deleted project ❌ Project-specific data in CORE Clean up CORE, move things to projects/<X>/

The principle: you run the test not once, but on every release. Ideally automated (a CI step: simulate removing the project in a throwaway branch and run the other builds).

🎨 Picture this: Jenga. Every time you add a block, you check that the whole tower is stable. In platform architecture this ritual is free and mandatory. Otherwise, a year later, the tower is "standing, but you can't pull anything out."


4 red lines (anti-coupling rules)

These are formal prohibitions. Not "best practices," but red lines, as in security. Breaking them creates technical debt that grows exponentially.

Red line 1: Don't put project-specific data in CORE

❌ Not allowed:

  • The bio of the real estate agency's brand face in the platform's IDENTITY.md
  • The online school's competitor list in a shared COMPETITORS-PLAYBOOK.md in CORE
  • The real estate agency's brand voice in the platform's CLAUDE.md
  • A project's customer personas in the platform's MEMORY.md

✅ The right way:

  • Project-specific data → projects/<X>/
  • CORE holds only universal principles and the identity of the platform itself

Red line 2: Don't change CORE while working on a project

If a session is declared as "we're working on the real estate agency," you don't touch the platform's overall plan, IDENTITY.md, VISION.md, or the owner's personal folder. Even if you "noticed a typo along the way." Especially if you "noticed a typo along the way": that's the most common leak channel.

If the typo matters, it gets a separate "we're working on the platform" session, a separate commit, a separate journal entry.

Red line 3: Don't duplicate data across namespaces

One source of truth per data type.

❌ Not allowed:

  • Brand rules both in the platform owner's personal rules and in projects/realty/BRAND-VOICE.md
  • Project decisions both in the platform's journals/decisions.md and in projects/<X>/decisions/
  • A customer list in both SHARED and PROJECT-OWNED

✅ The right way:

  • One file, one source of truth
  • In the project folder, a reference @~/platform/IDENTITY.md if you need CORE context

Red line 4: Don't mix up the platform owner with project personas

❌ Not allowed:

  • Applying a project's brand style to the platform owner
  • Applying a project persona's bio (for example, the real estate agency's brand face) to another project's content (for example, the online school)
  • Using one project's voice in platform communications

✅ The right way:

  • The platform owner is the keeper of the system, not a product
  • Each project persona is its own identity inside projects/<X>/
  • The online school (if it launches) is yet another separate persona

Sale scenario walkthrough

Say a buyer comes to you for the realty project, ready to pay a serious amount. What do you hand over?

Handed over (PROJECT-OWNED):

  • The entire projects/realty/ folder
  • The project's codebase
  • The brand: BRAND-VOICE.md, assets, logo
  • The customer base (with a privacy-law-compliant transfer, such as GDPR where it applies, or with pseudonymization if the contract requires it)
  • The domain (realty-example.com, if it's registered to the project)
  • The project's Cloudflare account (if isolated) OR a migration plan to the buyer's new account
  • Bot tokens (for example, for messaging apps) and API keys specific to the project (through a secure handoff, not email)
  • HANDOFF.md, the runbook for the new owner
  • The decisions/ decision log

Stays (CORE + SHARED + PRIVATE):

  • The whole platform
  • All the other projects
  • The platform owner's personal rules and data
  • Engineering Charter, IDENTITY, VISION
  • The encrypted key file, the hardware key

What must be spelled out in the contract:

  • The deadline for migrating secrets (usually 7-14 days)
  • The deadline for transferring the domain (DNS propagation takes 48-72 hours)
  • What the buyer does NOT get: the platform's brand, the methodology, the code in SHARED (which the project only used)
  • A non-compete for the transition period (if applicable)
  • Support for the buyer during the first month (hours, format)

What has to happen technically in one day:

  1. A throwaway branch sale/realty-detach
  2. Delete projects/realty/ from the main branch (keep the git history; it shows the project existed)
  3. Run the builds of all the other projects: they must pass
  4. Run the platform tests: they must pass
  5. If something fails, there's hidden coupling. The deal is paused until it's fixed.

🎨 Picture this: renting an apartment. If you can't move out within a week, taking your stuff and leaving the place ready for the next tenant, you were living the wrong way. You hoarded junk in the shared storage rooms and parked your washing machine in the hallway. The Portfolio Pattern from day one is the habit of living so that moving out is always possible.


Strangler Fig pattern: breaking coupling gradually

What if everything is already tangled and the detachability test shows a disaster?

Don't do a big-bang refactoring. Strangler Fig is a pattern for breaking things apart gradually.

The idea: leave the old coupling alone, but write new code the right way. Components gradually move into the right namespaces, and the old ones die off.

Step by step:

  1. Freeze the coupling. Add a hook to CI: "new files in shared/ can't contain project names." Don't let the mess grow any further.
  2. Catalog what exists. One document: coupling-inventory.md. What's tangled where, severity, owner.
  3. Attack by priority. The most dangerous coupling is the kind that blocks a sale. Less dangerous is the kind that annoys development.
  4. One coupling, one PR. Not "cleaned up half the system in a week." Clean up one spot at a time, with regression tests at every step.
  5. Run the detachability test after every PR. Is the metric moving? Is the project any closer to detachable? If not, something is wrong with the approach.

A realistic pace: one significant coupling broken per week. In six months, the system moves to a clean state.


Anti-patterns (in detail)

Monorepo coupling

Symptom: all projects in one npm/cargo/Python workspace, a shared package.json with project-specific dependencies.

Why it's bad: when a project is sold, the buyer either gets the whole monorepo (90% of which they don't need), or you spend a month untangling dependencies.

The fix: per-project workspaces (projects/<X>/package.json) + a minimal shared core with an explicit interface.

A shared database without boundaries

Symptom: one Cloudflare D1 database, all projects write to the same tables, separated by a project_id column.

Why it's bad: when a project is sold, you have to export the rows where project_id = X, clean up foreign keys, and check that nothing broke for everyone else. All of it under production load.

The fix: a namespace per project. In Cloudflare, a separate D1 / KV namespace per project. In Postgres, a separate schema or a separate database. The added cost is minimal; the gain in detachability is huge.

Project-specific config in platform-wide files

Symptom: the platform's wrangler.toml lists routes for every project. The platform's .env holds secrets for every project, with prefixes.

Why it's bad: a project's new owner can't just copy the config; it contains other people's secrets and routes.

The fix: one wrangler.toml per project. One .env.<project> per project. At the platform level, only the infrastructure that serves everyone (monitoring, backups).

A "temporary" project name in CORE

Symptom: "for now the platform = the real estate agency, we'll split it later." A year later, the platform's IDENTITY.md talks about selling apartments.

Why it's bad: when the second project appears, that line becomes false. You'll have to fix it when you already have three projects and ten tangles. It's cheaper to never write project-specific lines in CORE in the first place.

The fix: when in doubt, write it in projects/<X>/. Promote it to CORE once you see the pattern in 2+ projects. This is the "rule of three" applied to namespace promotion.


🧪 Practice

Step 1: Audit the namespaces of your current project (15 minutes)

Open your project (the one you're working on right now) and take inventory. Create a coupling-inventory.md file in the project root:

Type this into the chat
# Coupling Inventory — <project-name>

**Date:** YYYY-MM-DD
**Goal:** figure out what lives in which namespace and where the coupling is

## CORE level (platform)
Files that mention the NAME OF THE CURRENT PROJECT (that's a leak):
- [ ] CLAUDE.md — is it mentioned?
- [ ] IDENTITY.md — is it mentioned?
- [ ] VISION.md — is it mentioned?
- [ ] MEMORY.md — is it mentioned?

## SHARED level
Files in shared/ or tools/ that are used ONLY by this project:
- [ ] list

## PROJECT-OWNED
What the project has of its own:
- [ ] BRAND-VOICE.md
- [ ] Project CLAUDE.md
- [ ] Isolated wrangler.toml
- [ ] Isolated D1/KV namespace
- [ ] Isolated secrets with the project prefix

## Cross-project dependencies
Imports from other projects/<Y>/ in the current project:
- [ ] list

## Hidden coupling
- [ ] Cron / queue / webhook that touch several projects
- [ ] Shared environment variables
- [ ] Shared webhooks

Fill it in honestly. Not "well, I don't think so," but grep for the names:

bash
# In the platform root
PROJECT_NAME="realty"  # put in your own

# Where the project name is mentioned outside its own folder
grep -r "${PROJECT_NAME}" . \
  --include="*.md" --include="*.ts" --include="*.js" --include="*.toml" \
  --exclude-dir="projects/${PROJECT_NAME}" \
  --exclude-dir="node_modules" \
  --exclude-dir=".git" \
  --exclude-dir="_archive"

Every match is a potential leak. Go through them one by one.


Step 2: The detachability test (10 minutes)

We simulate removing the project without actually removing it.

bash
# Create a throwaway branch
git checkout -b detachability-test/$(date +%Y%m%d)

# "Delete" the project
PROJECT_NAME="realty"
git rm -r projects/${PROJECT_NAME}/

# Run the builds of the other projects
for project in projects/*/; do
  name=$(basename "$project")
  echo "Building: $name"
  cd "$project"
  # Put in your own build command
  npm run build 2>&1 | tail -5
  cd - > /dev/null
done

# Run the platform tests
npm test 2>&1 | tail -10

# Run the platform lint / type check
npm run lint 2>&1 | tail -5

What should happen:

  • All the other projects' builds pass
  • The platform tests pass
  • Lint is clean
  • Not a single broken import

If something fails, that's your coupling. Write it down in coupling-inventory.md under "Hidden coupling."

Most important: after the test, run git checkout main && git branch -D detachability-test/.... This was a simulation; nothing was actually deleted.


Step 3: Clean up one coupling (15 minutes)

Pick one coupling you found, the cheapest one to fix. Apply Strangler Fig to it.

Example: shared/brand-voice.ts contains lines specific to the real estate agency.

typescript
// BEFORE — shared/brand-voice.ts
export const brandVoice = {
  tone: "a calm advisor, not a salesperson",  // specific to the real estate agency!
  avoidPhrases: [
    "I'm an expert",  // this project's rule
    "the best prices in town",  // this project's rule
    "buy before it's gone"  // this project's rule
  ]
}

Step 1. Move the project-specific part into the project:

typescript
// BECOMES — projects/realty/brand-voice.ts
import { BaseBrandVoice } from "@platform/shared/brand-voice"

export const realtyVoice: BaseBrandVoice = {
  tone: "a calm advisor, not a salesperson",
  avoidPhrases: [
    "I'm an expert",
    "the best prices in town",
    "buy before it's gone"
  ]
}

Step 2. SHARED becomes a universal interface:

typescript
// BECOMES — shared/brand-voice.ts
export interface BaseBrandVoice {
  tone: string
  avoidPhrases: string[]
  // ... other universal fields
}

export function validateContent(content: string, voice: BaseBrandVoice): ValidationResult {
  // universal validation logic
}

Step 3. Regression test:

bash
# Should pass just like before
npm test
# The detachability test should show progress
git checkout -b detachability-test/iter2
git rm -r projects/realty/
npm test  # now shared/brand-voice.ts passes without the real estate project's lines
git checkout main && git branch -D detachability-test/iter2

One coupling broken. Record it in decisions:

markdown
## YYYY-MM-DD: Broke the shared/brand-voice → realty coupling

**Context:** shared/brand-voice.ts contained project-specific lines
**Decision:** moved the interface into shared, the implementation into projects/realty/
**Impact:** detachability of the realty project improved
**Tag:** detachability-iter1

Step 4: Document HANDOFF.md (optional, 15 minutes)

Every PROJECT-OWNED folder should have a HANDOFF.md at its root. It's a document for a hypothetical new owner. Write it as if tomorrow you were handing the project to a stranger.

A minimal template:

markdown
# HANDOFF — <Project Name>

## What this is
1-2 paragraphs. What the project is, what problem it solves, who the audience is.

## Stack
- Frontend: ...
- Backend: ...
- Hosting: ...
- Database: ...
- Payments: ...

## How to run it locally
```bash
cd projects/<name>
npm install
cp .env.example .env  # fill in the secrets — see below
npm run dev
```

## Secrets (where to get them)
- `STRIPE_SECRET_KEY` — Stripe dashboard
- `CLOUDFLARE_API_TOKEN` — Cloudflare → My Profile → API Tokens
- ... (no values! only instructions on where to get them)

## Deploy
1 paragraph. What to do to push it to production.

## Customer base
Where it lives, how to export it, whether the structure is privacy-law compliant (e.g., GDPR).

## Domain & DNS
Where it's registered, whose account it's on, how to transfer it.

## Known risks
3-5 things to watch out for (deprecated APIs, a certificate expiring soon, tech debt in module X).

## Previous owner's contact
Email / support hours during the first month.

This document is the main detachability artifact. If you can write an honest HANDOFF.md in an hour, the project is ready to be sold. If you can't, there's hidden knowledge you need to get out of your head and into a file.


⚠️ Anti-patterns

❌ "We'll split it later": it never happens. Coupling that wasn't broken at the start only gets broken under pressure (a sale, burnout, a conflict with a partner). At that point it costs 10 times more.

❌ One Cloudflare account for all projects with no isolation: when you sell one project, you have to migrate it to a new account. If all the Workers / KV / D1 are in one account without project prefixes, the migration takes weeks.

❌ Project-specific routes in the platform's wrangler.toml: each project should have its own wrangler.toml. One file describing the routes of every project turns into a tangle you can't separate.

❌ The customer base in a shared table with a project_id column: in theory you can export the rows where project_id = X. In practice there are foreign keys to shared tables and JSON columns with references to platform entities, and the migration takes a sprint.

❌ The platform's brand and the project's brand mixed together: if the new owner of the real estate agency gets a BRAND-VOICE.md where half the rules are about the platform itself, they can't work with it without rewriting it.

❌ Moving coupling into _archive/ instead of breaking it: a common mistake. "The old code moved to the archive, now it's clean." But the imports from the main code into the archive are still there. That's not breaking coupling; it's just shuffling it around.

❌ Platform journals contain project decisions: the platform's journals/decisions.md should contain only CORE decisions. Project decisions go in projects/<X>/decisions/. Otherwise, when the project is sold, the new owner won't get the context behind the decisions that were made.

❌ The hardware key or master passphrase sits in a project folder: PRIVATE always stays PRIVATE. If anything secret has made its way into projects/<X>/, that's a violation and a potential leak during a sale.



✅ Checkpoint

Before moving on, go through the list. Every item should be an honest "yes."

If there are gaps, go back to the theory. This isn't a lesson to skim. Coupling you don't stop now will be limiting your freedom of action a year from now.


Sources

  • Martin Fowler — Strangler Fig Pattern (martinfowler.com)
  • Sam Newman — "Monolith to Microservices" — the chapters on database decomposition
  • AWS Well-Architected Framework — Reliability Pillar, fault isolation principles
  • Y Combinator — Startup Studio model, portfolio company independence
  • Cloudflare — Namespace isolation best practices (Workers KV, D1, R2)

More on this topic → Production Observability: what to measure and where to look when something goes wrong.

The mark stays in this browser only and is never sent anywhere. My progress