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.
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 namespaceREALTY_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.
Portfolio Pattern: a startup studio and independent startups
The cleanest model is a startup studio (a new-generation incubator).
| 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:
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.mdstays the same.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.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.
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/orapp/(source code)customers/ordb/(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).
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.mdin 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.mdand inprojects/<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.mdif 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:
- A throwaway branch
sale/realty-detach - Delete
projects/realty/from the main branch (keep the git history; it shows the project existed) - Run the builds of all the other projects: they must pass
- Run the platform tests: they must pass
- If something fails, there's hidden coupling. The deal is paused until it's fixed.
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:
- 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. - Catalog what exists. One document:
coupling-inventory.md. What's tangled where, severity, owner. - Attack by priority. The most dangerous coupling is the kind that blocks a sale. Less dangerous is the kind that annoys development.
- 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.
- 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:
# 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:
# 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.
# 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 -5What 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.
// 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:
// 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:
// 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:
# 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/iter2One coupling broken. Record it in decisions:
## 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-iter1Step 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:
# 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.
🔗 Related
- CLAUDE.md: your project's system prompt: how to separate platform context from project context
- Folder philosophy: the PARA system: a folder structure that supports the Portfolio Pattern
- Security in Claude Code: .env and secrets: isolating secrets per project as the foundation of detachability
- Deployment: Cloudflare Workers: a separate wrangler.toml per project, namespace isolation
- 3-tier templates: solo, mid, corporate: the broader context of production patterns
✅ 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