managarten/services/mana-sync/CLAUDE.md
Till JS d02428fca1 feat(uload): sync_changes integration, Stripe checkout, docs update
Sync integration:
- Redirect service reads links from mana-sync's sync_changes table
- Analytics service queries clicks from sync_changes
- Click tracking writes to sync_changes (visible to all clients)
- Public profile reads from sync_changes
- Server DB points to mana_sync database (not separate uload DB)
- Removed uload-database dependency from server

Stripe:
- Real Stripe checkout session creation (monthly/yearly)
- Webhook handler with signature verification
- Webhook route bypasses JWT auth

Documentation:
- Root CLAUDE.md: added uload to project table, dev commands, local-first list
- mana-sync CLAUDE.md: added uLoad, Taktik, Calc to connected apps

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 19:02:11 +02:00

6.2 KiB

mana-sync

Central sync server for local-first ManaCore apps. Handles data synchronization between IndexedDB (Dexie.js) clients and PostgreSQL via field-level Last-Write-Wins (LWW) conflict resolution.

Architecture

Client A (Browser)                Client B (Browser)
  IndexedDB (Dexie)                 IndexedDB (Dexie)
       |                                 |
       | POST /sync/{appId}              | GET /sync/{appId}/pull
       v                                 v
  ┌──────────────────────────────────────────┐
  │            mana-sync (Go)                │
  │  Port 3050 | JWT auth via JWKS           │
  │                                          │
  │  HTTP: sync + pull endpoints             │
  │  WS:   real-time sync-available notify   │
  │                                          │
  │  Conflict Resolution: Field-level LWW    │
  └──────────────────┬───────────────────────┘
                     |
                     v
               PostgreSQL
            (sync_changes table)

Quick Start

# From monorepo root
pnpm dev:sync           # Start server (requires DB + auth running)
pnpm dev:sync:build     # Compile Go binary

# Standalone
cd services/mana-sync
go build -o server ./cmd/server
JWKS_URL=http://localhost:3001/api/auth/jwks \
DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/mana_sync \
./server

Sync Protocol

Push (POST /sync/{appId})

Client sends a batch of changes, server records them and returns changes from other clients.

CLIENT -> SERVER:
{
  "clientId": "chrome-tab-abc123",
  "since": "2024-01-01T10:00:00.000Z",
  "changes": [
    {
      "table": "todos",
      "id": "todo-123",
      "op": "update",
      "fields": {
        "title": { "value": "Buy milk", "updatedAt": "2024-01-01T10:05:00Z" },
        "completed": { "value": true, "updatedAt": "2024-01-01T10:06:00Z" }
      }
    }
  ]
}

SERVER -> CLIENT:
{
  "serverChanges": [ ... changes from other clients ... ],
  "conflicts": [],
  "syncedUntil": "2024-01-01T10:06:15.123456789Z"
}

Pull (GET /sync/{appId}/pull)

Client requests changes for a specific collection since a timestamp.

GET /sync/todo/pull?collection=tasks&since=2024-01-01T10:00:00Z
Header: X-Client-Id: chrome-tab-abc123
Header: Authorization: Bearer <jwt>

WebSocket (GET /ws/{appId})

Real-time notifications when other clients sync. Client must authenticate first.

CLIENT -> SERVER: { "type": "auth", "token": "<jwt>" }
SERVER -> CLIENT: { "type": "auth-ok" }

// When another client syncs:
SERVER -> CLIENT: { "type": "sync-available", "tables": ["todos"] }

// Keepalive:
CLIENT -> SERVER: { "type": "ping" }
SERVER -> CLIENT: { "type": "pong" }

Conflict Resolution: Field-Level LWW

Each field update carries a timestamp. When the same field is modified by multiple clients, the latest timestamp wins.

Client A: title="Buy milk"    @ 10:05:00
Client B: title="Buy eggs"    @ 10:05:30
Result:   title="Buy eggs"    (Client B wins — later timestamp)

Client A: title="Buy milk"    @ 10:05:00
Client A: completed=true      @ 10:06:00
Client B: title="Buy eggs"    @ 10:05:30
Result:   title="Buy eggs", completed=true  (merged — different fields)

API Endpoints

Endpoint Method Auth Description
POST /sync/{appId} POST JWT Push changes, get server delta
GET /sync/{appId}/pull GET JWT Pull changes for a collection
GET /ws/{appId} WS JWT (in-band) Real-time sync notifications
GET /health GET No Health check with connection stats
GET /metrics GET No Prometheus metrics

Database Schema

Single table for all sync data:

sync_changes (
  id UUID PRIMARY KEY,
  app_id TEXT NOT NULL,
  table_name TEXT NOT NULL,
  record_id TEXT NOT NULL,
  user_id TEXT NOT NULL,
  op TEXT NOT NULL CHECK (insert | update | delete),
  data JSONB,
  field_timestamps JSONB DEFAULT '{}',
  client_id TEXT NOT NULL,
  created_at TIMESTAMPTZ DEFAULT now()
)

Indexes: (user_id, app_id, created_at), (table_name, record_id, created_at), (user_id, app_id, table_name, created_at)

Configuration

Variable Default Description
PORT 3050 Server port
DATABASE_URL postgresql://...localhost:5432/mana_sync PostgreSQL connection
JWKS_URL http://localhost:3001/api/auth/jwks mana-core-auth JWKS endpoint
CORS_ORIGINS http://localhost:5173,... Comma-separated allowed origins

Testing

cd services/mana-sync
go test ./... -v

Test coverage: auth (JWT extraction, validator), config (env loading), sync (validation, serialization, LWW types).

Project Structure

services/mana-sync/
├── cmd/server/main.go          — Entry point, routes, graceful shutdown
├── internal/
│   ├── auth/jwt.go             — EdDSA JWT validation via JWKS
│   ├── auth/jwt_test.go        — Token extraction, validator tests
│   ├── config/config.go        — Environment variable loading
│   ├── config/config_test.go   — Config defaults and env override tests
│   ├── store/postgres.go       — PostgreSQL schema, queries
│   ├── sync/handler.go         — HTTP endpoints, LWW logic, validation
│   ├── sync/handler_test.go    — Validation, serialization tests
│   ├── sync/types.go           — Protocol data structures
│   └── ws/hub.go               — WebSocket connection management
├── go.mod
└── CLAUDE.md

Security

  • JWT validated via EdDSA JWKS (same as NestJS backends)
  • WebSocket connections must authenticate within 10 seconds
  • Request body limited to 10 MB
  • Operation types validated (insert/update/delete only)
  • Table and record IDs required on all changes
  • RecordChange failures abort the entire sync (no partial writes)

Connected Apps (19)

Todo, Calendar, Clock, Contacts, Chat, Questions, Mukke, Context, Photos, ManaDeck, Picture, Presi, Storage, Zitare, SkillTree, CityCorners, NutriPhi, Planta, Inventar, uLoad, Taktik, Calc