Sekou is a senior freelance backend engineer with 13 years of Java and Go. At GEODIS he built two internal freight tender applications from scratch that the transport teams use every day, and at TF1 he was the observability referent for the ad-tech platform, taking incident resolution from days to about an hour. Canal+, Societe Generale, and SNCF complete the list.
His Rust project is a property management platform for individual landlords: buildings split into rentable units, leases with co-tenants and rent history, payments, maintenance with vendors, separate owner and tenant portals, and an in-app AI assistant. It runs on Dioxus 0.7 fullstack in a single crate, where everything that touches the database sits behind a server feature flag, with Axum sessions and SQLx on Postgres.
What makes it interesting is where the invariants live. In Java the type system stops at the class boundary. Here it reaches the database row and the endpoint signature: ids that cannot be mixed up, money that is never a float, statuses that mirror the schema's constraints, and authorization that the compiler checks.
13 yrs
Java and Go, high-reliability distributed systems
Freelance
Missions at TF1, Canal+, GEODIS, Societe Generale
MTTR < 1h
Incident resolution at TF1 ad-tech, down from days
3x
End-to-end performance gain on production workloads
LinkedIn rebranding
The challenge
Two roles, one crate, and an LLM in the request path
An owner sees every unit they manage, a tenant sees only their own lease, and both portals compile from the same crate to WASM and to the server. The first version passed ids around as bare UUIDs, money as f64, and statuses as strings, so nothing but discipline kept those boundaries intact.
Then the assistant added a component that reads natural language and can be talked into anything. Any design where the model chooses whose data to read is a cross-tenant leak waiting for the right prompt.
•Owner and tenant boundaries on every server function
•Ids, money, and statuses with no type-level meaning
•An LLM that must never widen the caller's scope
The outcome
Invariants in the types, scope in the server
Each entity id is its own type, so a lease id passed where a property id belongs no longer compiles. Money is Decimal from the database to the screen. An owner endpoint without its permission check fails the build.
The assistant answers owners and tenants from their own data only, because the scope is resolved from the session before the model is involved, not from anything the model says.
✓Typed ids, Decimal money, and enum statuses end to end
✓Building and unit split with co-tenant leases
✓AI assistant with no path to another user's data
[ Domain model ]
Types that reach all the way to the database row
In Java, a lease id and a property id are both a UUID, and nothing stops one from landing where the other belongs. Sekou wrote an entity_id! proc macro that generates a newtype per entity, with SQLx Encode and Decode emitted only under the server feature, so the WASM build never pulls in SQLx. Money stopped being f64 and became a MonetaryAmount over Decimal, decoded straight from Postgres numeric.
Lease, payment, and maintenance statuses are enums deriving sqlx::Type, mirroring the CHECK constraints in the schema and matched exhaustively instead of string-compared. Expiring was removed from LeaseStatus on purpose: it is not a stored state but a function of the end date and today, so it became an is_expiring() method. Property status (vacant, occupied, late) is computed by a SQL CASE and cast directly onto its enum through query_as!, which deleted the intermediate row struct and the mapping code.
[ Authorization ]
An endpoint that does not compile without its permission check
Every owner-side server function uses #[secured_server], an attribute macro Sekou wrote as a drop-in replacement for Dioxus's #[server]. It injects the session user, generates a Controller marker type per function, and adds a compile-time assertion that the marker implements AccessController. Forgetting the permission check is a build error, not a code review comment.
Tenant-side functions take the other route and are safe by construction: every query is bound to the caller's own id from the session, so there is no parameter to tamper with. Both owner and tenant layouts are default-deny and redirect to login unless the auth resource holds a user. Errors cross the server function boundary through one AppError type, so the client can still tell NotFound from Unauthorized.
[ Schema ]
A building is not a rentable unit
The first schema had one properties table and one tenant per lease. Real landlords own buildings with several units and sign leases with couples and roommates. Sekou split buildings from rentable units, replaced the single tenant foreign key with a lease_tenants join table, moved free-text maintenance providers to a vendors table, and added rent history.
Every query then joins through units and lease_tenants, and dashboard occupancy counts units rather than buildings. Vacant units show their asking rent by falling back to the unit's target rent when no active lease exists. Repository functions take &mut PgConnection instead of &PgPool, so the same functions compose inside a transaction without a second version of the data layer.
[ AI assistant ]
Context-first, because the transport has no tool calls
The assistant started as a tool-calling agent, not RAG: the data is live, per-user Postgres rows, and “what is my collection rate this month” is a query, not a similarity search. Then the transport ruled tool calls out (the headless Claude CLI with tools disabled, or plain chat completions), so Sekou flipped the design: the server gathers the caller's scoped data first through the same server functions the pages use, and the model only ever answers from that JSON.
The scope comes from the session, never from the model, and the system prompt is explicitly not treated as a security boundary. Owner context reuses existing functions and filters open maintenance and late payments in Rust, with zero new SQL. CLI calls run behind a semaphore and a timeout with kill_on_drop, so an abandoned request does not leave a child process behind. A per-user sliding-window rate limit lives in memory, with the move to a shared store documented for when the app runs on more than one instance, and every turn is logged to chat_events with duration, success, and error type.
The journey
One clear path through the build
Step 011 / 5
Foundations
Dioxus fullstack auth on Axum sessions, then owner and tenant portals built against hardcoded data to settle the product before the schema.
Step 022 / 5
Schema
Domain migrations and a seed built around real edge cases: a terminated lease re-let, a waived payment, a returned deposit, a cancelled maintenance request.
Step 033 / 5
Domain model
Property, lease, payment, and maintenance as top-level domains, with typed ids, Decimal money, enum statuses, and #[secured_server] on every owner endpoint.
Step 044 / 5
Data layer
Statuses computed in SQL and decoded straight onto enums, repository functions on &mut PgConnection, and AppError carrying NotFound versus Unauthorized to the client.
Step 055 / 5
Final build
Buildings split into rentable units with co-tenant leases, vendors, and rent history, default-deny layouts, and the in-app AI assistant.
Drag to explore · Select a milestone to read the full story