Architecture
Plate is a read-only consumer of the Snackbox REST API — it fetches content, renders Markdown to HTML via a Remark/Rehype pipeline, and serves the result through a Handlebars theme. It has no database, no authentication, and no admin interface.
For deployment and operations, see docs/ADMINISTRATOR.md. For contributing to the codebase, see docs/DEVELOPER.md. For building a custom theme, see docs/THEME-DEVELOPER.md.
Deployment topology
graph TD
Browser["Browser"]
Caddy["Caddy\n(80 / 443)"]
Plate["Plate\n(3000)"]
Snackbox["Snackbox\n(8080)"]
Prometheus["Prometheus"]
Metrics["Metrics endpoint\n(9091)"]
Browser -->|"HTTPS"| Caddy
Caddy -->|"/media/*"| Snackbox
Caddy -->|"all other requests"| Plate
Plate -->|"REST API"| Snackbox
Prometheus -->|"scrape"| Metrics
Plate -->|"exposes"| Metrics
| Port | Component | Notes |
|---|---|---|
| 80 / 443 | Caddy | Public-facing; TLS termination; routes /media/* to Snackbox |
| 3000 | Plate | HTTP only; bind to 127.0.0.1 or Docker network — never expose publicly |
| 8080 | Snackbox | REST API and media files |
| 9091 | Plate metrics | Prometheus scrape target; bind to 127.0.0.1 — never expose publicly |
Caddy is optional but recommended. Any reverse proxy that can terminate TLS and forward headers works. The /media/* proxy to Snackbox is required only if media assets are served from the same domain as Plate.
Application layers
graph TD
Request["Incoming request"]
subgraph Middleware
Helmet["Helmet\n(CSP, security headers)"]
Compression["Compression"]
RateLimit["Rate limiter"]
ReqMetrics["Request metrics\n(prom-client)"]
AccessLog["Access log\n(pino-http)"]
GlobalCtx["Global context\n(settings, nav, tags, social)"]
Helmet --> Compression --> RateLimit --> ReqMetrics --> AccessLog --> GlobalCtx
end
subgraph Routes
Home["Home"]
Posts["Posts"]
Pages["Pages"]
Tags["Tags"]
Health["Health"]
end
subgraph Core
SnackboxClient["SnackboxClient\n(HTTP + SimpleCache)"]
Renderer["Renderer\n(Remark → rehype → HTML)"]
ThemeManager["ThemeManager\n(hot-reload, fallback)"]
end
Request --> Middleware
Middleware --> Routes
Routes --> Core
Key design decisions
SnackboxClient + SimpleCache — All Snackbox API calls go through a single client singleton. Admin-controlled data (settings, navigation, social accounts, tags) is cached with a short 15 s TTL. Content (posts, pages) is cached with a 60–300 s TTL, scalable via CACHE_TTL_MULTIPLIER. Snackbox 1.2.0 will add webhook-based invalidation, replacing the short TTL for admin resources.
Theme hot-reload — The globalContext middleware compares the active theme name from Snackbox settings against the currently loaded theme on every request. On mismatch it awaits ThemeManager.reload() before rendering. Concurrent reload calls coalesce on a single in-flight Promise. Setting THEME_NAME disables hot-reload entirely.
Renderer — Markdown is converted to sanitized HTML via a Remark/Rehype pipeline. rehype-sanitize strips unsafe HTML before the output reaches any template.
Two HTTP servers — Plate starts a main Express server (port 3000) and a separate plain HTTP server for the Prometheus metrics endpoint (port 9091). This keeps metrics off the public request path without routing overhead.
src/fixture/ — A built-in Snackbox fixture server (startFixture / stopFixture) backed by realistic edge-case content. It uses Node.js built-in http only (no Express), ships in dist/, and is the single source of truth for integration tests, the benchmark, and theme:dev.
src/cli/ — The plate CLI entry point (npx plate theme:*). Thin wrappers around existing lib functions: validateTheme for validation, startFixture for live preview. The bin.plate field in package.json points here.