Neemota
Fleet Hire-Purchase SaaS
Multi-tenant SaaS for fleet operators running hire-purchase schemes in emerging markets: drivers, vehicles, payments, and revenue reporting on one platform, structurally isolated per operator.
Business Context
Neemota lets multiple fleet operators run their full hire-purchase business: drivers, vehicles, payment collections, revenue reporting, independently and securely on one platform, without bespoke infrastructure per customer.
Why I Built It
I own 3 cars on hire-purchase and was managing everything manually: chasing payments, tracking balances, remembering who owed what. As an engineer with no time to babysit a spreadsheet, I built Neemota to run it for me. I added role-based staff access so someone could help manage collections without touching sensitive data they didn't need. A friend heard about it, started running his own fleet through it, and suddenly it was a product.
The Problem
Fleet operators handle sensitive financial data: outstanding balances, payment histories, driver personal details. A shared Firestore collection with tenant-ID fields looks simple but puts the isolation contract in application code. One missing
where('tenantId', '==', ...) clause in any resolver and you have a data breach. At the GraphQL layer, passing tenant context without polluting every resolver signature is its own problem: passing tenantId as a query argument means every client call carries it and every resolver validates it, fragile and redundant.The Architecture Decision
I evaluated shared collections with tenant ID fields versus collection-per-tenant namespacing. Shared collections are easier to query globally but push isolation into application code: every resolver, every query, every new developer who touches the codebase.
I went with collection-per-tenant namespacing:
Tenant resolution happens once: Firebase Auth custom claims carry the
I went with collection-per-tenant namespacing:
vehicles_{tenantId}, drivers_{tenantId}, payments_{tenantId}. Isolation is structural, not logical. There is no query you can write that crosses tenant boundaries by accident because the collections don't overlap.Tenant resolution happens once: Firebase Auth custom claims carry the
tenantId, extracted server-side in the GraphQL context builder. Every resolver receives context.tenantId, clean, consistent, not injectable from the client. Firestore security rules become trivially auditable because the collection names themselves encode the boundary.The Trade-off
Cross-tenant aggregation: platform-wide stats, operator benchmarking, global reporting, requires explicit aggregation logic rather than simple collection queries. That was an acceptable cost. Neemota's operators are independent businesses, not competitors being compared. Data isolation was non-negotiable; cross-tenant analytics were out of scope.
Outcome
The platform supports multiple live tenants with structural data isolation. No security rules configuration, no resolver-level filtering discipline required to maintain it. Onboarding a new operator is a single Firebase Admin SDK call to create a user with the correct custom claims. Zero cross-tenant exposure surface in the data layer.
Tech Stack
TypeScriptNuxtGraphQLFirebaseFirestoreFirebase AuthCloud Functions
Link
