A free, open-source, self-hostable weekly planner.
A calm, paper-planner week grid for your tasks — drag them around, check them off, ink-tag them. An open alternative to tweek.so, teuxdeux.com, and weektodo.me, built to self-host.
Openweek is a minimalist weekly to-do app. The whole interface is a week grid: seven day columns plus a rail of "Someday" lists, where each task is a bullet you can drag between days, tick off, tag with a highlighter ink, and annotate. There's no hourly scheduling — the week itself is the canvas. It's designed to feel quiet and structural, not a SaaS dashboard: a cool neutral ramp, one accent, hairline rules, generous whitespace. Ships as Paper (light) and Ink (dark).
It's AGPL-3.0 and self-host first — your tasks live in your own Postgres, in your own container.
- 🗒️ Week grid — 7 day columns + a rail of lists; previous / next / this-week navigation, focus-a-day, collapse-done, and a weekends toggle.
- ✅ Tasks — create, edit, complete, ink-tag, time, and annotate.
- 🔀 Drag & drop — reorder within a day or list and move tasks across both; every drag is paired with a keyboard "Move to…" path.
- ↪️ Auto-rollover (opt-in) — unfinished past tasks roll forward to today, with a review banner to send them back.
- 📅 Calendar sync (read-only) — mirror Google Calendar, CalDAV (Apple/Nextcloud/Fastmail), and iCal feeds into your week. Connect multiple accounts, show/hide and recolour each calendar; tokens are encrypted at rest.
- 🔐 Accounts — email/password + optional Google sign-in. The first user to register becomes the admin.
- 🎨 Paper / Ink — light and dark, a selectable accent, typeface and text size.
- 🐳 Self-hostable — one
docker compose up, or a Portainer stack, or a Proxmox LXC/VM.
Not yet: subtasks, recurring tasks, reminders, two-way calendar sync, and offline/PWA — these are planned for later. The data model already leaves room for them.
Paper and Ink are one design in two palettes — identical layout and behaviour, every colour re-tuned rather than inverted. Pick either, or follow your OS.
Google, CalDAV and iCal feeds side by side in the toolbar. Turn one off and its events leave the week immediately — no reload, no refetch.
Rename, recolour or hide any calendar, see what each one contributes to the week you're looking at, and connect more accounts. Sync is read-only — nothing is ever written back to your calendar.
Paper, Ink or System; an accent, a typeface and a text size; plus the week-start, weekends, collapse-done and rollover switches.
Email and password out of the box, with optional Google sign-in. The first account registered on a fresh instance becomes its admin.
Nuxt 4 · TypeScript · PostgreSQL + Drizzle ORM · Zod · Tailwind CSS v4 · Better Auth · Pragmatic drag-and-drop · Docker. Full list and rationale: docs/tech-stack.md.
Openweek ships as a single Nuxt app plus Postgres. There is no prebuilt image yet — the compose stack builds from source, so every route below starts from a clone of this repository.
Before you start, generate two secrets and keep them somewhere safe:
openssl rand -base64 32 # BETTER_AUTH_SECRET
openssl rand -base64 32 # OPENWEEK_ENCRYPTION_KEY ← back this up (see "Backups")Pick your route:
| Route | Good for | Guide |
|---|---|---|
| Docker Compose | A box you already have SSH on. The reference path. | below |
| Portainer | Managing it from a web UI alongside your other stacks. | below |
| Proxmox | A dedicated LXC or VM on your homelab hypervisor. | below |
Reverse proxy, TLS, backups, and every environment variable: docs/self-hosting.md.
Requires Docker Engine + the Compose plugin.
git clone https://github.com/silo/openweek.git
cd openweek
cp .env.example .env
# Edit .env — at minimum BETTER_AUTH_SECRET, OPENWEEK_ENCRYPTION_KEY and BETTER_AUTH_URL.
# BETTER_AUTH_URL must be the URL you will actually open in a browser, e.g.
# http://192.168.1.50:3000 or https://openweek.example.com
docker compose up -d --buildOpen the URL you set. The first account you register becomes the admin.
The first build takes several minutes (it runs a full Nuxt production build). Migrations are applied automatically by the container entrypoint on every start, so there is no separate migrate step.
docker compose logs -f app # follow the app
docker compose down # stop (the openweek-db volume keeps your data)
git pull && docker compose up -d --build # updateHarden it before exposing it. The defaults are tuned for a laptop:
- Set
POSTGRES_PASSWORDin.envbefore the firstup. Postgres only reads it when it initialises the data directory; changing it later needs anALTER USERinside the running container. - The database port is published on
127.0.0.1only, so it is reachable from the host but not the network. If you don't needpsqlfrom the host, delete theports:block from thedbservice — the app talks to it over the compose network either way. - Put it behind a reverse proxy with TLS and set
BETTER_AUTH_URLto the public HTTPS URL. Sessions and OAuth redirects are derived from it, so a mismatch shows up as login loops. See docs/self-hosting.md.
Portainer builds the image itself from a Git-backed stack, so you never touch a shell.
-
Stacks → Add stack, name it
openweek, choose Repository. -
Repository URL
https://github.com/silo/openweek· Referencerefs/heads/main· Compose pathdocker-compose.yml. For a private fork, tick Authentication and use a personal access token. -
Under Environment variables, add:
Name Value BETTER_AUTH_SECRETyour first generated secret OPENWEEK_ENCRYPTION_KEYyour second generated secret BETTER_AUTH_URLthe URL you'll browse to, e.g. https://openweek.example.comPOSTGRES_PASSWORDa strong password (set it now — see the note above) OPENWEEK_PORThost port, if 3000is takenCompose fails fast and loudly if the two secrets are missing, so a typo here shows up immediately in the deployment log rather than as a broken app.
-
Deploy the stack. The first deployment builds the image — expect several minutes with no output; watch Containers → openweek-app-1 → Logs for
→ Applying database migrations…then→ Starting Openweek.
To update: Stacks → openweek → Pull and redeploy, with Re-pull image enabled so the build re-runs.
Portainer's stack editor keeps your environment variables, so they survive redeploys. They are stored in
Portainer's own database in plaintext — treat that database as sensitive, or hand the secrets in through a
.env on the host instead.
Proxmox doesn't run OCI containers itself; you give it a guest that runs Docker. Two options.
The path Proxmox itself recommends for Docker — full kernel isolation, no nesting caveats, and snapshots that actually capture the whole thing.
- Create a VM from a Debian 12 or Ubuntu Server 24.04 cloud image. 2 vCPU / 2 GB RAM / 20 GB disk is a comfortable starting point.
- Install Docker:
curl -fsSL https://get.docker.com | sh - Follow Docker Compose above.
- Snapshot the VM once it's up, before you put real data in it.
Uses noticeably less RAM and disk, at the cost of some setup friction. Docker inside an unprivileged LXC needs two features enabled — without them the daemon starts but containers fail with cryptic overlayfs or keyring errors.
-
Create a Debian 12 container, unprivileged, 2 vCPU / 2 GB RAM / 20 GB disk.
-
On the Proxmox host, enable nesting and keyctl for it (replace
110with your CTID):pct set 110 --features nesting=1,keyctl=1 pct reboot 110Or in the web UI: Container → Options → Features → Nesting + keyctl.
-
Inside the container:
apt update && apt install -y curl && curl -fsSL https://get.docker.com | sh -
Follow Docker Compose above.
If you use ZFS as the container's storage backend, set Docker's storage driver to
overlay2on an ext4 subvolume, or Docker will fall back tovfsand images will be enormous and slow.
- Give the guest a static IP or DHCP reservation —
BETTER_AUTH_URLis baked into the stack's config, so a changing address means logging in stops working. - Back up the guest with Proxmox Backup Server and take Postgres dumps (docs/self-hosting.md). A filesystem snapshot of a running database is not a guaranteed-consistent backup.
Set these in .env (or as stack environment variables). The app validates its config at boot and fails
fast with a clear message if something is missing or malformed.
| Variable | Required | Purpose |
|---|---|---|
DATABASE_URL |
✅ | Postgres connection string. Compose sets this for the app automatically. |
BETTER_AUTH_SECRET |
✅ | Signs sessions. |
BETTER_AUTH_URL |
✅ | Your public URL, e.g. https://openweek.example.com. |
OPENWEEK_ENCRYPTION_KEY |
✅ | Base64 of exactly 32 bytes — encrypts calendar tokens at rest. Back it up. |
POSTGRES_PASSWORD |
recommended | Bundled Postgres password. Defaults to openweek; set before first start. |
OPENWEEK_PORT |
optional | Host port for the app (default 3000). |
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET |
optional | Google sign-in and/or Google Calendar sync. |
OPENWEEK_SYNC_INTERVAL |
optional | Calendar poll interval (default 15m). |
OPENWEEK_EVENT_WINDOW |
optional | Event cache window (default -1w..+6w). |
For working on Openweek itself. Requires Node 24 (the active LTS — .nvmrc and .node-version pin it, so
nvm use / fnm use picks it up on cd), pnpm via corepack, and Docker for the database.
cp .env.example .env
pnpm install
pnpm db:up # start just the Postgres container, wait until healthy
pnpm db:migrate # apply the schema
pnpm db:seed # optional: a demo account + a week of test data
pnpm dev # http://localhost:3000docker compose reads the same .env as the app, so if you change OPENWEEK_DB_PORT (handy when several
checkouts run side by side) keep the port in DATABASE_URL in sync.
Other scripts:
pnpm typecheck # vue-tsc strict type-check — blocking gate, the build does not type-check
pnpm lint # ESLint
pnpm test # Vitest (see docs/testing.md)
pnpm test:coverage # Vitest with a coverage report
pnpm db:stop # stop the database (the openweek-db volume keeps your data)
pnpm db:reset # destroy the volume, recreate the database, re-migrate
pnpm db:psql # psql shell inside the container
pnpm db:logs # tail Postgres logs
pnpm db:generate # generate a migration after editing the schema
pnpm auth:gen # regenerate Better Auth tables after auth config changes
pnpm build # production buildBefore any PR: pnpm lint && pnpm typecheck && pnpm test must be green.
| Doc | What's in it |
|---|---|
| docs/README.md | Documentation index. |
| docs/tech-stack.md | Dependencies, versions, and what we rejected and why. |
| docs/architecture.md | Folders, layers, data flow, state model. |
| docs/data-model.md | Database schema and constraints. |
| docs/calendar-sync.md | How calendar connections and sync work. |
| docs/design.md | Paper/Ink, tokens, accessibility, responsive layout. |
| docs/self-hosting.md | Operations: env, Docker, reverse proxy, backups. |
| docs/testing.md | Test strategy and the seeded fixtures. |
| docs/roadmap.md | Build phases and scope. |
| docs/decisions.md | Why key choices were made. |
Contributions are welcome. Openweek is in early development, so the most useful help right now is feedback on
the plan and decisions. Before opening a PR, please run
pnpm lint && pnpm typecheck && pnpm test. (A CONTRIBUTING.md will follow with the first release.)
AGPL-3.0. You can self-host and modify Openweek freely; if you run a modified version as a network service, the AGPL requires you to offer your users the modified source. This is deliberate — so hosted forks stay open.




