# Architecture Demarkus is a Go monorepo with four modules connected by local `replace` directives. ## Module Boundaries - **protocol/** — Wire format types, parsing, serialization. No network code. The foundation everything else depends on. - **server/** — QUIC server. Accepts Mark Protocol requests over QUIC streams, routes to handlers. Depends on protocol. - **client/** — CLI, TUI (Bubble Tea), and MCP server. All three share the same protocol client. Depends on protocol. - **tools/** — Dev utilities like `demarkus-token` for generating auth tokens. ## Key Design Decisions ### Stream Abstraction `handler.Stream` is just `io.ReadWriteCloser`. This means handler tests don't need QUIC — they use mock streams (bytes.Buffer or similar). The entire handler layer is testable without network setup. ### Frontmatter as map[string]string Parsed frontmatter uses `map[string]string` rather than typed structs. This avoids YAML auto-typing issues (e.g., `yes` becoming `true`) and keeps the parsing layer simple. Type conversion happens at the handler level where context is known. ### Versioned Store with Symlinks Documents are stored as versioned files with symlinks pointing to current: ``` content-root/ doc.md -> versions/doc.md.v3 versions/ doc.md.v1 doc.md.v2 doc.md.v3 ``` Each version is immutable once written. SHA-256 hash chain links versions together for integrity verification. ### Capability-Based Auth Tokens grant capabilities, not identities. Server stores SHA-256 hashes in TOML, never raw tokens. Each token is scoped to path globs and specific operations. The server doesn't know or care who you are — only what you're allowed to do. ### Path Traversal Defense `filepath.Clean` + `..` check. Returns `not-found` (not `forbidden`) to avoid leaking information about server structure. ### Status as Text Human-readable status strings (`ok`, `not-found`, `conflict`) instead of numeric codes. Fits the markdown-native philosophy — everything should be readable. ### Size Limits All protocol-level size constants live in `protocol/request.go`: | Limit | Constant | Value | |---|---|---| | Request line | `MaxRequestLineLength` | 4096 bytes | | Request metadata | `MaxRequestFrontmatterLength` | 64 KiB | | Document body | `MaxBodyLength` | 1 MiB | `ParseRequest` enforces these at the parsing layer using `io.LimitReader` to prevent unbounded memory allocation. The store adds a small allowance (`maxStoreFrontmatter = 256`) for its own version/hash-chain frontmatter when checking on-disk file sizes. ## Verb Set The protocol now covers the full document lifecycle: | Verb | Purpose | |---|---| | FETCH | Read a document | | LIST | List directory contents | | VERSIONS | Get version history | | PUBLISH | Create or update a document (new immutable version) | | APPEND | Append content to an existing document (new immutable version) | | ARCHIVE | Soft-delete a document | APPEND reuses the same auth (`publish` capability), versioning, hash chain, and optimistic concurrency infrastructure as PUBLISH. The key difference: APPEND sends only new content, the server concatenates it with the existing body. Document must exist (returns `not-found` otherwise), and combined size is checked against `MaxBodyLength`. ## Wire Format **Request**: `VERB /path\n` followed by optional YAML frontmatter and body. **Response**: YAML frontmatter (always includes status) + markdown body. Protocol constants: port `6309`, ALPN `"mark"`, scheme `mark://`. ## What I've Internalized The architecture rewards simplicity. When I'm tempted to add abstraction, the right move is usually to keep the function short and the flow obvious. Fritz and I have a shared instinct here: if a solution feels clever, it's probably wrong. The best code in this project reads like prose. ### Audit Logging All write operations (PUBLISH, APPEND, ARCHIVE, UNARCHIVE) emit structured slog records tagged with `"audit", true`. Each audit line includes: operation, path, version, success/failure, and `token_label` (the TOML key identifying which token made the write, e.g., `"fritz-laptop"`). Key design constraint: **identity information stays in server-side logs only.** Token labels are never stored in version files or exposed through the protocol. This preserves capability-based auth — tokens grant what you can do, not who you are. The operator controls log retention and persistence through their infrastructure (systemd journal, logrotate, etc.). Token labels are sanitized before logging to prevent control-character injection, consistent with how request paths are handled.