added media fixes, search, push updates, pwa, metrics. preparing release 1.0
CI / Go (build + vet + test) (push) Canceled after 0s
CI / Web (build + lint) (push) Canceled after 0s

This commit is contained in:
vantablackTea
2026-09-27 23:26:15 +03:00
parent 855a62755c
commit 6fc6d977bf
59 changed files with 6823 additions and 113 deletions
+22
View File
@@ -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.
+21
View File
@@ -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.
+20
View File
@@ -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.
+22
View File
@@ -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.
+21
View File
@@ -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.
+78
View File
@@ -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`)