AI Founder OS
Systems

SSOT + Invariants Flow

Not everything needs a config file or an API route on day one. This flow helps you decide what to centralize and when.

Decision flow: feature to SSOT to invariants to infraNeeds shared state?used by 2+ filesNew featureidea or requirementAdd to SSOTtheme / learnData / configDefine invariantsrules that must holdWire infraAPI / webhook / storageNo → build locally, skip SSOT

The decision pipeline

Every new feature passes through these gates. Most stop at step two.

SSOT feeding hotspots with a no drive-by edits warningSingle Sourceof TruthHotspotspage.tsxroute.tscomponent.tsxNo drive-by edits outside hotspots

SSOT feeds hotspots

One source, many consumers. Agents read from the SSOT. They never write to each other.

Decision checklist

Run through these five questions for every piece of shared state. If the answer is no at any point, stop. You do not need more infrastructure yet.

1

Is this value used by more than one file?

Yes

Move it to a central config or data file (SSOT).

No

Keep it local. You can extract later when a second consumer appears.

2

Could two agents edit this value independently?

Yes

It needs a single owner file with a clear import path.

No

Co-location is fine. One file, one owner.

3

Does this value have a shape contract (types, enums)?

Yes

Define the TypeScript interface next to the data. Export both.

No

Add a type now. Untyped shared state drifts within days.

4

If this value is wrong, does the build break?

Yes

That is an invariant. Add a typecheck or build-time assertion.

No

It is a preference, not a rule. Document it, do not enforce it.

5

Does this need a server round-trip (API, webhook, DB)?

Yes

Wire the infra. Define the route, guard it with auth, add error handling.

No

Keep it client-side or static. Do not add infra you do not need yet.

When each layer shows up

Four real examples showing when SSOT, invariants, and infra enter the picture. None of them arrive at the same time.

Auth (Clerk)

Module 0 — environment setup
SSOT
Clerk keys in .env.local, middleware in proxy.ts
Invariant
Every protected route calls auth() and redirects if no userId
Infra
ClerkProvider wraps the app. Webhook for metadata sync is optional until billing.

Auth is foundational infra. Set it up once, early, and do not revisit until you add a new auth surface.

Pro / Founding Member

After first deploy — when you need gating
SSOT
isFoundingMember lives in Clerk publicMetadata. One write path (webhook or admin grant).
Invariant
Dashboard checks metadata before rendering. No metadata means redirect to /pricing.
Infra
Stripe webhook writes metadata on checkout.session.completed. Admin endpoint as fallback.

Billing state should live in your auth provider, not a separate database. One source, two read paths.

Notifications

After core features ship — when users need to know things changed
SSOT
Notification preferences in user metadata or a simple preferences table.
Invariant
Never send a notification without checking the user opted in.
Infra
Email provider API route. Queue if volume grows. Start with direct sends.

Do not build notification infra before you have users who need notifications. Start with email, add push later.

Realtime (WebSocket / SSE)

Only when polling is visibly too slow for users
SSOT
Connection state managed in a single provider component.
Invariant
Reconnect on disconnect. Show stale-data indicator if the socket is down.
Infra
WebSocket server or SSE endpoint. Consider a managed service before self-hosting.

Realtime is expensive to build and maintain. Most apps do not need it until they have concurrent editing or live dashboards.

The rule is simple

If two files need the same value, move it to a single source. If a wrong value breaks the build, make it an invariant. If the value needs a server, wire the infra. If none of those are true, leave it where it is.