Documentation
Architecture
How Briter's layers fit together: web app, content storage, adapter, queue, and worker.
Architecture
Briter is a Next.js 16 application with a Bun runtime. It is split into two processes in production: a web app and a git-sync worker. They share a filesystem but never share a process.
Layers
Web app (app/, components/, lib/)
- Owns the editor UI, public reader, dashboard, upload endpoint, and API routes
- Reads and writes canonical content files via
FileContentRepository - Enqueues git-sync work (writes a JSON file to
storage/queue/) — never git-pushes directly - Serves uploaded assets through a route handler (
/uploads/[...path]) - Two route groups:
(public)(open) and(admin)(session-gated)
Content storage
- Canonical source: Markdown or MDX under
content/posts/ - Frontmatter carries
title,summary,publishedAt,tags,draft - Uploaded assets:
storage/uploads/posts/<slug>/ - Each upload has a manifest JSON with responsive variant paths and dimensions
Adapter layer (lib/adapters/)
The adapter hides target-site conventions from the editor core:
SiteAdapter (interface)
├── LocalMdxSiteAdapter — self-hosted reader
└── GitTargetSiteAdapter — push to external repo
The adapter is selected at startup based on storage/config.json. New adapters implement SiteAdapter and register in adapter-registry.ts.
Queue + worker
DiskJobQueuewrites JSON job files tostorage/queue/, one pending job per slug (per-slug dedup)- The worker process reads due job files, validates paths via the adapter's allowlist, stages, commits, and pushes
- Backoff: 1 minute → 5 minutes → 30 minutes on failure. After
BRITER_WORKER_MAX_ATTEMPTS(default 8) the job is dead-lettered (failed) - A single-flight lock (
storage/worker.lock) with stale-lock recovery prevents concurrent workers - The web app and worker share
storage/via the filesystem (or a Docker volume)
See Worker & Queue for the full job lifecycle, Scheduling & archiving for held jobs, and Sections for the section content config.
Preview pipeline (lib/preview/)
A shared unified/remark/rehype pipeline powers:
- The editor's live preview (client-side, via API route)
- The public reader's post rendering
- The in-app docs at
/docs
The pipeline is decoupled from git and deploy concerns.
Data flow (publish)
Editor save
→ FileContentRepository.save()
→ DiskJobQueue.enqueue({ kind: 'git-sync', postSlug, paths: [...], scheduledFor })
→ Worker picks up the job once scheduledFor has arrived
→ validatePaths() checks paths against adapter allowlist
→ git stage, commit, push (skipped when BRITER_WORKER_DRY_RUN is true)
Config and secrets
storage/
├── config.json — site identity, adapter settings (644)
├── secrets.json — bcrypt hash, TOTP secret, session secret (600)
├── queue/ — job files written by web, consumed by worker
└── uploads/ — image variants and manifests
RuntimeConfig loads on startup via loadRuntimeConfig(). Changes written by the setup wizard or settings API take effect on next request.
Key modules
| Module | Purpose |
|---|---|
lib/config/runtime-config.ts |
Layered config: defaults → env → config.json |
lib/config/secrets.ts |
Read/write secrets.json with 0600 perms |
lib/content/file-content-repository.ts |
Post CRUD backed by the filesystem |
lib/adapters/site-adapter.ts |
Adapter interface and types |
lib/queue/disk-job-queue.ts |
Filesystem-backed job queue |
lib/worker/git-sync-worker.ts |
Git stage/commit/push with path validation |
lib/uploads/image-pipeline.ts |
Variant generation, EXIF strip, budget check |
lib/preview/markdown-preview.ts |
Shared unified/remark/rehype pipeline |
lib/docs/docs-loader.ts |
Loads and renders /docs/*.mdx |
ADR: Middleware proxy
See docs/decisions/001-middleware-proxy.md for the rationale behind the edge middleware + handler two-stage auth model.