--- name: gametime-server-architecture-sync-sharing description: memory note gametime-server-architecture-sync-sharing metadata: type: project --- # GameTime — Architecture serveur headless, sync et partage Décision serveur pour le ticket #46. ## Stack retenue - Langage/runtime : Dart serveur. - Framework HTTP : `shelf` + `shelf_router`. - Base serveur : PostgreSQL. - Containerisation : Docker + `docker-compose.yaml` dans `server/`. Raison : cohérence forte avec le client Flutter/Dart et les contrats métier existants, faible surface framework, testabilité correcte, packaging Docker simple avec image officielle Dart, PostgreSQL robuste pour comptes, tokens, ownership, sync incrémentale et partage ciblé. Alternatives écartées pour la v1 : - FastAPI/Python : excellent écosystème, mais introduit un second langage et des DTO à dupliquer. - Node/NestJS : robuste mais plus lourd et moins cohérent avec l'existant. - Serverpod : intéressant en Dart, mais plus structurant/opinionated que nécessaire pour un serveur API headless simple. ## Architecture hexagonale serveur Sous-répertoire dédié : `server/`. Couches attendues : - `domain/` : entités serveur et invariants purs. - `application/` : use cases, ports repositories/services, DTO API indépendants de Shelf/PostgreSQL. - `infrastructure/postgres/` : adapters repositories PostgreSQL, migrations. - `infrastructure/security/` : hash password, token signing/verification, clock/id providers. - `api/` : routes Shelf, middleware auth, mapping request/response. - `bin/server.dart` : composition root. Le domaine serveur ne dépend pas de Shelf, Docker ou PostgreSQL. ## Entités serveur - `UserAccount` : id serveur, email/login unique, passwordHash, displayName?, createdAt, updatedAt, disabledAt?. - `AuthSession` / refresh token : id, userId, tokenHash, issuedAt, expiresAt, revokedAt?, userAgent?, deviceLabel?. - `SyncedResource` : ressource possédée par un utilisateur pour Exercise, Program, WorkoutTemplate, WorkoutHistory, MediaAsset metadata. - `Share` : partage ciblé vers un ou plusieurs comptes, jamais public ouvert. - `ShareRecipient` : user destinataire + statut `pending|accepted|declined|revoked`. ## Modèle sync serveur Une table générique `synced_resources` est acceptable pour la v1 afin d'éviter de dupliquer tout le schéma Drift côté serveur. Champs recommandés : - `server_id` UUID primary key. - `owner_user_id` FK users. - `resource_type` enum text : `exercise|program|workoutTemplate|workoutHistory|mediaAsset`. - `client_id` text : id local stable du client. - `payload_json` jsonb : snapshot de la ressource côté client. - `schema_version` int. - `client_updated_at` timestamptz. - `server_updated_at` timestamptz. - `deleted_at` timestamptz nullable. - `origin_device_id` text nullable. Contraintes/index : - unique `(owner_user_id, resource_type, client_id)`. - index `(owner_user_id, resource_type, server_updated_at)`. - index `(owner_user_id, server_updated_at)` pour pull global. Les médias binaires ne sont pas couverts en profondeur par #46 : stocker d'abord les métadonnées et prévoir le port `MediaObjectStore` pour ajout futur. ## Protocole sync LWW v1 Stratégie : last-write-wins simple basé sur `clientUpdatedAt`. En cas d'égalité, tie-breaker stable côté serveur (`serverUpdatedAt`, puis `serverId` si nécessaire). Pas de résolution interactive en v1. Endpoints recommandés : ### `POST /sync/push` Requête : ```json { "deviceId": "...", "items": [ { "resourceType": "exercise", "clientId": "...", "schemaVersion": 3, "clientUpdatedAt": "2026-07-18T10:00:00Z", "deletedAt": null, "payload": {} } ] } ``` Réponse : ```json { "serverCursor": "...", "results": [ { "resourceType": "exercise", "clientId": "...", "serverId": "...", "status": "accepted|ignoredOlder|conflictLwwApplied|error", "serverUpdatedAt": "2026-07-18T10:00:01Z" } ] } ``` ### `GET /sync/pull?since=` Retourne toutes les ressources de l'utilisateur modifiées après le curseur serveur, soft deletes inclus. Réponse : ```json { "serverCursor": "...", "items": [ { "resourceType": "workoutTemplate", "clientId": "...", "serverId": "...", "schemaVersion": 3, "clientUpdatedAt": "...", "serverUpdatedAt": "...", "deletedAt": null, "payload": {} } ] } ``` ### `POST /sync/exchange` optionnel Combine push puis pull pour simplifier le futur client Flutter. ## Partage ciblé Le partage n'est pas un lien public. Un utilisateur authentifié envoie un snapshot de `program` ou `workoutTemplate` à des destinataires identifiés. Endpoints : - `POST /shares` : créer un partage vers un ou plusieurs comptes. - `GET /shares/inbox` : lister les partages reçus. - `POST /shares/{id}/accept` : importer/copier la ressource dans l'espace du destinataire. - `POST /shares/{id}/decline`. - `POST /shares/{id}/revoke` pour l'émetteur. À l'acceptation, créer une nouvelle ressource syncable détenue par le destinataire avec nouveaux IDs côté serveur et payload importable côté client. Ne jamais modifier la ressource source de l'émetteur. ## Structure `server/` Structure cible : ```text server/ pubspec.yaml README.md Dockerfile docker-compose.yaml .env.example bin/ server.dart lib/ domain/ application/ infrastructure/ postgres/ security/ config/ api/ migrations/ scripts/ push-gitea-image.sh test/ ``` `docker-compose.yaml` doit laisser libres via variables : - bind host/IP de la machine Docker. - port API exposé. - URL publique HTTPS derrière reverse proxy. - origine/IP reverse proxy autorisée si contrôle réseau ajouté. - paramètres PostgreSQL (`POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD`, volume). - secrets auth/JWT. Le script `scripts/push-gitea-image.sh` ne doit contenir aucune URL/identifiant en dur. Il accepte registry/image/tag/user/token via variables d'environnement ou arguments. ## Tickets créés - #47 `[Server] Scaffolding serveur Dart headless hexagonal`. - #48 `[Server] Auth comptes utilisateurs et tokens API`, dépend de #47. - #49 `[Server] Schéma PostgreSQL sync-ready GameTime`, dépend de #47. - #50 `[Server] API de synchronisation incrémentale LWW`, dépend de #48 et #49. - #51 `[Server] Partage ciblé de programmes et séances entre comptes`, dépend de #48 et #49. - #52 `[Server] Packaging Docker Compose et push Gitea Registry`, dépend de #47. - #53 `[Server] Tests API, contrats OpenAPI et vérification d'intégration`, dépend de #50, #51 et #52. Ordre recommandé : #47, puis #48 et #49 en parallèle, puis #50 et #51, puis #52, puis #53.