my vibed list app
  • TypeScript 88.4%
  • JavaScript 5.6%
  • CSS 3.8%
  • Dockerfile 2.2%
Find a file
2026-10-22 23:43:10 +02:00
drizzle add a submit form for logged out users 2026-10-22 23:43:10 +02:00
public Add motzlist v1: self-hosted list app 2026-10-10 22:16:48 +02:00
scripts Add motzlist v1: self-hosted list app 2026-10-10 22:16:48 +02:00
src add a submit form for logged out users 2026-10-22 23:43:10 +02:00
.dockerignore Add motzlist v1: self-hosted list app 2026-10-10 22:16:48 +02:00
.env.example Add motzlist v1: self-hosted list app 2026-10-10 22:16:48 +02:00
.gitignore Add motzlist v1: self-hosted list app 2026-10-10 22:16:48 +02:00
docker-compose.yml Add motzlist v1: self-hosted list app 2026-10-10 22:16:48 +02:00
Dockerfile Add motzlist v1: self-hosted list app 2026-10-10 22:16:48 +02:00
drizzle.config.ts Add motzlist v1: self-hosted list app 2026-10-10 22:16:48 +02:00
next.config.ts Add motzlist v1: self-hosted list app 2026-10-10 22:16:48 +02:00
package-lock.json Add motzlist v1: self-hosted list app 2026-10-10 22:16:48 +02:00
package.json Add motzlist v1: self-hosted list app 2026-10-10 22:16:48 +02:00
postcss.config.mjs Add motzlist v1: self-hosted list app 2026-10-10 22:16:48 +02:00
README.md add a submit form for logged out users 2026-10-22 23:43:10 +02:00
tsconfig.json Add motzlist v1: self-hosted list app 2026-10-10 22:16:48 +02:00

motzlist

A small, self-hosted list app: cards for each list, plain or todo lists (Apple Notes-style checkboxes), username/password login, everything in one SQLite file.

Next.js 16 (App Router) · TypeScript · Tailwind v4 · SQLite (better-sqlite3 + Drizzle) · argon2id.

Run with Docker

cp .env.example .env
# set SESSION_SECRET to a random string of 32+ chars, e.g.:
#   openssl rand -base64 48
docker compose up -d --build
docker compose exec app npm run create-user -- alice   # prompts for the password

Open http://localhost:3000 and sign in. The port is published on 127.0.0.1 only; set BIND_ADDRESS=0.0.0.0 (and ALLOW_INSECURE_COOKIES=true) in .env to try it from another device over plain http.

Database migrations run automatically every time the container starts.

Environment

Variable Default Notes
SESSION_SECRET — (required) 32+ chars. Session IDs are stored as HMACs keyed with it; changing it signs everyone out.
PORT 3000 Port the app listens on and is published on.
BIND_ADDRESS 127.0.0.1 Host address the port is published on. Loopback = only reachable via the reverse proxy.
ALLOW_INSECURE_COOKIES false true drops the Secure cookie flag on plain-http requests. Only for local/LAN testing.
SQLITE_JOURNAL_MODE WAL Use DELETE if the DB file lives on a network filesystem (NFS); run a single instance.
DATABASE_PATH /data/motzlist.db Set in the image; the motzlist-data volume is mounted at /data.

Behind a reverse proxy

Terminate TLS at the proxy and forward to 127.0.0.1:$PORT. The proxy must set Host and X-Forwarded-Proto. The app uses them for Secure cookies, redirects and its same-origin check. It must also overwrite X-Forwarded-For with the client address, because login throttling is keyed on it.

Sessions last 90 days and roll forward when used (re-issued at most once a day). Logging out deletes the session server-side.

Users

No public signup. Manage users with the CLI inside the container:

docker compose exec app npm run create-user -- alice            # new user (password prompt)
docker compose exec app npm run create-user -- alice --reset    # new password, signs out all sessions
docker compose exec -e PASSWORD='…' app npm run create-user -- bob   # non-interactive

Usernames are lowercase a-z 0-9 . _ -. Passwords need at least 8 characters and are hashed with argon2id.

Anonymous submissions

/submit is a public page with a single text field, and where logged-out visitors land when they open / (a "Log in" button in the header leads to the login page). Whatever is sent there lands at the top of the inbox list. Visitors only see a confirmation, never the list or its items. Turn a list into the inbox with the tray button in the list view. There is one inbox across all users, so turning it on for a list turns it off wherever it was. With no inbox, /submit says submissions are closed. Submissions are throttled per IP (20 per 10 minutes).

Backup & restore

The backup uses SQLite's online backup API, so it's consistent while the app is running:

docker compose exec app npm run backup -- /data/motzlist-backup.db
docker compose cp app:/data/motzlist-backup.db ./motzlist-backup.db
docker compose exec app rm /data/motzlist-backup.db

Without an argument the backup is written to /data/backup-<timestamp>.db.

Restore (the volume is named <project>_motzlist-data, here motzlist_motzlist-data):

docker compose stop app
docker run --rm -v motzlist_motzlist-data:/data -v "$PWD":/backup alpine sh -c \
  'cp /backup/motzlist-backup.db /data/motzlist.db && rm -f /data/motzlist.db-wal /data/motzlist.db-shm && chown 1001:1001 /data/motzlist.db'
docker compose start app

Note: docker compose down keeps the data volume. docker compose down -v deletes it.

Development

npm install
SESSION_SECRET=dev-secret-dev-secret-dev-secret-123 npm run migrate
SESSION_SECRET=dev-secret-dev-secret-dev-secret-123 npm run dev
npm run create-user -- alice

Locally the DB lives in ./data/motzlist.db. After changing src/db/schema.ts, run npm run db:generate and commit the new file in drizzle/.

Layout

src/app/            pages (/, /login, /lists/[id]), JSON API (/api/...), /healthz
src/proxy.ts        auth gate + rolling session refresh
src/db/             Drizzle schema and connection
src/lib/            sessions, auth helpers, list queries, shared ordering rules
src/components/     grid, list editor, todo checkbox, theme toggle
scripts/            migrate / create-user / backup (plain Node, run inside the image)
drizzle/            generated SQL migrations