Initial commit

This commit is contained in:
siliconsolder committed 2026-09-07 09:54:30 +00:00
commit 38c24fc9dc
166 files changed
+22002

No files matched your search

+238
View File
@@ -0,0 +1,238 @@
# 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