Skip to content
raludePublic

About

POS e inventario offline-first para supermercados · Electron + Fastify + SQLite · DDD/Hexagonal

Topics

Resources

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

235 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Cullen

Plataforma de punto de venta e inventario para supermercados, construida offline-first.

Electron · React · Fastify · SQLite · TypeScript · DDD + Arquitectura Hexagonal

TypeScript Tests ADRs License

El nombre es por Michael J. Cullen, fundador de King Kullen en 1930 — el primer supermercado moderno de EE. UU. Le propuso la idea a su empleador, Kroger, y se la rechazaron; la construyó por su cuenta de todas formas. Yo propuse este sistema para un supermercado real y también fue rechazado, así que lo sigo construyendo solo, como MVP de referencia.


TL;DR (English) — Offline-first POS and inventory platform for supermarkets. TypeScript monorepo built with tactical DDD and hexagonal architecture: pure domain, use-case layer with ports, swappable adapters. 1,282 tests across 198 files, 31 ADRs and 44 forward-only migrations, with architecture boundaries enforced by ESLint. Handles integer money arithmetic, multi-currency, crash-recoverable fiscal state, idempotent commands, optimistic concurrency and per-node aggregate ownership. LAN synchronization runs over mutually authenticated HTTPS with durable outbox delivery, at-least-once semantics and no duplicate effects, verified across eleven network-partition scenarios with three independent SQLite nodes. Identity administration, auditable authorization, at-rest data protection and log redaction shipped and were audited, and the node installs as a supervised Windows service. This is a non-certified reference running with explicitly simulated fiscal behavior. Detailed docs are in Spanish.


El problema

El comercio minorista venezolano opera con condiciones que rompen los supuestos de un POS convencional:

Restricción real Consecuencia técnica
Múltiples monedas simultáneas con tasas volátiles El dinero no puede ser float ni asumir una moneda única; cada conversión exige tasa, fuente y vigencia explícitas
Conectividad intermitente Cada terminal debe operar autónoma y reconciliar después, sin last-write-wins
Impresoras fiscales que fallan a mitad de una operación El estado fiscal debe ser persistente y recuperable tras un reinicio, sin reimprimir a ciegas
Auditoría comercial y fiscal obligatoria Toda operación sensible necesita actor, terminal, nodo, UTC y motivo

Cullen es mi respuesta de ingeniería a ese problema: un MVP técnico de referencia que trata esas restricciones como invariantes de diseño, no como casos borde.


Lo técnicamente interesante

Estos son los problemas que resolví y que considero representativos de mi trabajo.

💰 Dinero entero, nunca punto flotante

Money almacena unidades menores enteras + código de moneda, y opera internamente con BigInt para que multiplicar por cantidad o porcentaje no pierda precisión ni desborde silenciosamente. Quantity aplica el mismo criterio a cantidades escaladas (kg, litros, unidades). Sumar dos Money de monedas distintas es un error de dominio, no un bug de producción.

🔌 Estado fiscal recuperable en cuatro ejes independientes

Cuando una impresora fiscal deja de responder, "no sé qué pasó" no es un estado aceptable. Cada documento persiste evidencia en cuatro ejes ortogonales —despacho, efecto del comando, commit fiscal e impresión— de modo que un timeout nunca se confunde con un rechazo. La recuperación es determinista y fail-closed: ante evidencia ambigua el sistema bloquea la repetición y exige reconciliación explícita.

🌐 Ownership por nodo, sin last-write-wins

Cada agregado tiene un único nodo dueño (originNodeId), inmutable desde su creación y respaldado por triggers de base de datos. Un nodo que no es dueño rechaza el comando de escritura en la capa de aplicación (AGGREGATE_OWNER_MISMATCH); si el ownership no puede resolverse, toda mutación se rechaza en lugar de adivinar. Es la base para la sincronización LAN sin resolución de conflictos ad hoc.

📡 Sincronización LAN probada contra cortes de red reales

Un POS completa ventas sin coordinador y entrega sus hechos después. La entrega es at-least-once sobre un outbox durable con orden por agregado y claims generacionales: solo un ACK contractual del destino correcto confirma; un timeout, una respuesta truncada o un ACK de otro ciclo no lo hacen. El transporte es HTTPS con autenticación mutua en un listener técnico separado de la API de operadores, y falla cerrado — sin material TLS completo no escucha, en vez de degradar.

Lo que lo hace verificable son once escenarios de corte automatizados con un coordinador y dos terminales, tres archivos SQLite independientes y listeners reales: caída antes del commit remoto, caída entre el commit y el ACK, reentrega del mismo eventId tras reiniciar ambos nodos, dos terminales vendiendo la última unidad offline, agotamiento del retry con reanudación autorizada, y corte de Internet distinguido del corte de LAN. Ninguno simula la pérdida de red borrando eventos.

🔁 Idempotencia por intención, no por reintento

Cada comando de negocio se ejecuta dentro de executeIdempotentCommand: misma clave + mismo fingerprint devuelve exactamente la misma respuesta; misma clave con distinto contenido falla con IDEMPOTENCY_KEY_CONFLICT. El renderer mantiene una clave por intención de formulario, de modo que doble clic, timeout y reintento comparten clave, pero una intención nueva genera otra.

🧾 Contratos HTTP verificados contra los casos de uso

Cada endpoint declara su permiso en un contrato compartido entre servidor y renderer. Una prueba recorre la matriz completa y falla si el permiso declarado se desvía del que el caso de uso realmente exige — el tipo de deriva silenciosa que hace que la UI ofrezca acciones que el servidor rechaza. El renderer deriva su navegación y sus botones de los permisos efectivos de la sesión, no de suposiciones.

🛡️ Invariantes defendidos en dos capas

Las reglas viven en el dominio, pero las críticas tienen además un respaldo en SQLite: 125 triggers que impiden borrado físico de historia, mutación de evidencia inmutable, ownership sin resolver o identificadores duplicados. Si una ruta de escritura futura olvida la regla, la base de datos la detiene.

🔐 Datos en reposo protegidos, con el límite escrito

El PIN se guarda como hash scrypt con sal individual, el token de sesión solo como SHA-256 y el ticket de enrolamiento solo como hash; ninguno viaja entre nodos. El directorio que los contiene se restringe por ACL y el arranque verifica los permisos efectivos en vez de suponer que el instalador los aplicó: si el directorio es legible por otras cuentas, el nodo falla cerrado con DATA_DIRECTORY_NOT_PROTECTED y no abre el API. Respaldos y secretos se sellan con AES-256-GCM bajo una clave del almacén del sistema operativo, y los logs técnicos redactan por nombre de campo, de modo que un pin, token o cardNumber nuevo queda censurado sin tocar la configuración del logger.

El límite también está declarado: el archivo SQLite operativo no se cifra, por decisión explícita de ADR-0029 D7.1 — con un servicio desatendido la clave viviría en la misma máquina que el atacante con Administrador local, así que cifrarlo compraría una promesa, no una protección.

📐 Fronteras arquitectónicas que el linter hace cumplir

El grafo de dependencias no es una convención documentada: eslint.config.js lo impone con no-restricted-imports por capa. core/domain no puede importar Fastify, Drizzle, Electron ni React — y el build falla si alguien lo intenta.


El recorrido, en seis pantallas

Las capturas salen de una corrida completa sobre un clon limpio del commit publicado, siguiendo el guion de la demo: instalar, verificar, aprovisionar, recibir mercancía, vender, facturar y cerrar la caja. Ninguna está montada. El rótulo SIMULACIÓN que se ve en todas no es decorativo: la fiscalidad es un driver simulado y el sistema lo dice en cada pantalla, en cada documento y en cada reporte.

Los dos RIF que aparecen son inventados y el catálogo es la seed de ejemplo.

Pantalla de ingreso Ingreso. Código de operador y PIN. La sesión vive en una cookie del nodo local; el PIN nunca sale del terminal ni se guarda en claro en ninguna tabla.
Pantalla de inicio Inicio. La navegación muestra solo lo que los permisos de la sesión alcanzan, y el servidor vuelve a verificar cada acción: ocultar una pantalla nunca sustituye a autorizar. Abajo, el modo fiscal declarado al arrancar.
Pantalla de venta Venta. Ticket, catálogo y cobro. Todos los importes los calcula el nodo —la pantalla no hace aritmética de negocio— y el botón de completar explica siempre por qué está deshabilitado.
Factura simulada Factura. Emitirla es un paso propio, no parte de completar la venta: si el dispositivo fiscal fallara, no puede revertirse un cobro ya asentado en el turno. El documento conserva su reintento y su evidencia.
Kardex de un producto Inventario. El kardex es append-only: nada se edita ni se borra, y el saldo siempre se deriva de sus movimientos. Aquí, la recepción de 20 y la salida de 2 que dejó la venta.
Reportes y auditoría Auditoría. Cada operación sensible queda con su actor, su acción, la entidad afectada, el motivo y el terminal. La auditoría no expone el contenido del agregado, solo el hecho de que cambió.

Falta una séptima, que no está aquí porque para un lector sería casi idéntica a la del inventario: tras detener el nodo y el terminal y volver a arrancarlos, el kardex repite el mismo saldo y los mismos movimientos. Es la afirmación más fuerte del recorrido y queda registrada como evidencia de la etapa.


Calidad verificable

Pruebas (Vitest, todas en verde) 1.282 en 198 archivos
Código de producción / código de prueba 53.3k / 34.4k líneas
Clases de aplicación exportadas 129
Contratos HTTP v1 publicados 107
Permisos granulares 53
Migraciones forward-only (con checksum SHA-256) 44
Registros de decisión arquitectónica (ADR) 31
Escenarios de fallo documentados 11
Triggers de invariante en SQLite 125
pnpm pipeline    # lint + typecheck + 1.282 pruebas

Ese mismo pipeline corre en GitHub Actions sobre windows-latest en cada pull request y cada cambio de main: instalación congelada, lint, typecheck, las 1.282 pruebas de los 198 archivos y la compilación de artefactos. La cifra es un check remoto, no solo una corrida en la máquina del autor.

TypeScript va en modo estricto con exactOptionalPropertyTypes, y las migraciones se prueban sobre SQLite temporal, incluyendo el backfill de datos históricos.


Arquitectura

DDD táctico + arquitectura hexagonal. La dependencia apunta siempre hacia adentro:

┌─────────────────────────────────────────────────────────────┐
│  apps/desktop (Electron + React)   apps/server (Fastify)    │  Composición y transporte
└───────────────────────────┬─────────────────────────────────┘
                            ▼
┌─────────────────────────────────────────────────────────────┐
│  packages/drivers/*  db · fiscal · security · exchange-rate │  Adaptadores (implementan puertos)
└───────────────────────────┬─────────────────────────────────┘
                            ▼
┌─────────────────────────────────────────────────────────────┐
│  core/application   casos de uso · DTOs · puertos           │  Orquestación
└───────────────────────────┬─────────────────────────────────┘
                            ▼
┌─────────────────────────────────────────────────────────────┐
│  core/domain    entidades · agregados · value objects       │  Reglas de negocio puras
└───────────────────────────┬─────────────────────────────────┘
                            ▼
┌─────────────────────────────────────────────────────────────┐
│  packages/shared     Money · Quantity · Result · contratos  │  Primitivas transversales
└─────────────────────────────────────────────────────────────┘

Reglas no negociables: el dominio no conoce frameworks; los adaptadores no contienen reglas de negocio; las rutas y componentes React solo adaptan entrada/salida; una integración fiscal nueva se agrega como driver sin tocar dominio ni casos de uso.

Contextos delimitados

sales · cash · inventory · catalog · purchasing · currency · fiscal · identity · config · sync · reporting

Los módulos se comunican por contratos y eventos de dominio en pasado (SaleCompleted, StockMovementRegistered, SaleReturned), nunca leyendo tablas ajenas. Las tablas relacionales son la fuente de verdad operativa; un ledger append-only conserva la historia y un outbox garantiza la entrega — sin event sourcing completo, que sería complejidad no justificada para este alcance.


Stack

Capa Tecnología Por qué
Escritorio Electron 44 + React 19 Terminal POS instalable con acceso a hardware local
API / LAN Fastify Cada terminal expone su propio nodo HTTP; el negocio viaja por HTTP, no por IPC
Persistencia SQLite + Drizzle ORM Autonomía offline real, un solo archivo por nodo, single-writer
Lenguaje TypeScript (estricto) Un solo lenguaje de dominio a UI, invariantes en el sistema de tipos
Pruebas Vitest Outside-in TDD (ADR-0007)
Monorepo pnpm workspaces Fronteras de paquete explícitas y verificables

Cómo ejecutarlo

Lo que hay que tener antes

Requisito Por qué
Node.js 20.6+ (probado en 24.18) los scripts usan --env-file e --import
pnpm 11 packageManager fija 11.17.0
openssl alcanzable en el PATH la emisión de material TLS de la LAN envuelve el openssl del sistema en vez de sumar una dependencia de criptografía. Sin él, pnpm pipeline falla las tres pruebas de generate-lan-material.test.ts. En Windows suele vivir en C:\Program Files\Git\usr\bin
Una terminal interactiva el paso 1 pide el PIN sin eco y exige un TTY: en una shell no interactiva falla con «PIN provisioning requires an interactive local terminal»

Las versiones con las que el pipeline se reproduce, en local y en CI, están en el pipeline de verificación.

# --frozen-lockfile instala exactamente lo que el lockfile declara
pnpm install --frozen-lockfile

# Verificación completa
pnpm pipeline

# 0. Perímetro de datos protegido. Una sola vez por máquina: el nodo verifica la
#    ACL de donde escribe y no arranca sobre un directorio que cualquier cuenta lee.
pnpm --filter @supermarket/server prepare-development-storage

# 1. Administrador inicial. Pide código, nombre y PIN en el terminal.
pnpm --filter @supermarket/server bootstrap-admin:dev

# 2. Configuración operativa: caja, métodos de pago y políticas. Sin esto no hay
#    turno ni cobro. Ningún valor fiscal tiene default: se declaran al ejecutarlo.
pnpm --filter @supermarket/server bootstrap-operations:dev -- \
  --database ./.data/db/node.sqlite --currency USD \
  --discount-max-basis-points 1500 --igtf-basis-points 300 \
  --igtf-payment-methods CARD --igtf-currencies USD

# 3. Catálogo de ejemplo para probar (opcional). Las tres opciones son obligatorias:
pnpm --filter @supermarket/server seed:products \
  --database ./.data/db/node.sqlite --currency USD --tax-rate-basis-points 1600

# 4. Nodo servidor (Fastify + SQLite)
pnpm --filter @supermarket/server dev

# 5. Terminal de escritorio (Electron + React)
pnpm --filter @supermarket/desktop dev

El paso 0 crea apps/server/.data/{db,backups,keys} y le aplica la misma ACL restringida que el instalador aplica en %ProgramData%\Cullen, concediendo la cuenta que desarrolla en lugar de la de servicio. Sin él, el arranque falla con DATA_DIRECTORY_NOT_PROTECTED, que es la verificación de ADR-0029 D7.2 haciendo su trabajo sobre un directorio del árbol de trabajo, legible por cualquier cuenta de la máquina.

Los pasos 2 y 3 escriben en la base y exigen que el servidor no esté corriendo: SQLite admite un solo proceso dueño por nodo y, con el nodo activo, fallan con DATABASE_NODE_LOCKED. Autenticarse no basta para operar: sin el paso 2, abrir un turno falla con CASH_REGISTER_NOT_FOUND y cobrar con POLICY_NOT_CONFIGURED.

El paso 5 descarga el binario de Electron la primera vez que se ejecuta en una máquina, unos 100 MB que quedan en caché para los arranques siguientes. pnpm install no lo trae: desde Electron 44 el paquete no declara postinstall y expone el instalador como un ejecutable aparte, así que dev y start lo invocan antes de arrancar. Es idempotente y sale sin hacer nada cuando el binario ya está.

Qué siembra ese catálogo, por qué se puede repetir y qué no hace —existencias, usuarios ni distribución automática a las terminales— está en la guía de la seed. El recorrido completo de una jornada —abrir caja, vender, facturar, cerrar con arqueo y leer el kardex— está en la guía de operación diaria.

Instalar y sostener una estación

Lo anterior levanta el árbol de trabajo. Una estación real se instala como servicio de Windows supervisado por WinSW desde un MSI de WiX, cuya definición vive en packaging/ y cuyo procedimiento reproducible está en instalación de una estación. El MSI se construye en un host con WiX Toolset; no se produce ni se firma dentro de pnpm test, y no se publica.

Una vez instalada, el nodo trae sus propios comandos operativos:

# Copia manual de la base, sellada con la clave del nodo (la diaria corre sola a las 03:00)
pnpm --filter @supermarket/server backup

# Material TLS de la LAN: autoridad interna, emisión inicial y alta de una terminal nueva
pnpm --filter @supermarket/server generate-lan-material

# Rotación de la clave AES-256-GCM y del material TLS ya emitido
pnpm --filter @supermarket/server rotate-protected-material

Cada uno tiene su runbook: respaldo operativo, emisión de material LAN y rotación de material protegido. Los respaldos van cifrados con la clave del nodo, que vive en el almacén del sistema operativo de esa máquina: perder ese almacén los vuelve irrecuperables, por decisión expresa de ADR-0029.


Estado del proyecto

Desarrollo por fases con cronograma versionado. El cronograma es la única fuente de verdad del avance: cada fase tiene sus sub-fases, sus criterios de aceptación y sus deudas abiertas por escrito.

v0.1.0 está publicado desde el 2026-09-10, con el código entregado hasta Fase 11. Distribuye código fuente y una demo reproducible; no adjunta el MSI sin firma y no habilita un piloto ni un despliegue comercial. El recorrido completo se ejecutó desde un clon limpio del commit etiquetado, y volvió a verificarse clonando desde el tag. El cierre técnico del MVP continúa con Fase 12.

Fases Alcance Estado
0 – 7 Arquitectura, infraestructura, dominio, persistencia, ledger/outbox, caja, inventario, driver fiscal simulado ✅ Completadas
8 Integración serial con impresoras fiscales reales ⏸️ Suspendida (dependencia externa)
9 · 9B Interfaz de operación y capacidades de negocio: costos y margen, devoluciones, conteos físicos, proveedores y recepciones, KPIs, arqueos y los cinco perfiles operativos ✅ Completadas
10 Sincronización LAN: outbox durable, protocolo de eventos entre nodos, receptor autenticado, operación offline y reconexión ✅ Completada
11 Seguridad: administración de identidad, autorización auditable, transporte, cifrado en reposo y hardening de logs ✅ Completada y auditada
v0.1.0 Release de portafolio como código fuente y demo reproducible en modo fiscal simulado ✅ Publicado el 2026-09-10
12 Optimización medida de comunicación HTTP local, SQLite y mantenibilidad estructural 🚧 Habilitada por el release
12B Manual de usuario no técnico: las doce pantallas de la navegación, con capturas y recorridos por perfil ⏳ Planificada después de v0.1.0

En paralelo, desde el 2026-09-09 corre el paquete pre-piloto, que no es una fase: entrega la capacidad de despliegue que el gate de piloto ya exigía —empaquetado del nodo como servicio de Windows, respaldo operativo y material TLS de la LAN— sin reabrir la Fase 11 ni adelantar la 12. Los tres planes están entregados; la firma de ejecutables y la validación del MSI en hardware de tienda siguen abiertas, y son parte de lo que falta para hablar de certificación.

Post-MVP — aprobado y planificado, sin iniciar: almacenes por ubicación (13), plataforma central PostgreSQL (14), sincronización SQLite–PostgreSQL (15), web app interna con Next.js (16), sistema de diseño propio (16B) y validación integral con despliegue gradual (17). La evolución post-MVP fija esa secuencia; ninguna de esas fases se presenta como implementada.

Declaración honesta de alcance: este es un MVP de referencia no certificado. La integración con impresoras fiscales reales (Fase 8) está suspendida por dependencia externa —hardware, protocolo del fabricante y laboratorio de certificación— y el sistema opera con un driver fiscal simulado explícitamente rotulado como SIMULACION. No se presenta como cumplimiento normativo, ni como software en producción: no ha corrido un piloto en una tienda real y el gate de piloto en tienda sigue abierto.

Prefiero declarar ese límite antes que insinuar una capacidad que no puedo demostrar.

Cómo trabajo: un caso concreto

"Terminado" solo cuenta si sobrevive a una auditoría. En septiembre de 2026 revisé el código contra los criterios de aceptación que yo mismo había marcado como cumplidos, y no cuadraban: una prueba contractual estaba en rojo, un ajuste de inventario sin costo podía borrar en silencio la valoración histórica de un artículo, varias lecturas autorizaban en la ruta pero no en la capa de aplicación, y el ownership por nodo no se estaba persistiendo.

En lugar de seguir sumando funcionalidad sobre una base falsa, congelé el avance con un gate correctivo bloqueante: cinco cortes en orden obligatorio, cuatro decisiones normativas que debían quedar aceptadas en su ADR antes de escribir código, y una regla simple — ningún corte empieza con la suite en rojo.

El gate se cerró completo. Cada hallazgo terminó con una prueba observable que falla antes de la corrección y pasa después, y las decisiones quedaron escritas en ADR-0016, ADR-0017 y 12-sincronizacion-y-ownership.md para que la próxima persona sepa por qué el sistema hace lo que hace.

Ese hábito se volvió el método. La Fase 11 partió de una línea base verificada contra el árbol real, cerró sus cinco sub-fases y luego pasó una auditoría de cierre que corrigió trece hallazgos, cada uno con su prueba. El plan ejecutado conserva las decisiones y límites que gobernaron el trabajo, sin presentar el resultado como certificación de seguridad ni como habilitación de una tienda real.


Mapa del repositorio

apps/
  desktop/          Electron + React (renderer, preload, main)
  server/           Fastify: rutas, composición de dependencias, bootstrap
packages/
  shared/           Money, Quantity, Result, errores y contratos HTTP v1
  core/
    src/domain/     entidades, agregados, value objects y eventos — sin dependencias externas
    src/application/casos de uso, DTOs y puertos
  drivers/
    db/             SQLite + Drizzle: repositorios, migraciones, unit of work, auditoría
    fiscal/         puerto fiscal + implementación simulada con evidencia recuperable
    security/       hashing de PIN (scrypt), tokens de sesión, identidad de nodo, UUIDv7
    exchange-rate/  proveedor externo de tasas (sugiere; un humano confirma)
    hardware/       reservado para scanner y báscula — aún sin implementación
    logging/        redacción de logs técnicos por nombre de campo y contexto técnico
docs/
  architecture/     arquitectura por responsabilidad + 31 ADRs
  cronograma/       fases, sub-fases, planes y decisiones
  failure-scenarios/semántica de fallo de operaciones críticas
  operacion/        runbooks: instalación, jornada diaria, respaldo, material LAN y rotación
  producto/         alcance por nivel de entrega
packaging/          definición del MSI (WiX) y del servicio de Windows (WinSW)
tests/              pruebas de fronteras arquitectónicas sobre la configuración de ESLint
.agents/skills/     instrucciones para agentes; material de terceros, fuera del producto

Lo que el repositorio redistribuye sin haberlo escrito lleva su propio aviso en THIRD-PARTY-NOTICES.md.

Documentación

Documento Contenido
docs/architecture/README.md Arquitectura por responsabilidad: capas, módulos, agregados, eventos, errores
docs/architecture/adr/ 31 decisiones arquitectónicas con contexto, alternativas y consecuencias
docs/cronograma/README.md Estado por fase y registro de replanificaciones
docs/failure-scenarios/ Qué garantiza el sistema cuando algo falla a mitad de una operación
docs/operacion/ Runbooks de operación: jornada diaria, instalación de estación, respaldo, material LAN y rotación
packaging/README.md Empaquetado del nodo: servicio de Windows, MSI y prerrequisitos del host de construcción
AGENTS.md Reglas operativas del proyecto — fuente única para colaboradores humanos y agentes de IA
CONTRIBUTING.md Cómo preparar el entorno, qué checks exige un cambio y cómo se escriben commits y PRs
SECURITY.md Canal privado para reportar una vulnerabilidad y los límites de seguridad ya declarados
THIRD-PARTY-NOTICES.md Material de terceros que el repositorio redistribuye, con la licencia de cada uno

Gerardo · LinkedIn · Apache 2.0

Proyecto personal. Abierto a conversaciones sobre arquitectura de software, sistemas offline-first y desarrollo de producto.

About

POS e inventario offline-first para supermercados · Electron + Fastify + SQLite · DDD/Hexagonal

Topics

Resources

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages