Zweek — A Real-Time Weekly Planner Built to Stay Simple
How I built a weekly planner that syncs phone and desktop instantly without a sync framework: a Vue 3 SPA, a Hono API, a MySQL database moved next to the app after Turso failed in production, and per-user WebSockets — small enough for one person to own end to end.
- Problem: a weekly planner that stays in sync between my phone and desktop, without the bloat of existing apps.
- Stack: Vue 3 + Pinia, Hono on Node, MySQL with Drizzle, WebSockets, a self-hosted GitLab runner.
- Key decision: moving the database off Turso and onto the same VPS as the API.
- Outcome: live, used daily, shipped to production with a single push.
The problem
I plan my life in weeks: seven columns, Monday to Sunday, plus a few undated lists for the inbox, groceries and “someday”. Existing planners either buried that simple model under reminders, subtasks and labels, or felt slow to use.
One requirement set the architecture: I use it on my phone and my desktop at the same time. A task checked on mobile has to be checked on the web straight away. Real-time sync wasn’t something to add later. It was the product.
The whole product: one week, seven columns, a few loose lists.
Constraints
- One maintainer: a stack small enough to keep in my head.
- Real-time across devices, with as little sync infrastructure as possible.
- Data only: no files or uploads. Everything stored is a row.
- Self-hosted on my own VPS, at near-zero cost.
Key decisions
1. Move the database next to the app
Problem. The first version ran on Turso (edge-hosted SQLite). In real use it added latency from my region and dropped connections now and then. The app felt broken even though the code was fine.
Decision. Replace it with a MySQL instance on the same VPS as the API. Drizzle ORM stayed, so the switch was a driver swap plus a few type changes.
Why it holds. Every query is now a local round-trip. The network failure didn’t need handling anymore, because it no longer exists.
2. Real-time sync without a sync engine
Problem. Keep several devices in sync without CRDTs, a message broker or a hosted realtime service.
Decision.
- The server keeps a set of WebSockets per user and broadcasts every change to them.
- The client applies each message straight to its store.
- Conflicts resolve as last-write-wins.
Why it holds. Each account has one user, so real conflicts are rare. Because everything is plain data, a sync message is just the new row as JSON.
Check a task on the phone and it’s already checked on the desktop.
3. One write per drag-and-drop
Problem. Reordering a list usually rewrites the position of every item below the drop point. Each of those writes then has to be broadcast to every device.
Decision. Positions are fractional numbers: a dropped item takes the midpoint between its new neighbours. One drop writes one row. The list is only rebalanced when the gap between two items gets too small.
Why it holds. A drag costs the same no matter how long the list is, which matters when every write goes out to every open device.
4. CI next to production
Problem. I use this app daily and didn’t want deploys to depend on a CI platform that had been unstable.
Decision. GitLab, with a self-hosted runner on the same VPS the app runs on. A push to main builds, syncs files, migrates and restarts the process.
Why it holds. No CI minutes, and no artifacts to upload and download: the builder and the target are the same machine.
Hard problems solved
- The platform was the bug. The Turso failures couldn’t be fixed in code. Changing where the database lived fixed them.
- Defensive updates. Socket messages are ignored for calendars that aren’t on screen, and they never overwrite a field the user is editing.
- Reconnecting safely. The client retries with backoff, but stops when the session has expired, so it never loops forever.
- Saying no. Month views, notes and recurrence were all useful, and all exactly the scope that makes a fast planner slow.
Trade-offs
- Self-hosting: full control at near-zero cost, but backups and uptime are on me.
- Last-write-wins: right for one user per account. Shared calendars would need a real conflict strategy, which is why they aren’t built.
- No files ever: keeps sync trivial. Right for a planner, wrong for a notes app.
Result
Zweek is live at zweek.zwinglio.com and I use it every day on my phone and desktop. API, database, realtime layer and deploy pipeline all live in one repository.
The public landing page, with a live demo of the planner.
The roadmap is tracked on Fast Kanban as ZWK tickets.