Antonin Leprest Get in touch

Ilikethedecisions morethanthetools, andIwritedownwhy.

I own the platform under Arbore, an augmented-reality gardening app on the App Store: infrastructure as code, release engineering, observability, and the pipeline that generates every plant in its catalogue. Before that I shipped a second app alone, end to end.

Lyon, France. In Québec City from March 2027, post-graduation work permit approved.

A houseplant in a patterned white pot, generated by the asset pipeline and rendered from the file the app ships.
Generated, quality-gated and cut from its pot by the pipeline described below, then rendered from the USDZ the app ships.

A paid 3D API, replaced by a workshop that pulls its own work

Arbore measures your space, asks about light and constraints, then lets you place true-to-scale 3D plants in augmented reality, then keeps them alive with a care calendar. I own the platform underneath: infrastructure, backend, releases, documentation, and the pipeline that produces every model in the catalogue.

Arbore’s home screen, listing the gardens you have designed.
Your gardens, kept between sessions.
Choosing a garden style from four options during onboarding.
Eight questions, then a shortlist.
A plant placed in a room in augmented reality, with a light reading of 1059 lux.
Placed to scale, with the light measured.
The plan view of a garden, with each plant marked on a map.
Where everything sits, and what it needs.
The care calendar, showing watering routines for June.
The feature the survey said people wanted most.
A basket of plants, soil and tools recommended for the garden.
Built from what is actually planted.
The Arbore launch screen, with the app’s tree mark.
Four languages, one catalogue of 123.

The pipeline underneath

Context

Every plant in the catalogue needs a 3D model, and we were buying them. Thirty credits a plant, on a €38 plan for two thousand credits, which works out at €0.57 each. Nobody had yet decided how big the catalogue would get.

Decision

Self-host TRELLIS.2 and generate them ourselves, for roughly one cent of compute a plant. Colab has no execution API, because a session only starts when a human is in front of a browser. So instead of the server driving Colab, Colab pulls the work. You don’t open a generation. You open a workshop, and it works for as long as it stays open.

Consequence

A quality gate rejects multiple subjects, cubes and slabs on a 3.7× dominance margin, and retries on a fresh seed, three times, without asking. Geometry that is invented but plausible still goes to a person, because no metric sees it. A model with three plants in it should never reach review: a retry costs 165 seconds of GPU where a human round-trip costs attention.

Rejected

Depth Anything for reconstruction. Explored and measured, then set aside. It was more work than it would have bought us at the quality we needed.

Two criteria, each blind where the other sees

A pot wall is a vertical surface of revolution: horizontal radial normals, a radius that tightens. Geometry finds it whatever colour the pot is, and goes mute the moment the pot is crumpled and stops being a revolution. Colour catches that crumpled pot, and then swallows the parts of the plant that aren’t green, like a beige caudex or magenta petioles.

Six models out of nine have a wall signature at 100%, so geometry handles them alone and the other three are flagged. Every result carries the method it came from, so review knows what it is looking at rather than guessing. The downstream half runs in plain numpy with no GPU, which means the day pot removal improves, it replays across the whole catalogue without regenerating a single model.

One trap already paid for: the gate measures the exported mesh, not the one before decimation. Decimation splits components, so the anthurium read 87.8% dominant before and 54.9% after, depending only on when you measured. Textures ship as PNG rather than WebP, because SceneKit does not read EXT_texture_webp.

Who it is actually for

Before committing the roadmap I ran a quantitative survey: 75 responses, 55 complete questionnaires, 42 questions covering segmentation, purchase behaviour, competition, Likert scales and a Van Westendorp price ladder.

What people actually wanted, of 75 asked. Up to three answers each.
A care plan with reminders 43% Suggestions matched to my ground 40% Diagnose a sick plant from a photo 39% See plants in augmented reality at home 23% Weather with watering advice 23% A catalogue with detailed cards 16% Buy the plants from the app 3%

The augmented reality came fourth. It is the feature the app is built around, and a care calendar beat it by twenty points. So the roadmap changed.

And 47% said they dislike subscriptions on principle, which is a packaging problem rather than a pricing one.

One caveat: 51% of respondents are 18 to 24, which is young for a gardening product. I would rather say so than let the numbers imply a market they don’t describe.

Twelve of the 55 complete ladders are excluded from the price chart: nine ranked the four questions out of order, three put the ceiling above €50 a month.

What they would pay per month. Four cumulative curves over 43 coherent answers; the prices are where they cross.
0% 25% 50% 75% 100% €5.45 €0€2€4€6€8€10€12 4.99 7.17
  • Too cheap to trust
  • A bargain
  • Expensive
  • Too expensive to buy
€4.99
Floor. Below it, more people doubt the quality than find it expensive.
€5.45
Optimal price. Fewest people reject it outright, in either direction.
€7.17
Ceiling. Above it, resistance outruns the sense of a bargain.

A band of €4.99 to €7.17, which is narrow. The indifference price is €5.28 and the optimal price €5.45, seventeen cents apart, so there is no premium tier hiding above the sweet spot.

Plotted the standard way: the cumulative percentage at each of the thirty prices people actually named, joined by straight segments, with the four prices read off where the lines cross. The axis stops at €12, by which point all four crossings are behind us; the highest ceiling anyone named was €40.

An orchestrator that never learns anyone’s name

Crédit Agricole’s app for family caregivers coordinates the logistics between relatives. Nothing in it carries the state of the person being cared for: whether she had lunch, whether she got up, whether the kitchen light came on this morning. That hole is the product, and the target named on stage was mental load.

The IBA box and its cat, as we pitched it. The clip is generated. The thing that actually runs our code is a Raspberry Pi and looks nothing like this.
A sapphire crystal cup, turning slowly.
The Collaborative Spirit prize, at Hackathon AIdant.

Decision

An orchestrator fronting six domain agents (directory, telephony, home, tasks, finance and notes) that reach the Go API through an MCP server. Every person is a token. First names are restored locally, after the model has already answered, and the test bench shows all three steps side by side: what went out, what came back, and the mapping table. Verifiable, not asserted.

Consequence

Confirmation is enforced by a tool rather than a prompt: the call refuses to act without it, because a rule written in a prompt can be talked around. Authorisation sits in three independent places. The tool does not exist for the cared-for person’s profile, the API answers 403, and the gateway authorizer rejects the token before it reaches MCP at all.

Tested

Thirty-nine end-to-end checks against the real pipeline, deliberately kept out of CI because they call Bedrock and cost money, and because the unit tests go green on a completely broken pipeline. The pre-commit hook only runs the packages you touched, since a slow hook gets bypassed.

Rejected

Two Bedrock clients. Two credential conventions, incompatible, one of which was going to rot quietly. One is left.

Allowed caregiver profile Blocked 3 independent layers
Kitchen screen cared-for profile IBA inside Nello caregiver profile Orchestrator Opus 5, one profile per role directory Nova Lite telephony Sonnet 5 home Nova Lite tasks Nova Lite finance Sonnet 5 notes Nova Lite every person is a token above this line MCP server Gateway authorizer: token valid Nello API, Go and Gin 200 OK: caregiver role verified

Two surfaces, one orchestrator, six domain agents, and the line no first name crosses.

Above the redaction line, every person is replaced by a local token before the prompt leaves the device. Below it, the MCP gateway authorizer and the Go API enforce role boundaries independently of what the model was asked to do.

Thirteen documents came out of those two days, including the camera metadata contract against MDR, the AI Act and HDS, a note on why pseudonymisation is not anonymisation, and a page of the shortcuts we took, written down because a jury will find them anyway.

Built alone, end to end, and still running

From a designer’s Figma file to the App Store: Flutter, Node, Postgres, Docker on a VPS, GDPR compliance and release. No third-party UI framework: every component is drawn from Flutter primitives, so the build stays faithful to the file it came from.

Ganesh’up home screen with the day’s revisions.
What to revise, and when.
The magic marker hiding passages of a study sheet.
The marker. No library behind it.
The built-in Pomodoro timer.
A timer, because the method needs one.
Setting spaced-repetition reminders.
Reminders that survive going offline.
The library of study sheets by subject.
Stored locally first, synced after.

Decision

Resolve sync conflicts per field, not per record. The next reminder keeps the earlier date, because one reminder too many beats one missed. The count keeps its maximum, so history is never lost. Whether it is enabled is last-write-wins, because that one is a statement of intent and the latest intent is the real one.

Consequence

A persistent operation queue that survives the app being killed, exponential backoff, and delta sync. The audit log is written before the deletion it records. Write it after and you have deleted your own proof.

Rejected

A component library. It would have saved weeks and quietly rewritten the designer’s work. The first version of the marker lagged on large sheets instead, until background images were capped at 4096 px, which is Flutter’s GPU texture limit.

Taking apart my own production stack, on purpose

I am preparing for a solutions architect role, so I am auditing a system I actually shipped rather than a tutorial. Ganesh’up’s backend works: one node, Docker Compose, a one-minute deploy script I wrote myself. Reviewed against the Well-Architected Framework it also has six structural bottlenecks, and those are now public.

Context

The API decodes up to 50 MB of Base64 on Node’s single thread and writes the result to a local disk. Memory grows 91× under a burst, health checks starve behind the upload, and a second replica would serve 404s for media its sibling holds.

Decision

Make the compute stateless: S3 with presigned URLs so binaries never touch the API, SQS and Lambda for the asynchronous half, a rolling update with maxUnavailable: 0 and an init container for migrations, EventBridge and Secrets Manager instead of a cron hitting a public endpoint, and immutable SHA-tagged images built in CI rather than on the production host.

Rejected

Lift and shift. An idle Redis container nobody used would have become a billed ElastiCache cluster, and in-process rate limiting would have silently multiplied its own limit by the number of replicas.

Ten concurrent uploads, fifteen media items each, the same workload replayed against both stacks.

85 ms

8.3× faster

down from 704 ms

Total latency

362 MB

−51% peak

down from 735 MB

Peak resident memory

+12 MB

60× flatter

down from +727 MB

Growth over idle

0

Stateless

down from 150

Binary files written to local disk

350 MB

+92 MB SDK baseline

up from 258 MB

Resident memory per replica

62.2%

+2.4 pts URL signing

up from 59.8%

Peak CPU on one core

What it cost: three AWS SDK clients add a constant 92 MB per replica. That is a sizing cost rather than a scaling one, because unlike the old cost it does not grow with traffic. Peak CPU still touches 62% while signing 150 presigned URLs, which is the argument for rate limiting at the ingress instead of in Node’s memory.

Two labs follow this one: pulling a brick out of the Ugram monolith onto S3, SQS and Lambda with a strangler fig, and decomposing an open-source legacy monolith. All three carry C4 views and decision records for the trade-offs, which is the part I am actually practising.

A university project that had to survive being public

Most coursework stops at a working localhost. This one went to production on AWS and stayed there.

Decision

Elastic Beanstalk for the application, RDS for Postgres, S3 for media and CloudFront in front, with HTTPS end to end rather than terminating somewhere convenient. Deployment through GitHub Actions, so nothing reached the environment from a laptop.

Consequence

CloudWatch logs, dashboards and alarms, plus Sentry on the frontend with session replay. Observability is a design tool here rather than an afterthought, because production is where a design gets contradicted.

  1. 2024 to now

    Teaching assistant

    Epitech, Lyon

    Machine learning modules (computer vision, pre-processing), the first-year piscine, low-level C++, and SQL. Two spells: 2024–25, and again from September 2026.

  2. 2025

    R&D engineer, AI and NLP

    Valraiso, Lyon

    Built an MCP client and server giving LLM integrations context-aware access to internal data tools. Fine-tuned and evaluated RoBERTa embedding models on ski product data, over gold and silver sets.

  3. 2023

    Chief technology officer

    Lebourgmestre sàrl, Lausanne (remote)

    Set technical strategy alongside the CEO, led mobile and web development, designed the REST APIs, and ran a fully containerised stack. Ganesh’up came out of this.

  4. 2023

    R&D developer, volunteer

    PoC Innovation, Lyon

    Built SecureEnv, a tool for managing and synchronising a team’s environment variables without putting them in the clear.

What I work with

Cloud and operations
AWS (S3, RDS, CloudFront, Elastic Beanstalk, Lambda, SQS, EventBridge, Secrets Manager, Bedrock), Terraform, LocalStack, Docker, k3d, GitHub Actions, Nginx, Cloudflare, SOPS and age, fastlane
Architecture
C4 views, architecture decision records, REST, gRPC, WebSocket and WebRTC, infrastructure as code, Well-Architected reviews, strangler fig, event-driven design
Observability
CloudWatch, Sentry, Prometheus, Grafana
Backend
Go (Gin, Fiber), Node and TypeScript, NestJS, FastAPI, Rust (actix)
Languages
Go, TypeScript, Swift, Dart, Rust, C and C++, Python, Haskell
Mobile and 3D
SwiftUI, ARKit, RoomPlan, RealityKit and SceneKit, USDZ and glTF, Flutter, React, Next.js, OpenGL, openFrameworks, ray tracing, PBR
Machine learning
NLP pipelines, embeddings, Model Context Protocol, Strands agents, Amazon Bedrock, TRELLIS.2, on-device CoreML, MLX
Security
Wireshark, Nmap, Metasploit, Burp Suite, Hydra, Hashcat, OWASP testing
Product
Quantitative surveys, Van Westendorp pricing, segmentation, KPI trees