Initial commit
This commit is contained in:
commit
38c24fc9dc
166 files changed
+22002
No files matched your search
@@ -0,0 +1,126 @@
|
||||
# 01 — Architecture
|
||||
|
||||
## Guiding constraint
|
||||
|
||||
Single-user / small-household homelab. Two containers, no queues, no
|
||||
microservices. Everything that looks like it needs a worker is instead done
|
||||
in-process, synchronously or via a lightweight interval timer, because volume
|
||||
is low (dozens to low-thousands of notifications/day, not per-second traffic).
|
||||
|
||||
## Components
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ notizarr (single Node.js/TypeScript container, modular monolith)│
|
||||
│ │
|
||||
│ Listener A 0.0.0.0:8080 "web" │
|
||||
│ - Serves the built React SPA (static assets + index.html) │
|
||||
│ - Session-cookie authenticated JSON API (/api/v1/*) │
|
||||
│ - OIDC login/callback, or proxy-header auth │
|
||||
│ - SSE stream for live UI updates │
|
||||
│ - Web Push subscribe/unsubscribe, diagnostics │
|
||||
│ - Admin API (role=admin) │
|
||||
│ - /docs (OpenAPI/Swagger UI) │
|
||||
│ - /health, /ready │
|
||||
│ │
|
||||
│ Listener B 0.0.0.0:8081 "ingest" │
|
||||
│ - Bearer-token authenticated only. No cookies, no static SPA. │
|
||||
│ - POST /api/v1/notifications, /api/v1/topics/:slug │
|
||||
│ - ntfy-compat: POST /:topic │
|
||||
│ - Webhook: POST /api/v1/webhook/:source │
|
||||
│ - /health, /metrics (Prometheus) │
|
||||
│ │
|
||||
│ Shared internal modules: │
|
||||
│ - Notification service (insert, dedupe, fan-out) │
|
||||
│ - Push service (VAPID sign + send via web-push lib) │
|
||||
│ - Rate limiter (in-memory token bucket, per-token & per-IP) │
|
||||
│ - Retention cleanup (interval timer, e.g. every 1h) │
|
||||
│ - Audit logger │
|
||||
│ │
|
||||
│ On boot: run pending DB migrations, load/generate VAPID keys │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
│ single internal Docker network,
|
||||
│ Postgres not published to host
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ notizarr-postgres (postgres:16-alpine) │
|
||||
│ Single source of truth: users, topics, notifications, │
|
||||
│ subscriptions, tokens, push subscriptions, delivery history, │
|
||||
│ audit log, VAPID keys, system settings. │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Pangolin (external, not part of this compose stack) sits in front of listener
|
||||
A only. Listener B is reachable only from the LAN (see §06 networking).
|
||||
|
||||
## Why one process, two listeners, instead of two containers
|
||||
|
||||
Splitting "public API" and "ingestion API" into two containers would mean two
|
||||
copies of the same DB access layer, two deployments, two healthchecks, for no
|
||||
real isolation benefit — a compromised process still has the same DB
|
||||
credentials either way. The actual security boundary that matters is
|
||||
*network reachability* (LAN-only port vs. reverse-proxied port) and
|
||||
*auth method* (bearer token vs. session), both of which are enforced per
|
||||
listener inside one process. This keeps the "prefer one app container +
|
||||
Postgres" principle intact.
|
||||
|
||||
## Data flow: LAN publish → history → push
|
||||
|
||||
```
|
||||
LAN service (Home Assistant, Seerr, ...)
|
||||
│ POST http://<lan-ip>:8081/api/v1/notifications
|
||||
│ Authorization: Bearer nzr_xxxxxxxx
|
||||
▼
|
||||
Ingestion listener
|
||||
1. Resolve token → validate hash, not revoked/expired
|
||||
2. Check token's allowed_topics / allowed_source / allowed_cidrs against request
|
||||
3. Rate limit check (per-token bucket, per-IP bucket)
|
||||
4. Validate payload (topic exists, sizes, required fields)
|
||||
5. Deduplication check (deduplication_key within window) → increment
|
||||
occurrence_count on existing row instead of inserting, if matched
|
||||
6. Insert notification row (always — storage never depends on push)
|
||||
7. Write audit log entry (only for rejects/rate-limit events, to keep the
|
||||
audit log meaningful rather than noisy)
|
||||
8. Publish SSE event to any connected web/PWA clients subscribed to that topic
|
||||
9. For each user with a user_topic_subscriptions row where push_enabled=true
|
||||
for this topic (or no row = default push_enabled=true), and who has one
|
||||
or more active push_subscriptions:
|
||||
- build a small push payload (ids + short title/body + deep link)
|
||||
- send via web-push (VAPID), record push_delivery_attempts
|
||||
- on 404/410 from the push service, mark subscription disabled_at
|
||||
10. Return { id, status: "accepted" }
|
||||
```
|
||||
|
||||
## Data flow: browser session
|
||||
|
||||
```
|
||||
Browser (PWA or desktop)
|
||||
│ HTTPS → Pangolin → :8080
|
||||
▼
|
||||
notizarr web listener
|
||||
- AUTH_MODE=oidc: Authorization Code + PKCE against Pocket ID (or any OIDC IdP)
|
||||
- AUTH_MODE=proxy: trust identity headers only if request's peer IP is in
|
||||
TRUSTED_PROXY_CIDRS (Pangolin already validated OIDC upstream)
|
||||
- Sets httpOnly, Secure, SameSite=Lax session cookie
|
||||
▼
|
||||
React SPA fetches /api/v1/topics, /api/v1/notifications, opens /api/v1/stream (SSE)
|
||||
- New notifications arrive over SSE while the tab is open → instant UI update
|
||||
- Push notifications handle the case where the tab/app is closed
|
||||
```
|
||||
|
||||
## Modular monolith layout (backend)
|
||||
|
||||
Each module owns its DB access, routes, and types; only `db/schema.ts` is
|
||||
shared. See [07-folder-structure.md](07-folder-structure.md) for the concrete
|
||||
tree.
|
||||
|
||||
- `auth` — OIDC flow, proxy-header trust, session management
|
||||
- `users` — profile, role
|
||||
- `topics` — CRUD, subscription toggle, access control
|
||||
- `notifications` — ingestion, history, search, read/star state
|
||||
- `tokens` — API token CRUD, hashing, scoping
|
||||
- `push` — VAPID, subscribe/unsubscribe, fan-out, diagnostics
|
||||
- `admin` — dashboard stats, noisy-source detection, settings
|
||||
- `audit` — write-only log + admin read view
|
||||
- `rate-limit` — shared in-memory limiter used by `notifications` ingestion
|
||||
Reference in new issue
Block a user