JTJavier Thomson
+++ NW 101 Mercado Pago → ARCA: EN PRODUCCIÓN +++ NW 102 Eterno Instante: EN PRODUCCIÓN +++ NW 103 Cuaderno Digital: EN PRODUCCIÓN +++ NW 104 CD Network Player: MVP OK +++ NW 105 Tu equipo: EMBARCANDO ++++++ NW 101 Mercado Pago → ARCA: IN PRODUCTION +++ NW 102 Eterno Instante: IN PRODUCTION +++ NW 103 Cuaderno Digital: IN PRODUCTION +++ NW 104 CD Network Player: MVP WORKING +++ NW 105 Your team: BOARDING +++

Bienvenido, reclutadorWelcome, recruiter

Escribí tu nombre para personalizar la visita.Type your name to personalize your visit.

Desde acá accedés a:From here you can open:

Buscar en el portfolioSearch the portfolio

Proyectos@JT

3 de 4 sistemas EN PRODUCCIÓN:3 of 4 systems IN PRODUCTION: visitá Eterno Instante en vivovisit Eterno Instante live

Contacto y chatContact & chat

Información de JavierOnlineJavierOnline information

  • Desarrollador full stack y fundador de Nowagon Software, en Ezeiza, Buenos Aires.Full stack developer and founder of Nowagon Software, based in Ezeiza, Buenos Aires.
  • Modalidad presencial, híbrida o remota. Español nativo, inglés técnico.On-site, hybrid or remote. Native Spanish, technical English.
  • ¿Preferís la versión actual? Usá la pestañaPrefer the current version? Use the Volver a 2026Back to 2026 tab.

EduardoJavierThomson

Desarrollador full stack y fundador de Nowagon Software.Full stack developer and founder of Nowagon Software.

Pasé 15 años manteniendo en pie infraestructura crítica: servidores, redes y bases de datos, los últimos tres en el aeropuerto de Ezeiza. Hoy diseño sistemas completos y los llevo a producción para negocios reales: pagos, facturación electrónica, procesamiento de imágenes y streaming.

I spent 15 years keeping critical infrastructure running: servers, networks and databases, the last three at Ezeiza International Airport. Today I design complete systems and ship them to production for real businesses: payments, e-invoicing, image processing and streaming.

SalidasDepartures sistemas que diseñé y operosystems I designed and run

EZE · Buenos Aires--:--
Tocá un vuelo para abrir el caso de estudio.Tap a flight to open its case study. Nowagon Software

ProyectosProjects

Cuatro sistemas construidos de punta a punta, tres de ellos en uso diario por negocios reales. Cada uno trae una demo para probar la idea técnica central y los problemas reales que tuve que resolver.

Four systems built end to end, three of them in daily use by real businesses. Each one comes with a small demo of its core technical idea and the real problems I had to solve.

NW 101En producciónIn production2026

Mercado Pago → ARCA

Detecta cada ingreso de dinero en Mercado Pago y emite sola la factura electrónica en ARCA (la ex AFIP). Reemplazó un proceso que se hacía a mano, factura por factura.

Detects every incoming payment in Mercado Pago and automatically issues the electronic invoice with ARCA, Argentina's tax authority. It replaced a process done by hand, one invoice at a time.

El problemaThe problem

En un comercio la plata entra por muchos caminos: QR, links de cobro y, sobre todo, transferencias al alias. Cada ingreso tiene que terminar facturado. Lo difícil: Mercado Pago no avisa cuando llega una transferencia directa. Hay que descubrirla en un reporte que tarda horas en generarse.

Money reaches a shop through many channels: QR codes, payment links and, above all, direct bank transfers. Every one of them must be invoiced. The hard part: Mercado Pago sends no event for direct transfers. They have to be found in a report that takes hours to generate.

Lo que construíWhat I built

Webhooks firmados para los pagos y sondeo del reporte para las transferencias. Cada ingreso se registra una sola vez, se decide si va en factura individual o en la consolidada del día, y un único worker emite las Facturas B en orden, con PDF y QR fiscal. Es multi-tenant: otro negocio se da de alta solo, sin tocar código.

Signed webhooks for payments and report polling for transfers. Each movement is recorded exactly once, routed to an individual invoice or the daily consolidated one, and a single worker issues invoices in strict order, with PDF and fiscal QR code. It's multi-tenant: a new business onboards itself with no code changes.

Mi rolMy role

Todo, en solitario: reglas de negocio, API, panel, modelo de datos, Docker y la operación en producción, incluidas las auditorías contra datos reales.

All of it, solo: business rules, API, admin panel, data model, Docker and production operations, including audits against real data.

~5.000
líneas de TypeScript en la APIlines of TypeScript in the API
7
pantallas en el panel Next.jsscreens in the Next.js panel
40
scripts de auditoría de sólo lecturaread-only audit scripts

Simulá un día de ventasSimulate a day of sales

datos de ejemplosample data

Umbral de ejemplo: $10.000. Lo que lo supera se factura individual; lo menor se acumula para la factura del cierre. Probá reenviar un webhook: no se factura dos veces.

Sample threshold: $10,000. Anything above it gets its own invoice; smaller amounts are batched for the end-of-day invoice. Try resending a webhook: nothing is invoiced twice.

Ingresos detectadosDetected income
    Acumulado para el cierreBatched for daily close
    $0
    Cola de facturaciónInvoice queue

      Problemas que resolvíProblems I solved

      Facturar en orden estricto con ingresos simultáneosStrictly ordered invoicing under concurrent income

      Problema. ARCA exige numeración correlativa. Los ingresos llegan en ráfagas y por dos canales a la vez; emitir en paralelo genera rechazos y huecos.

      Solución. Una cola en una tabla de PostgreSQL y un único worker que toma el trabajo más antiguo y lo marca dentro de la misma transacción. Sin Redis ni RabbitMQ: cero infraestructura extra y el estado se inspecciona con SQL.

      Problem. ARCA requires sequential invoice numbers. Income arrives in bursts through two channels at once; issuing in parallel causes rejections and gaps.

      Solution. A queue in a PostgreSQL table and a single worker that claims the oldest job inside the same transaction. No Redis or RabbitMQ: zero extra infrastructure and the state is inspectable with SQL.

      Una factura con fecha equivocada: el síntoma no era la causaAn invoice with the wrong date: the symptom wasn't the cause

      Problema. El PDF mostraba un día y ARCA tenía autorizada la factura con el siguiente. Parecía un bug de zona horaria ya corregido.

      Causa. La fecha real enviada a ARCA nunca se guardaba: el PDF la recalculaba con otra fórmula. Corregí la raíz con una única fuente de verdad y consulté a ARCA en modo lectura cada factura histórica para encontrar cualquier otra afectada.

      Problem. The PDF showed one date while ARCA had authorized the invoice on the next day. It looked like an already-fixed timezone bug.

      Cause. The actual date sent to ARCA was never stored; the PDF recomputed it with a different formula. I fixed the root with a single source of truth and queried ARCA read-only for every past invoice to find any other affected one.

      Un contador que se desincronizaba: rediseñar en vez de parcharA drifting counter: redesign instead of patching

      Problema. Una auditoría con datos reales encontró facturas consolidadas cuyo monto no coincidía con la suma de sus movimientos, tras una caída del servidor.

      Solución. Eliminé el contador. Ahora el monto se calcula sumando los movimientos reales en la misma transacción del cierre: por construcción no puede desincronizarse. Las correcciones se aplicaron con scripts en dry-run por defecto y nunca se tocó una factura ya emitida.

      Problem. An audit against real data found consolidated invoices whose amount didn't match the sum of their movements, after a server outage.

      Solution. I removed the counter. The amount is now computed from the real movements inside the closing transaction, so it cannot drift by construction. Fixes ran through scripts that default to dry-run, and no issued invoice was ever mutated.

      Dinero que sale no es una ventaMoney going out is not a sale

      Problema. Pagos a proveedores y transferencias enviadas se estaban registrando como ingresos. Y en ciertas operaciones Mercado Pago informa como pagador al propio titular.

      Solución. Clasificar por la forma del payload comparando pagador y cobrador contra la cuenta real, consultar el pago en vivo a la API y reconciliar el histórico con scripts idempotentes sin tocar comprobantes emitidos.

      Problem. Supplier payments and outgoing transfers were being recorded as income. On some operations Mercado Pago also reports the account owner as the payer.

      Solution. Classify by payload shape, comparing payer and collector against the real account, query the payment live from the API, and reconcile history with idempotent scripts without touching issued invoices.

      ArquitecturaArchitecture

      Ver código: el worker que toma trabajos de a unoView code: the worker that claims one job at a time (worker.ts)
      // Toma el trabajo PENDING más antiguo y lo marca PROCESSING dentro de la
      // misma transacción, para que dos workers nunca tomen el mismo trabajo.
      async function claimNextJob() {
        return prisma.$transaction(async (tx) => {
          const next = await tx.invoiceJob.findFirst({
            where: { status: "PENDING" },
            orderBy: { createdAt: "asc" },
          });
          if (!next) return null;
          return tx.invoiceJob.update({
            where: { id: next.id },
            data: { status: "PROCESSING" },
            include: { tenant: true, movements: { select: { occurredAt: true } } },
          });
        });
      }
      
      // Si la emisión falla, el job queda en ERROR para revisión humana:
      // reintentar a ciegas un comprobante fiscal puede duplicar una factura.
      • TypeScript
      • Node.js 22
      • Fastify
      • Next.js
      • PostgreSQL 16
      • Prisma
      • Docker Compose
      • Cloudflare Tunnel
      • Mercado Pago API
      • ARCA WSFEv1
      • AES-256-GCM
      NW 102En producciónIn production2026

      Eterno Instante

      Plataforma para que fotógrafos de playa, calle y eventos vendan sus fotos en el momento: el cliente encuentra su sesión, paga con Mercado Pago y descarga el original.

      A platform that lets beach, street and event photographers sell their photos on the spot: customers find their session, pay with Mercado Pago and download the original.

      El problemaThe problem

      Un fotógrafo de playa saca cientos de fotos por jornada a gente que sigue caminando. Imprimir es caro y lento, mandar por WhatsApp no asegura el cobro, y la mayoría de las ventas se pierde. Además hay que organizar miles de archivos y evitar que alguien se lleve las fotos sin pagar.

      A beach photographer takes hundreds of shots a day of people who keep walking. Printing is slow and expensive, WhatsApp doesn't guarantee payment, and most sales are lost. On top of that, thousands of files need organizing and nobody should walk away with photos they didn't pay for.

      Lo que construíWhat I built

      El fotógrafo marca la sesión desde el celular y vuelca la tarjeta de memoria. Un worker en Python asocia cada foto a su sesión por la hora EXIF y genera cinco variantes. El cliente ve las fotos protegidas con el efecto lupa, paga y descarga con un link firmado que vence. Paneles por rol para fotógrafo, mostrador y sysadmin.

      The photographer marks the session on their phone and dumps the memory card. A Python worker matches each photo to its session by EXIF time and renders five variants. Customers browse protected previews with a magnifier effect, pay, and download through a signed, expiring link. Role-based panels for photographer, counter staff and sysadmin.

      Mi rolMy role

      Relevamiento, diseño, desarrollo, despliegue, soporte y documentación. Corre en un servidor Ubuntu propio con Docker Compose, Samba y Cloudflare Tunnel, sin puertos abiertos.

      Requirements, design, development, deployment, support and documentation. It runs on a self-hosted Ubuntu server with Docker Compose, Samba and Cloudflare Tunnel, with no open ports.

      ~7.700
      líneas en Node, React y Pythonlines across Node, React and Python
      5
      roles con permisos por rutaroles with per-route permissions
      ~1.500
      fotos por jornada, en paralelophotos per day, processed in parallel

      Ver el sitio en producciónVisit the live site ↗

      Probá el efecto lupaTry the magnifier

      foto generadagenerated photo

      Pasá el mouse o el dedo por la foto. El cliente evalúa el encuadre sin poder llevarse la imagen: la vista previa está desenfocada y con marca de agua.

      Move your mouse or finger over the photo. Customers can judge the framing without being able to take the image: the preview is blurred and watermarked.

      Problemas que resolvíProblems I solved

      Órdenes ajenas al alcance de un númeroOther people's orders one number away

      Problema. Las URLs de orden eran secuenciales: probando /orden/1, /orden/2 se podían ver órdenes ajenas y sus links de descarga.

      Solución. Token público aleatorio criptográfico por orden, backfill de las existentes y el ID secuencial queda sólo como dato interno.

      Problem. Order URLs were sequential: trying /orden/1, /orden/2 exposed other customers' orders and download links.

      Solution. A cryptographically random public token per order, backfilled for existing ones; the sequential ID stays internal.

      Un worker lento con lotes grandesA slow worker on large batches

      Análisis. Con ~1.500 fotos por jornada, el cuello de botella era la CPU (redimensionar, desenfocar, marca de agua), no la RAM ni el disco.

      Solución. ProcessPoolExecutor con concurrencia configurable, pasando datos planos entre procesos en lugar de conexiones a la base.

      Analysis. At ~1,500 photos a day the bottleneck was CPU (resize, blur, watermark), not RAM or disk.

      Solution. ProcessPoolExecutor with configurable concurrency, passing plain data between processes instead of database connections.

      Falsa alarma de worker colgadoA false "worker stuck" alarm

      Problema. El heartbeat se escribía al terminar el lote completo; con cientos de fotos, el panel lo marcaba en rojo aunque estuviera trabajando.

      Solución. Heartbeat por foto terminada con as_completed, y un panel de estado con semáforo, contadores y errores recientes.

      Problem. The heartbeat was written only after a whole batch; with hundreds of photos the panel showed red while the worker was busy.

      Solution. A heartbeat per finished photo using as_completed, plus a status panel with health light, counters and recent errors.

      Cientos de fotos sin sesiónHundreds of photos without a session

      Problema. Las fotos huérfanas había que asignarlas una por una.

      Solución. Agrupación automática por fotógrafo y cercanía temporal (ventana configurable), con confirmación humana. En las pruebas apareció un bug que mezclaba fotógrafos; lo corregí filtrando por autor.

      Problem. Orphan photos had to be assigned one at a time.

      Solution. Automatic grouping by photographer and time proximity (configurable window), with human confirmation. Testing surfaced a bug that mixed photographers; I fixed it by filtering by author.

      CapturasScreenshots (datos de demodemo data)

      Ver código: heartbeat por foto y originales protegidosView code: per-photo heartbeat and protected originals
      # image-worker/worker.py
      with ProcessPoolExecutor(max_workers=config.WORKER_CONCURRENCY) as executor:
          futures = [executor.submit(_process_photo_in_subprocess, p) for p in pending]
          # as_completed (no .map): reacciona a CADA foto que termina,
          # así el heartbeat refleja la realidad.
          for future in as_completed(futures):
              future.result()
              now = conn.execute("SELECT datetime('now')").fetchone()[0]
              dbmod.set_setting(conn, "worker_last_heartbeat", now)
      
      // backend/src/controllers/media.controller.js
      // Los originales NUNCA pasan por esta ruta: sólo variantes protegidas.
      if (!segments.includes('thumbnails') && !segments.includes('previews') && !segments.includes('reveals')) {
        return res.status(403).json({ error: 'No autorizado' });
      }
      • React 18
      • Vite
      • Node.js
      • Express
      • SQLite (WAL)
      • Python
      • Pillow
      • watchdog
      • Docker Compose
      • Ubuntu Server
      • Samba
      • Cloudflare Tunnel
      • Mercado Pago Checkout Pro
      • JWT
      NW 103En producción desde agosto 2026In production since August 2026

      Cuaderno Digital

      La libreta de fiados del kiosco, sin papel y sin errores de cuenta. Reemplazó el cuaderno de un comercio de barrio que la usa todos los días.

      The corner shop's credit notebook, without paper or math mistakes. It replaced the paper ledger of a neighborhood store that uses it every day.

      El problemaThe problem

      En un kiosco de barrio se vende mucho "al fiado" y todo se anota en un cuaderno: buscar al cliente con gente esperando, sumar pagos parciales a mano, no saber quién debe hace más tiempo. Y si el cuaderno se moja, la deuda se pierde.

      Corner shops in Argentina sell a lot on credit (fiado), all written in a paper notebook: finding a customer while people wait, adding partial payments by hand, not knowing who has owed the longest. If the notebook gets wet, the debt is gone.

      Lo que construíWhat I built

      Una PWA instalable pensada para el mostrador: búsqueda instantánea, cuentas familiares, antigüedad de deuda con lógica FIFO y alertas a los 30, 60 y 90 días, un PDF del detalle para compartir por WhatsApp y backups automáticos diarios. Arquitectura en capas (rutas, servicios, repositorios).

      An installable PWA built for the counter: instant search, shared family accounts, FIFO debt aging with alerts at 30, 60 and 90 days, a PDF statement shareable on WhatsApp and automatic daily backups. Layered architecture (routes, services, repositories).

      Mi rolMy role

      Escribí el análisis del dominio antes de programar, diseñé la arquitectura, lo implementé, lo desplegué con PM2 y Cloudflare Tunnel y doy soporte al comercio. Ya diseñé e implementé la versión multi-tenant, hoy en validación.

      I wrote the domain analysis before coding, designed the architecture, built it, deployed it with PM2 and Cloudflare Tunnel, and support the shop. I've already designed and implemented the multi-tenant version, now in validation.

      ~30
      endpoints RESTREST endpoints
      ~60 kB
      de JS gzip: sin Redux ni librería de UIof gzipped JS: no Redux, no UI kit
      DiarioDaily
      backup automático desde el 6/8/2026automatic backup since Aug 6, 2026

      Encontrá el bug de la antigüedadSpot the debt-aging bug

      cuenta de ejemplosample account

      Cada pago cancela primero la deuda más vieja. Mi primer cálculo sólo miraba los pagos anteriores a cada deuda, así que una cuenta saldada seguía marcada como morosa. Saldá la cuenta y compará.

      Each payment settles the oldest debt first. My first version only looked at payments made before each debt, so a settled account still showed up as overdue. Settle the account and compare.

      Familia Pereyra · saldoPereyra family · balance
      $0
      Deudas pendientes (cola FIFO)Pending debts (FIFO queue)
      Últimos movimientosLatest entries

        Problemas que resolvíProblems I solved

        Cuentas saldadas que seguían morosasSettled accounts still flagged as overdue

        Causa. Por cada deuda, el cálculo sólo miraba los pagos anteriores a ella; un pago posterior nunca la cancelaba.

        Solución. Recorrer el historial en orden con una cola FIFO: cada deuda entra, cada pago consume desde el frente. Cuando la cola se vacía, la antigüedad se reinicia sola.

        Cause. For each debt, the calculation only looked at earlier payments; a later payment never settled it.

        Solution. Walk the history in order with a FIFO queue: each debt is enqueued, each payment consumes from the front. When the queue empties, the aging resets by itself.

        Deuda y pago con la misma fecha y horaA debt and a payment with the same timestamp

        Problema. Un cliente real con saldo $0 seguía con "deuda antigua". Al migrar el cuaderno, los movimientos se cargaron a las 12:00 y el desempate por id podía procesar el pago antes que la deuda.

        Solución. Un criterio explícito en el ORDER BY: ante empate, la deuda va primero. El orden de los datos es parte de la lógica de negocio.

        Problem. A real customer with a $0 balance still showed "old debt". Migrated paper entries were stored at 12:00, and tie-breaking by id could process the payment before the debt.

        Solution. An explicit ORDER BY criterion: on a tie, the debt goes first. Data ordering is part of the business logic.

        Publicar a internet desde una PC del comercioExposing a shop PC to the internet

        Decisión. Evalué VPS, DDNS y túnel; elegí Cloudflare Tunnel: sin abrir puertos ni depender de IP fija.

        Endurecimiento. helmet, CORS por variable de entorno, límite de 8 intentos de login cada 15 minutos por IP y el servidor no arranca si falta la configuración.

        Decision. I compared a VPS, DDNS and a tunnel, and picked Cloudflare Tunnel: no open ports, no fixed IP needed.

        Hardening. helmet, env-based CORS, login limited to 8 attempts per 15 minutes per IP, and the server refuses to start without configuration.

        Días sin backupDays without a backup

        Problema. Si la PC estaba apagada a las 20:00, ese día no había copia.

        Solución. Backups organizados por mes con retención de 60 copias, y en la versión 2 una verificación al arrancar que genera el backup faltante usando la API nativa de SQLite.

        Problem. If the PC was off at 8 p.m., there was no copy that day.

        Solution. Monthly backup folders with 60-copy retention, and in version 2 a startup check that creates the missing backup using SQLite's native backup API.

        CapturasScreenshots (datos ficticiosfictional data)

        Ver código: la cola FIFO que corrigió el bugView code: the FIFO queue that fixed the bug (cuentaService.js)
        function fechaDeudaMasAntigua(cuenta_id) {
          const movimientos = movimientoRepo.listarPorCuenta(cuenta_id).slice().reverse();
          const colaDeudas = [];
          for (const mov of movimientos) {
            if (mov.tipo === 'deuda') {
              colaDeudas.push({ fecha: mov.fecha, hora: mov.hora, restante: mov.monto });
              continue;
            }
            // un pago cancela deudas empezando por la más antigua pendiente
            let pagoRestante = mov.monto;
            while (pagoRestante > 0 && colaDeudas.length > 0) {
              const masAntigua = colaDeudas[0];
              if (masAntigua.restante <= pagoRestante) {
                pagoRestante -= masAntigua.restante;
                colaDeudas.shift();
              } else {
                masAntigua.restante -= pagoRestante;
                pagoRestante = 0;
              }
            }
          }
          if (colaDeudas.length === 0) return null; // sin deuda: la alerta se reinicia sola
          return colaDeudas[0];
        }
        • React 18
        • Vite
        • PWA
        • Node.js
        • Express
        • SQLite (WAL)
        • PDFKit
        • node-cron
        • helmet
        • PM2
        • Cloudflare Tunnel
        • Docker
        • JWT + bcrypt
        NW 104MVP funcionando con hardware realMVP working on real hardware2026

        CD Network Player

        Metés un CD en la lectora del servidor de casa y lo escuchás en vivo desde cualquier celular de la red, sin rippear ni guardar el audio en ningún disco.

        Put a CD in the home server's drive and listen live from any phone on the network, without ripping or storing the audio on any disk.

        El problemaThe problem

        Tengo una colección de CDs y un servidor Linux en casa. Quería escucharlos desde el celular sin comprar un reproductor, sin convertir toda la colección a archivos y sin depender de un servicio externo.

        I have a CD collection and a Linux server at home. I wanted to play the discs from my phone without buying a streamer, converting the whole collection to files, or relying on an outside service.

        Lo que construíWhat I built

        Un backend async en FastAPI que arma un WAV bit-perfect en streaming: calcula el tamaño exacto desde la tabla de contenidos del CD, escribe el header de 44 bytes y pipea cdparanoia directo a la respuesta HTTP. Un WebSocket mantiene sincronizados a todos los clientes.

        An async FastAPI backend that streams a bit-perfect WAV: it computes the exact size from the CD's table of contents, writes the 44-byte header and pipes cdparanoia straight into the HTTP response. A WebSocket keeps every client in sync.

        Mi rolMy role

        Proyecto personal, en solitario: arquitectura, backend, frontend, Docker y la integración con una lectora física en un servidor que ya corría otros servicios.

        Personal project, solo: architecture, backend, frontend, Docker and the integration with a physical drive on a server already running other services.

        0 B
        de audio escritos a discoof audio written to disk
        1
        dispositivo expuesto al contenedor, sin privilegeddevice exposed to the container, no privileged mode
        Android
        reproduce con la pantalla bloqueadakeeps playing with the screen locked

        Dos clientes, un solo estadoTwo clients, one shared state

        disco de demodemo disc

        Controlá el reproductor desde cualquiera de los dos dispositivos: el WebSocket difunde el cambio y ambos muestran lo mismo. Cortá Internet y la música sigue sonando.

        Control playback from either device: the WebSocket broadcasts the change and both show the same thing. Cut the internet and the music keeps playing.

        /dev/sr0 · cdparanoia -r
        0 Benviados por la redsent over the network
        0 Bescritos a discowritten to disk
        —Content-Length desde la TOCfrom the TOC

        Problemas que resolvíProblems I solved

        Streaming sin archivos temporalesStreaming with no temp files

        Un proceso de cdparanoia por reproducción, con su ciclo de vida atado a la conexión HTTP: si el cliente corta, el proceso se termina en el finally. Cero escritura a disco.

        One cdparanoia process per playback, its lifecycle tied to the HTTP connection: if the client disconnects, the process is killed in the finally block. Zero disk writes.

        Acceso mínimo al hardware desde DockerLeast-privilege hardware access from Docker

        El servidor corre otros servicios en producción. Primero identifiqué en el host el dispositivo exacto, y el contenedor recibe sólo ese: nunca privileged ni /dev completo.

        The server runs other production services. I first identified the exact device on the host, and the container gets only that one: never privileged, never all of /dev.

        Metadata que nunca rompe la reproducciónMetadata that can never break playback

        La consulta a MusicBrainz corre en segundo plano y desacoplada. Si falla, tarda o se saca el disco a mitad de consulta, un contador de generación descarta el resultado y el reproductor sigue con "Track 01".

        The MusicBrainz lookup runs decoupled in the background. If it fails, stalls, or the disc is ejected mid-lookup, a generation counter discards the result and playback continues with "Track 01".

        CapturasScreenshots

        Ver código: un WAV armado a mano, sin FFmpegView code: a hand-built WAV, no FFmpeg (streaming.py)
        SECTOR_BYTES = 2352  # 588 muestras estéreo de 16 bits por sector CDDA
        SECTORS_PER_SECOND = 75
        
        def _wav_header(pcm_data_size: int) -> bytes:
            byte_rate = SAMPLE_RATE * CHANNELS * BITS_PER_SAMPLE // 8
            block_align = CHANNELS * BITS_PER_SAMPLE // 8
            return b"".join([
                b"RIFF", struct.pack("<I", 36 + pcm_data_size), b"WAVE",
                b"fmt ", struct.pack("<IHHIIHH", 16, 1, CHANNELS, SAMPLE_RATE,
                                    byte_rate, block_align, BITS_PER_SAMPLE),
                b"data", struct.pack("<I", pcm_data_size),
            ])
        
        def content_length(track, start_seconds=0.0) -> int:
            # conocemos el largo por la TOC: el WAV tiene tamaño real, no "infinito"
            return 44 + remaining_pcm_bytes(track, start_seconds)
        • Python
        • FastAPI
        • asyncio
        • React
        • Vite
        • WebSocket
        • cdparanoia
        • libdiscid
        • MusicBrainz API
        • Docker Compose
        • Ubuntu Server

        Del rack al códigoFrom the rack to the code

        Antes de programar estos sistemas pasé años del otro lado: servidores Windows y Linux, Active Directory, VPNs, SQL Server y backups, atendiendo incidentes en entornos donde caerse no era opción.

        Eso cambia cómo programo. Pienso desde el principio en qué pasa cuando el servidor se reinicia, cuando una API externa se traba o cuando hay que restaurar una base. Por eso mis proyectos tienen heartbeats, backups automáticos, monitoreo y despliegues sin puertos abiertos.

        Before building these systems I spent years on the other side: Windows and Linux servers, Active Directory, VPNs, SQL Server and backups, handling incidents where going down wasn't an option.

        That shapes how I code. From day one I think about what happens when the server reboots, when an external API stalls or when a database must be restored. That's why my projects ship with heartbeats, automatic backups, monitoring and deployments with no open ports.

        1. HoyNow

          Nowagon Software

          Fundador y desarrollador full stack. Sistemas a medida de punta a punta: frontend, backend, datos, pagos e infraestructura.Founder and full stack developer. Custom systems end to end: frontend, backend, data, payments and infrastructure.

        2. 2022–25

          Administrador de Sistemas IT · Aeropuertos Argentina, EzeizaIT Systems Administrator · Aeropuertos Argentina, Ezeiza Airport

          Infraestructura crítica para la operación aeroportuaria, incidentes, proveedores y SQL Server en alta disponibilidad.Critical infrastructure for airport operations, incident response, vendors and high-availability SQL Server.

        3. 2024

          Programador Web · CodeaRockWeb Programmer · CodeaRock

          HTML, CSS, JavaScript

        4. 2019–21

          Administrador de Redes y Terminales · DotComNetwork and Endpoint Administrator · DotCom

          Lideré migraciones para clientes corporativos: Active Directory, DNS, DHCP, file servers, VPNs y backups.Led migrations for corporate clients: Active Directory, DNS, DHCP, file servers, VPNs and backups.

        5. 2019

          Técnico Especialista · QosIT, Villa La AngosturaIT Specialist · QosIT, Villa La Angostura

          Redes cableadas e inalámbricas, servidores y estaciones de trabajo.Wired and wireless networks, servers and workstations.

        6. 2014

          Programador · IndraDeveloper · Indra

          Procedimientos y optimizaciones en Oracle PL/SQL para proyectos de Telefónica.Oracle PL/SQL procedures and optimizations for Telefónica projects.

        7. 2007–12

          Técnico Especialista · Insystems / All-komIT Specialist · Insystems / All-kom

          Windows Server, Exchange, IIS y SQL Server sobre plataformas IBM y HP.Windows Server, Exchange, IIS and SQL Server on IBM and HP platforms.

        8. 2008

          Ingeniería en Sistemas · UAISystems Engineering · UAI

          24 materias aprobadas.24 courses completed.

        9. 2003

          Técnico Informático · E.E.S.T. N°5IT Technician diploma · E.E.S.T. N°5

        Cómo trabajoHow I work

        Reglas que salieron de incidentes reales en producción, no de un manual.

        Rules that came from real production incidents, not from a handbook.

        Evidencia antes que hipótesisEvidence before hypotheses

        Antes de tocar algo que afecta montos, corro scripts de diagnóstico de sólo lectura contra los datos reales. Así pude probar que no se había perdido ningún pago, en vez de suponerlo.Before touching anything that affects money, I run read-only diagnostic scripts against real data. That's how I proved no payment had been lost, instead of assuming it.

        La causa, no el síntomaThe cause, not the symptom

        Si un dato puede recalcularse de dos formas, tarde o temprano diverge. Prefiero rediseñar para que el error no pueda ocurrir antes que agregar otro parche.If a value can be computed two ways, sooner or later they diverge. I'd rather redesign so the bug can't happen than add another patch.

        Dry-run por defectoDry-run by default

        Toda corrección masiva primero se simula. Los comprobantes fiscales ya emitidos no se modifican: se compensan.Every bulk fix is simulated first. Issued tax documents are never modified; they're compensated.

        Pensar como quien lo operaThink like the operator

        Backups, heartbeats, paneles de monitoreo, reinicio automático y secretos fuera del repositorio. El sistema tiene que aguantar la madrugada sin mí.Backups, heartbeats, monitoring panels, auto-restart and secrets kept out of the repo. The system has to survive 3 a.m. without me.

        Escuchar al usuario realListen to the real user

        Las mejoras del fiado salieron de mirar cómo se usa en el mostrador; las del fotógrafo, de una jornada en la playa.The ledger improvements came from watching it used at the counter; the photographer ones, from a day on the beach.

        IA como herramienta, criterio propioAI as a tool, my own judgment

        Uso Claude Code para acelerar el desarrollo. Yo defino requisitos y arquitectura, y reviso y valido cada cambio antes de que llegue a producción.I use Claude Code to speed up development. I define requirements and architecture, and I review and validate every change before it reaches production.

        HabilidadesSkills

        Tocá una tecnología para ver dónde la usé.

        Tap a technology to see where I used it.

        Próximo destino: tu equipoNext destination: your team

        Busco sumarme a un equipo como desarrollador full stack. Traigo lo que construí y los años de infraestructura que hay detrás.

        I'm looking to join a team as a full stack developer. I bring what I've built and the years of infrastructure behind it.

        javierthomson1986@gmail.com

        Teléfono y WhatsAppPhone and WhatsApp: +54 11 3188-3446

        PasajeroPassenger
        Eduardo Javier Thomson
        OrigenFrom
        Ezeiza, Buenos Aires
        ModalidadWork mode
        Presencial, híbrido o remotoOn-site, hybrid or remote
        EmbarqueBoarding
        Disponibilidad inmediataAvailable immediately
        IdiomasLanguages
        Español nativo, inglés técnicoNative Spanish, technical English

        © 2002–2026 Eduardo Javier Thomson · Nowagon Software. Todos los derechos reservados.All rights reserved.