Self-Hosted First policy
RFC — Self-Hosted First policy
| Field | Value |
|---|---|
| Status | *ccepted*(2026self-hosted-first, 5 gates, status enum experimental | stable | official | legacy, maintainer proposes / Stack→ official transition, free |
| Author(s) | Rodrigo (with Claude as scribe) |
| Date | 2026 |
| Affects | New policy meta/docs/stack/policies/self-hosted-first.kmd (sibling of reuse-first.kmd and code-first.kmd); subsumes web-server.kmd and the kodec Flipping Point note in CLAUDE.md as applied cases; introduces a [self_hosted] block in koder.toml; autokoder-spec-audit self-hosted |
| Depends on | policies-RFC-001-reuse-first-hierarchy.md (sibling pattern recognised), engines/sdk/koder_kit/docs/rfcs/RFC-001-spec-encapsulation-across-platforms.md (sibling enforcement axis), policies-RFC-003-rfc-phase-pickup.md (mechanism that opens phase tickets automatically — adopted ahead of time so this RFC's Phase 4 ticket is auto-created when Phase 3 lands) |
1. Summary
Formalise the principle that when a Koder Stack component meets a maturity bar against an external alternative, new code should prefer the Koder component — and define the maturity bar with five explicit gates, the data model used to evaluate it, and the discovery mechanism that keeps AI sessions and human reviewers from carrying stale knowledge.
The policy is *lgorithmstatic, datadynamic* the five gates and the decision protocol live in the policy text; the percomponent status (which gates each candidate has cleared) lives in generated registry agglomerates the data; AI sessions read the registry, not the policy text, when deciding.koder.toml; an auto
This RFC introduces policies/self-hosted-first.kmd as a sibling of reuse-first.kmd (crosscutting reuse) and `codefirst.kmd` (mechanicalvsanalytical). The three policies are orthogonal axes; all three apply at every implementation decision.
2. Motivation
2.1 Pointwise precedents
The Stack already takes selfhostedfirst decisions case by case, but each is its own document and has its own ad-hoc justification:
- *odec Flipping Point*—
meta/context/CLAUDE.md§ Arquitetura de Referência declares 20260428 as the date when Koder Koda AOT became the official media-engine implementation, with Rust crates inengines/kodec/kept as legacy. Rationale embedded in CLAUDE.md, no decision protocol. - *web
server.kmd5` as a documented exception. The policy is normative, but the gates that justified the decision are not written down anywhere.** — decrees Koder Jet as the only web server / reverse proxy, withpoc - *kicon
** — referenced incodefirst.kmdfirst.kmd` (the buildandreusetooling category) as the canonical iconvariant generator. Implicit self-hosted choice; no formalised gates.
Each of these is right, but the absence of a shared framework means the next case (Koder Koda vs Rust for new modules; Koder Hub vs OCI registries; kdbnext vs PostgreSQL) gets relitigated from scratch.
2.2 Anti-patterns this policy targets
Two failure modes recur in shared-stack work:
- *witching by hype* adopting a self-hosted alternative before the gates are met — frequently driven by enthusiasm for a freshly stable Stack component. The cost is regression risk in production code (libs changing under it) and rework when the Stack component's API stabilises late.
- *witching by inertia* continuing with the external alternative after the gates are clearly met — frequently driven by familiarity or "it ain't broke". The cost is dependency drag (security patches, version churn, multi-vendor coordination) and divergence from the Stack's reference architecture.
Both are silent failures in the absence of an explicit decision protocol. This RFC's gates are the protocol that surfaces both.
2.3 Generalisation, not enforcement
This policy is not a mandate to migrate every external dependency to a Koder alternative. It is a decision aid: when both options exist and an AI session or human dev is choosing, run the gates. If the Koder component clears them, prefer it; if not, document which gate failed and (optionally) open a backlog ticket toward closing the gap. Existing legacy code is governed by deprecation cycles defined in runtime-lib-first.kmd § Strict SemVer, not by this policy.
3. Design
3.1 The five gates
A self-hosted Koder component is preferred over an external alternative for *ew code*when, for the specific use case at hand, all five gates pass:
| Gate | Question | Pass criterion (default) |
|---|---|---|
| *1 — Feature parity* | Does the Koder component cover the case |
The required capabilities (FFI / IDE |
| *2 — Performance parity* | Is the Koder component within an acceptable margin of the external on this case? | Hot |
| *3 — Stability* | Has the Koder component left experimental and held for ≥ 3 releases without a regression that affects this case? |
Documented in koder.toml [self_hosted].status = stable / official / legacy and confirmed by koder-spec-audit self-hosted --history. |
| *4 — Capability gate* | Are the categorical capabilities for this kind of use case present? | Perpolicies/reuse/ defines its category gate; this policy reads them, not redefines them. |
| *5 — Critical-path readiness* | Is the component proven in production in the Stack? | Used by ≥ 2 consuming Koder modules currently in production (not behind a feature flag, not in a smoke-test setup). |
Default thresholds are written in the policy. A specific component may *ighten*any gate (e.g. mandate ≤ 2% performance margin for a critical hot path) but cannot loosen them without an RFC amendment.
The gates are *onjunctive* missing any gate means the external alternative remains preferred for this case until the gap is closed. The component may already be official for other cases at the same time — gates are evaluated per caseofuse, not per component globally.
3.2 Data model — koder.toml [self_hosted] block
Components that compete with an external alternative declare their state declaratively in their own koder.toml:
[self_hosted]
# External alternatives this component can substitute. Free-form;
# names are matched against the case-of-use string the AI session
# describes in question 1 of the decision protocol.
replaces = ["nginx", "caddy", "apache", "traefik"]
# Maturity status. Enum: experimental | stable | official | legacy.
# `experimental` — public API may change; gate G3 fails.
# `stable` — API stable; gate G3 passes for cases the component
# documents support for.
# `official` — Stack default for the case; new code MUST prefer it
# unless an explicit gate fails for the specific case.
# `legacy` — superseded by another self-hosted component; new
# code MUST NOT use it.
status = "official"
# Gates the component currently clears, declared per-case where
# relevant. Generic gates (no case suffix) apply to all cases.
gates_passed = [
"feature_parity",
"performance",
"stability",
"capability:tls",
"capability:reverse_proxy",
"capability:hot_reload",
"production_proven",
]
# Gates known to be missing, with optional ticket / RFC link in the
# value position. Each entry suppresses the AI from recommending the
# component for the affected case until the gate is closed.
gates_pending = [
# "capability:http3 = sites/jet/backlog/pending/047-http3-support.md",
]
# Evidence — where to verify the gate claims.
benchmark_ref = "infra/net/jet/bench/jet-vs-nginx-2026-03.md"
production_ref = ["s.forge", "s.poc.vivver.com"]The schema is small on purpose: one block per component, one row per gate, optional pointers to evidence. Validation is structural (gatespassed + gatespending are disjoint; status is in the enum; etc.) and lives in koder-spec-audit self-hosted --validate.
3.3 Auto-generated registry
meta/docs/stack/registries/self-hosted-pairs.md is the agglomerated view of every [self_hosted] block. It is regenerated by koder-spec-audit self-hosted --report and reemitted on every `/khousekeep cycle. The file header carries <!-autogenerated by koderspecaudit selfhosted; do not edit by hand -` and any manual edits are overwritten.
The registry is the table that AI sessions read to apply the policy. The policy text never carries the table directly — only the algorithm. New components register themselves by adding their [self_hosted] block; the registry picks them up automatically.
3.4 Anti-drift heuristic
A component is plausibly a self-hosted alternative if it satisfies any of:
- module name starts with
koder-and is not a product (products/) - path is under
engines/sdk/,infra/, orproducts/dev/ - name appears in CLAUDE.md § Arquitetura de Referência or in
web-server.kmd's precedent list koder.tomldeclarescategory = "engine"or"infra-tooling"(this field is introduced in Phase 4; see §5)
When the audit finds such a candidate *ithout*a [self_hosted] block in its koder.toml, it emits a warning. The warning is advisory in Phase 1–3 and strict in Phase 5.
3.5 Decision protocol (3 questions answered in chat)
When an AI session is choosing between a Koder component and an external alternative, before the first Write/Edit it answers, in its own response:
- *hat case am I solving?*A one
sentence caseofuse string ("a TLSterminating reverse proxy fors.forgevhosts"; "a Linux mipmap launcher icon for an Android app"; "a high-throughput H.264 decoder for a 4K transcoding pipeline").
- *eading
meta/docs/stack/registries/self-hosted-pairs.md, is there a Koder component that lists my external alternative inreplacesand clears all 5 gates for this case?*- Yes → use the Koder component; cite the registry row.
- Yes for some gates, no for others → name the failing gates explicitly; either accept the external alternative (and optionally open a promotion ticket) or override (and accept the maintenance burden).
- No (no candidate found) → the case is outside this policy's reach; pick the external alternative without ceremony.
- *oes my decision warrant an entry in the component's CHANGELOG or a follow-up ticket?*
- Adoption of a Koder alternative for the first time in a high-traffic module → CHANGELOG note.
- Continued use of an external alternative after gates were closed → ticket recommending migration on next refactor.
- Discovery of a missing gate on a candidate → ticket on the candidate's backlog to close it.
The protocol is symmetric to the protocol of reuse-first.kmd and code-first.kmd — three questions, answered visibly in the response, before any code is written.
4. Current pairs (snapshot — informative, not normative)
The following pairs exist in the Stack today. The data is informative for this RFC; the normative state will live in koder.toml [self_hosted] blocks once Phase 2 lands. After Phase 2 the registry is the source of truth and this section may be removed in a future amendment.
| Self-hosted | External | Status today | First case |
|---|---|---|---|
Koder Jet (infra/net/jet) |
nginx, Caddy, Apache, Traefik | official | TLS reverse proxy + static serving |
kodec (engines/kodec) |
FFmpeg, GStreamer | stable for mp3 / mp4 / wav; experimental for other codecs | media decoding |
kicon (products/dev/kicon) |
ImageMagick scripts, hand-drawn PNGs | official | icon variants per platform |
Koder Hub (products/dev/hub) |
OCI / npm / Cargo registries | official (within Koder publishing flow) | publish/install Koder packages |
kdb-next (infra/data/kdb) |
PostgreSQL, SQLite | experimental | OLTP storage for ID v2 |
Koder Koda (engines/lang/lang) |
Rust, Go, C | stable for hot-path lib code (kodec); experimental for general use | hot-path / native AOT |
Koder Talk + Kmail (products/horizontal/{talk,kmail}) |
Matrix, Signal, IMAP+SMTP | experimental | messaging / mail |
This is the seed for Phase 2's registry generation.
5. Migration plan
Five phases, each leaving the Stack in a green state.
Phase 1 — Author the policy (1 PR)
- Create
policies/self-hosted-first.kmdwith §3 (gates), §3.5 (decision protocol), § Related (siblings:reuse-first,code-first; subsumesweb-server.kmd). - Frontmatter declares the new
[self_hosted]block schema inkoder.toml(informative; obligation lands in Phase 4). - Cross
link with `reusefirst.kmd(sub-policies referenceselfhostedfirst` for categoryspecific gate G4 subgates).
Phase 2 — Annotate the seven seed components (1 PR per component, parallelisable)
For each of the seven pairs in §4:
- Add
[self_hosted]block to the component'skoder.toml. - Validate via
koder-spec-audit self-hosted --validate <module>. - Run
koder-spec-audit self-hosted --reportto regenerate the registry.
The registry's first generation lands at the end of Phase 2. From that point, AI sessions can apply the policy.
Phase 3 — Reference sweep (1 PR)
- Update CLAUDE.md trigger row pointing at
policies/self-hosted-first.kmd. - Update
policies/web-server.kmdto declare itself as an applied case ofself-hosted-first(Koder Jet pair); the existing decree stands. - Update CLAUDE.md § Arquitetura de Referência (kodec Flipping Point) to cite the policy and the registry as the formal home for the decision.
Phase 4 — koder.toml schema strengthening (1 PR)
- Add
categoryfield tokoder.tomlwith enum{ product, engine, infra-tooling, runtime-lib, protocol, sdk-binding }. - Make
[self_hosted]block *andatory*for components whosecategory∈{ engine, infra-tooling }AND whose name matches the anti-drift heuristic list. koder-spec-audit self-hosted --validatebecomes strict (build-blocking) for missing blocks where the rule applies.- Retrofit existing components to declare
category(mostly mechanical).
This phase is automatically picked up by the mechanism in policies-RFC-003-rfc-phase-pickup.md once Phase 3 lands; no manual ticket needed.
Phase 5 — Substantive behavioural audits (1 PR per audit, batched)
koder-spec-audit self-hosted --gate-checkvalidates that gatespassed claims are backed by evidence (benchmark file exists; productionref hosts respond; CHANGELOG markers present).- Audit fails CI if a component declares
status = officialfor a case while itsgates_pendingcontains a non-empty gate for that case. - Anti-drift heuristic moves from advisory to strict.
6. Decisions to ratify before Phase 1 lands
- *olicy name.*Three candidates:
self-hosted-first(favoured),koder-stack-first,dogfood-first. Recommendation:self-hosted-first— describes the criterion (the alternative is hosted inside the Stack) without invoking the cultural connotation of "dogfood". - *ate count and granularity.*Five gates as proposed, or six (split G4 capability into "language
level capability" + "operational capability")? Recommendation: five. Split capability percategory (each sub-policy underpolicies/reuse/already does the split for its own kind of code). - *tatus enum*
experimental | stable | official | legacy. Add a fifth valuerecommendedbetweenstableandofficial? Recommendation: no. Three live states + one terminal (legacy) is simpler; the official/stable distinction already captures "ready by default" vs "ready with consideration". - *uthority over status transitions.*Who marks a component as
official— the maintainer of the selfhosted component or a Stacklevel review? Recommendation: the maintainer proposes via PR; Stack-level review (Rodrigo or designate) approves the transition toofficial.experimental → stableis the maintainer's call. - *ranularity of the case string.*Free
form (as proposed) or preregistered case taxonomy? Recommendation: free-form for now. Pre-registration is overhead; the registry already groups cases as they emerge.
7. Risks and mitigation
| Risk | Mitigation |
|---|---|
Components forget to declare [self_hosted] block when they should |
Phase 4 makes the block mandatory for category ∈ { engine, infra-tooling }; anti |
Status creep (official declared without gates clearing) |
Phase 5 audit fails CI on official + gates_pending |
| Premature migration of legacy code | The policy applies only to new code; legacy follows runtime-lib-first.kmd § Strict SemVer deprecation cycles |
| Benchmark gaming (component picks favourable benchmarks) | benchmark_ref is a path; review checks the benchmark covers the case under test |
| AI sessions caching stale policy text and ignoring registry | Decision protocol (§3.5) is explicit: "reading the registry"; not "applying the policy from memory" |
8. Alternatives considered
8.1 Per-pair RFCs (status quo)
Pro: each decision gets the depth it deserves. Contra: each pair rederives the framework; crosspair consistency is accidental; new pairs (koder-talk vs Matrix; kdb-next vs PostgreSQL) start from scratch. Rejected.
8.2 Single koder-stack-first.kmd policy with hardcoded list of pairs
Pro: simpler than this proposal. Contra: every new component requires a policy edit; same drift problem reuse-first.kmd solved with the registry approach. Rejected.
8.3 Evaluate gates only at code-review time, not in koder.toml
Pro: no schema change. Contra: AI sessions and asynchronous reviews lose the sourceoftruth; reviewer cognitive load increases. Rejected.
8.4 Treat self-hosted-first as a subpolicy under `reusefirst/`
Pro: locality. Contra: the axis is genuinely different — reuse-first is "share Koder code with other Koder modules"; self-hosted-first is "prefer Koder code over nonKoder code". The two compose: a UI widget might be reusefirst scope (share between Koder products) AND selfhostedfirst scope (use koder_kit instead of an external Flutter widget). Sibling, not child. Rejected.
9. Open implementation notes
- The
[self_hosted]block format may benefit from validation via JSON Schema published undermeta/docs/stack/specs/koder-toml/. Defer until a second consumer ofkoder.tomlvalidation appears. - The registry generation step is mechanical and a candidate for
code-first.kmdpromotion to a binary on day one. Phase 2 ships the binary, not a markdown step. - The cross
link between this RFC's Phase 4 and `policiesRFC003rfcphasepickup.md` is intentional: Phase 4 is a futuretense item that this RFC alone cannot guarantee will be picked up. The autopickup mechanism from RFC-003 covers it generically.
10. Status / next steps
- This RFC is *roposed* It needs ratification (status → *ccepted* before any phase lands.
- On ratification, Phase 1 is one PR (1 day's work to author the policy and the schema description).
- Phases 2 onwards are paced by the components' maintainers' adoption of the
[self_hosted]block.