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:

ProjectRoleKey files
listFront‑end UI & public site serverlist/src/actions/app.ts, list/src/containers/ListContainer.jsx, list/Dockerfile
writerAuthenticated editor UIwriter/src/Root.jsx, writer/src/actions/index.ts
tasksBackground job runner & email/AI servicestasks/app.js, tasks/services/db.js, tasks/services/queue.js
publisherContent publishing API & note handlingpublisher/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 .env and 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 (USER directive 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.