The Problem
Self‑hosting a personal‑blog platform typically requires wiring a front‑end, an API, background workers and a database together. Existing open‑source solutions either expose a monolithic codebase that is hard to reason about, or they lack the scaffolding (Docker, CI, CI‑gate) needed for a production‑grade deployment.
What This Does
btw ships four loosely coupled projects that together implement a Medium‑style publishing stack:
| Project | Role | Key files |
|---|---|---|
| list | Front‑end UI & public site server | list/src/actions/app.ts, list/src/containers/ListContainer.jsx, list/Dockerfile |
| writer | Authenticated editor UI | writer/src/Root.jsx, writer/src/actions/index.ts |
| tasks | Background job runner & email/AI services | tasks/app.js, tasks/services/db.js, tasks/services/queue.js |
| publisher | Content publishing API & note handling | publisher/logic/notes.js, publisher/Dockerfile |
All projects share a common toolchain: React, Tailwind, Express, TypeScript, Docker and npm. The deploy/docker-compose.dev.yml orchestrates the four containers and a Postgres instance.
How It Is Wired
Execution starts in each container’s Dockerfile. For the public site the entry point is list/src/actions/app.ts, which creates an Express server, loads environment from .env, and mounts the React build (list/assets/index.html). Requests flow through the actions index (list/src/actions/index.ts) – the most‑connected module (8 inbound, 8 outbound imports, instability 0.5) – then to specific containers (e.g., ListContainer).
The editor (writer) follows the same pattern, bootstrapping via writer/src/actions/index.ts (7 ↔ 7 imports).
Background processing is driven by tasks/app.js, a hub with 16 outgoing imports (instability 1). It spawns workers defined in tasks/services/db.js (12 inbound, 0 outbound – a stable data layer) and tasks/services/queue.js (11 inbound, 0 outbound). These services are the only modules that interact with the PostgreSQL database, using connection strings from deploy/docker-compose.dev.yml (TASKS_DATABASE_URL).
The publishing API (publisher) imports shared utilities from the other projects but remains isolated; its main logic resides in publisher/logic/notes.js.
No circular dependencies were detected across the 350 analyzed files, but several hub modules (list/src/actions/index, tasks/logic/user) concentrate import traffic, meaning changes there have a wide blast radius.
How To Use It
# Clone the repo
git clone https://github.com/moses-y/btw.git
cd btw
# Install npm dependencies for each sub‑project
npm ci # root lockfile (no scripts)
cd list && npm ci && cd ..
cd writer && npm ci && cd ..
cd tasks && npm ci && cd ..
# Prepare runtime configuration
cp .env.example .env # edit ADMIN_EMAIL, ADMIN_SLUG, SECRET, etc.
# Variables are read by the Docker compose file.
# Start the stack (Docker required)
docker compose -f deploy/docker-compose.dev.yml up --build
The public site becomes reachable at http://localhost:3000, the editor at http://localhost:3001, and the task worker runs in the background. Logs are streamed to the console; the Procfile in list/ defines the web process for Heroku‑style runtimes.
Real‑World Use
A SaaS provider could deploy btw per customer to give each a white‑label blog. The provider would run the Docker compose stack on a Kubernetes pod, replace the hard‑coded ADMIN_EMAIL/SLUG with per‑tenant values, and point the S3 configuration (S3_ENDPOINT, S3_BUCKET, etc.) at a tenant‑scoped bucket. Content authored in writer is persisted via the API, queued for background processing (e.g., email notifications), and finally served by list.
Code Health & Issues
Measured findings (static analysis)
- High cognitive load – deep nesting (28 occurrences) and oversized files (11 ≥ 688 LOC) in
tasks/services/db.js,tasks/logic/ai.js,list/src/containers/IntelligenceContainer.jsx. - Medium resilience – empty catch blocks (
list/src/fingerprint.js, etc.). - Medium clarity – hub modules (
tasks/logic/user.js,tasks/services/db.js). - High clarity – duplicated 6‑line blocks across config files.
- Medium cognitive load – high branching density in
publisher/logic/notes.js, etc.
Code‑health audit
- HIGH – Add a CI build gate for
list/Dockerfile. - HIGH – Remove the committed
.envand rotate its secrets. - MEDIUM – Enable Dependabot/Renovate for dependency updates.
- MEDIUM – Pin Docker base image by digest in
list/Dockerfile. - MEDIUM – Add a pre‑commit secret‑scan hook.
- MEDIUM – Run container as a non‑root user (
USERdirective missing). - LOW – Add repository convention files (
.editorconfig,.gitattributes, formatter config).
Additional hygiene notes: tests exist (164 files) and CI runs via Travis (.travis.yml) and GitHub Actions (.github/workflows/push.yml). A licence file is present, but the repository currently ships a secret‑filled .env.
The Bottom Line
btw offers a functional, Docker‑ready stack for self‑hosted personal blogs, with clear separation between front‑end, editor, and background workers. However, the codebase suffers from deep nesting, large modules and duplicated configuration, and it ships a committed .env that must be removed before production use. It is suitable for teams comfortable refactoring high‑impact hub modules and tightening the DevSecOps pipeline.