Case study · Mobile app · Serverless backend
Anaaya Plus
A car-service booking app for the Saudi market: phone/OTP sign-in through serverless functions and Firebase, vehicle and location management, race-safe time-slot booking, and a tracked booking lifecycle in Arabic and English.
- Role
- Solo — product scope, Flutter client, Firestore data and rules, Edge Functions
- Context
- Personal project
- Status
- PrototypeTest application — payment, SMS and technician dispatch are simulated
- Stack
- Flutter
- Dart
- Riverpod
- go_router
- Firebase Auth
- Cloud Firestore
- FCM
- Supabase Edge Functions
- Deno
- GitHub Actions


Overview
Anaaya Plus is a Flutter app for booking at-home car services. A customer signs in with their phone number, chooses a service, vehicle, location and time slot, completes a test payment, and follows the booking through a technician-dispatch lifecycle. It is a portfolio/test application, not a live service.
Problem & requirements
A service booking depends on several steps that each need real backing: who the customer is, which car, where, and when — and two customers must never get the same slot. The aim was to implement that whole journey with real authentication, persistence and authorization instead of a set of static screens.
- Phone-number sign-in with one-time codes, verified server-side.
- Concurrent customers must not be able to claim the same time slot.
- Arabic-first (RTL) with a full English (LTR) experience.
- A zero-cost test environment: no real payments, SMS or dispatch.
My role
Solo project. I defined the scope and built the Flutter client, the Firestore data model and security rules, and the Supabase Edge Functions behind the OTP sign-in.
Key features
Phone + OTP sign-in
Edge Functions generate, hash and verify one-time codes with expiry, attempt limits, resend cooldown, rate limiting and replay rejection, then mint a Firebase custom token the app signs in with.
Race-safe scheduling
Slot availability lives in Firestore, and claiming a slot runs in a transaction so two clients cannot book the same time.
Vehicles, services and locations
Vehicle management, a service catalogue with per-service options and pricing, and saved, current or map-picked locations (OpenStreetMap).
Booking lifecycle and notifications
Booking, test payment, cancellation where the state allows it, a simulated Assigned → On the way → In progress → Completed lifecycle, and push notifications via FCM.
Arabic and English
Arabic RTL and English LTR experiences generated from ARB sources with Flutter gen-l10n.
Architecture
Firebase is the session and application-data boundary; Supabase Edge Functions provide isolated backend execution for the custom OTP flow, so the authentication logic does not live in the client.
Flutter client
Feature modules (auth, booking, cars, location, payment, scheduling, notifications…) with repository abstractions over data access.
- Flutter / Dart
- Riverpod
- go_router
- gen-l10n
Firebase
Authenticated session, application data guarded by Firestore security rules, and push notifications.
- Firebase Auth
- Cloud Firestore + rules
- FCM
Supabase Edge Functions
Deno functions for the OTP flow: send-otp stores a hashed code and sends it; verify-otp enforces the OTP controls and returns a Firebase custom token.
- Deno
- Firebase Admin (custom token)
External services (test mode)
SMS delivery through Twilio's test API; maps from OpenStreetMap.
- Twilio TEST API
- OpenStreetMap
Data model
Cloud Firestore, organised per user so security rules can scope every read and write to its owner. Shared slot documents record which user claimed them.
users/{userId}
- profile
Parent of all per-user data
cars
- make, model, year, plate
users/{userId}/cars
bookings
- service, vehicle and location snapshots
- price
- status · statusUpdatedAt · estimatedArrival
users/{userId}/bookings — snapshots and price are create-only; one payment sub-document
locations
- saved addresses
users/{userId}/locations
notifications · devices
- notification history
- device tokens for FCM
users/{userId}/…
serviceSlots
- claimedByUid
Top-level; rules allow a claim only on the caller's own behalf
API design
Edge Functions invoked over HTTPS. The OTP functions are public because they are the authentication step; everything else requires a verified Firebase identity.
| Endpoint | Purpose |
|---|---|
POSTsend-otp | Generate, hash and store a one-time code and send it via the Twilio test API. |
POSTverify-otp | Check { phone, otp } against the OTP controls; on success return a one-shot Firebase custom token and whether the user is new. |
POSTverify-firebase-auth | Verify a Firebase ID token and return the caller's uid, without holding any Firebase secret. |
GEThealth-check | Confirm the Edge Functions deployment responds. |
Engineering decisions
Custom OTP flow instead of Firebase phone auth
Native Firebase phone authentication depends on real SMS delivery and device-integrity checks, which did not fit a zero-cost test setup. Edge Functions own the OTP logic and hand back a Firebase custom token.
Trade-off: Two backend platforms and more moving parts than the native option (documented in ADR 0001).
Serverless functions over a custom server
A single monolithic server would add operational work without adding value at this scope; Edge Functions give an isolated backend boundary without running one.
Authorization in Firestore rules
Per-user data paths let security rules enforce ownership on the server, rather than trusting the client to filter data.
Booking lifecycle enforced on the server
Security rules allow an update to touch only the status fields and only along approved transitions (upcoming → on the way or cancelled, on the way → in progress → completed). The Dart-side check is a fast pre-check, not the only guard. Bookings can never be deleted by a client.
Trade-off: There is no technician role yet, so the customer's own app drives the demo lifecycle, and payment records are client-written — acceptable only because payments are simulated.
Simulate what would need real money or real people
Payment, SMS and technician dispatch are explicitly test-mode, and the repository lists them as known limitations instead of hiding them.
Testing & reliability
Documented in the repository
The repository documents automated tests across the client and the backend functions. These results are taken from the repository's own testing document and were not re-run for this site.
- Flutter unit, repository and widget tests: 629 passed, 0 failed (documented baseline).
- Deno tests for the shared backend utilities: 70 passed, 0 failed (documented baseline).
- Coverage includes OTP rules, custom-token exchange, booking/payment/notification state transitions, slot claiming, and Arabic/English widget flows.
- A GitHub Actions workflow is configured for the Flutter project.
Screenshots

Home — upcoming booking and service catalogue (Arabic, RTL) 
Scheduling — available slots for the selected day 
Tracking — simulated technician-dispatch lifecycle 
Vehicles — default car and saved cars
Outcome & next steps
A complete customer-side booking journey with real authentication, authorization and persistence, intentionally stopping short of production infrastructure.
Next steps
- A real payment gateway and SMS delivery behind trusted backend services.
- A technician-facing app and role, with live location instead of a simulated lifecycle.