Kingdom.md — MVP Design
A self-hosted Laravel web app for reading repositories of Markdown files
("kingdoms") with beautiful, fast rendering. This document is the validated
design for the MVP (the first deliverable) plus the roadmap that follows it.
It is the input to the planning skill (/planning), which will turn it into an
executable plan (working/mvp-plan.md).
Status: design validated 2026-06-10 (brainstormed + walked section by section). Ready for planning.
What this is
Kingdom.md is the human-facing, read-only window into Markdown corpora —
Obsidian vaults, agentic skill libraries, and an AI's own memory/, plans, and
design docs. The writer (e.g. Claude) updates flat files; Kingdom.md reads and
renders them. That decoupling is deliberate: the app never writes content, so
it poses zero risk to the write path.
It exists as a roll-your-own alternative to off-the-shelf viewers (e.g. Perlite) — for dogfooding, ecosystem fit, and because its headline differentiator, native in-app OAuth + RBAC, is the reason to build rather than deploy something else.
- First-class audience: self-hosted, multi-user instances (a team, family, or org sharing one deployment; single-tenant per deployment).
- Business posture: FOSS-first, with a possible SaaS later whose main
purpose would be to fund the FOSS (an open-core stance). The
kingdom.mddomain is reserved for docs/demo and to keep a SaaS pivot cheap. - Identity term: a served directory of Markdown is a kingdom (first-class vocabulary, kept throughout the product's evolution; leans into a light medieval identity for future marketing).
Governing principle
Three decisions, one principle, applied relentlessly through this design:
Lean MVP, additive roadmap, architecture that never paints the ambition out.
The sorting heuristic for what lands in the MVP vs. the roadmap:
The MVP claims what is high-quality for free out of the box. Anything that needs fiddling to land "just right" is deliberately roadmapped — so effort is spent intentionally, when there are spoons for it, not bolted on prematurely.
Concretely: choose data/model shapes now that make the deferred features (OAuth, rich RBAC, wikilinks, theming, Docker) additive migrations, not rewrites — but do not build them yet.
Understanding summary
- What: a self-hosted Laravel app for reading Markdown kingdoms, rendered beautifully and fast. Read-only viewer at the core; a far-future arc reaches toward a web-Obsidian (editing/file management), explicitly out of scope now.
- Why: a decoupled, human-facing reader over corpora that something else writes; a personal build over off-the-shelf for fit + dogfooding; OAuth + RBAC as the differentiator.
- Who: self-hosted multi-user first; FOSS-first with possible SaaS-to-fund-FOSS later. Brad is the first user.
- Success bar: elegant, minimal, fast. Ugly, clunky, laggy JavaScript, or too many menus/clicks = failure.
- Data: flat files for content (live-read each request); the database holds only Kingdom-specific metadata (users/groups/roles/settings — none of it exercised at MVP). DB-agnostic.
Assumptions
- The MVP is login-less — a pure renderer on the operator's own server; access is controlled at the network/server level. Local auth is the first post-MVP tier, not MVP itself.
- A kingdom = a served directory of Markdown; the MVP browse model is a file-tree sidebar + a rendered reading pane, with an always-available kingdom dropdown to switch the active kingdom (no separate landing page).
- The frontend is server-rendered (Blade + light Alpine), never a heavy SPA — directly serving the no-laggy-JS bar.
- Markdown is parsed server-side in PHP (
league/commonmark) to HTML on every request — no caching/Redis at MVP.
Scope
In scope — the MVP
A classic, "boring," stateless Laravel request/response app:
- Runs on bare php-fpm + nginx in
/var/www(not Docker yet). - Reads one or more configured kingdoms (named directory → filesystem path).
- File-tree sidebar (folders +
.mdfiles; hidden/system entries ignored) with an always-visible kingdom dropdown. - Click a file → full-page render of that Markdown: CommonMark + GFM essentials (tables, task lists, strikethrough, autolinks), with YAML frontmatter displayed as a styled metadata card.
- Code blocks render as plain monospace (no syntax highlighting yet).
- Beautiful baseline via Tailwind Typography (
prose). - No auth, no RBAC, no cache, no Redis, no theming.
Roadmap — deferred, in stated order
These are designed-for (not blocked) but not built at MVP:
- Rendering: syntax highlighting (an early follow) → embedded-image/asset serving (deliberately deferred to "do it right") → wikilinks + backlinks (needs a vault link-index) → expanded Markdown syntax → full Obsidian compatibility (callouts, embeds, math, Mermaid, graph view).
- Auth: local email/password (first post-MVP tier — via Fortify headless backend + a custom UI) → OAuth via major providers (GitHub/Google via Laravel Socialite) → generic OIDC.
- RBAC: per-kingdom scoping with a small set of fixed roles + groups → rich RBAC (user-defined roles via a permission-atom engine, per-directory/file overrides).
- UI/perf: dark mode → a few built-in themes → a full theming engine; SSR optimization + heavy Redis caching of rendered output.
- Frontend evolution: layer in Livewire when the first feature genuinely needs server-driven reactivity (search, RBAC admin UI), trending toward the full TALL stack.
- Packaging: Docker /
docker-compose upas the canonical adoption story (with its host-dir → volume-mount storage considerations). - Sources: Git-backed kingdoms, upload/app-managed storage, a pluggable source abstraction (MVP is filesystem-mounted only).
Explicit non-goals (now)
- No editing or file management (read-only).
- No authentication or authorization of any kind.
- No wikilinks, backlinks, graph, or search.
- No embedded-image/asset serving.
- No theming or dark mode.
- No Docker, no caching layer, no SSR optimization.
- No Git/upload/pluggable sources.
The stack
- Laravel 13 (current stable), clean install — no official starter kit. The Laravel 12+ starter kits are fresh-install-only full project templates; the only one fitting our stance (Livewire + Flux UI) would force auth + Livewire into the MVP, against the login-less / lean / incremental-Livewire decisions. Auth arrives later via the add-anytime path: Laravel Fortify (headless auth backend — routes/controllers/logic, no views) + a custom Blade/Livewire login UI, with Socialite for OAuth. (Fortify, Breeze, and Socialite are all retrofittable; the starter kits are not.)
- Blade templates, classic multi-page, full-page renders. Alpine.js only for micro-interactions (tree expand/collapse). No SPA.
- Tailwind CSS + Tailwind Typography (
@tailwindcss/typography, theproseclasses) as the beauty engine. league/commonmark— front-matter extension + GFM extensions. Chosen for extensibility (the wikilinks/Obsidian roadmap needs custom inline parsers).- Database: DB-agnostic from day one (Eloquent migrations). SQLite for local dev, MariaDB for dev/test, Postgres supported. Dormant at MVP.
- End-state: full TALL stack, assembled incrementally; never the heavy Inertia+Vue/React SPA (the laggy-JS risk named as a failure).
Design
1. Architecture & request lifecycle
A stateless request/response app — no SPA, no API layer, no queue, no cache.
Components
- Kingdom config —
config/kingdom.php(env-driven), mapping named kingdoms to filesystem paths. Multiple kingdoms supported at MVP. - Kingdom (filesystem) service — scans a kingdom into a file tree, resolves a requested relative path to a real file, reads contents. Owns path safety and filtering.
- Markdown renderer — raw Markdown → CommonMark (front-matter + GFM) →
returns
(frontmatter array, HTML body). - Controllers + routes — list/redirect, show-kingdom, show-file.
- Blade views (Tailwind) — one layout: top bar + kingdom dropdown + file-tree sidebar + reading pane.
- Database — dormant at MVP; user/group/role tables arrive with the auth tier.
Request flow (every load, full page, re-parsed):
click a file in the tree → GET /{kingdom}/{path} → service validates the path is inside the kingdom root → reads the file → renderer returns frontmatter + HTML → Blade renders the full page (sidebar + content).
2. Navigation & file access
Layout (one Blade shell):
- Slim top bar: the kingdom dropdown (always visible) + app name.
- File-tree sidebar for the active kingdom — nested folders +
.mdfiles, folders collapsible via Alpine. - Reading pane — the rendered file.
URLs (clean, shareable):
/→ redirect to the default kingdom./{kingdom}→ that kingdom; opensREADME.md/index.mdif present, else a gentle "pick a file" empty state./{kingdom}/{path}→ a specific file.
The Kingdom service owns all file access and safety:
- Path safety: resolve
{path}against the kingdom root, canonicalize, and reject anything that escapes the root (../traversal → 404). This is the one security-critical bit in a login-less app. - Tree building: recursively scan the kingdom root; show folders +
.mdfiles only; ignore hidden/system entries (.git,.obsidian,node_modules, dotfiles). - Config validation: a kingdom whose configured path doesn't exist on disk fails loudly at boot, not mysteriously at request time.
3. Rendering & the "beautiful" baseline
- Parse pipeline:
league/commonmarkwith the front-matter extension (splits YAML frontmatter from the body) and GFM essentials (tables, task lists, strikethrough, autolinks). Code blocks → plain<pre><code>monospace (no highlighting yet), Tailwind-styled for cleanliness. - Frontmatter display: render the parsed YAML as a styled metadata card at
the top of the document — distinct from the body (e.g.
name/description/typeon memories,statuson designs). Collapse-to-toggle comes later. - Beauty engine: Tailwind Typography (
prose) wrapped around the rendered HTML — tasteful defaults for headings, lists, code, blockquotes, tables out of the box; constrained content width, comfortable line-height. The baseline that makes "basic Markdown" look gorgeous on day one, refined as spoons allow.
4. Project structure
config/kingdom.php— the name→path registry (env-driven).app/Services/KingdomService.php— scan → tree, path resolution + traversal guard, file read.app/Services/MarkdownRenderer.php— the CommonMark environment; returns(frontmatter, html).app/Http/Controllers/KingdomController.php— index (→ default kingdom), show-kingdom, show-file.routes/web.php— the three routes.resources/views/—layout.blade.php(top bar + dropdown + sidebar + pane),partials/tree.blade.php(recursive, Alpine collapse),show.blade.php(frontmatter card +prosebody).- DB dormant at MVP — connection configured portably (SQLite default for dev), no domain migrations until the auth tier.
5. How you'll know it works
Manual smoke: point a kingdom at a folder, load it — the tree lists the right
files (no .git/.obsidian), clicks render cleanly, the frontmatter card shows,
tables/lists/quotes look polished via prose, code is clean monospace, loads feel
instant.
Automated (Pest feature tests) against a fixture kingdom committed in tests/:
- tree-building lists expected files and omits hidden/system entries;
- a file route returns 200 with the expected rendered HTML + parsed frontmatter;
- the security test — a
../-style traversal request is rejected (404), never escaping the root; - unknown kingdom / missing file → 404; empty kingdom → empty state.
These tests double as the planning skill's Confirm-by anchors, with the traversal test guarding the one security-critical path in a login-less app.
Decision log
| Decision | Alternatives considered | Why |
|---|---|---|
| Self-hosted multi-user is first-class; FOSS-first, SaaS-later-to-fund-FOSS | Purely personal; multi-tenant SaaS from day one; self-hosted with no SaaS ever | Matches the real ambition (teams on one instance) without paying for tenant isolation now; open-core keeps the SaaS pivot cheap and aligned with funding the FOSS. |
| Filesystem-mounted, live, read-only content (MVP) | Git-backed; upload/app-managed; pluggable sources | Simplest live "Claude writes / app reads" decoupling; other sources are additive later. |
| DB holds only Kingdom metadata; DB-agnostic | DB-backed content (BookStack-style) | Keeps content live + flat; portability (SQLite/MariaDB/Postgres) serves diverse self-hosters. |
| Per-kingdom RBAC w/ fixed roles + groups (when RBAC lands); rich RBAC later | Full configurable engine + UI at v1; global roles only; minimal | Read-only verb set is thin, so custom roles buy little now; ship a working RBAC, make it sing later. Data model kept additive. |
| Local auth first → OAuth (big providers) → OIDC | Federated-only; big-providers-only; local-only | Trivial Docker/first-run bootstrap; incremental delivery of the "OAuth" headline; OIDC (table-stakes for self-hosters) sequenced but not first. |
| MVP is login-less | Minimal login even at MVP | "Just see it on my screen and click a tree"; keeps the first plan off the user system entirely. |
| Basic-but-beautiful rendering; GFM essentials in, fancy out | Minimal CommonMark; full Obsidian-grade incl. graph | "Free now vs. tinker-later": GFM essentials are high-quality out of the box and the corpus needs tables; wikilinks/graph/etc. need real fiddling → roadmap. |
| Code as plain monospace at MVP | Server-side highlighting now; client-side highlighter | Highlighting is an early follow but not free-enough for MVP; plain monospace is honest and clean. |
| Embedded images deferred | Minimal safe asset route in MVP | Brad chose to do asset handling right later rather than bolt it on; near-term corpus is text-first. |
| Server-rendered Blade + Alpine + Tailwind; Livewire incrementally → TALL | Full TALL (Livewire) from day one; Inertia+Vue/React SPA | Don't pay for Livewire until a reactive feature earns it; SPA is the named laggy-JS failure risk. |
| Laravel 13, clean install, no official starter kit; auth added later via Fortify + Socialite | Start from the fresh-only Livewire starter kit (Fortify + Flux UI) now; pin Laravel 12 | Laravel 12+ starter kits are fresh-install-only full templates; the Livewire kit would force auth + Flux + Livewire into a login-less/lean MVP. Fortify (headless), Breeze, and Socialite are all add-anytime, so a clean install forgoes nothing the incremental plan wants. Target the current stable (13) for a greenfield app. (Research-backed 2026-06-10.) |
Tailwind Typography (prose) as the beauty engine |
Hand-rolled CSS; classless CSS framework | Highest-leverage "beautiful for free"; a strong baseline to refine. |
league/commonmark parser |
Parsedown; php-markdown | Spec-compliant + extensible — the wikilinks/Obsidian roadmap needs custom inline parsers. |
| Multiple kingdoms + a persistent dropdown at MVP | Single root; a landing page listing kingdoms | "Kingdoms" is first-class vocabulary; a dropdown is fewer clicks than a landing page. |
| No Redis/cache; full-page re-parse per load | Redis-cached SSR at MVP | Simplest robust thing; performance work is spoon-gated and additive. |
Open items for planning
- The precise
league/commonmarkextension set + config for the GFM-essentials floor. - Fixture-kingdom contents for the Pest test suite.
- Whether the MVP runs any migrations at all, or defers the DB connection setup to the auth tier (leaning: configure portably, no domain migrations).
- Which kingdom is "default" for the
/redirect — leaning: the first configured kingdom unless an explicitdefaultkey is set inconfig/kingdom.php.