// ~/projects — products, research, coursework & side builds
$

Projects by Christian Bile ·

--filter=
./asata/(4)
// Asata — a suite of SaaS products I design, build and run end-to-end since March 2026: one shared PostgreSQL, Java 25 / Spring Boot 4 services, Angular and React Native clients, Docker, CI, backups and monitoring.
mokili/README.mdJava · Angular

Mokili — applications manager & DevOps console

DevOps console2026 —private repo · walkthrough on request

The applications manager of Asata, and the catalogue the suite itself is registered in. A team keeps its applications, environments, roles, versions and source repositories in one place, and from there follows CI pipelines, streams job logs, launches deployments and runs a Scrum tracker.

problemSmall teams scatter the lifecycle of their software across GitHub, GitLab, spreadsheets and chat. Mokili ties application → version → repository → pipeline → deployment → environment into one traceable chain, scoped per team, with notifications when something lands.
who uses itEngineering teams and their managers, from a two-person startup to a department; it is also the catalogue the Asata suite itself is registered in.
what I builtEverything: the backend (24 modules), the Angular web app, the GitHub and GitLab integrations, the deployment and tracker features, the database schema, CI and backups. Screens designed in Pencil.
statusActive since March 2026, running on Docker Compose with CI on GitHub Actions; the deploy workflow is linted but not yet exercised on GitHub.
## stack
Java 25Spring Boot 4Spring Security · OAuth2Spring Data JPAPostgreSQL 17Angular 21PrimeNGApexChartsTypeScript 5.9S3 · MinIOTestcontainersDocker ComposenginxGitHub ActionspgBackRest
## architecture
  • Spring Boot backend split into 24 modules (auth, team, app info, environment, role, version, SCM connection, pipeline, webhook, deployment, tracker, notification, platform, console, CDN…), each with an API interface, controller, services and JPA entities.
  • GitHub Actions and GitLab CI pipelines arrive by signed webhooks (HMAC, constant-time comparison, each delivery processed once) with a 5-minute sync as safety net; deployments start the repository's own deploy workflow and a reconciler follows the run.
  • Angular 21 frontend on PrimeNG, signals for state, routes and sidebar driven by the user's permissions; 66 tracker screens and 16 deployment screens designed in Pencil for laptop, ultrawide and phone, light and dark.
  • Short-lived access tokens, refresh tokens stored hashed and rotated, optional Google sign-in, permissions re-read per team on every request; connection secrets encrypted with AES-256-GCM, only the last 4 characters ever shown.
  • Shared PostgreSQL database (47 mokili_* tables) backed up with pgBackRest: WAL archiving, weekly full and daily differential, AES-256, weekly automated restore drill. Logos over 100 KB go to S3 (MinIO locally).
## challenges → how I solved them
- [x] Launching a deployment when the provider's answer may be lost.
→ The provider is asked once, outside any transaction, with no automatic retry. A timeout makes the deployment `unknown` and a reconciler finds the run by the deployment id embedded in the workflow's run name. Final states can never be changed by a late event, and one deployment at a time per target is enforced by a unique index.
- [x] Matching pipelines to versions without being fooled by late or re-run events.
→ Matched by commit, then by tag, never by branch name. Mokili always asks the provider for the latest commit instead of trusting the event, so a late delivery never moves a version back.
- [x] A one-click “Connect GitHub” that cannot be hijacked.
→ A 32-byte single-use install state, stored as SHA-256 and valid 10 minutes; the user's OAuth code proves they can reach the installation, which must belong to the official app and to exactly one team. Installation tokens are cached until 5 minutes before expiry.
- [x] Showing CI job logs without storing gigabytes.
→ Logs are streamed, never stored: the last 5,000 lines, at most 20 MB read from the end, ANSI colours and sections parsed, GitLab jobs followed live every 5 seconds.
- [x] A tracker that is more than a board.
→ Backlog with 9 item types, sprints, burndown, increments tied to versions, and velocity, throughput, cycle-time and cumulative-flow reports.
>>> tests = 305 backend>>> modules = 24>>> tables = 47>>> screens designed = 80+
umoja/README.mdJava · Angular

Umoja — organizations, teams & identity

identity & teams2026private repo · walkthrough on request

The identity and organization hub of Asata. Companies, administrations and associations manage their people, hierarchical teams, access to applications, roles and permissions, approval requests, offices and rooms, agenda and billing. Umoja is also the single sign-on that Oro and the other services rely on.

problemEvery B2B product needs the same unglamorous core: who belongs to which organization and team, who may do what, how access is requested and approved, and who gets invoiced. Umoja builds it once, isolated per organization, so each product only has to plug in.
who uses itCompanies, administrations and associations: their employees on one side, their administrators on the other. The other Asata products plug into it for sign-on and team membership.
what I builtThe whole product: backend, Angular web app, data model (31 tables), sign-on and account security, approval workflows, billing, and the integrations with Oro and Sululu.
statusFunctionally complete for a first launch; CDN and external OAuth login are in place but off by default; no payment provider yet.
## stack
Java 25Spring Boot 4.1Spring Security · OAuth2Spring Data JPAPostgreSQL 17Angular 21PrimeNG 21TypeScript 5.9RxJSVitestTestcontainersAWS S3 SDKDocker ComposenginxCaddy
## architecture
  • Spring Boot backend at /umoja behind nginx, which proxies Oro on the same origin (no CORS); Caddy in front in production for HTTPS, HSTS, security headers and SSE flushing.
  • Angular 21 frontend with standalone components and signals: an employee side (my apps, my teams, requests, agenda, messages, notifications, profile), an organization admin console, and a platform-staff area with the app marketplace and pricing plans per country.
  • 31 umoja_* tables on the shared PostgreSQL schema: hierarchical teams, memberships with lifecycle states, service-scoped roles, resource:action permissions, configurable request types with approval chains, audit log, subscriptions and invoices.
  • Auth: hashed passwords, short-lived access tokens verified by the other services without any shared secret, refresh tokens kept in a secure cookie, stored hashed and rotated, lockout after repeated failed attempts.
  • Integration: membership snapshots pushed to Oro, notifications through Sululu from a transactional outbox (localized en · fr · es · pt), a live bell fanned out to browsers over SSE, S3-compatible CDN for media, OpenStreetMap address autocomplete.
## challenges → how I solved them
- [x] Let other services trust Umoja's users without sharing a secret.
→ Replaced a shared secret with signed tokens that other services verify through a public key, with audience checks and key rotation. Oro can verify a token but holds nothing that could issue one.
- [x] A stolen refresh token must not give lasting access.
→ Tokens are stored hashed, rotated inside a family, and any reuse of a replaced token revokes the whole family, with a 30 s grace so two tabs refreshing at once do not lock the user out.
- [x] Oro must never re-grant an access that Umoja revoked, even if a sync arrives late.
→ Snapshots are versioned by read time and reconciled every 10 minutes; a stale snapshot is ignored instead of applied.
- [x] Approval workflows that execute actions must not corrupt a request when the action fails.
→ Built-in actions run in their own transaction on final approval. If it fails, the request stays approved for an admin to complete by hand.
- [x] One organization must never see another.
→ Cross-organization administration lives under /platform behind a dedicated permission. An integration test sends another organization's ids to every endpoint and asserts nothing is read or changed.
>>> tests = 271 backend · 186 frontend>>> languages = en · fr · es · pt>>> data model = 31 tables + shared identity
oro/README.mdJava · React Native

Oro — real-time messaging

messaging2026open repo

The messaging service of Asata, shared by two products: Oro for personal messaging (friends, family groups, communities) and Umoja for workplace messaging (direct messages, groups, one chat per team). Real-time over Server-Sent Events, Web Push notifications, and a mobile app for iOS and Android.

problemChat looks simple and is not: strict message order, retries that never duplicate, receipts, catching up after being offline, thousands of open streams, files that may carry malware, and privacy rules (requests from strangers, blocks, no history for newcomers, GDPR export and erasure). Oro solves all of that once, for both products.
who uses itPeople, for personal chat with friends, family and communities; and organizations on Umoja, for workplace messaging with one chat per team.
what I builtBackend, database schema and security model, real-time streaming, push notifications, attachment scanning, the iOS/Android app, load tests, monitoring and runbooks.
statusBackend and mobile app working on the local stack; single instance for now (LISTEN/NOTIFY planned), phone push and object storage not yet done.
## stack
Java 25Spring Boot 4.1PostgreSQL 17Spring SecuritySSEWeb PushApache TikaClamAVReact Native 0.88Expo 58React 19TypeScriptTanStack QueryTestcontainersk6Java Flight RecorderPrometheusAlertmanagerCaddyGitHub Actions
## architecture
  • One Spring Boot service at /oro over the shared PostgreSQL 17 database: 16 tables, 27 row-level-security policies, 16 triggers and 21 functions in plain SQL with a versioned migration runner.
  • Product isolation in the schema itself: every row carries a product id and every reference is a composite foreign key, so the database keeps an Oro user out of a Umoja conversation.
  • Transactional outbox → in-memory LiveHub → one SSE stream per user, each with its own bounded queue and virtual-thread writer; events carry ids only and clients re-read content through the API.
  • Attachments: type sniffed from the bytes with Apache Tika, EXIF stripped, ClamAV scan that fails closed, purge job and restic backups. Web Push (RFC 8291 / 8292) implemented with the JDK only.
  • Identity: Umoja users verified through Umoja's public key; Oro's own accounts use short-lived access tokens and rotated refresh tokens (secure cookie for browsers, Keychain / Keystore for the app). Prometheus + Alertmanager with 20 alert rules, ECS logs that never hold message text.
  • Mobile app in Expo / React Native with expo-router, TanStack Query and FlashList, designed in Pencil for light and dark themes.
## challenges → how I solved them
- [x] Strict order and safe retries inside a conversation.
→ Each send locks the conversation row and takes the next sequence number, so there are no gaps and no reordering. A client message id is unique per sender and conversation, so a retry, even a concurrent one, returns the stored message. Receipts are positions, not per-message rows.
- [x] Catching up after disconnects and restarts without replaying events.
→ Every stream `hello` makes the client re-read what changed since its last sequence. In a load test, a restart lost none of ~58,000 generated messages and 300 of 300 simulated clients caught up.
- [x] Message search took 5 s for a heavy user: ILIKE is not leakproof under row-level security, so the trigram index was skipped.
→ A SECURITY DEFINER search function re-applies the RLS rule and uses the index: 5,030 ms → 663 ms for a power user, 16 ms → 2 ms for a typical one.
- [x] A graceful stop took over 45 s and one slow client could block everyone.
→ Presence fan-out was O(n²): indexing streams by organization cut the hub's work from 1,715 ms to 505 ms for 5,000 reconnects; per-stream queues isolate slow consumers; a stop now takes 0.2 s.
- [x] A security self-audit found 10 confirmed defects.
→ All fixed: the API login could escalate to the system role, internal endpoints were reachable from the internet, streams outlived revoked tokens, interleaved syncs could restore revoked access, and there were no rate limits or quotas.
>>> tests = 223 in 49 classes>>> load test = 3,000 concurrent simulated users on 4 CPUs>>> load-test delivery = 318,326 / 318,683 tracked · none lost, none twice>>> query plans = page of 40 in 0.2 ms over 9.9M generated messages
sululu/README.mdJava · Spring Boot

Sululu — notification service

notifications2026private repo · walkthrough on request

The notification backbone of Asata. Any product sends email, in-app and push notifications through one HTTP API; each product is a tenant with its own API keys and only ever sees its own data.

problemEvery product needs notifications that are never lost, never sent twice by accident and arrive in order per recipient, even when the mail provider, the broker or a worker fails. Sululu takes that problem out of each app and solves it once.
who uses itThe Asata products themselves (Mokili, Umoja, Oro and the next ones), and through them every one of their users.
what I builtThe whole service: API, delivery workers, queue integration, security model, metrics and alerts, load tests, deployment and runbooks.
statusWorking end-to-end on Docker Compose with load tests and runbooks; not yet on the production server, single node, push through FCM only.
## stack
Java 25Spring Boot 4.1Spring Data JPA · Hibernate 7PostgreSQL 17NATS JetStreamSpring SecurityMapStructOpenTelemetryPrometheusTestcontainersk6Docker ComposeCaddyGitHub Actions
## architecture
  • Modular monolith: one image, three processes (public API, admin API with provider webhooks, delivery worker), each with its own database login and only the rights it needs.
  • Accept → relay → deliver: a notification, one delivery per channel and an outbox event are committed in one transaction; a relay publishes the outbox to NATS JetStream; durable pull consumers per channel deliver over SMTP, the in-app inbox or FCM.
  • PostgreSQL is the source of truth, the broker only distributes wake-ups: retries, expired leases, lane sweeping and retention all run from the database, so the API keeps accepting while NATS is down.
  • Row-level security on every table, API keys stored as SHA-256 and checked through SECURITY DEFINER functions, one circuit breaker per provider, quarantine for poison deliveries, bounded retries with jittered backoff.
  • Prometheus metrics with 13 unit-tested alert rules, ECS structured logs with correlation ids, OpenTelemetry traces from the request to the delivery.
## challenges → how I solved them
- [x] Keep deliveries in order per recipient with many workers, redeliveries and restarts.
→ Each notification takes a sequence number under a row lock; a worker claims a delivery with a single conditional UPDATE that only succeeds if it is the head of its lane. Verified by tests running 12 workers over shuffled, duplicated wake-ups with injected faults; a mutation test proves the guard matters.
- [x] SMTP and FCM have no idempotency key: a timeout after sending is ambiguous.
→ Such outcomes become `unknown` and are never retried blindly; an operator replays them. Processing is at-least-once with durable deduplication, and the documentation says so instead of claiming exactly-once.
- [x] The first load test reached only ~380 of 1,000 req/s, bound by PostgreSQL WAL writes (p95 7.3 s, 41% timeouts).
→ Profiled the wait events, cut the statements per notification from ~31 to ~14, collapsed the claim into one UPDATE, dropped indexes and added fail-fast overload protection. Result in the load test: 1,000 req/s sustained for 5 minutes with p95 at 53 ms.
- [x] A flood of fake API keys could exhaust the database pool.
→ Key ids are cached in memory and at most 4 credential reads run concurrently. The worker's login was removed from the API process and the admin API moved to its own process.
>>> tests = 101 automated>>> load test = 1,000 req/s sustained 5 min · p95 52.8 ms>>> load-test drain = ~2,960 deliveries/s>>> load-test integrity = 0 ordering violations · 0 duplicates over ~1.8M deliveries
./ai/deep_learning/(3)
dnn_debugging.pyPython

Mining a DNN's blind spots

INRIA internship: mines neuron groups correlated with wrong predictions to detect future errors.

INRIA · HyAIAI2021
adversarial_robust.pyPython

Adversarial robustness

Robust classifier of high/low-voltage units against small and large perturbations, for Enedis.

Enedis2020—21
simpsons_cnn.pyPython

The Simpsons classifier

My first DL project — compared techniques to classify Simpsons characters; a CNN won.

CNN2018
./ai/computer_vision/(3)
./ai/reinforcement/(2)
./ai/machine_learning/(4)
./graphics/(3)
./software/frameworks/(3)
./software/web/(3)
./software/systems/(2)
production_mgmt/C++ / Qt

Production management app

SOGEA internship: desktop + tablet app managing clothes production for Imperial Fashion.

Qt2018
feynman_it/C++ / Qt

School IT system

IT system for Liceo Richard Feynman: Moodle-like features, student & staff management.

Qt2017