# 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 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=` → **paquete offline** (delta si viene `since`). - `GET /api/v1/templates?since=` → 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`: ```json { "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`): ```json { "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: ```json { "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/media` — **subida 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`, `issue` → **last-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.