PetID — Documento técnico de plataforma

Una placa que un desconocido puede escanear

El código de una placa es la única credencial que protege los datos de contacto de un tutor. Cómo se genera para que no se pueda adivinar ni reconstruir, qué ve exactamente quien encuentra al animal, y por qué las reglas del dominio viven en un paquete propio que la app móvil y la web comparten.

Superficie pública · una sola Reglas puras · 20 módulos, todos con prueba Pruebas · 49 archivos Clientes · móvil y web sobre el mismo núcleo
01

Qué resuelve

Una mascota lleva una placa con un código. Quien la encuentra lo escanea con la cámara del teléfono, sin instalar nada y sin crear una cuenta, y obtiene lo necesario para devolverla.

Alrededor de esa idea hay una plataforma completa: identidad del animal, historial clínico, clínicas veterinarias con su agenda, adopciones, transferencia de tutor y un marketplace. Pero el problema de diseño que condiciona todo lo demás es el primero.

La tensión que hay que resolver

Para que la placa sirva, un desconocido tiene que poder ver el contacto del tutor. Para que sea segura, solo quien tiene la placa en la mano debe poder verlo — no cualquiera que pruebe direcciones.

Todo lo que sigue en las tres primeras secciones son las consecuencias de tomarse esa frase en serio.

02

El token de la placa

El código impreso en la placa es la credencial. No hay contraseña detrás ni segundo paso: quien lo tiene, ve la ficha.

Cómo se genera
24 bytes del generador criptográfico del sistema ↓ 32 caracteres en base64url NUNCA se deriva del identificador de la mascota, de su nombre ni de ningún dato del perfil.
La longitud está por encima del mínimo de 24 caracteres que exige el diseño, y muy lejos del alcance de un ataque por fuerza bruta contra la dirección pública.
Por qué no se deriva de nada

Derivar el token de datos del perfil es la tentación obvia: sale determinista, no hay que guardarlo y se puede regenerar. Y rompe las dos propiedades que lo hacen útil.

Primero, permitiría reconstruir la placa de cualquier mascota a partir de datos que sí son públicos — el nombre del animal, por ejemplo. Segundo, haría inútil la revocación: si el token es función del perfil, revocarlo y emitir otro daría el mismo resultado, y una placa perdida seguiría abriendo la ficha para siempre.

03

La única puerta sin sesión

La ficha pública es la única superficie del sistema accesible sin haber iniciado sesión. Por eso está tratada como lo que es: la parte expuesta.

SituaciónQué respondePor qué
El token no existeno encontradoNo distinguir los dos casos evita confirmarle a alguien que un token existió alguna vez
El token fue revocadono encontrado
El token está malformadorechazo inmediatoNo puede corresponder a ninguna placa: se descarta antes de contar contra el límite de peticiones y antes de tocar la base
Muchos intentos seguidoslímite de tasaCorta el bucle que prueba códigos al azar buscando una placa válida

El orden importa: descartar lo malformado antes del limitador significa que un atacante enviando basura no consume el presupuesto de peticiones de nadie, y que la base de datos no recibe consultas que no pueden dar resultado.

El handler nunca devuelve filas crudas

Este módulo está marcado en el código como módulo de referencia, con el patrón que los demás deben seguir: el router consulta la base, las reglas y la proyección de salida viven en el paquete de reglas, y el handler nunca entrega una fila tal como vino.

Es lo que impide el accidente clásico: alguien añade una columna a la tabla y esa columna aparece en la respuesta pública sin que nadie lo decida. Con una proyección explícita, el dato nuevo no sale hasta que alguien lo escribe.

04

La máquina de estados

Una mascota está en uno de cuatro estados, y las transiciones válidas están declaradas — no dispersas en condicionales.

Estados y transiciones
activo → perdido · fallecido · adoptado perdido → activo · fallecido · adoptado adoptado → (se comporta como activo) fallecido → terminal
Vive en el paquete de reglas y no en los handlers de la API, para que sea comprobable sin base de datos y no se duplique entre la app móvil y la web.
DecisiónRazonamiento
Adoptado se comporta como activoLa mascota sigue viva y bajo cuidado de un tutor. La diferencia es documental —cómo llegó— y no de ciclo de vida
Fallecido es terminalEl perfil se conserva para el historial clínico y para el historial de tutores, pero ya no admite cambios de estado

Que fallecido conserve el perfil en vez de borrarlo no es sentimentalismo: el historial clínico de un animal es información veterinaria, y la cadena de tutores es lo que respalda una transferencia anterior.

05

Perdido y encontrado

El flujo de pérdida se apoya en la máquina de estados en lugar de repetirla. Lo que añade es la relación entre el estado de la mascota y el reporte abierto — que es justo lo que el router no debe decidir por su cuenta.

Al recuperarla vuelve a activo, no al estado anterior

La tentación es restaurar lo que había antes. Pero si antes estaba adoptado, reponerlo tras una pérdida no aporta información nueva: adoptado describe cómo llegó a su tutor, no su situación actual. Volver a activo dice exactamente lo que pasó — la mascota está bien y con su tutor.

La fecha en que el tutor dice haber visto al animal por última vez se valida en la regla, no en el router, para que la app móvil y la web importen la misma validación en vez de copiarla cada una en su formulario.

El instante se inyecta
regla( estado, reporte abierto, última vez visto, AHORA )
El momento de referencia entra como parámetro en vez de leerse dentro de la función. Es lo que mantiene la regla pura: dada la misma entrada devuelve siempre lo mismo, y se puede comprobar cualquier escenario temporal sin esperar a que llegue esa fecha.
06

Un núcleo, dos clientes

Hay una aplicación móvil y una web. Las dos consumen la misma API y, sobre todo, importan las mismas reglas.

Los veinte módulos de reglas
Identidad auth-identity · pet-access · pet-status · pet-transfer Placa qr-token · rate-limit Pérdida lost-report Salud medical Clínicas organization-access · organization-agenda organization-invitations Servicios service-booking Adopción giving Comercio marketplace-access · marketplace-view marketplace-order · marketplace-pricing payment-gateway Avisos notifications
Cada uno tiene su archivo de prueba al lado. Son funciones sin base de datos y sin framework: reciben datos, devuelven una decisión.
Por qué esto no es solo orden

Cuando una regla vive en el handler de la API, la app móvil tiene que reimplementarla para validar antes de enviar — y a partir de ese momento hay dos versiones que divergen. El usuario ve un formulario que acepta algo que el servidor después rechaza, o al revés.

Con la regla en un paquete compartido, la validación del formulario y la del servidor son literalmente la misma función. No pueden discrepar.

07

Arquitectura y verificación

Monorepo
apps/mobile aplicación móvil apps/web aplicación web packages/api routers: mascotas · placa · ficha pública · clínico organizaciones · servicios · adopción · marketplace autenticación · administración packages/core las reglas puras, con sus pruebas packages/db esquema y migraciones
La dirección de las dependencias es la que importa: api y las dos aplicaciones dependen de core, y core no depende de nadie. Por eso se puede probar entero sin levantar una base de datos.

Dos niveles de prueba

NivelQué cubre
Reglas purasLos veinte módulos del núcleo: transiciones de estado, acceso, precios, reservas, generación de token
RoutersEl comportamiento contra una base real, incluidas las respuestas de la superficie pública

49 archivos de prueba en total. La puerta de calidad levanta una base de datos, aplica las migraciones y corre el chequeo de tipos y las pruebas de todos los paquetes antes de permitir un despliegue.