Racoonity

La puerta existe

Por primera vez una persona puede abrir Racoonity, configurarlo, entrar y salir usando el producto real

Por El equipo de Racoonity •
7 min de lectura
  • #racoonity
  • #desarrollo
  • #arquitectura
  • #frontend
  • #mvp

La puerta existe

El Primer MVP de Racoonity terminó con una idea bastante concreta: la ciudad ya podía sobrevivir.

Había runtime, contratos públicos, módulos, acciones, queries, permisos, sesiones, inventario, outbox, idempotencia y una arquitectura suficientemente sólida para seguir creciendo sin convertir cada cambio en una demolición controlada.

Pero todavía faltaba algo bastante importante.

Una persona no podía entrar.

No había una experiencia de producto real. No había setup, login, navegación, shell ni una frontera clara entre “el backend existe” y “Racoonity es algo que alguien puede abrir y usar”.

Por eso el Segundo MVP empieza con I0:

La puerta existe.

Del motor al producto

Esta iteración no buscaba “hacer el frontend”.

Eso habría sido demasiado amplio y, francamente, una excelente manera de terminar con veinte componentes bonitos conectados a nada.

El objetivo era mucho más pequeño y más difícil de esconder detrás de mocks:

abrir Racoonity
→ detectar el estado de la instalación
→ completar setup si hace falta
→ iniciar sesión
→ resolver el contexto del usuario
→ obtener navegación autorizada
→ entrar al Product Shell
→ cerrar sesión

Ese recorrido tenía que funcionar usando los contratos públicos ya construidos en el Primer MVP.

El frontend no debía reconstruir reglas del backend ni convertirse en una segunda implementación del sistema.

Primero: saber dónde estamos

El primer problema al abrir Racoonity no es “qué pantalla mostramos”.

Es:

¿en qué estado está la aplicación?

La nueva state machine de bootstrap resuelve, en orden:

health
→ setup.get_status
→ sesión local
→ auth.resolve_session
→ atlas.resolve_navigation

y termina en uno de cinco estados explícitos:

  • setup_required
  • unauthenticated
  • authenticated
  • backend_unavailable
  • fatal_configuration_error

No existe un “más o menos cargó”.

El Product Shell no aparece hasta que la sesión y la navegación fueron resueltas correctamente.

Eso también nos obligó a separar dos ideas que suelen terminar mezcladas: bootstrap y estado actual de la aplicación.

Bootstrap ocurre una vez.

Después, CurrentAppState mantiene el estado operativo vivo de la experiencia: setup pendiente, usuario no autenticado, usuario autenticado con su contexto y navegación, o una condición de error que impide continuar.

Así evitamos volver a consultar medio backend cada vez que cambia una ruta.

Setup real, no una pantalla decorativa

La primera experiencia real fue la configuración inicial.

El wizard consume el contrato verdadero de setup.initialize y permite crear la estructura mínima necesaria para que Racoonity exista como instalación operativa:

  • empresa;
  • sucursal;
  • administrador;
  • caja;
  • ubicación de inventario;
  • métodos de pago.

Aquí apareció uno de los puntos más interesantes de toda la iteración: qué hacer si el usuario envía Setup y la red desaparece antes de recibir respuesta.

No podemos asumir que la operación falló.

Tampoco podemos enviarla otra vez alegremente.

Por eso la UI conserva temporalmente:

Idempotency-Key
+ payload exacto

solo cuando el resultado es realmente incierto.

Si el usuario reintenta la misma intención, se reutilizan ambos.

Si modifica el formulario, es una nueva intención y recibe una nueva key.

Y si recarga la aplicación, Racoonity no intenta reconstruir secretos desde storage: vuelve a consultar el estado real del setup.

Simple por defecto. Recuperable cuando importa.

Login, sesión y contexto

Después del setup llegó la segunda mitad de la puerta: autenticación.

El login no termina cuando auth.login devuelve un token.

El flujo completo es:

auth.login
→ almacenar token
→ auth.resolve_session
→ atlas.resolve_navigation
→ authenticated

Solo entonces aparece el Product Shell.

Si el login fue confirmado pero falla la resolución posterior por un problema técnico, la sesión no se destruye arbitrariamente. El token se preserva y Racoonity muestra que el backend no está disponible.

También separamos explícitamente dos casos que suelen parecer iguales desde fuera.

Sesión inválida al abrir la aplicación

Se limpia el token y el usuario vuelve a Login.

Sesión que expira mientras se está usando Racoonity

SessionService.expire() limpia la sesión, emite el evento correspondiente y la aplicación vuelve a Login mostrando:

Tu sesión ha expirado.

Son situaciones distintas y la experiencia ahora las trata como tales.

La ciudad ya tiene vestíbulo

Con una sesión válida, Racoonity entra por primera vez a un Product Shell real.

El Shell tiene responsabilidades deliberadamente pequeñas:

  • identidad del usuario;
  • navegación;
  • área de contenido;
  • feedback global;
  • controles propios de la sesión.

La navegación no se calcula en React.

No se filtra localmente.

No se reordenan permisos.

Atlas entrega la navegación autorizada y el frontend la representa en el mismo orden recibido.

Eso mantiene una frontera que queremos conservar durante todo el proyecto:

el backend decide qué está permitido; la interfaz explica y representa esa decisión.

Incluso los errores conservan esa separación.

permission_denied lleva a una superficie de acceso denegado sin destruir la sesión.

session_expired, en cambio, sí cambia el estado de autenticación.

Cerrar sesión también cuenta

Logout parece sencillo hasta que el backend no responde.

La regla que adoptamos es:

auth.logout
→ intentar revocación remota
→ limpiar sesión local SIEMPRE
→ limpiar query cache
→ volver a Login

Si el backend confirmó el logout, mostramos:

Has cerrado sesión.

Si la red falló, Racoonity no afirma que el token remoto fue revocado.

Solo garantiza aquello que realmente puede garantizar: que la sesión local terminó.

Las fronteras también se prueban

Una parte importante de I0 no se ve en pantalla.

Agregamos checks automatizados para impedir que la arquitectura se vaya degradando silenciosamente.

Entre otras reglas:

  • Shell no puede importar internals de features.
  • Services no pueden depender de primitives.
  • Primitives no pueden depender de services/features.
  • fetch directo solo puede existir dentro de la frontera API.
  • localStorage directo solo puede existir dentro del adapter de sesión.
  • API Client no puede navegar.
  • API Client no puede modificar sesión.
  • Services no pueden depender de React.

Durante el hardening descubrimos incluso que el checker inicial no resolvía imports usando el alias @/.

La regla existía.

El código podía saltársela.

Ahora eso también está cubierto.

Y luego llegó el navegador real

Hasta este punto había 270 tests frontend verdes.

Eso estaba bien.

Pero no era suficiente.

La última prueba de I0 arranca desde una base PostgreSQL genuinamente vacía y levanta todo el recorrido:

fresh PostgreSQL
→ migrations
→ Go API
→ Vite
→ navegador real
→ Setup
→ Login
→ Product Shell
→ Logout

Sin mocks para sustituir el backend.

Sin entrar directamente a /setup.

Sin crear la sesión por fuera de la aplicación.

El navegador abre Racoonity y es el propio bootstrap quien descubre que la instalación todavía no existe.

Después:

  1. completa setup;
  2. llega a Login;
  3. utiliza las credenciales recién creadas;
  4. resuelve sesión y navegación;
  5. muestra el Shell;
  6. cierra sesión;
  7. vuelve a Login.

También verificamos en tráfico real que:

  • setup.initialize lleva X-Correlation-ID y X-Idempotency-Key;
  • auth.login lleva X-Correlation-ID;
  • auth.login no lleva una Idempotency Key.

La corrida final del journey tardó alrededor de cuatro segundos.

Electron sigue siendo el destino

Racoonity sigue siendo Electron-first.

Pero I0 no necesita filesystem, impresión, scanners, IPC ni lifecycle nativo para demostrar su objetivo.

Por eso el cierre se ejecutó con Vite y un navegador real.

No es un cambio de dirección.

Es simplemente no introducir Electron antes de que exista una razón real para necesitarlo.

La frontera queda preparada para ello.

El resultado

I0 cerró con:

  • 147/147 tareas completas
  • 45/45 escenarios del spec en PASS
  • 270 tests frontend
  • 39 archivos de test
  • 0 gaps conocidos
  • P0: 0
  • P1: 0
  • regresión completa del Primer MVP en verde
  • journey vertical real en verde

Pero el número importante no es 270.

Es este:

una base vacía
→ puede convertirse en una instalación usable
→ usando Racoonity

El Primer MVP construyó la máquina.

El Segundo MVP empieza enseñándole a una persona cómo entrar.

La puerta existe.

Y detrás de ella, por fin, ya empieza a verse la ciudad.