Files
GameTime/.ideai/memory/gametime-server-architecture-sync-sharing.md
Blomios c3158f665e docs(ideai): mémoire architecture serveur, tickets #44/#45 en QA, cadrage #47-#53
Ajoute la note mémoire d'architecture serveur (sync/sharing), passe les
tickets #44/#45 en QA, ajoute le cadrage du chantier serveur (tickets
#47 à #53).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 00:04:40 +02:00

6.6 KiB

name, description, metadata
name description metadata
gametime-server-architecture-sync-sharing memory note gametime-server-architecture-sync-sharing
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 :

{
  "deviceId": "...",
  "items": [
    {
      "resourceType": "exercise",
      "clientId": "...",
      "schemaVersion": 3,
      "clientUpdatedAt": "2026-07-18T10:00:00Z",
      "deletedAt": null,
      "payload": {}
    }
  ]
}

Réponse :

{
  "serverCursor": "...",
  "results": [
    {
      "resourceType": "exercise",
      "clientId": "...",
      "serverId": "...",
      "status": "accepted|ignoredOlder|conflictLwwApplied|error",
      "serverUpdatedAt": "2026-07-18T10:00:01Z"
    }
  ]
}

GET /sync/pull?since=<cursor>

Retourne toutes les ressources de l'utilisateur modifiées après le curseur serveur, soft deletes inclus.

Réponse :

{
  "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 :

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.