ingest
build
deploy
events.log · 0
// state: idle · 0 packet · 3 stages
Portrait of Romain Caillé

Romain CAILLE

Fullstack developer, ready for the agentic era.

status
Open to work · available now
location
Remote or relocation · from Nantes, France
experience
Spotime · 2025–nowEnedis, fullstack apprentice · 2023–2025U Tech, fullstack apprentice · 2022–2023
education
Master's, IT & Information Systems · EPSI
english
C1
resume
resume.pdf
// 01 · how to read this site

Each project is an event. It goes through three stages.

ingest

Collection: what enters the system, as is.

build

Processing: validation, enrichment, decisions. This is where the trade-offs live.

deploy

Exposure: APIs, dashboards, what others consume.

event spotime →deploy

Spotime

problemField crews of 10 to 30 people, spread across sites, track hours on paper. Every month-end turns into hours of manual re-entry before payroll can start. Spotime replaces that with three surfaces: a web back office for admin and scheduling, an employee mobile app (iOS/Android) and an Electron desktop app for on-site clock-in, published on the Windows Store.

decisionClock-ins are stored local-first and synced as soon as the network returns, with no data loss or duplicates. GPS location is captured only at the moment of clock-in. Development runs with a team of subagents (PO, architect, tester, developer, reviewer), framed by user stories, a DoR/DoD and TDD.

Tanstack RouterPostgreSQLNestJSTypeScriptshadcn/uiPlausible (self-hosted)Expo/React NativeElectron
+ trade-offs & what broke

trade-offCold email to construction SMEs wasn't landing, so I switched to cold calling. Traded reach for signal quality, on purpose.

what broke / learnedConstruction still showed no traction, even on direct calls. Pivoting to personal care services proved more receptive, with a client coming in inbound. Testing the channel first, then the market, beats rewriting the pitch.

spotime.fr
Spotime — screenshot1 / 2
deploy ✓

Two paying clients: one in agriculture, and one in personal care services who came in inbound.

event fraud.engine →deploy

Fraud Engine

problemLearn Go and event-driven design on a case where event ordering matters: fraud detection. The pipeline has to absorb growing load and scale horizontally without code changes.

decisionThree independent Go binaries (generator, scorer, sink) linked by Kafka, partitioned by card_id so a card is always scored by the same worker. Redis holds per-card velocity in a sliding window, ClickHouse stores scored transactions. Adding a scorer only triggers a partition rebalance.

GoKafkaRedisClickHousePrometheusGrafanaDocker
+ trade-offs & what broke

trade-offWhen its 1,000-slot buffer is full, the generator drops messages instead of blocking. Acceptable for a load test, since the ceiling stays visible in metrics. On a real payment pipeline, losing an event before the fraud check is not: backpressure or a persistent buffer instead.

what broke / learnedThe first version ran 3 scorers at ~3 tx/s. kafka-go's default 1s BatchTimeout capped writes at about one message per second, and each Redis command was its own round-trip. Explicit batching (size or timer, first one wins) and a pipelined Redis call fixed it: one optimized scorer now keeps up with a generator alone.

grafana · scorer-group lag
2 generators · 1 → 3 scorers
0:00 / 0:00
deploy ✓

Live scaling: with 1 scorer for 2 ramping generators, lag climbs to ~410 messages (~8s of traffic). Starting 2 more scorers drains it to 0. Three scorers handle ~220 tx/s combined.

event event.hub →deploy

Event Hub

problemWeb analytics count visitors. Stripe counts money. Nothing joins the two, so teams selling through Stripe can't tell which source, campaign or page actually brings revenue without a data warehouse or a tag manager.

decisionOne endpoint, POST /v1/events, for every source: browser SDK, backend, CI, Stripe webhooks. The server derives the fields it guarantees (source, session, visitor); the client only sends jsonb context. Payments link back to the first visit through a visitor id in Stripe metadata. The core never knows what an event type means, and a test fails the build if it does.

NestJSPostgreSQLDrizzleZodTanStack StartStripe Connectshadcn/uiTurborepo
+ trade-offs & what broke

trade-offCookieless by default: the visitor key is an HMAC that rotates daily, and IP and User-Agent are never stored. Enough to count, not to follow, so renewals and LTV only attribute in consented mode. Privacy first, attribution depth second.

what broke / learnedOne Stripe payment fires four webhooks that all carry an amount. Mapping them to disjoint event types and reading one precise type keeps the same euro from being counted four times. In tests, ad-hoc supertest ports failed once every ~66,000 requests on a random suite; a shared harness with one server per suite fixed it.

github.com/romain-cll/events-hub
Event Hub — screenshot1 / 5
deploy ✓

Working proof of concept: web analytics and Stripe revenue attribution shipped, with traffic, revenue and deploy markers on one chart. Next: a per-project ingestion cap and cohort views in the dashboard.

// 3 events · 1 sink

This is what I do. Let's talk.

githublinkedinresume.pdf
Some of my work is in private repositories.