- TypeScript 88.4%
- JavaScript 5.6%
- CSS 3.8%
- Dockerfile 2.2%
| drizzle | ||
| public | ||
| scripts | ||
| src | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| docker-compose.yml | ||
| Dockerfile | ||
| drizzle.config.ts | ||
| next.config.ts | ||
| package-lock.json | ||
| package.json | ||
| postcss.config.mjs | ||
| README.md | ||
| tsconfig.json | ||
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