Premise
Most software architecture advice predates the era in which AI writes the code. When humans were the bottleneck, we picked stacks for human ergonomics, organized code for human comprehension, and amortized development costs across many customers via SaaS. AI changes all three economics:
- The cost of building bespoke software collapses, because AI generates and maintains it.
- The cost of building bespoke frontends collapses faster than the cost of building correct backends, because UI generation is what AI does best and domain modeling is what AI does worst.
- The case for one-size-fits-all SaaS — already strained — disappears for any company willing to take ownership of its own operating system.
This document describes an architecture designed for that world: a “SaC” that encompasses every aspect of one specific business, contains exactly the features that business needs, and evolves continuously as the business does.
Core Principles
- Model first, schema second, screens last. The business logic — the model, the rules, the invariants, the algorithms — is the durable part. Schema serves the model. Screens serve the schema. Front-first development inverts this and produces systems that are sophisticated plumbing around an undefined center.
- Two layers with opposite engineering economics. The presentation layer is fast, disposable, and many. The skills layer is engineered, stable, and singular per domain. Iteration speed at the top must not contaminate engineering rigor at the bottom.
- Skills are autonomous and removable. Each skill owns its data, runtime, and contract. No skill reaches into another skill’s database. Cross-skill communication happens via published contracts and events. Removing a skill leaves no orphans.
- Polyglot where it matters, boring where it doesn’t. TypeScript by default. Python where ML lives. Rust/Go where compute demands it. The contract is the only commitment.
- Contracts as the product surface. OpenAPI 3.1 + MCP descriptors are the actual product. Everything else (UIs, agents, integrations) is a consumer of the contract.
- The system shrinks. Skills can be removed cleanly, dead code paths flagged, unused endpoints pruned. The OS continuously converges toward “exactly what this company actually does, no more.”
- Org structure drives architecture. Skills are scoped by organizational boundaries, not departmental specializations. When the company restructures, no skill code changes — permissions and scopes re-derive from the org model.
- AI is constrained, not empowered, by the stack. Strong types as contracts, schemas at boundaries, single canonical patterns, fast deterministic feedback. Boring is the feature.
- No containers in production. A cloud VM is already a VM. Running Docker on top of it is a container inside a container — a layer that exists only because we used to need to pack workloads onto scarce hardware. AI agents can provision a fresh VM in seconds; one service per VM is simpler, faster, and easier to debug than any container orchestration story.
- Size for the business, never for scale. A SaC serves one company — typically tens to low thousands of internal users, not millions of public ones. Almost everything modern cloud architecture optimizes for (horizontal scale-out, eventual consistency between services, stateless servers, multi-region failover, sharded databases, read replicas, service meshes) is irrelevant at this scale and adds latency, complexity, and failure modes for benefits the system will never collect. Pick the simplest, fastest, most synchronous design that handles 10× the actual load on a single fat VM. If McKinsey-scale problems ever arrive, address them then. They probably never will. (Note: this principle is about scale-out machinery. Background queues for inherently long-running work — imports, exports, document generation, ML inference, scheduled jobs, retries on flaky external APIs — are a property of the work itself, not a scale pattern, and remain essential. See the Background Work section below.)
The Architecture
┌────────────────────────────────────────────────────────────────────────────┐
│ PRESENTATION LAYER ─ speed-first, disposable, many │
│ │
│ Built by non-coders + AI (Cursor, v0, Bolt, Lovable, AI agents) │
│ Each app is a thin consumer of the skills below. │
│ Hosted on Vercel / Cloudflare Pages / Netlify — save→live in seconds. │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Sales │ │ Finance │ │ Mobile app │ │ AI agent │ │
│ │ dashboard │ │ dashboard │ │ (Expo) │ │ (LLM) │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ └───────────────┴────────────────┴────────────────┘ │
│ │ │
│ OIDC token in every call · REST + MCP + WebSocket │
└──────────────────────────────┼─────────────────────────────────────────────┘
▼
┌────────────────────────────────────────────────────────────────────────────┐
│ SKILL LAYER ─ this company's actual operations │
│ │
│ ┌──── Common skills (curated catalog, opt-in per company) ──────────┐ │
│ │ Contacts · Calendar · Files · Messaging · Tasks · Notes │ │
│ │ Basic Accounting · Basic CRM · Employee Directory · Vendor Mgmt │ │
│ │ (companies adopt as-is, fork, or skip entirely) │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──── Bespoke skills (unique to THIS company) ─────────────────────┐ │
│ │ BatchYieldTracking · RouteOptimization · ClinicalEnrollment │ │
│ │ CommissionCalculation · UnderwritingRules · … │ │
│ │ (AI-generated initially, refined over time, owned by the company)│ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ Each skill exposes: REST (canonical) + events + own DB + own authz │
│ MCP is a Python sidecar above each skill that calls the skill's REST │
│ Skills are small and focused. Composable. Removable without orphans. │
└──────────────────────────────────┬─────────────────────────────────────────┘
▼
┌────────────────────────────────────────────────────────────────────────────┐
│ KERNEL ─ the irreducible base every SaC shares │
│ │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ Identity (OIDC IdP) │ │ Permission Engine │ ← who can do what │
│ │ users · service princ│ │ RBAC + ABAC + scopes │ in which org scope │
│ └──────────────────────┘ └──────────────────────┘ │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ Org Model │ │ Audit Log │ ← every action, │
│ │ org chart · teams · │ │ immutable, queryable │ immutably recorded │
│ │ roles · processes │ │ │ │
│ └──────────────────────┘ └──────────────────────┘ │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ Event Bus │ │ Contract Registry │ ← every skill's │
│ │ (NATS) │ │ OpenAPI + MCP + perms│ API discoverable │
│ └──────────────────────┘ └──────────────────────┘ │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ Skill Lifecycle Mgr │ │ Observability │ ← install · upgrade · │
│ │ install · remove · │ │ OTel traces · logs · │ remove · fork · │
│ │ fork · upgrade · diff│ │ metrics · usage │ usage analytics │
│ └──────────────────────┘ └──────────────────────┘ │
└────────────────────────────────────────────────────────────────────────────┘
The Two-Layer Split
The presentation layer and the skills layer have opposite properties on every axis. Recognizing this is what lets each layer be optimized correctly.
| Property | Presentation Layer | Skills Layer |
|---|---|---|
| Who edits it | Non-coders + AI, anyone in the company | Engineers (with AI assistance) |
| Iteration speed | Save → live in seconds | Deliberate, gated by tests + review |
| How many | Many (one per use case, team, user) | One per domain |
| State | Stateless | System of record |
| Failure cost | ”My dashboard looks weird" | "We lost / corrupted / leaked data” |
| Engineering rigor | Minimal — speed matters more | Maximal — durability matters more |
| Authorization | Cannot enforce; only display | Enforces everything |
| Lifespan | Short — torn down and rebuilt freely | Long — measured in years |
| What AI is good at | Excellent (UI generation is AI’s sweet spot) | Mediocre (domain modeling needs human thought) |
The split aligns with what AI is actually good at: generating views over a stable model. Humans engineer the skills carefully; AI runs wild on the presentation. Each layer plays to a different strength.
The Kernel
The kernel is what makes this an “operating system” rather than a collection of microservices. It is small, stable, boring, and almost the same across all companies — like the Linux kernel itself. It is the part you do not rewrite.
| Kernel component | Why it lives in the kernel, not a skill |
|---|---|
| Identity (OIDC IdP) | Every skill must trust the same source for “who is this?” |
| Permission Engine | Authorization decisions must be uniform; skills must not each invent their own |
| Org Model | The shape of the company is what scopes everything. Skills query “is this person in the same org branch as this resource?” |
| Audit Log | Compliance, forensics, debugging. Every skill writes here through a write-only API |
| Event Bus | Cross-skill communication has to go through one channel or the system loses coherence |
| Contract Registry | Skills must discover each other and be discovered by AI/UI generators |
| Skill Lifecycle Manager | Install / remove / fork / upgrade is a first-class operation, not a manual deploy |
| Observability | A polyglot, evolving skill set is undebuggable without unified telemetry |
The Org Model as a First-Class Primitive
Most enterprise software treats organization as a configuration option (“multi-tenancy”). Here, the org model is the architecture. The kernel knows:
- The org chart (units, sub-units, reporting lines)
- People and their roles in each unit
- Processes — named workflows that cross units (e.g. “Hire”, “Close customer deal”, “Ship product”)
- Permissions scoped by org branch, not just by role
Skills do not model their own permissions in isolation. They consume the org model. When the company reorganizes — splits a division, merges teams, changes who reports to whom — no skill code changes. Permissions, scopes, and visibility re-derive from the new org model. The OS bends to the company instead of the company bending to the software.
The same skill can also be configured differently per org branch. “Approval threshold for purchases” is a function of the org unit, not hardcoded in the procurement skill.
The Skill Lifecycle Manager
The genuinely new component. Traditional systems are designed for addition, not subtraction. The Skill Lifecycle Manager handles:
- Install a skill from a template, from another company’s published version, or from a fresh AI generation
- Fork a common skill to customize it (the company now owns the divergence)
- Upgrade a skill (handles schema migration, contract version bump, deprecation flow for consumers)
- Remove a skill cleanly:
- Verify no other skill depends on its events / contracts
- Archive its data (or export, or destroy, per policy)
- Remove its routes from the gateway
- Stop and decommission its MCP sidecar (the per-skill Python sidecar that exposed its tools)
- Remove its permissions from the permission engine
- Update the contract registry
- Diff a skill against actual usage — flag dead code paths, unused endpoints, fields no one writes to, MCP tools no one calls
The diff capability is what makes “remove features you don’t need” continuously true rather than a one-time choice. SaaS vendors cannot shrink themselves because removing a feature might break a customer somewhere. A SaC can shrink because the entire user base is one company, and the kernel knows exactly what is in use.
Skills
Granularity
A skill in this architecture is much smaller than a typical SaaS app. “CRM” is not one skill; it is many small ones the company composes:
LeadCaptureLeadQualificationOpportunityPipelineQuoteGenerationContractTemplatingCommissionCalculationRenewalForecasting
A company that does not sell on commission does not install CommissionCalculation. A company that does not do quotes does not install QuoteGeneration. The SaC is the intersection of the company’s operations and the available skills, not the union of every feature any business might need.
The discipline: a skill should be small enough that one engineer + AI can understand it end-to-end in under an hour. If a skill is bigger than that, split it.
Skill Anatomy
Every skill has the same shape, regardless of runtime. The skill exposes REST as its canonical contract; MCP is a separate Python sidecar that consumes that REST. See the MCP Adapter Architecture section below for why.
service-ledger/ # the skill itself; whatever runtime fits the domain
src/
domain/ # the model — pure functions, invariants, no I/O
actions/ # one file per action: schema + permission + handler
adapters/
http.ts # REST routes wrapping actions, generates OpenAPI
events.ts # event bus publishers / subscribers
persistence/ # repository functions over the skill's own DB
schema/ # migrations
ops/
provision.sh # idempotent VM setup: install runtime, deps, user
ledger.service # systemd unit — runs the skill, restarts on failure
deploy.sh # git pull + build + systemctl restart
openapi.yaml # generated, committed, the public contract
The skill exposes REST. The action layer declares the skill’s behavior — input/output schemas, scope, ABAC mode, side-effect annotations — and is the source from which OpenAPI is generated; OpenAPI is the canonical machine-readable contract that browsers and the MCP sidecar both consume. The domain layer is the model. The persistence layer is private to the skill. The ops/ directory replaces the Dockerfile — a small set of scripts that any AI agent can run against a freshly provisioned VM to bring the skill up.
The MCP surface for this skill lives in a separate Python sidecar repo:
service-ledger-mcp/ # the MCP sidecar; always Python (FastAPI)
src/
tools/ # one file per MCP tool: description + handler
curation.py # response trimming, multi-step composition
auth.py # JWT validation + service-credential to ledger
ops/
provision.sh
ledger-mcp.service
pyproject.toml # depends on sac-mcp-base
The sidecar uses sac-mcp-base (a shared Python library) for JWT validation, tool-list filtering, error-envelope translation, identity pass-through, audit emission, and idempotency. Per-skill code in the sidecar is just the curated tool list and the per-tool handlers, each of which calls the skill’s REST and projects the response. Adding a new tool typically does not require any change in the skill itself — only in the sidecar — provided the necessary REST endpoint already exists.
Common vs Bespoke
- Common skills are a curated catalog (open-source, shared, templated) that handle the operations every business has: contacts, calendar, files, messaging, accounting basics. Companies adopt as-is, fork, or skip.
- Bespoke skills are unique to one company’s operations. AI-generated initially, refined over time, owned by the company. They follow the same contract pattern as common skills so they are interchangeable from the consumer’s perspective.
MCP Adapter Architecture
MCP is the AI-era integration surface, but its construction differs from REST in a way that matters for the architecture. The default model in this OS is stacked, not peered: each skill exposes REST as its canonical machine-readable contract, and MCP lives in a separate Python FastAPI sidecar above the skill that consumes that REST and projects it as task-scoped tools.
┌──────────────────────────────┐
[LLM consumer] ──> │ skill-foo-mcp (Python) │ ──┐
│ one sidecar per skill │ │
└──────────────────────────────┘ │
│ HTTPS / localhost
┌──────────────────────────────┐ │
[Browser / SPA] ──> │ skill-foo (any runtime) │ <─┘
│ REST + actions + domain │
└──────────────────────────────┘
│
[skill DB]
Why stacked over REST, not peered inside the skill
A naïve reading of “every skill exposes REST and MCP” would have the MCP server live inside the skill, peer to the REST adapter, both wrapping the same action layer. That model is clean on paper but fragile in practice once you have more than one skill in more than one runtime, and the failure modes accumulate fast in a real organization. The stacked model trades one HTTP hop for the following structural benefits:
- Cross-skill code reuse, by construction. Every MCP sidecar is Python and depends on
sac-mcp-base: JWT validation, tool-list filtering, error-envelope translation, audit emission, identity pass-through, idempotency cache, dry-run scaffolding. Per-language adapters force you to reimplement all of that in every runtime, watch conventions drift across implementations, and stop being able to ship MCP-wide improvements as one library bump. - Best language for MCP, full stop. Python’s MCP ecosystem is the strongest today and is improving fastest. Forcing TypeScript / PHP / Node / Java skills to also speak MCP natively is a tax on every skill team that buys nothing the sidecar can’t provide.
- Independent release cadence. The sidecar redeploys without touching the skill; the skill redeploys without touching the sidecar, as long as REST stays compatible. The peered model couples the two.
- Existing systems become first-class citizens, not exceptions. Most organizations adopting this architecture have battle-tested existing systems they cannot rewrite — CakePHP monoliths, Rails apps, Java backends. The stacked model treats them the same way it treats greenfield skills: each has a Python sidecar over its REST. There is no “special case for legacy.”
- Operational uniformity. Every MCP adapter is the same stack: FastAPI + uvicorn + the same systemd unit shape + the same log format + the same metrics. One way to operate, one way to observe.
- REST quality compounds. REST is the single curated contract that humans (the browser) and machines (the LLM) both consume. Investment in REST pays off twice. The peered model would force every M-level capability to be wrapped twice, duplicating the integration effort instead of compounding it.
- Dev velocity for new tools. Most new MCP tools are field projection over existing REST — a Python file in the sidecar, no skill change. The skill changes only when the MCP tool needs an aggregation, bulk operation, cross-entity query, sensitive-field filter, auth-aware filter, or computed field that isn’t already exposed. Those changes are additive REST endpoints that benefit the browser too.
What lives where
| Concern | Lives in |
|---|---|
| Domain logic, validations, invariants | The skill (M / domain layer) |
| Database schema | The skill |
| Authorization (scope + ABAC enforcement) | The skill (it is the only durable place; both browser and sidecar pass identity through to it) |
| REST endpoints + OpenAPI document | The skill |
| MCP tool list, descriptions, input/output schemas | The sidecar |
| Response trimming, multi-step composition into single tools | The sidecar |
| Agent identity, JWT validation, identity pass-through wiring | The sidecar (via sac-mcp-base) |
| Idempotency keys, dry-run scaffolding | The sidecar |
| Error envelope shape | The skill (on REST failures); the sidecar re-emits verbatim and additionally translates framework-native exceptions it catches itself |
| Audit envelope (cross-skill) | The sidecar |
| Domain events on the kernel bus | The skill (emitted in the same transaction as the business write) |
| Browser-specific concerns (CSRF, cookies, sessions, redirects) | The skill |
The bright line: the skill is the source of truth for what the system is and does. The sidecar is the source of truth for what the LLM sees and how it sees it. Both consume the same REST, so they cannot disagree on behavior.
Trade-offs honestly named
| Concern | How it’s handled |
|---|---|
| Latency (HTTP hop instead of in-process call) | 1–3 ms on localhost. Noise for interactive LLM tool calls; never a real bottleneck at company scale. |
| MCP and REST drifting in field shape | OpenAPI is the canonical machine-readable contract. The sidecar can validate its tool input/output schemas against the skill’s OpenAPI at boot, detecting drift mechanically. |
| Two audit logs (skill’s own + the kernel-side audit envelope) | The skill’s internal audit is for skill-internal investigation; the kernel-side envelope (per audit-envelope.md) is for the cross-skill record. They serve different purposes. |
| Multi-step atomic transactions across MCP | Cannot be composed by the LLM across multiple tool calls. Solution: expose multi-step workflows as a single REST endpoint that runs in one skill-side transaction; the sidecar wraps that one endpoint as one tool. |
| Sidecar process per skill is N more processes to operate | Yes. Each is small (a few hundred MB of Python and your tools), uniform, and managed by systemd identically. The operational cost is real but bounded; the cross-language code-reuse savings far exceed it for any portfolio with more than one skill runtime. |
When stacked is not the right answer
Two narrow exceptions, both rare in practice:
- A single greenfield skill in a single MCP-friendly language with no other skills. The peered-inside-skill model has slightly less surface area. As soon as a second skill in a different runtime appears, the stacked model wins decisively, so this exception only applies to truly standalone skills.
- A skill where MCP latency is on the millisecond-critical path. Vanishingly rare for SaC workloads. If it ever appears for a specific skill, that skill alone can run an in-process MCP adapter; the rest of the architecture is unaffected.
The default is stacked. Diverge only with explicit justification documented in the skill’s README.
The role of the aggregator
The architecture’s earlier framing referred to a kernel-side “MCP aggregator” that fanned a single consumer session out to multiple per-skill MCP servers. Under the stacked model that aggregator simplifies to a thin network-layer multiplexer (or a manifest the consumer reads directly), since per-skill MCP servers are uniform Python sidecars that can be addressed individually. The aggregator is no longer doing identity translation, error harmonization, or any other semantic work — sac-mcp-base handles those uniformly inside each sidecar. The aggregator remains useful as an addressing convenience and is the right place to apply per-consumer rate limits, but it is not on the critical-path of correctness.
For deeper guidance, see the contracts repo:
mcp-server-spec.md— the Tier 1 protocol contract a sidecar must implement.rest-and-mcp-conventions.md— wire-level differences between the REST and MCP surfaces.mcp-adapter-architecture.md— the sidecar topology in detail, what stays in the skill vs the sidecar, and how to apply the pattern to existing systems.
Contracts
Contracts are the product surface in this architecture, but they are also the part most likely to be over-designed up front or under-designed and retrofitted painfully later. The discipline is knowing which contracts to pin early and which to let emerge.
Who plays which role
| Role | Who plays it |
|---|---|
| Contract author | Each skill |
| Contract enforcer | Each skill at its own boundary |
| Contract registry | Kernel component |
| Universal contract consumer | Cross-cutting consumers (the AI agent, code generators, integrations) |
| Stress-tester | The first universal consumer — incoherence shows there first |
The skill is the contract’s authority. The kernel is its registry. A universal consumer is its first real customer — and the place where inconsistency surfaces.
Triage: what to define when
Not all contracts deserve the same treatment. The cost of getting a contract wrong is asymmetric: some are trivial to retrofit; some are brutal. The test:
If two engineers built two skills tomorrow without coordinating, would they end up incompatible on this concern?
If yes → pre-define before any consumer is built. If no → discover at runtime, defer, or let it emerge from real overlap.
Tier 1 — Pre-define before the first consumer ships
| Contract | What it specifies |
|---|---|
| JWT claims envelope | What every skill receives in a verified token: sub, email, org_unit, roles, scopes, iss, aud, plus any custom fields |
| MCP server expectations | Initialize handshake, per-user tools/list filtering, tools/call enforcement contract, error shapes, identity-passing pattern |
| Standard error envelope | One shape across all skill responses (RFC 7807 Problem Details or equivalent) so consumers render errors uniformly |
| Identity pass-through | The default for how user identity flows from consumer → skill: pass-through user JWT vs service-token + X-On-Behalf-Of. Per-skill overrides only when justified |
| Authorization context | How a user’s roles, scopes, and org-position are expressed for skills to enforce against |
| Audit log envelope | The shape every skill writes to the audit log |
| Event bus message envelope | Topic conventions, headers, schema-version field |
Tier 2 — Define alongside the first consumer or first skill
Useful to capture; the right shape depends on first build. Refine as evidence accumulates.
- Agentic loop conventions (how tool results pass into LLM context; how prelude outputs are framed)
- Prelude tool output shape
- Per-skill telemetry event conventions (
<skill>.<event>) - Pagination, cursor format, and sort conventions for REST
- OpenAPI conventions: tags, error responses, security schemes
Tier 3 — Defer until evidence of overlap
Canonical entity types — Person, OrgUnit, Money, Document, File, Address, Date. The risk that the same entity gets modeled three different ways across three skills is real, but the answer is not to pre-design entity shapes from imagination — real-world shapes vary, and pre-designed types rarely survive first contact. Harvest the canonical shape from observed overlap once two skills genuinely use it. Until then, each skill defines its own local types and migrates when the kernel canonicalizes.
Where contracts live
The Tier 1 contracts must be authoritatively located somewhere so every consumer and every skill points to the same source.
| Option | Trade-off |
|---|---|
| In the first consumer’s repo | Easy now; bad long-term, because future skills shouldn’t depend on a presentation app |
| Duplicated in each consumer’s repo | Drift inevitable |
Separate sac-contracts repo | Canonical, versioned, every skill + consumer cites it |
Option three is the seed for the kernel’s eventual Contract Registry. Initial structure:
sac-contracts/
jwt-claims.md shape of every product's JWT
mcp-server-spec.md what an MCP server must implement to be consumable
error-envelope.md standard error shape
identity-passthrough.md default JWT-forwarding pattern
authz-context.md how identity maps to permission scope at a skill
audit-envelope.md what every skill writes to the audit log
event-envelope.md bus message conventions
README.md index, versioning policy, change process
Each doc is short — 1-2 pages. The repo is small on purpose. Skills and consumers cite specific versions; v2 is allowed and expected.
Discovered at runtime, not pre-defined
A correctly contracted system requires very little to be pre-defined per skill, because each skill’s MCP and OpenAPI descriptors are themselves machine-readable contracts. A universal consumer:
- Hits each skill’s
tools/list(MCP) or OpenAPI document at startup or on demand. - Receives the per-user filtered tool catalog plus full input/output JSON schemas.
- Validates payloads against those schemas before forwarding.
This is what makes the architecture composable: cross-cutting contracts are pre-defined and small; per-skill contracts are self-published and discovered. Adding a new skill never requires updates to existing consumers.
The stress-tester role
The first universal consumer of skill contracts — typically the AI agent — plays a privileged role: it reveals contract incoherence faster than any human review could. If org_unit is a string in skill A and {id, name} in skill B, the AI agent fails to compose them and the gap surfaces immediately.
This is a feature, not a bug. Build the universal consumer early to stress-test the contracts. Treat early breakages as contract problems to fix in sac-contracts, not consumer problems to work around.
Sequencing
| Phase | Work |
|---|---|
| 0 | Stand up sac-contracts with the Tier 1 documents. Two to three days of focused doc writing, not weeks. |
| 1 | Build the first universal consumer and the first MCP server. Each cites sac-contracts v1. The first MCP server is the executable proof that mcp-server-spec is implementable. |
| 2 | As the second skill comes online, harvest canonical entity types from observed overlap into sac-contracts v2. |
| 3 | When the kernel ships its Contract Registry component, migrate the doc set into it. |
Risks of doing this wrong
| Approach | Risk |
|---|---|
| Skip Tier 1; let the first consumer set the de facto contract | Accidental decisions become permanent; future skills inherit them silently |
| Pre-define everything (Tier 1 + 2 + 3) up front | Bikeshedding on entity types without use cases; contracts don’t survive first contact with reality |
| Tier 1 only, then build | Small chance Tier 1 is slightly wrong; cheap to v2 because Tier 1 is small |
The middle path is bounded. Tier 1 is small. The cost of getting it slightly wrong is small. The cost of skipping it is large.
Stack Recommendations
The right stack differs by layer because the engineering economics differ.
Skills Layer (engineered, stable)
| Need | Pick | Why |
|---|---|---|
| Default service runtime | TypeScript on Bun, strict mode, Hono framework | Largest training corpus of any typed language; types catch hallucinations; Hono has a tiny stable API; Bun bundles runtime + test + package manager into one tool |
| ML / data service runtime | Python 3.12 + FastAPI + Pydantic v2 + uv | The only sane choice for ML; FastAPI generates OpenAPI from type hints; uv finally fixed Python packaging |
| Compute-heavy runtime | Rust + Axum (or Go + chi) | Use only when perf demands it; otherwise default to TS |
| Validation at boundaries | Zod (TS) / Pydantic (Py) / serde (Rust) | Runtime validation that mirrors static types — non-negotiable |
| Database | PostgreSQL 16+, one per skill | Postgres is the relational store, the JSON store, the queue, the vector store (pgvector), the search engine. One database to know deeply. |
| Migrations | Atlas (language-agnostic, declarative) | Same tool across all skills regardless of language |
| Event bus | NATS with JetStream | One binary, pub/sub + request/reply + persistence |
| Real-time gateway | Centrifugo | Single binary, scales, decoupled from skill lifecycles |
| Identity | WorkOS (commercial) or Zitadel (self-host) | OIDC, SAML, SCIM out of the box. Never roll your own |
| API gateway | Caddy + JWT verification, or Cloudflare Workers | Boring, well-documented |
| MCP | Python FastAPI sidecar per skill, calling the skill’s REST, built on a shared sac-mcp-base library. Optional thin aggregator at the network layer. See MCP Adapter Architecture. | One language for all MCP code; cross-skill code reuse via the shared base; the skill itself stays in whichever runtime fits its domain |
| Compute / deploy | One service per cloud VM (EC2 / Hetzner / Linode), generously sized, managed by systemd. DNS points at the VM; Cloudflare in front for TLS / DDoS / caching. | A cloud VM is already a VM — Docker on top is a container inside a container. AI provisions fresh VMs in seconds. Simpler, faster, easier to debug. No load balancer needed at company scale. |
| Provisioning | Cloud API directly (or Terraform / Pulumi for declarative state), invoked by the AI agent | Spin up / tear down VMs as a normal operation. The provisioning script lives in the skill’s ops/ directory. |
| Process management | systemd (auto-restart, logs to journald, resource limits, dependency ordering) | Built into every Linux distro, well-understood, no extra runtime. |
| Deploy | git pull && ./ops/deploy.sh over SSH, or push-to-deploy via a tiny webhook on the VM | No image build, no registry push, no rollout controller. New version is live in seconds. A 3-second restart blip is fine for an internal company system. |
| Capacity | Provision generously up front. A 4-core / 16GB VM handles 1000s of RPS — orders of magnitude more than any SaC workload. If a skill outgrows its VM, move it to a bigger one (vertical scale). Horizontal scale-out is rarely justified. | A 200-person company generates a few RPS at peak. Designing for 10× headroom on a single VM costs ~$20/month and removes an entire category of complexity. |
| Observability | OpenTelemetry SDKs → Grafana Cloud (Tempo/Loki/Mimir) | OTel is the universal protocol |
| CI/CD | GitHub Actions + per-skill pipelines + Atlas for migrations | Stable, well-known |
Presentation Layer (speed-first, disposable)
| Need | Pick | Why |
|---|---|---|
| UI runtime | Next.js / Astro / plain React on Vercel or Cloudflare Pages | Save → live in seconds, preview URL per branch, zero infra |
| Mobile | Expo + React Native | Same TS, OTA updates, AI-friendly |
| Generated API clients | openapi-typescript or orval, regenerated on contract change | The presentation layer never invents schemas — it consumes what skills publish |
| Real-time client | Native WebSocket, subscribed to Centrifugo | Skills emit events; gateway pushes; UI renders |
| Auth | Clerk (drop-in) | Five lines for OIDC, orgs, RBAC, MFA, magic links |
| State / data fetching | TanStack Query | Caches, invalidates, retries — pairs cleanly with generated clients |
| AI coding environment | Cursor for code + v0 / Bolt / Lovable for UI scaffolds | Where the actual work happens |
Why These Specific Picks
When AI writes the code, the criteria for stack selection invert:
- Strong static types + schema validation at every boundary — the compiler is your code review
- Massive training corpus — AI is fluent in mainstream stacks and hallucinates in niche ones; boring beats clever
- One canonical way to do things — frameworks with five ways to write a route make AI generations drift across files
- Fast feedback loops — compile-check-test in seconds, not minutes
- Schema-first tooling — OpenAPI / Pydantic / Zod generate code; AI fills in handlers
- Single binary or single process per VM — less surface area for AI to break, no nested isolation layers
What stops mattering: developer ergonomics, conciseness, “elegant” abstractions, hiring pool, learning curve.
What to Actively Avoid
- Magic frameworks (Spring, Rails, Django, NestJS) — AI produces them fluently but failures are hard to diagnose
- Niche-but-elegant languages (Elixir, OCaml, Clojure, Gleam) — smaller training corpus = more hallucinations
- GraphQL — couples services through the schema; AI keeps drifting on resolver patterns; OpenAPI + REST is what AI tooling actually understands
- ORMs with heavy DSLs (Prisma’s runtime, SQLAlchemy’s session magic, Hibernate) — use thin query builders (Drizzle, Kysely, sqlc) or just SQL
- Yarn / pnpm / npm permutations — pick Bun and stop. Pip / poetry / pipenv — pick uv and stop
- Custom DSLs and meta-frameworks — every layer of indirection is a layer where AI can wander
- Microservice frameworks with sidecars and service discovery — DNS and HTTP are enough until they aren’t
- Docker in production — a cloud VM is already a VM; running containers inside it solves a problem (workload packing on scarce hardware) that no longer exists when AI can provision a fresh VM in seconds. Use systemd on a dedicated VM per skill instead.
- Kubernetes / ECS / Nomad / any orchestrator — these exist to manage fleets of containers at scale. A SaC has neither containers nor scale.
- Container-based “platforms” (Cloud Run, Fly.io, ECS Fargate) — excellent products if you’re committed to containers and elastic scale-out; unnecessary infrastructure if you aren’t.
- Load balancers, auto-scaling groups, multi-region failover, sharded databases, read replicas, CQRS, event sourcing as the default storage model, eventual consistency between services — these are scale-out patterns. They cost latency, complexity, and bug surface. None of them apply at company scale, where the actual workload fits comfortably on one machine and synchronous in-process operations are both faster and simpler. Do not adopt any of them speculatively. Adopt them only when you have measured a specific problem they solve. (This list explicitly does not include background queues for long-running work — those are essential at any scale; see Background Work.)
- Microservices “for scaling” — the polyglot skill model is for runtime fit and lifecycle isolation, not for horizontal scale. Don’t import the rest of the microservices ideology along with it.
- Stateless-server orthodoxy — at company scale, holding session or cache state in process is fine. Sticky sessions are a non-problem when there’s one server. In-memory caches beat Redis for almost everything.
The VM-Native Deploy Model
Containers were a pragmatic answer to a specific problem: how do you safely pack many workloads onto scarce hardware? VMs were too heavy, bare metal too dangerous. Docker gave you process-level isolation with VM-like boundaries, and Kubernetes managed the resulting fleet. That entire stack — image registry, orchestrator, sidecars, service mesh — exists to solve the workload-packing problem at hyperscale.
In 2026, neither side of that problem is real for a SaC:
- A cloud VM is already an isolated execution environment with its own kernel namespace, networking, and resource limits.
- AI agents can provision a fresh VM in seconds via the cloud provider’s API.
- VMs are cheap enough that “one service per VM” is no longer a luxury.
- Running Docker on top of a VM is two layers of isolation doing the same job.
- And — most importantly — a SaC doesn’t need to scale. It serves one company. The workload fits comfortably on one generously sized machine per skill.
So the model for the skills layer is:
- One skill per VM, sized generously. A 4-core / 16GB VM costs roughly $20–40/month and handles vastly more load than any internal company workload will ever produce. Pick a size that gives you 10× headroom on day one and stop thinking about capacity.
- systemd manages the process. Auto-restart, journald logs, resource limits, dependency ordering — all built into every Linux distro and well-understood by AI.
- Provisioning is a script in the skill’s repo.
ops/provision.shis idempotent: install runtime, install deps, create user, drop the systemd unit, start the service. An AI agent runs it against any fresh Ubuntu/Debian VM and the skill is up. - Deploys are
git pull && systemctl restart. Or a tiny webhook on the VM that does the same thing when a tag is pushed. No image build. No registry. No rollout controller. A 3-second restart blip is fine for an internal system; nobody is paging anyone. - DNS points straight at the VM. Cloudflare in front for TLS, DDoS, caching. No load balancer. There is nothing to balance.
- Failure is replacement, not failover. If a VM goes bad, the AI provisions a new one and runs the script. State lives in Postgres / object storage, not on the VM. Downtime during the swap is measured in minutes and is acceptable for an internal system. If you genuinely need 99.99% uptime for a specific skill, that skill gets a warm standby — not the whole architecture.
- Outgrew a VM? Move it to a bigger one. Vertical scale is almost always sufficient. A 32-core / 128GB VM is still one VM with one systemd unit and zero distributed-systems thinking. Horizontal scale-out only enters the conversation when you have hard evidence that vertical scale won’t work, which for a SaC is almost never.
The trade-offs, honestly:
| Concern | How it’s handled without containers, orchestrators, or scale machinery |
|---|---|
| Reproducibility across environments | The provision script is the artifact. Re-run it; you get the same VM. AI maintains it. |
| Multiple services on one machine | Don’t. One VM per skill. VMs are cheap. |
| Resource isolation | Provided by the VM. Optionally tighten with systemd MemoryMax, CPUQuota. |
| Local dev | Run the service directly via bun run dev / uvicorn --reload. The provision script is for VMs, not laptops. |
| Onboarding new engineers | The provision script is shorter and more readable than a Dockerfile + compose file + Kubernetes manifests. |
| Cost | $20–40/month per VM × maybe a dozen skills = a few hundred dollars/month. Far cheaper than the per-seat cost of the SaaS this replaces. |
| High availability | Snapshot the VM nightly. Keep a known-good provision script in git. Acceptable RTO for internal business software is hours, not seconds — and even that is rarely tested. |
| What if traffic spikes? | At company scale, the spike is “the all-hands meeting just ended and 200 people refreshed their dashboard.” A right-sized VM handles this in its sleep. |
When containers or scale-out machinery might still make sense (intellectually honest exceptions):
- The presentation layer’s serverless platforms (Cloudflare Workers, Vercel) — but here the platform manages everything; you never see a Dockerfile.
- An ML inference service with GPU sharing across many tenants — but this is workload-packing, and a SaC rarely has that problem.
- A skill that genuinely faces public internet traffic at scale (e.g. a customer-facing ordering system for a restaurant chain). At that point, that specific skill gets the scale-out treatment; the rest of the OS does not.
For the SaC as described, none of these apply by default. Skills run on right-sized VMs. systemd runs the skills. AI runs the VMs. Scale is not a goal.
Background Work
Plenty of work in a SaC is inherently long-running and must execute outside the request/response cycle, regardless of how few users the system has:
- Imports and exports (CSV, spreadsheet, ERP dumps)
- Document generation (PDFs, contracts, reports)
- Email and notification dispatch (with retries on transient failures)
- ML inference jobs that take seconds to minutes
- Scheduled jobs (nightly billing runs, weekly reports, monthly close)
- Webhook delivery to external systems with retry-and-backoff
- Batch operations triggered by a single user action
- Data migrations and backfills
Synchronous handlers cannot do this work. The user’s HTTP request would time out, and a crash mid-operation would leave inconsistent state. So every SaC needs a background-work mechanism. This is about the nature of the work, not about scale, and it is essential at any size.
The right tool at company scale is the simplest one that gives you durability and retries:
| Need | Pick | Why |
|---|---|---|
| Background queue + scheduled jobs (default) | Postgres-as-queue using FOR UPDATE SKIP LOCKED (libraries: River for Go, pg-boss / Graphile Worker for Node, Procrastinate for Python) | You already run Postgres. No extra infrastructure. Durable, transactional with your business writes, observable with SQL, plenty fast at company scale. |
| Worker process | A second systemd service per skill: ledger-worker.service alongside ledger.service | Same VM or its own; same deploy model; no orchestration. |
| Cron / scheduled jobs | systemd timers, or a cron entry in the queue table the worker reads | Built into the OS. No Kubernetes CronJob needed. |
| Long-running, multi-step durable workflows | Inngest or Trigger.dev if you want managed durable execution; Temporal if you want self-hosted | Use only when the workflow has multiple steps that each need durability and you want the framework to manage retries, timeouts, and resumption. Most company workflows do not need this. |
The default is: Postgres queue + a worker systemd service per skill that needs background work. The skill enqueues a job in the same transaction as its business write (so the job is guaranteed to be enqueued iff the business state changed); the worker pulls it, runs it, retries on failure, records the outcome. No external queue server, no message broker, no Kafka. The whole thing is a few hundred lines of code or a single library import.
This is queue-based async done for the correct reason — durable execution of inherently long work — not for the wrong reason of decoupling services for horizontal scale-out.
The Hardening Path
For new companies / projects, the speed-first version of the presentation layer can also extend temporarily into the skills layer to bootstrap quickly. Each shortcut has a clear “stabilize trigger” — the moment to reverse it.
| Shortcut | What you lose | When to reverse |
|---|---|---|
drizzle push instead of migrations | Schema history, safe rollbacks | The first time real production data exists. Most expensive to defer — switch to Atlas immediately at that point. |
| No OpenAPI contract registry | Versioning, deprecation, third-party clients | When the second team starts consuming the API |
| No event bus, services call each other directly | Resilience to one service being down | When you have >3 skills and start seeing cascading failures |
| Managed everything (Clerk, Convex, Inngest) | Cost at scale, vendor lock-in | When the monthly bill exceeds the ops cost of self-hosting equivalents |
| No OpenTelemetry | Distributed tracing | When “why is this slow” or “where did this fail” stops being answerable from logs |
| No CI/CD gates | Catching regressions before deploy | When the first bad deploy reaches a real user and costs something real |
| Single VM per skill, no warm standby | Brief downtime during VM replacement | Almost never. Internal company software tolerates minutes of downtime fine. Add a warm standby only for the specific skill where someone has signed up to be paged. |
Why This Works in the AI Era and Not Before
The traditional objection to building bespoke per-company software was: “Building a CRM costs $10M. Maintaining it is unsustainable. SaaS exists because amortizing development across thousands of companies makes it cheap.” That economic argument is what AI breaks.
| Old assumption | What AI changes |
|---|---|
| Building a CRM costs $10M | Building a 7-skill CRM tailored to one company costs ~weeks of human + AI time |
| Maintaining custom software is unsustainable | AI can maintain it. Skills are small enough that AI can comprehend and refactor each one |
| Best practices have to be embedded by experienced vendors | Best practices are now in training data and can be templated into generated skills |
| Compliance is too complex to build per company | Compliance becomes a skill (or kernel component), enforced uniformly |
| Integration is the hard part | MCP and OpenAPI as universal contracts make integration cheap |
| You need to hire developers to maintain anything custom | The “developer” is one engineer + AI, or in the limit, one operator + AI |
The economic argument for SaaS was always “fixed cost spread across many customers.” When AI collapses the fixed cost, the case for tailored systems opens up — and tailored systems can do something SaaS structurally cannot: be exactly the size of the business.
Failure Modes and Hard Parts
This architecture is not free. The new failure modes are:
- Skill sprawl if there is no curation. A hundred bespoke skills with overlapping concerns is worse than Salesforce. The discipline of “one skill per coherent operation” must be enforced; AI does not impose it by default.
- Inter-skill consistency. If
Customeris modeled in three skills with three different shapes, you have the same pain as misaligned microservices. The kernel must publish canonical types for cross-cutting entities (Person, Org Unit, Money, Document, File) that skills must use when they touch those concepts. - The kernel becoming a god module. It must be ruthlessly minimal. Anything that can be a skill should be.
- Migration when org structure changes meaningfully. Easier than in Salesforce, but still real work.
- AI generating transaction scripts instead of domain models. The single most common failure mode — handlers full of inline business logic, no domain layer, invariants drifting across actions. Resist this aggressively. Make the
domain/layer a non-negotiable convention. Review AI output for “is this just a script over the DB?” - Best practices missed by AI generation. Tax rules, GDPR, accounting standards, security patterns — AI doesn’t always know what it doesn’t know. Templates and shared kernel components for these concerns matter.
These are real, but they are qualitatively different from the failure modes of the SaaS-per-domain status quo, and they can be managed by a small team with AI — where the SaaS-stack failure modes (vendor lock-in, irrelevant features, integration hell, data scattered across 30 vendors) cannot.
The End State
The end state is not “software for the company.” It is software as the company — an executable, evolving model of how the business actually operates, scoped to exactly what it does and shaped by who works there.
- The kernel is the operating system.
- The skills are the company’s actual operations, captured as code.
- The presentation layer is whatever views any team or person needs today.
- The contracts make it all composable.
- AI keeps it shaped to the business as the business changes.
A company that runs on this no longer asks “what software should we buy?” It asks “what does our business do, and what skills should we install, fork, or build?”