Context / Problem

I wanted a weekly planner that behaves the way I actually think about a week: seven columns (Mon–Sun), tasks sitting in the day they’re due, and a few loose lists below for everything that has no date yet (inbox, groceries, someday). No reminders, no subtasks, no labels, no calendar integrations — I explicitly did not want any of that. Existing products were either too heavy or too slow, so I built my own. Zweek is a “simple as hell” week planner.

One requirement shaped the whole architecture: I use it on my phone and on my desktop, often at the same time. If I check a task off on mobile, it must already be checked on the web. Real-time sync across devices was a core feature from day one, not an afterthought.

Constraints

  • Keep it small — in code size and in mental overhead. A one-person project needs a stack I can hold entirely in my head.
  • Data only, no files — no attachments, no uploads, no media pipeline. Everything the app stores is structured data, which keeps the model, the API, and the sync layer simple.
  • Real-time across devices with the minimum possible moving parts.
  • Self-hosted — my own VPS, my own database, my own CI runner.
  • Private by default — every calendar, list, and task belongs to exactly one user.

Architecture Decisions

Vue 3 for the frontend. Vue 3 with <script setup> + TypeScript, Vite, Pinia for state, and Tailwind CSS v4. It’s the framework I’m most productive in, and it stays out of the way for a small SPA: the planner grid is the whole app, plus auth and a landing page. Drag-and-drop uses native HTML5 events instead of a DnD library — full control over the drop payload and zero dependencies.

Hono over a heavier backend. The API runs on Hono over @hono/node-server (Node 22+). Hono is lighter than the usual suspects and built on Web Standards, so the entire backend reads as a handful of focused route files instead of framework ceremony. For an API this size, that’s the difference between maintaining a framework and maintaining your app.

MySQL over Turso. The first version ran on Turso (libSQL). It worked until it didn’t: latency from my region, few edge locations, and intermittent connection failures that made the app feel broken. A local MySQL instance on my own VPS eliminated all three problems — the database sits next to the API, so every query is a local round-trip. Drizzle ORM stayed in place, so the swap cost was a driver change plus a few type-level adjustments (bigint timestamps, date strings).

WebSockets for real-time sync. The server keeps a WebSocket per user (multiple tabs share the same socket set) and broadcasts task and list mutations to that user’s open sockets. The client applies incoming messages directly to the Pinia store and reconnects with exponential backoff, stopping only when the session is genuinely invalid. That’s how checking a task on mobile shows up instantly on the web.

Data only, no files. Everything Zweek stores is a row in MySQL — users, calendars, lists, tasks. The payoff is a tiny API surface (auth, calendars, lists, tasks, week) and a sync protocol that only has to think about JSON.

GitLab over GitHub. GitHub’s status has been unstable lately, and for a project I depend on daily I’d rather not have the platform’s health be a moving target. GitLab hosts the repository and runs the CI/CD pipeline.

My own GitLab runner. The pipeline runs on a self-hosted runner tagged vps — the same infrastructure the app deploys to. Every push to main builds the server and web bundle, ships them to the VPS over SSH/rsync, runs migrations, and restarts the PM2 process. No cloud minutes, no artifact round-trips: the runner and the deploy target live next to each other.

Per-list privacy. All data is owner-scoped by the model — a user can only ever read or write their own calendars, lists, and tasks. Personal lists get explicit privacy treatment on top of that, so sensitive lists (a grocery list, a personal journal list) never surface anywhere they shouldn’t.

Ordering that costs one write. Drag-and-drop order is stored as a fractional position (the midpoint between neighbors), so one drop writes exactly one row. A rebalance only triggers when the gap gets too small. Combined with UUIDv7 primary keys (time-ordered), the schema stays simple and fast.

Auth without a framework. Email/password with bcrypt, a JWT session in an httpOnly cookie, and CSRF defense via a custom header. It’s old-school and boring, which is exactly what a small self-hosted app wants.

Challenges

The Turso → MySQL migration was the sharpest lesson. The failure wasn’t in the code — it was in the platform: remote database latency, a thin region map, and connection failures under real usage. The fix was architectural (self-host the database) and it changed how I think about managed services: for a small, single-VPS app, the nearest database is the best database.

Real-time without conflict headaches. Because each account has a single user, last-write-wins is a fine strategy — but broadcasts had to be scoped per user and applied defensively on the client (ignore messages about calendars you’re not viewing, don’t clobber in-flight edits). The reconnect logic also needed care: reconnect with backoff, but stop permanently when the session is invalid.

Resisting feature creep was a constant fight. The product constraint (“simple as hell”) meant saying no to month views, notes, and recurrence — all useful, all exactly the kind of scope that turns a clean planner into a slow one.

Trade-offs

  • Self-hosted everything means I own backups, uptime, and OS updates. The trade: total control and near-zero cost, in exchange for operations work a managed platform would absorb.
  • No file support keeps the system simple, but it also means no attachments, ever. For a task list that’s the right call; for a notes app it wouldn’t be.
  • Native HTML5 drag-and-drop gives me a dependency-free, predictable implementation — but I give up the polished animations a DnD library would provide for free.
  • Single-user calendars keep the data model and the realtime layer trivial. Shared/team calendars are the main feature that would force a redesign, and I’m intentionally not building them yet.

Result

Zweek is live at zweek.zwinglio.com and in daily use on mobile and desktop browsers. The whole system — API, database, realtime layer, and deploy pipeline — fits in one repository with two workspaces, and a single push to main ships it to production. It’s the planner I actually wanted: fast, private, always in sync, and simple enough that I never think about the machinery behind it.

Next steps are tracked live on the Zweek roadmap board at fast-kanban.zwinglio.com — ideas land there as they come up and move through Todo, Doing, Review, and Done.