- T01: Go module (keycape), full directory skeleton, Makefile, CI workflow - T02: spec/canonical-model.yaml with 6 entities + Go domain types - T03: spec/ldap-schema.yaml + validator binary with structural/semantic rules - T04: Error taxonomy — 4 stable error types, JSON format, HTTP helpers 28 tests pass, go vet clean, go build clean. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4.2 KiB
id, title, status, date, decided_by, hub_decision_id, workstream, alternatives_considered
| id | title | status | date | decided_by | hub_decision_id | workstream | alternatives_considered | |
|---|---|---|---|---|---|---|---|---|
| ADR-0001 | Implementation language for KeyCape: Go | accepted | 2026-03-13 | Bernd | 620beb04-fa3f-4a9d-9806-02890a7a2b0d | KEY-WP-0001 (keycape-implementation) |
|
ADR-0001 — Implementation Language: Go
Status
Accepted
Context
KeyCape must be implemented in a language that satisfies the requirements of spec §11:
Keycape SHOULD be implemented in Go or Rust. Key requirements: stateless, small memory footprint, simple deployment, clear logging, structured telemetry.
Both Go and Rust are valid per spec. A decision was needed before T01 (project setup).
What KeyCape actually does
KeyCape is an orchestrating boundary service, not a protocol or security engine:
- delegates authentication UI and session to Authelia
- delegates identity storage to LLDAP
- delegates MFA enforcement to privacyIDEA
- its own code is: HTTP endpoints, config loading, JWT signing (via library), JSON/JWKS handling, structured telemetry, adapter glue, migration CLI tooling, LDAP schema validator
The main risks are understandability and clean integration boundaries, not memory safety in hot loops or complex parser internals.
Decision
Go.
Rationale
Why Go fits KeyCape
Go is especially strong for the actual implementation surface of KeyCape:
| What KeyCape does | Go fit |
|---|---|
| HTTP API server | excellent |
| Config loading and static validation | excellent |
| Adapter code to Authelia, LLDAP, privacyIDEA | excellent |
| JWT/JWKS/JSON handling | excellent |
| Structured logging and Prometheus metrics | excellent |
| CLI tooling (migration, validator, export) | excellent |
| Integration tests in containers | excellent |
Go also provides:
- faster iteration to a working, testable v1
- simpler dependency and build model
- easy static binaries and minimal container images
- low enough runtime overhead for the stated lightweight target
- straightforward output for coding agents across a growing infra codebase
Why not Rust for this scope
Rust's advantages are real — stronger compile-time safety, better memory control, excellent for security-critical infrastructure — but they pay back most clearly when:
- substantial protocol machinery is implemented internally
- complex async concurrency or parser-heavy code is required
- the service is intended as a long-lived, standalone security product for others
KeyCape's design intentionally avoids all of that. It stays narrow and delegates to existing components. For a façade/orchestrator, developer friction matters more than theoretical maximal correctness.
Decision rule
Pick Go if KeyCape is primarily an orchestrating boundary service. Pick Rust if KeyCape starts becoming a real protocol/security engine.
Based on the v0.1 spec, KeyCape is clearly the first.
Consequences
Positive
- fastest path to a working, testable, operationally simple implementation
- simple build, deploy, and CI story
- lower friction for coding agents producing coherent infra code
Negative / risks
- weaker compiler-enforced invariants than Rust
- easier to write sloppy code if discipline lapses
- error handling and domain modeling can drift if not designed carefully
Compensating guardrails (mandatory)
To recover the rigor that Rust would provide via the type system:
- Typed domain models for the profile contract — no raw maps or untyped JSON in business logic
- Narrow adapter interfaces —
server/layer never sees LDAP, Authelia, or privacyIDEA types directly - Layered architecture — protocol layer | domain layer | adapter layer | migration layer (hard boundaries)
- Strict schema/config validation at startup and in CI
- Fuzz and property tests around the LDAP schema validator, redirect URI checker, and claim mapping
- No cleverness — small, deterministic functions
Revisit trigger
Reconsider this decision if a subcomponent (e.g. LDAP schema validator, token normalization engine, or a future high-throughput policy evaluator) demonstrably needs stronger guarantees. That subcomponent could be redesigned in Rust without regretting the overall Go choice — Go and Rust interop via CGo or separate binaries is feasible.