239 lines
10 KiB
Markdown
239 lines
10 KiB
Markdown
# Notizarr
|
||
|
||
Self-hosted notification hub — replaces ntfy for a small homelab, with a
|
||
polished web UI/PWA, Web Push, per-topic push control, and scoped API tokens
|
||
for LAN publishers. Full design rationale lives in
|
||
[docs/phase1/](docs/phase1/00-README.md).
|
||
|
||
Status: **Phases 2–9 complete**, Phase 10 (hardening/test-gap-filling) in
|
||
progress. Auth, topics, notification ingestion (canonical + ntfy-compat +
|
||
webhook), rate limiting, dedup, SSE live updates, retention cleanup, the
|
||
full user-facing UI, Web Push, and the admin UI are all built and verified
|
||
against a live dev stack. See
|
||
[docs/phase1/08-implementation-plan.md](docs/phase1/08-implementation-plan.md)
|
||
for the phase breakdown and [MVP acceptance status](#mvp-acceptance-status)
|
||
below for where things stand against the original 22 criteria.
|
||
|
||
> **Note:** this was written in an environment without Node.js or Docker
|
||
> installed, so none of it is executed here — it's been verified
|
||
> incrementally against a real dev stack running on the target Unraid box
|
||
> instead. `AUTH_MODE=oidc` has since been tested end-to-end against a real
|
||
> Pocket ID instance over real HTTPS (via Pangolin), and Web Push has been
|
||
> confirmed delivering to an installed iPhone PWA — see "Auth modes" and
|
||
> [docs/phase1/05-pwa-web-push-design.md](docs/phase1/05-pwa-web-push-design.md).
|
||
|
||
## Auth modes
|
||
|
||
Set via `AUTH_MODE` (`.env` / `docker-compose.yml` environment): `disabled`,
|
||
`proxy`, or `oidc` — see [docs/phase1/02-threat-model.md](docs/phase1/02-threat-model.md).
|
||
`disabled` refuses to start when `NODE_ENV=production` (it grants every
|
||
request implicit admin access — dev/test only). `oidc` is implemented
|
||
against the `openid-client` v5 API but hasn't been run against a real IdP
|
||
yet; test it once Pocket ID (or similar) is configured.
|
||
|
||
All mutating requests on the web listener (`POST`/`PUT`/`PATCH`/`DELETE`
|
||
under `/api/`) require a `X-Requested-With: notizarr` header — this is the
|
||
CSRF defense described in the threat model, not optional. The ingestion
|
||
listener (`:8081`) instead requires `Authorization: Bearer nzr_...` — create
|
||
a token via Settings → Admin → API Tokens.
|
||
|
||
## Repo layout
|
||
|
||
```
|
||
apps/server/ Node.js + TypeScript backend (Fastify), two listeners
|
||
apps/web/ React + TypeScript + Vite frontend, installable PWA
|
||
packages/shared/ Reserved for cross-package types (unused so far)
|
||
docker/ docker-compose.yml (dev) and docker-compose.prod.yml (Unraid template)
|
||
scripts/ backup.sh / restore.sh for the Postgres database
|
||
docs/phase1/ Architecture, threat model, DB/API/PWA design, implementation plan
|
||
docs/ Migration guides (ntfy, Home Assistant/Seerr/Uptime Kuma)
|
||
```
|
||
|
||
Full breakdown: [docs/phase1/07-folder-structure.md](docs/phase1/07-folder-structure.md).
|
||
|
||
## Local development
|
||
|
||
Requires Node.js 22+ and pnpm (`corepack enable` will fetch the pinned
|
||
version automatically).
|
||
|
||
```bash
|
||
pnpm install
|
||
pnpm dev
|
||
```
|
||
|
||
This runs the web app in `vite build --watch` and the server via `tsx
|
||
watch` concurrently. The server serves the built web assets itself — open
|
||
http://localhost:8080. `PORT_INGEST` (8081) is the LAN ingestion listener.
|
||
|
||
Needs a local Postgres — either run one yourself and set `DATABASE_URL`, or
|
||
use the dev Docker Compose stack below, which includes one.
|
||
|
||
## Local development via Docker
|
||
|
||
```bash
|
||
docker compose -f docker/docker-compose.yml up -d
|
||
```
|
||
|
||
Brings up `notizarr-postgres-dev` and `notizarr-dev` (hot-reload: the repo
|
||
is bind-mounted in). Default host ports (deliberately not 8080/8081/5432,
|
||
since those are commonly already taken on an Unraid box):
|
||
|
||
- Web UI: http://localhost:18080
|
||
- Ingestion listener: http://localhost:18081
|
||
- Postgres: `127.0.0.1:15432` (`psql -h localhost -p 15432 -U notizarr
|
||
notizarr`, password `notizarr_dev_password`)
|
||
|
||
Whenever a `package.json` dependency changes, rebuild **and** renew the
|
||
anonymous `node_modules` volumes in one go, or the container keeps running
|
||
against the old, pre-change `node_modules`:
|
||
|
||
```bash
|
||
docker compose -f docker/docker-compose.yml up -d --build --force-recreate -V
|
||
```
|
||
|
||
Override host ports if they also collide, either inline or via a
|
||
`docker/.env` file:
|
||
|
||
```bash
|
||
HOST_PORT_WEB=28080 HOST_PORT_INGEST=28081 HOST_PORT_PG=25432 \
|
||
docker compose -f docker/docker-compose.yml up -d
|
||
```
|
||
|
||
Check health:
|
||
|
||
```bash
|
||
curl http://localhost:18080/health
|
||
curl http://localhost:18080/ready # 503 until Postgres is reachable
|
||
curl http://localhost:18081/health
|
||
```
|
||
|
||
Generate the PWA icon set once (needs `sharp`, already a devDependency —
|
||
this writes real PNGs into `apps/web/public/icons/`, committed to the repo
|
||
rather than regenerated on every build):
|
||
|
||
```bash
|
||
docker compose -f docker/docker-compose.yml exec notizarr pnpm --filter @notizarr/web generate-icons
|
||
```
|
||
|
||
## Unraid deployment (production)
|
||
|
||
Deployment root: `/mnt/tank/appdata/notizarr`. Full design:
|
||
[docs/phase1/06-docker-unraid-design.md](docs/phase1/06-docker-unraid-design.md).
|
||
|
||
1. Build and tag the image (from a repo checkout, e.g. on the Unraid box or
|
||
any machine with Docker, then push/copy it to where Unraid can pull it —
|
||
or build directly on Unraid if the repo is checked out there):
|
||
```bash
|
||
docker build -f apps/server/Dockerfile --target runtime -t notizarr:0.1.0 .
|
||
```
|
||
2. On the Unraid host, create the deployment directory structure:
|
||
```bash
|
||
mkdir -p /mnt/tank/appdata/notizarr/{postgres,app,backups,logs}
|
||
```
|
||
3. Copy `docker/docker-compose.prod.yml` to
|
||
`/mnt/tank/appdata/notizarr/docker-compose.yml`, and `.env.example` to
|
||
`/mnt/tank/appdata/notizarr/.env` — fill in real values (DB password,
|
||
OIDC settings, `PUBLIC_BASE_URL`, and especially `BIND_ADDR_INGEST`, which
|
||
defaults to `127.0.0.1` until you explicitly set it to your LAN IP).
|
||
4. From `/mnt/tank/appdata/notizarr`:
|
||
```bash
|
||
docker compose up -d
|
||
```
|
||
|
||
Point Pangolin at `notizarr:8080` (same Docker network, preferred) or the
|
||
host's `PUBLIC_BASE_URL`. Never point it at 8081.
|
||
|
||
## Backup and restore
|
||
|
||
Everything that matters — notification history, users, topics, API tokens,
|
||
push subscriptions, the VAPID keypair, the session secret — lives in
|
||
Postgres (see [docs/phase1/06-docker-unraid-design.md](docs/phase1/06-docker-unraid-design.md#backup--restore)),
|
||
so backing up the database is a complete backup.
|
||
|
||
```bash
|
||
cd /mnt/tank/appdata/notizarr
|
||
./scripts/backup.sh
|
||
```
|
||
|
||
Writes a timestamped, gzipped `pg_dump` plus a copy of the compose file
|
||
(env vars, including the DB password and OIDC client secret, are inline
|
||
there rather than in a separate `.env` — keep `backups/` off shared or
|
||
public storage) into `backups/`, keeping the last 14 of each. Restore
|
||
with:
|
||
|
||
```bash
|
||
./scripts/restore.sh backups/notizarr-db-<timestamp>.sql.gz
|
||
```
|
||
|
||
This is destructive (replaces all current data) and asks for confirmation.
|
||
Consider scheduling `backup.sh` via an Unraid User Scripts cron entry for
|
||
regular backups.
|
||
|
||
## Migrating from ntfy / integrating other services
|
||
|
||
- [docs/migration-ntfy.md](docs/migration-ntfy.md) — endpoint mapping,
|
||
before/after examples, what's intentionally not replicated
|
||
- [docs/home-assistant.md](docs/home-assistant.md) — `rest_command` example
|
||
replacing `ntfy.publish`, plus Seerr, Uptime Kuma, and generic webhook
|
||
configuration
|
||
|
||
## Troubleshooting
|
||
|
||
- **A `docker compose up -d --build` didn't pick up a dependency change.**
|
||
Anonymous `node_modules` volumes are reused by default — add `-V
|
||
--force-recreate` (see [Local development via
|
||
Docker](#local-development-via-docker)).
|
||
- **A mutating API call returns `csrf_check_failed`.** The web app must
|
||
send `X-Requested-With: notizarr` on every `POST`/`PATCH`/`DELETE`
|
||
(`apps/web/src/lib/api.ts` does this automatically for the built-in UI —
|
||
this only bites hand-rolled `curl` testing). If the error is specifically
|
||
`"origin mismatch"`, the browser's `Origin` header didn't match the
|
||
request's own `Host` header — that's a real cross-origin request being
|
||
correctly blocked, not a false positive.
|
||
- **Push notifications don't work / diagnostics show "Service worker: Not
|
||
registered".** Service workers and the Push API require a secure context
|
||
— HTTPS, or `localhost`. A plain `http://<lan-ip>:port` origin will never
|
||
register a service worker; this isn't a bug, it's the browser enforcing
|
||
the spec. Test push once Pangolin is serving Notizarr over real HTTPS.
|
||
- **iPhone won't show push notifications even over HTTPS.** Confirm iOS
|
||
16.4+, and that Notizarr was installed via Share → Add to Home Screen —
|
||
Safari does not support Web Push for ordinary browser tabs, only
|
||
installed PWAs. Check Settings → Push diagnostics in the app.
|
||
- **`AUTH_MODE=disabled` refuses to start.** This is intentional when
|
||
`NODE_ENV=production` — that mode grants every request implicit admin
|
||
access and is dev/test-only (see `apps/server/src/index.ts`). Use `oidc`
|
||
or `proxy` for anything reachable off localhost.
|
||
- **First-ever login should be an admin but isn't.** Bootstrap-admin only
|
||
promotes the very first user *ever created* in the `users` table. If a
|
||
non-admin user was created first (e.g. during testing), promote them
|
||
manually via Settings → Admin → Users using an existing admin account, or
|
||
directly in Postgres if none exists yet.
|
||
|
||
## MVP acceptance status
|
||
|
||
All 22 hard acceptance criteria are built and verified against a live dev
|
||
stack reachable over real HTTPS (Docker Compose, Postgres healthy,
|
||
auth/topics/tokens/publish/push-toggle/PWA-install all confirmed working,
|
||
history and settings survive container restarts, OIDC login against a real
|
||
Pocket ID instance, and push confirmed arriving on an installed iPhone
|
||
PWA — 17–18 were the last two blocked on real HTTPS and are now closed
|
||
out too).
|
||
|
||
## Commands
|
||
|
||
| Command | Does |
|
||
|---|---|
|
||
| `pnpm dev` | Run web + server in watch mode |
|
||
| `pnpm build` | Production build of both |
|
||
| `pnpm typecheck` | TypeScript, no emit, across all workspaces |
|
||
| `pnpm lint` | ESLint across the repo |
|
||
| `pnpm test` | Tests across all workspaces |
|
||
|
||
## Documentation
|
||
|
||
- [docs/phase1/](docs/phase1/00-README.md) — architecture, threat model,
|
||
database model, API design, PWA/Web Push design, Docker/Unraid design,
|
||
implementation plan
|
||
- [docs/migration-ntfy.md](docs/migration-ntfy.md),
|
||
[docs/home-assistant.md](docs/home-assistant.md) — migration guides
|