added media fixes, search, push updates, pwa, metrics. preparing release 1.0
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
# ADR 0001: Monolith over microservices
|
||||
|
||||
- Status: accepted
|
||||
- Date: 2026-09
|
||||
|
||||
## Context
|
||||
The messenger serves a single family (tens of users) on a home-lab mini PC
|
||||
with ~8 GB RAM. A microservice split would add operational overhead
|
||||
(networking, service discovery, per-service containers) with zero benefit at
|
||||
this scale.
|
||||
|
||||
## Decision
|
||||
Build a single Go process exposing REST, WebSocket, media serving and static
|
||||
files, with PostgreSQL as the only external state. Internal packages
|
||||
(`store`, `auth`, `realtime`, `api`, `media`) are cleanly separated with
|
||||
explicit interfaces, so extraction later is mechanical.
|
||||
|
||||
## Consequences
|
||||
- One container, trivial `docker compose up`.
|
||||
- In-process WebSocket hub gives O(1) presence checks for push decisions.
|
||||
- If the service ever needs horizontal scaling, `realtime` and `store`
|
||||
already hide their implementations behind interfaces.
|
||||
@@ -0,0 +1,21 @@
|
||||
# ADR 0002: In-memory WebSocket hub
|
||||
|
||||
- Status: accepted
|
||||
- Date: 2026-09
|
||||
|
||||
## Context
|
||||
Realtime delivery (messages, typing, presence, read receipts) must reach
|
||||
connected clients immediately. Options: external broker (Redis pub/sub,
|
||||
NATS), Postgres LISTEN/NOTIFY, or an in-process hub.
|
||||
|
||||
## Decision
|
||||
Use an in-process hub: `map[userID] -> connections` plus
|
||||
`map[conversationID] -> subscribers`, guarded by a mutex. Membership and
|
||||
read-state are resolved through a `MembershipProvider` interface implemented
|
||||
by the store.
|
||||
|
||||
## Consequences
|
||||
- Sub-millisecond fan-out, no extra moving parts.
|
||||
- Presence checks (`IsOnline`) are lock-free reads used by the push notifier.
|
||||
- Multi-node scaling would require a broker; the hub's publish methods are
|
||||
the seam where that would plug in.
|
||||
@@ -0,0 +1,20 @@
|
||||
# ADR 0003: Search via ILIKE instead of pg_trgm
|
||||
|
||||
- Status: accepted
|
||||
- Date: 2026-09
|
||||
|
||||
## Context
|
||||
Message search must find substring matches across a user's conversations.
|
||||
pg_trgm + GIN gives indexed trigram search, but the extension is not
|
||||
installed on the target system and cannot be created by a non-superuser role
|
||||
(and adding it to the Docker image is an OS-level dependency).
|
||||
|
||||
## Decision
|
||||
Search with `body ILIKE '%term%'` restricted to the user's conversations,
|
||||
ordered by message id descending with a limit.
|
||||
|
||||
## Consequences
|
||||
- No extension or superuser requirement; works on any PostgreSQL install.
|
||||
- Sequential scan is instant for thousands of messages (family scale).
|
||||
- Scaling path documented: `CREATE EXTENSION pg_trgm` + GIN index with
|
||||
`gin_trgm_ops`, keeping the same SQL semantics.
|
||||
@@ -0,0 +1,22 @@
|
||||
# ADR 0004: Media behind an S3-like interface
|
||||
|
||||
- Status: accepted
|
||||
- Date: 2026-09
|
||||
|
||||
## Context
|
||||
Attachments (images, files, videos) need storage, thumbnails for images, and
|
||||
bounded upload size. Storage must survive container restarts and, ideally, be
|
||||
swappable without touching API code.
|
||||
|
||||
## Decision
|
||||
Define a `Storage` interface (`Save`/`Open`/`Delete` keyed by opaque keys) in
|
||||
`internal/media`. Phase 2 ships `Local` — files on disk with atomic
|
||||
temp-file+rename writes and path-traversal protection. API handlers depend
|
||||
only on the interface.
|
||||
|
||||
## Consequences
|
||||
- Metadata lives in Postgres (`media` table); bytes live in storage.
|
||||
- Replacing disk with MinIO/S3 means a new `Storage` implementation, no
|
||||
handler changes.
|
||||
- Image thumbnails are generated with stdlib decoding + `x/image/draw`,
|
||||
avoiding CGO and external binaries.
|
||||
@@ -0,0 +1,21 @@
|
||||
# ADR 0005: E2EE deferred, libsignal when implemented
|
||||
|
||||
- Status: accepted
|
||||
- Date: 2026-09
|
||||
|
||||
## Context
|
||||
A family messenger should eventually protect message content at rest and in
|
||||
transit, ideally with end-to-end encryption. Rolling a custom crypto protocol
|
||||
is unsafe and a red flag in review.
|
||||
|
||||
## Decision
|
||||
- In transit: TLS everywhere (Caddy on the VPS / reverse proxy; homelab
|
||||
ingress).
|
||||
- At rest: Postgres and media disk live on trusted hardware.
|
||||
- E2EE: deferred to a future major version, implemented with an established
|
||||
library (libsignal) — never hand-written primitives.
|
||||
|
||||
## Consequences
|
||||
- The wire protocol and data model are ready for E2EE (opaque message bodies,
|
||||
device-agnostic session model).
|
||||
- No false-security claims in the README.
|
||||
@@ -0,0 +1,78 @@
|
||||
# Architecture
|
||||
|
||||
Messenger is a self-hosted monolith: a single Go binary serves the REST API,
|
||||
WebSocket hub, media files and the built frontend. PostgreSQL is the only
|
||||
stateful dependency; attachments live on local disk behind an S3-like
|
||||
interface.
|
||||
|
||||
## Component diagram
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Browser[Browser / PWA]
|
||||
UI[React + Vite + Tailwind]
|
||||
SW[Service Worker<br/>offline cache + Web Push]
|
||||
end
|
||||
|
||||
subgraph Server[Go binary - single process]
|
||||
HTTP[REST API /api/v1]
|
||||
WS[WebSocket hub<br/>in-memory]
|
||||
MEDIA[Media service<br/>thumbnails]
|
||||
PUSH[Push notifier<br/>VAPID]
|
||||
end
|
||||
|
||||
subgraph Data
|
||||
PG[(PostgreSQL 17)]
|
||||
DISK[(local disk<br/>S3-like Storage)]
|
||||
end
|
||||
|
||||
UI -->|REST + WS| HTTP
|
||||
UI --> WS
|
||||
SW -->|Web Push| PUSH
|
||||
HTTP --> PG
|
||||
WS --> PG
|
||||
PUSH --> PG
|
||||
MEDIA --> DISK
|
||||
HTTP --> MEDIA
|
||||
WS -.->|presence check| PUSH
|
||||
```
|
||||
|
||||
## Request flow for a message
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant A as Alice (PWA)
|
||||
participant API as API
|
||||
participant PG as PostgreSQL
|
||||
participant WS as WS hub
|
||||
participant B as Bob (PWA)
|
||||
|
||||
A->>API: POST /conversations/{id}/messages
|
||||
API->>PG: INSERT message
|
||||
API->>WS: publish message.created
|
||||
WS-->>A: message.created (echo)
|
||||
WS-->>B: message.created
|
||||
API->>PUSH: notify offline members
|
||||
PUSH-->>B: Web Push (if offline)
|
||||
API-->>A: 201 message
|
||||
```
|
||||
|
||||
## Key decisions (see docs/adr)
|
||||
|
||||
| Area | Decision | Rationale |
|
||||
|---|---|---|
|
||||
| Shape | Monolith, one container | Family scale; trivial deploy; no fake microservices |
|
||||
| Realtime | In-memory WS hub | Presence and delivery in one process; pg LISTEN/NOTIFY is the scaling path |
|
||||
| Search | ILIKE substring | No extension dependency; instant at family scale; pg_trgm noted as upgrade |
|
||||
| Media | S3-like Storage interface | Swap local disk for MinIO without touching handlers |
|
||||
| Auth | JWT access + rotated refresh in httpOnly cookies | No token in JS; rotation limits replay |
|
||||
| Crypto | TLS in transit; E2EE via libsignal on roadmap | Never hand-roll crypto |
|
||||
|
||||
## Deployment
|
||||
|
||||
docker-compose runs: api, postgres, victoriametrics, grafana.
|
||||
See `deploy/` for compose + Grafana dashboard provisioning.
|
||||
|
||||
- **Metrics**: `GET /metrics` (Prometheus format), scraped by VictoriaMetrics
|
||||
- **Grafana**: http://localhost:3000, dashboard «Messenger» auto-provisioned
|
||||
- **Push**: optional; requires VAPID keys (`go run ./cmd/keygen`)
|
||||
Reference in New Issue
Block a user