Files
construprogress/docs/MOBILE_SYNC_PROTOCOL.md
T
javier ba363e7e18 docs: mobile offline-first sync protocol (Sanctum API tokens)
Approved plan/protocol for connecting a mobile app to the webapp: offline-first
with device outbox, PULL (bundle/delta/versioned templates/tombstones), PUSH
(/api/v1/sync idempotent by client uuid), media via multipart, conflict policy,
schema additions, security, and phased webapp deliverables. Auth decided:
Laravel Sanctum API tokens. No implementation yet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 19:36:35 +02:00

6.6 KiB

Protocolo de sincronización móvil offline-first

Estado: plan aprobado (2026-06-17). Auth decidida: Laravel Sanctum (API tokens). Alcance de este documento: lo necesario en la webapp para que una app móvil descargue plantillas/datos, trabaje sin conexión y sincronice al recuperar red. No cubre la implementación de la app móvil (la consume este contrato).

1. Modelo general

Offline-first con cola en el dispositivo (outbox) + sync bidireccional:

  • PULL (descarga): la app baja un "paquete" del proyecto (estructura + plantillas + registros) para trabajar sin red.
  • Trabajo offline: cada cambio se guarda local con un UUID generado en el móvil y se encola.
  • PUSH (subida): al volver la conexión, la app envía la cola; el servidor hace upsert idempotente por UUID y responde resultado por ítem.
  • Sincronización delta por updated_at (solo lo cambiado desde el último sync).

2. Autenticación — Laravel Sanctum (decidido)

  • Instalar laravel/sanctum. Tokens personales por dispositivo (no SPA-cookie; modo API token).
  • Endpoints:
    • POST /api/v1/login{ email, password, device_name }{ token, user }.
    • POST /api/v1/logout — revoca el token actual.
    • GET /api/v1/me — usuario + permisos efectivos.
  • El móvil envía Authorization: Bearer <token>.
  • Token con abilities (p. ej. mobile-sync) y registro de dispositivo (tabla devices) para revocar/caducar.
  • Caducidad de token configurable + endpoint de refresco o re-login.

3. Cambios de esquema

Añadir a las tablas sincronizables (features, inspections, issues, progress_updates, media):

  • uuid CHAR(36) único — lo genera el móvil; permite crear offline y upsert idempotente.
  • updated_at (ya existe) — delta + last-write-wins.
  • client_updated_at TIMESTAMP nullable — marca de tiempo del dispositivo (resolución de conflictos).
  • Soft-deletes (ya existen) — se exponen como tombstones (ids/uuids borrados) en el PULL.

Tablas nuevas:

  • devices (id, user_id, name, token_id, last_seen_at, …).
  • sync_logs (auditoría: device, operación, entidad, uuid, resultado, timestamp).

4. API (routes/api.php, prefijo /api/v1, stateless + Sanctum)

Descarga / PULL

  • GET /api/v1/projects → proyectos accesibles (reusa Project::accessibleBy).
  • GET /api/v1/projects/{id}/bundle?since=<ISO8601>paquete offline (delta si viene since).
  • GET /api/v1/templates?since=<ISO8601> → plantillas de inspección con version/hash (descarga incremental).
  • GET /api/v1/media/{id} o URLs firmadas dentro del bundle → adjuntos existentes.

Ejemplo de respuesta bundle:

{
  "server_time": "2026-06-17T20:00:00Z",
  "project": { "id": 1, "uuid": "…", "name": "…", "updated_at": "…" },
  "phases":   [ { "id": 4, "name": "…", "updated_at": "…" } ],
  "layers":   [ { "id": 4, "phase_id": 4, "name": "…", "updated_at": "…" } ],
  "features": [ { "id": 5, "uuid": "…", "layer_id": 4, "geometry": {}, "status": "in_progress", "progress": 40, "updated_at": "…" } ],
  "templates":[ { "id": 1, "version": 3, "fields": [  ] } ],
  "inspections": [  ],
  "issues": [  ],
  "deleted": { "features": ["uuid…"], "inspections": ["uuid…"] }
}

Subida / PUSH

  • POST /api/v1/sync — lote de operaciones (idempotente por uuid):
{ "operations": [
  { "entity": "progress_update", "op": "create", "uuid": "…", "client_updated_at": "…", "data": { "phase_id": 4, "progress": 60, "comment": "…", "location": {} } },
  { "entity": "inspection",      "op": "create", "uuid": "…", "client_updated_at": "…", "data": { "feature_id": 5, "template_id": 1, "data": {}, "result": "pass" } },
  { "entity": "feature",         "op": "update", "uuid": "…", "client_updated_at": "…", "data": { "status": "completed", "progress": 100 } },
  { "entity": "issue",           "op": "create", "uuid": "…", "client_updated_at": "…", "data": { "feature_id": 5, "title": "…", "priority": "high" } }
] }

Respuesta por operación:

{ "results": [
  { "uuid": "…", "status": "applied",   "server_id": 123 },
  { "uuid": "…", "status": "duplicate", "server_id": 124 },
  { "uuid": "…", "status": "conflict",  "server": { "status": "verified", "updated_at": "…" } },
  { "uuid": "…", "status": "error",     "error": "validation: …" }
] }
  • POST /api/v1/mediasubida de fotos por multipart (no base64), referenciando al padre por uuid (parent_entity, parent_uuid, file). Soporta reintento; troceado si el archivo es grande.

5. Idempotencia y conflictos

  • Idempotencia: el uuid evita duplicados si se reenvía la cola (re-sync seguro).
  • Append-only (sin conflicto): progress_updates, inspections → siempre insertan.
  • Editables (con política): feature.status/progress, issuelast-write-wins comparando client_updated_at vs updated_at del servidor. Si el servidor es más nuevo → conflict y se devuelve el valor del servidor para que el móvil decida/avise.

6. Seguridad

  • Nunca Model::create($payloadCliente) crudo. Usar FormRequests/DTO; fijar project_id/user_id en el servidor desde el contexto autorizado; validar que feature/phase pertenece a un proyecto del usuario (anti-IDOR).
  • Autorizar cada operación con permisos Spatie (update progress, create inspections, …) + pertenencia al proyecto (accessibleBy).
  • Rate limiting, caducidad de token, sync_logs para auditoría.

7. Versionado

  • Prefijo /api/v1; cabecera X-App-Version; el servidor responde versión mínima soportada (forzar update del móvil).
  • Versión/hash por plantilla (descarga incremental).

8. Qué reutilizar / retirar

  • OfflineSyncController + PendingSync: el vocabulario de acciones (progress_update, inspection, feature_create, media_upload, task_complete) es buena base para las operaciones de /sync. Pero hay que: pasar a API+token, añadir uuid/validación/autorización, y mover la cola al dispositivo (la PendingSync del servidor deja de ser necesaria para el móvil; se puede retirar o reaprovechar como sync_logs).

9. Entregables en la webapp (por fases)

  • Fase A — Auth & esqueleto API: Sanctum, routes/api.php, login/logout/me, tabla devices, abilities.
  • Fase B — PULL: projects, bundle + delta, templates versionadas, tombstones.
  • Fase C — PUSH: /sync idempotente con validación/autorización/conflictos (recoge y endurece la lógica actual).
  • Fase D — Media: subida multipart + descarga.
  • Fase E — Endurecimiento + Docs: rate-limit, sync_logs, OpenAPI/Swagger como contrato para el equipo móvil.