Cadena de frío
que sí emite el PDF
Cadena de frío, procesos y nómina para una procesadora de alimentos venezolana, offline-first y sin servidor propio.
Rol
Análisis del negocio, especificación técnica, arquitectura, sistema de diseño y desarrollo Flutter + Firebase
Estado
En desarrollo activo — Fase 0 y 0.5 (cimientos, identidad de marca, acceso y roles) construidas sobre un plan de 11 fases ya especificado
Lo que no salía
Una aplicación de AppSheet que se quedó a medias. Tres fallas concretas la hacían inservible.
AVERÍA 01
El PDF que nunca salió
La nota de recepción no se emitía en PDF con la dirección completa del cliente.
Es la razón por la que el dueño pagó la aplicación anterior.
AVERÍA 02
La nota de entrega sin IVA
No había forma de emitir el documento sin IVA que exige el negocio.
Sin ese papel no se despacha mercancía de la cava.
AVERÍA 03
El estado vivía en una cabeza
El avance real de cada trabajo no estaba en el sistema, sino en la memoria de alguien.
Nadie podía decir, al kilo, qué tiene cada cliente y desde cuándo.
El parte completo
La planta trabajaba con una aplicación de AppSheet que se quedó a medias. Tres fallas concretas la hacían inservible: la nota de recepción nunca salía en PDF con la dirección completa del cliente, no había forma de emitir la nota de entrega sin IVA que exige el negocio, y el estado real de cada trabajo vivía en la cabeza de alguien en vez de en el sistema. A eso se suma el contexto: cavas con señal de móvil pobre y cortes eléctricos, operarios manipulando el teléfono con guantes de nitrilo mojados a −20 °C o bajo sol directo, doble moneda con tasa BCV e IGTF, kilos que hay que cuadrar al gramo entre lo que entra, lo que se procesa, la merma y lo que sale, y una nómina venezolana con feriados, descuento del día de descanso, prestaciones y utilidades. El dueño necesitaba saber, al kilo, qué tiene cada cliente en cada cava y desde cuándo, y poder cobrar por ello.
Backend sin backend
Cada hueco que deja el plan gratuito tiene su compensación explícita, escrita en la especificación.
Una aplicación Flutter única para Android (planta) y web (administración) sobre Firebase en plan gratuito, diseñada desde el primer día como offline-first: la caché de Firestore siempre encendida y sin límite de tamaño, identificadores generados en el cliente, correlativos por bloques reservados para poder emitir documentos sin red, y fechas de negocio escritas como literales porque el sello de servidor vale nulo mientras no sincroniza. La seguridad no depende de la interfaz: las reglas de Firestore deniegan todo por defecto y se abren colección por colección, el rol vive en el documento del usuario y se lee con un único get() por evaluación, la auditoría y el kardex son estrictamente append-only, y ningún usuario puede tocar los campos de control de su propio perfil. El dinero se guarda en centavos enteros y el peso en gramos enteros, con la tasa a ocho decimales y aritmética en BigInt, porque un double en Firestore descuadra cuentas por cobrar con hiperinflación. Y toda la identidad visual sale del logo real de la empresa: el azul petróleo de las ondas del mar significa frío y bajo control, el naranja del sol significa que algo se salió de rango.
Seis piezas que aguantan el frío
Backend sin backend: todo el diseño cabe en el plan gratuito
Sin Cloud Functions no hay triggers, ni cron, ni custom claims, ni numeración en servidor, ni render de PDF remoto. Cada uno de esos huecos tiene su compensación explícita: reglas de seguridad que validan lo que un trigger validaría, transacciones para los correlativos, cómputo al abrir la pantalla, PDF generado en el propio dispositivo y scripts de Node con firebase-admin que el desarrollador ejecuta a mano. La documentación incluye una tabla de qué cambia el día que se active Blaze, y la recomendación de no activarlo hasta la Fase 5.
Reglas que deniegan por defecto, sin comodines
firestore.rules no tiene ningún match /{document=**} permisivo: una subcolección futura no puede quedar expuesta por olvido. Hoy solo están abiertas usuarios, config y auditoria. El perfil propio se lee siempre porque el guard de sesión lo necesita, pero listar la colección es solo del administrador; nadie se auto-eleva el rol porque la actualización propia bloquea los campos rol, activo, clienteId y estacionesPermitidas; y los usuarios no se borran, se desactivan, para no perder la trazabilidad de quién hizo qué.
Offline-first porque el camión llega de madrugada a una cava sin señal
La persistencia de Firestore arranca con caché ilimitada en main.dart. El identificador de cada documento es generado en el cliente y el número legible va en un campo aparte, porque los correlativos exigen transacción y una transacción no funciona sin red: un documento creado offline queda como «S/N (pendiente de numerar)» con chip ámbar y tiene prohibido pasar a estado cerrado hasta que se numere. Las fechas de negocio se derivan en el cliente a hora de Caracas y se escriben como literales, porque serverTimestamp vale nulo en la caché local hasta que sincroniza.
Enteros escalados: ni un céntimo ni un gramo en coma flotante
Peso en gramos enteros, dinero en centavos enteros, tasa BCV a ocho decimales, porcentajes en centésimas y temperatura en décimas de grado. Todo producto de tasa por monto se calcula en BigInt y se convierte al final, porque en Flutter web los enteros de Dart son doubles de 53 bits y el error aparece justo al conciliar contra el extracto bancario. El motivo está escrito en la especificación: FieldValue.increment sobre double acumula error de coma flotante en el saldo y en los kilos en cava; con enteros el incremento es exacto y conmutativo.
Paleta muestreada del logo, con cada contraste verificado
Los hexadecimales no son inventados: se muestrearon píxel a píxel del logo real de la empresa y cada par de colores está calculado con la fórmula WCAG 2.1. De ahí salen dos reglas duras que el código repite en comentarios: sobre el naranja del sol el texto es #2B1400 y nunca blanco, porque el blanco da 2,37:1; y el teal medio y el oliva de la colina son decorativos, jamás fondo de texto. Hay además un anti-objetivo escrito: ningún tono entre 260° y 330° en toda la aplicación, porque el lila es el color por defecto de AppSheet y es exactamente lo que el dueño rechaza.
Diseñado para guantes mojados, sol directo y penumbra de cava
El sistema de diseño define un modo campo con paleta de alto contraste, tipografía dos puntos mayor, botones de 64 dp, bordes de 2 dp y gradientes sustituidos por color plano porque los degradados pierden legibilidad con reflejo. Los objetivos táctiles nunca bajan de 48 dp y el teclado de pesos usa teclas de 72 a 88 dp separadas 12 mm, porque con guante de nitrilo el dedo tiene unos 14 mm de área efectiva. Toda la animación pasa por un único helper que la anula de golpe cuando el sistema pide reducir movimiento.
Un solo paquete, tres capas
Paquete Flutter único llamado sistema_cavas, con Clean Architecture por feature en tres capas —domain, infrastructure, presentation— bajo lib/features, y un núcleo compartido en lib/core. El sistema de diseño vive entero en lib/core/design_system y no depende de ninguna feature: tokens de marca muestreados del logo, colores semánticos como ThemeExtension, escala tipográfica, escala de espaciado en múltiplos de cuatro, radios, duraciones y curvas, puntos de corte y el pintor animado del logo; ninguna pantalla declara un Color literal. El estado es Riverpod sin generación de código, con providers escritos a mano. La navegación es go_router con un ShellRoute que envuelve el cascarón adaptativo y un redirect que lee un estado de sesión síncrono y devuelve null cuando el usuario debe quedarse donde está, precisamente para no entrar en bucle. La autenticación combina el flujo de Firebase Auth con el perfil en usuarios/{uid} escuchado como stream, y el rol se persiste como cadena en mayúsculas, nunca como índice de enum, para que reordenar el enum no cambie los permisos de nadie. Firebase corre en el proyecto sistemas-de-cava en plan Spark: Auth con correo y contraseña, Firestore en modo nativo con persistencia y caché ilimitada, reglas que deniegan todo por defecto, hosting apuntando a build/web y emuladores configurados en 9099, 8080, 9199 y 4000. Lo que exige privilegios de servidor vive fuera de la aplicación, en scripts de Node ESM con firebase-admin que se ejecutan a mano desde el portátil del desarrollador: no son Cloud Functions y el propio package.json lo aclara.
Puertos de los emuladores
Proyecto sistemas-de-cava, plan Spark. Hosting apuntando a build/web.
Decisiones a −18 °C
Cada ficha tiene su parte fría —lo que se rompía— y su parte templada: lo que quedó escrito para que no se repita.
Sin Cloud Functions no se pueden escribir custom claims, así que el rol no puede vivir en el token y las reglas se quedan sin la vía normal de autorización.
El rol se guarda en usuarios/{uid} y las reglas lo leen con get(). Como cada get() de una regla es una lectura facturable, las funciones se diseñaron para invocarse una sola vez por operación, con la cadena habilitado → perfil → rol encapsulada; y un rol desconocido se trata como «sin permisos», nunca como un valor por defecto permisivo.
createUserWithEmailAndPassword desloguea al administrador que está creando la cuenta, y en Spark no hay Admin SDK en servidor para evitarlo.
Alta desde una instancia secundaria de FirebaseApp: se crea el usuario contra esa instancia, se escribe su documento de perfil y se cierra sesión solo en la secundaria, sin tocar nunca la app principal. Como respaldo y para los casos que la app no cubre quedan los scripts crear_admin.mjs y crear_usuario.mjs con firebase-admin, que corren en el portátil.
El guard de navegación de un proyecto anterior entraba en bucle porque consultaba la base de datos dentro del redirect.
Se introdujo un enum EstadoSesion con cinco casos que se resuelve de forma síncrona a partir de dos streams ya escuchados, y un puente ChangeNotifier entre Riverpod y refreshListenable. El redirect devuelve null cuando el usuario debe quedarse donde está: devolver siempre una ruta es exactamente lo que produce el bucle, y así quedó anotado en el código.
serverTimestamp vale nulo en la caché local mientras no hay red, de modo que ordenar o agrupar por fecha rompe la aplicación justo en la planta, que es donde no hay señal.
Las fechas de negocio se derivan en el cliente a hora de Caracas (UTC−4 fijo, sin horario de verano) y se escriben además como cadenas literales de día, mes, año y semana; se ordena por esa cadena más un identificador ULID, las reglas validan coherencia con el reloj del servidor con tolerancia de dos días, y si el desfase de reloj del dispositivo supera cinco minutos la app bloquea la captura y pide corregir la hora.
El borrador del kardex marcaba el movimiento original como revertido y además lo filtraba al reconstruir saldos, con lo que el reverso se contaba dos veces y el saldo quedaba mal.
Se eliminó el campo revertido del modelo. La reconstrucción suma todos los movimientos sin filtro y el par original-reverso se anula solo; la interfaz pinta tachado el original consultando si existe un movimiento que lo apunte. El kardex y la auditoría quedaron append-only por reglas, sin excepción para ningún rol.
El naranja del sol del logo, que es el color más reconocible de la marca, no alcanza contraste con texto blanco: da 2,37:1.
Se muestreó el logo píxel a píxel, se calculó cada par con la fórmula WCAG 2.1 y se construyeron dos esquemas de color completos donde el naranja solo aparece como color terciario con texto #2B1400 encima. Los colores decorativos —el teal medio de las ondas y el oliva de la colina— quedaron marcados en el código como prohibidos para fondo de texto, y el azul puro del wordmark se ajustó a #1740B3 para el rol de información.
La versión más reciente de firebase_core rompía la compilación web con el SDK de Dart en uso, y la última de intl no resolvía contra flutter_localizations.
Se fijaron versiones exactas con el motivo escrito junto a cada una en pubspec.yaml: firebase_core en 4.13.0 y un límite directo sobre firebase_core_web porque la 3.11.0 llama a un método de interoperabilidad que no existe en ese Dart, e intl en 0.20.2 porque es la versión exacta que exige el SDK. Sin esa nota, el siguiente que actualice dependencias repite el fallo.
Las pantallas que ya compilan
Cinco pantallas construidas en la Fase 0, recreadas aquí con los mismos colores, medidas y textos que pinta la app.
Inversiones MarSaLe
Splash — el sol sale detrás de las ondas
Inversiones MarSaLe
Sistema de cavas y procesos
Iniciar sesión
Olvidé mi contraseña
RIF J-40776405-5 · Empresa Procesadora de Alimentos
Login — tarjeta blanca sobre el mar
Buenas tardes,
Andrés Marcano
—
Kilos en cava
—
Recepciones hoy
—
Órdenes en proceso
—
Por cobrar
Los indicadores se llenan a partir de la Fase 1, cuando entren las primeras recepciones.
Panel — saludo, cuatro indicadores y una advertencia honesta
Inversiones MarSaLe
AMAndrés Marcano
Gerente de planta
Cerrar sesión
Inventario
Esta sección se construye en la Fase 2.
Cascarón adaptativo — la misma app en el teléfono de planta y en la PC de oficina
Crea tu contraseña
Entraste con una contraseña temporal. Elige una propia para continuar.
Mínimo 8 caracteres
Las contraseñas no coinciden
Cerrar sesión
Crea tu contraseña — el primer ingreso obligatorio
Arrastra para ver las cinco pantallas
8 de los 10 destinos del administrador
Buenos días,
Andrés Marcano
—
Kilos en cava
—
Recepciones hoy
—
Órdenes en proceso
—
Por cobrar
Los indicadores se llenan a partir de la Fase 1, cuando entren las primeras recepciones.
Cascarón adaptativo — la misma app en el teléfono de planta y en la PC de oficina
Las maquetas reproducen la interfaz real; los datos que muestran son de ejemplo.
De esta imagen salió cada hexadecimal
Ningún color del sistema de diseño está inventado: se muestrearon píxel a píxel del logo real de la empresa.

#0E4C5A
Teal profundo
Primario · botones y acentos
#14707A
Teal medio
Secundario · decorativo
#5EC1BC
Cresta de onda
Trazo superior del logo
#1B3F4A
Onda profunda
Trazo inferior del logo
#F49021
Naranja del sol
Terciario · sólo con texto #2B1400
#FFC15E
Centro del sol
Degradado radial del disco
Regla escrita en el código
Sobre el naranja del sol el texto es #2B1400 y nunca blanco: el blanco da 2,37:1.
Anti-objetivo
Ningún tono entre 260° y 330° en toda la aplicación. El lila es el color por defecto de AppSheet, y es exactamente lo que el dueño rechaza.
Mood de marca · Frío industrial y mar profundo: azules petróleo sacados de las ondas del logo para decir «bajo control», con el naranja del sol reservado para cuando algo se sale de rango. Sobrio, legible bajo sol directo, sin un solo tono lila porque ese es el color de AppSheet y es justo lo que el cliente rechaza.
Kardex de lo construido y lo especificado
Append-only, como el kardex del proyecto: lo que entró en la Fase 0 y lo que ya tiene especificación esperando turno.
16
funcionalidades ya construidas
6
módulos con especificación cerrada
Con qué está hecho
Aplicación
Estado y navegación
Firebase (plan Spark)
Interfaz, tipografía y formato
Herramientas y administración
El proyecto en cifras
9
documentos de especificación técnica
14.126
líneas de especificación escritas
11
fases en el plan de ejecución
8
roles con permisos diferenciados
10
secciones de navegación por rol
14
rutas registradas en go_router
6
pantallas construidas en la Fase 0
35
colecciones y subcolecciones modeladas
3
colecciones abiertas hoy en las reglas
0
Cloud Functions: todo corre en plan gratuito
Once fases, dos ya escarchadas
El tramo con escarcha es el que ya está en frío. El tubo desnudo es lo que queda por conectar.
Paleta muestreada del logo, tema claro y oscuro verificados contra WCAG, logo animado con CustomPainter, splash, login, guard de sesión sin bucles y reglas en modo denegar-por-defecto.
Acceso y roles: ocho roles con permisos distintos, cascarón adaptativo por rol, cambio de contraseña obligatorio, pantalla de «sin acceso» y scripts de alta en Node.
Hoja de recepción fiel a la plantilla en papel, con servicios como selección múltiple, condición de la mercancía, temperatura y tabla de bruto, jaula, cesta, neto, unidades y lote; y PDF en dos copias, original para el cliente y copia para archivo.
Inventario por lote con kardex append-only, cavas y ubicaciones, etiquetas QR de lote imprimibles y escaneables, alertas de estadía prolongada y tablero de ocupación.
Órdenes de proceso con los tres estados reales de la planta, aplicación del operador con tarjetas grandes, modo guantes y háptica, captura de mermas por tipo y balance de masa con glaseo y tolerancia por producto.
Tarifario con vigencias, devengo de almacenaje por kilo-día por tramos, nota de entrega sin IVA con doble moneda y tasa BCV congelada, IGTF configurable y apagado por defecto, abonos, estado de cuenta y antigüedad de saldos.
Exportación de cualquier tabla a PDF con membrete y a Excel con celdas numéricas reales, más reportes de kilos por cliente, ingresos por servicio, mermas y rendimiento, ocupación de cavas y productividad por operario.
Módulo de recursos humanos con expediente del empleado, asistencia con origen controlado, feriados, parámetros legales versionados, motor de nómina en Dart puro y recibo en PDF.
Once fases en el plan · dos construidas · nueve documentos de especificación por delante
El proyecto, contado entero
Sistema de gestión de cavas de congelación, procesos de planta y nómina para Inversiones MarSaLe 0216, C.A. (RIF J-40776405-5), una procesadora de alimentos que recibe pescado y mariscos, los almacena en cámaras frigoríficas, los procesa y los factura por kilo y por día de estadía.
Sustituye a una aplicación hecha en AppSheet que nunca llegó a emitir el PDF de la nota de recepción ni la nota de entrega con la dirección completa del cliente: las dos cosas por las que el dueño pagó y que el desarrollador anterior no logró resolver.
El proyecto arranca con nueve documentos de especificación ejecutable —más de catorce mil líneas— que cubren seguridad y roles, modelo de datos Firestore, núcleo operativo, comercial y cobranza, RRHH y nómina venezolana, documentos y exportación, sistema de diseño, arquitectura offline-first y módulos adicionales. Cada uno fue revisado de forma adversarial contra su propio borrador y las correcciones están anotadas con su motivo.
La restricción que define toda la arquitectura es el plan Spark de Firebase: sin Cloud Functions, sin Admin SDK en servidor, sin custom claims en tiempo de ejecución, sin cron y sin FCM enviado desde servidor. Todo lo que en otro proyecto sería un trigger aquí se resuelve con reglas de seguridad, transacciones, cómputo en el cliente o scripts de Node que corren a mano en el portátil.
Lo que hoy está construido y compila es la Fase 0: paleta muestreada del logo real, tema claro y oscuro verificado contra WCAG, logo dibujado y animado con CustomPainter, splash, login, guard de sesión sin bucles, cascarón de navegación adaptativo por rol y panel con indicadores todavía vacíos, más las reglas de Firestore y Storage en modo denegar-por-defecto.
Puerta cerrándose
La cava queda a −18,0 °C y el sistema sigue funcionando sin señal.
Sistema de Cavas
¿Quieres algo así para tu producto?
Un ERP de cadena de frío que emite documentos sin red, cuadra kilos al gramo y cabe entero en el plan gratuito de Firebase. Si tienes una operación con reglas propias —almacenaje, procesos, nómina venezolana— este es exactamente el terreno que conozco.