Initial commit
This commit is contained in:
commit
38c24fc9dc
166 files changed
+22002
No files matched your search
@@ -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
|
||||
Reference in new issue
Block a user