OUKITEL B2B Suite
Four Flutter applications and a web client sharing one NestJS backend, covering business sales, repair intake and returns.
- 4
- 1
- Play Store
Architecture
- Client
- Service
- Data store
- External
The problem
OUKITEL's regional operation needed tooling for distinct roles that all touch the same inventory and customer records — a sales app, a repair-tracking app, a programme app and a returns workflow.
Four separate audiences, four separate apps, one set of business records. Building four backends would have guaranteed four divergent definitions of a device, a customer and a repair. Building one app with four modes would have shipped every role a cluttered interface for jobs it never does.
The approach
One NestJS API with role-scoped surfaces, and four thin Flutter clients that each present only what their role needs. Files move through presigned S3 URLs rather than through the API process, so uploading repair photographs never occupies a request worker. Slow work — notifications, exports, report generation — goes onto BullMQ queues.
Decisions that mattered
One API, four clients
A single NestJS service owns the schema and exposes role-scoped surfaces, documented with Swagger. Each Flutter app is a thin client over it. Adding a fifth role means adding a surface, not a backend.
Presigned uploads
Repair photographs go straight from device to S3 through presigned URLs. The API issues the credential and records the result; it never proxies the bytes. Upload throughput stops competing with request handling for the same worker pool.
Queues for anything slow
Notification fan-out, spreadsheet exports and report generation run on BullMQ over Redis. A request that triggers a thousand notifications returns immediately and the work drains in the background.
Localisation in the API, not the clients
`nestjs-i18n` resolves language server-side so four clients never carry four drifting copies of the same message catalogue.
Hard problems
Four apps, one release discipline
Shared Dart models and a common HTTP layer are lifted out of the individual apps, so a contract change lands once rather than four times. The alternative — copy-pasting a model across four repositories — fails silently the first time someone forgets one.
Field conditions
Repair intake happens in workshops with unreliable connectivity. Secure local storage holds credentials and in-progress records so a dropped connection loses nothing, and writes reconcile when the device comes back.
Authorisation that survives a decompiled client
Role gating in the app controls what a user sees; it is not what stops them. Every endpoint enforces its own guard server-side, on the assumption that any client can be modified.
My role
Sole engineer across the backend, all four Flutter applications and the Next.js client.
Status
The business application is published on Google Play. The suite is in production use.
Stack
- FlutterDartFirebase
- NestJSPrismaPostgreSQLRedisBullMQSwagger
- AWS S3Presigned URLsMinIO
- Next.jsTypeScript