Zarni Hlawn at his desk
Zarni Hlawn.
Software Engineer & Architect

Full-stack documentation CMS — Postgres-backed Markdown, hierarchical chapters, Cmd+K search, and a single-admin GitHub OAuth panel. CS50x final project.

Repository

Docs

Full-stack documentation CMS — Postgres-backed Markdown, hierarchical chapters, Cmd+K search, and a single-admin GitHub OAuth panel. CS50x final project.


Overview

Docs is a personal documentation platform: public readers get a searchable, categorized site with nested pages, breadcrumbs, previous/next navigation, and a table of contents; the author signs in with GitHub and manages everything from a browser admin. Content is Markdown stored in Neon Postgres — publish, unpublish, and reorder without git commits or redeploys. It was built as the CS50x final project and is the canonical full-stack reference app in the zarnihlawn.com monorepo.


Why not static files?

Static generators are excellent for versioned engineering docs, but they couple every typo fix to a deploy. Docs keeps the authoring loop in the database: draft preview for the logged-in admin, 404 for anonymous visitors on unpublished pages, SQL ILIKE search across title and body, and drag-and-drop sort order on the dashboard. Hierarchy uses a self-referential parentDocumentId with a hard max depth of three so sidebars and breadcrumbs stay readable.


Public experience

  1. Landing (/) — hero, feature grid, tech stack, code preview, and category entry points; all editable via site_settings without shipping new code
  2. Docs index (/docs) — published list with ?q= server search
  3. Doc pages (/docs/[slug]) — marked + DOMPurify HTML, category sidebar tree, TOC (title + H1/H2), breadcrumbs, prev/next
  4. Search modal — Cmd+K / navbar trigger hitting /api/search with highlighted snippets
  5. Draft preview — admin sees unpublished pages with a banner; everyone else gets a hard 404

Admin CMS

Routes under /admin require a Better Auth session. The first GitHub sign-up becomes the sole admin; databaseHooks reject later registrations so the site stays single-author.

  • Dashboard — document stats and DocumentOrderPanel drag-and-drop reorder within category/parent groups
  • Documents — list, create, and edit; CodeMirror Markdown editor with manual save only (no autosave)
  • Categories — CRUD with sort order (seed defaults: getting-started, guides, reference, examples, changelog)
  • Tags — many-to-many via document_tag
  • Settings — branding, landing copy/JSON sections, default publish/theme, favicon upload, sign-out

Content model

Core tables use UUID primary keys (defaultRandom). Documents require a category, optional parent, excerpt, published flag, and timestamps. site_settings is a singleton row (id = default) holding hero CTAs, tech stack/features JSON, footer social links, and theme defaults (system / winter / night). Slugs auto-generate from titles when left blank and validate as kebab-case.


Markdown pipeline

Server-side marked parses GFM; isomorphic-dompurify sanitizes before {@html}. Heading extraction feeds the right-rail TOC. Search highlight utilities wrap matching snippets for the modal and index results — unpublished content never leaks into anonymous search.


Validation & safety

Every form action and API handler runs Zod 4 safeParse before touching the database. Invalid input returns fail(400) with field errors for the UI. Route params (slug, UUID) are validated the same way. Admin multi-field forms use AdminFormTable — a fixed four-column label/field grid — instead of ad-hoc CSS grids so settings and document forms stay aligned.


Auth model

  1. GitHub OAuth only — email/password disabled
  2. disableImplicitSignUp after bootstrap; login UI flips Sign up → Sign in when hasAdmin() is true
  3. hooks.server.ts wires Paraglide locale, Better Auth session, and idempotent DB seed
  4. Admin layout redirects to /login when locals.user is missing

Stack

SvelteKit 3 next + Svelte 5 (runes, remoteFunctions experiments in vite config) · Neon Postgres + Drizzle ORM + drizzle-kit migrations · Better Auth + Drizzle adapter + sveltekitCookies · Zod 4, Tailwind CSS 4, daisyUI 5 (winter/night) · CodeMirror 6, marked, isomorphic-dompurify · Paraglide (English, Burmese, Japanese) · Vitest + Playwright; adapter-node for deployment


Design decisions

  1. Database-backed Markdown — publish without redeploy; enables SQL search and draft preview
  2. Single-admin GitHub OAuth — personal docs do not need multi-tenant identity
  3. Depth-capped hierarchy — keeps navigation honest instead of infinite nesting
  4. Zod at every boundary — form data, query strings, and JSON bodies never reach Drizzle raw
  5. Manual save in the editor — authors choose when content becomes durable

What I learned

Building Docs forced end-to-end ownership of HTTP, auth, SQL, validation, and UX in one TypeScript codebase. Patterns from this app — AdminFormTable, page-title helpers, closed registration, service-layer tree helpers — became the template for other SvelteKit products in the monorepo.

enmyja