Skip to content

Build Status & Roadmap

Snapshot: 27 August 2026 · production e56066f · in sync with main

This page is the state of the build: what is deployed, what has been verified against production, what is genuinely broken or missing, and what has to happen before launch. It is deliberately separate from Scope & Roadmap, which is the authoritative contract for what belongs in MVP / Scale / Vision. That page decides scope; this one reports reality.

Everything below was checked against production on the date above, not recalled from memory. Where a claim rests on evidence, the evidence is named.


1. Live systems

Surface Where State
API api.yam3at.com Live. 315 routes, 89 of them admin-only and all permission-gated.
Web dashboards app.yam3at.com Live. 64 pages — 30 admin, 17 vendor.
Mobile TestFlight only 1.0.0+1. 48 screens. Android not on Play, by decision.
Infra Hetzner 89.167.17.109 8 containers up: api, web, nginx, mysql, redis, meilisearch, scheduler, worker. Disk 18% (39G/226G).

Shared box. Five other projects run on this host. Never restart anything outside the yam3at compose project.

Backups. Daily at 02:00 to /var/backups/yam3at/, 17 retained. Verified present. Restore procedure: operations/runbook.md.

Scheduler. 7 recurring jobs: quote expiry, booking auto-complete, subscription processing, vendor licence checks, calendar sync, FX refresh, ticket reservation expiry.

Security headers on the API, verified live: HSTS (2 years, includeSubDomains), X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin. APP_DEBUG=false, APP_ENV=production.

Timezone. The platform runs on Asia/Kuwait (UTC+3, no DST), with the MySQL connection pinned to +03:00 to match. Both halves are required: TIMESTAMP columns convert on read using the session timezone, so an app sending Kuwait wall-clock to a UTC session would store every new instant three hours wrong. Verified live — PHP and MySQL both report the same local time, exactly three hours from UTC, and API responses carry +03:00.


2. Integrations: wired, test, or fake

This table is the honest answer to "is it done?".

Integration Configured Actually usable
Payments MyFatoorah + Tap keys present No — PAYMENT_DRIVER is unset, so it falls back to fake. MyFatoorah points at apitest.myfatoorah.com; the Tap key is sk_test_….
Email Resend Yes
SMS / OTP Twilio (+ Verify service) Yes
Push FCM (project, client email, private key all set) Yes
AI Anthropic, claude-sonnet-5, with daily + monthly budget caps Yes
Search Meilisearch via Scout Yes
Maps Google Maps key (mobile-restricted) Yes, gated behind MAPS_ENABLED at build time
Social login Google OAuth; Apple on iOS Yes
WhatsApp No. Sender is a no-op until Business API credentials exist.

The mobile app's kOnlinePaymentEnabled build flag defaults to false, which correctly matches the fake driver: both payment surfaces now say "قريبًا" rather than offering a button that leads nowhere.


3. Production data

Table Rows Note
users 55 37 are load-test accounts
vendors 15 12 are [LT] load-test, 1 is Audit Test Vendor
services / packages 14 / 29
events 31
RFQs / quotations 28 / 77
bookings 6 event bookings (quote → accept)
orders 3 direct bookings
reviews 0 nothing has been reviewed yet
notifications 30
categories 31 real catalogue
media 3 effectively no imagery
admins 1

Platform settings in force: commission 5%, deposit 20%, payout hold 7 days, referral reward 2 KWD, loyalty 1 point/KWD redeemed at 100, free vendor RFQ limit 5, super_app_enabled true, maintenance_mode false, registrations open.


4. Verified working, end to end

These were walked on a real device (Android emulator) against production, not asserted from code:

  • Direct booking: browse → service → package → date → slot → address → payment → order confirmed (YM-2026-000003), then cancelled cleanly.
  • Events / RFQ: create event with date and guest count → RFQ to a vendor → vendor quotes → customer accepts → booking created with the deposit computed correctly at 20% (290.000 of 1450.000).
  • Planner: generated a checklist whose due dates are back-calculated from the event date — budget 120 days out, venue 90, guest count 60, invitations 30, final headcount 3.
  • Auth: registration, login, session surviving app restart from the Keychain, logout, role-based navigation (customer vs vendor).
  • Vendor panel: pending-state banner, RFQ inbox, quoting, analytics, pricing intelligence, gallery, calendar, blocked dates.
  • Localisation: Arabic renders correctly throughout, including notification titles and pricing advice, in both ar and en.

469 API tests pass (1 632 assertions).


5. Launch blockers

Ranked. Nothing below is a nice-to-have.

B1 — No real payments

PAYMENT_DRIVER is unset, so production uses FakePaymentProvider. MyFatoorah is on the test sandbox and the Tap key is a test key. TAP_WEBHOOK_SECRET is empty, so Tap webhook signatures cannot be verified even once a live key lands.

To close: obtain the merchant account, set PAYMENT_DRIVER, swap to live keys and base URLs, populate the webhook secret, then flip the mobile build's ONLINE_PAYMENT dart-define. Until all of that is true, no money can move on either the direct-booking or the events path.

B2 — Test data is live and public

37 load-test users, 12 [LT] vendors and Audit Test Vendor are approved and published, so they appear in real vendor discovery.

To close: php artisan yam3at:load-test --wipe removes the [LT] set precisely (it matches only rows whose owner's email ends in the load-test domain, and is covered by tests). Audit Test Vendor must be removed separately.

B3 — No imagery

3 media rows platform-wide. Vendor cards, service heroes and galleries fall back to placeholder tiles. A marketplace that looks empty converts like one.

To close: real vendor photography, plus the mascot/brand artwork. Note the launcher icon problem in G4.

B4 — Zero reviews, and rating is a ranking input

reviews is empty, so every listing shows "جديد / New" and the "Top rated" sort — the default ordering on the browse screen — has nothing to rank by. The machinery is correct and tested; it simply has no data.

To close: seed trust through early real bookings, or launch with a different default sort until reviews accumulate.


6. Known gaps, not blocking

# Gap Impact
G2 WhatsApp channel is a no-op. Notifier dispatches to it behind an enabled() check that is always false. Medium — WhatsApp is the dominant channel in Kuwait
G4 ic_launcher_foreground.png is the full square icon, not a transparent foreground layer, so Android 12+ paints a yellow circle with a black square inside it on every cold start. The adaptive-icon XML is correct; only the asset is wrong. Low, but it is the first thing every user sees
G5 Android release signs with debug keys (buildTypes.release uses signingConfigs.debug). Harmless while Android stays off Play, blocking the moment it doesn't. Deferred by decision

Closed on 27 August

  • G1 — mixed datetime types. event_milestones.scheduled_at was the only DATETIME among 210 datetime columns; the rest are TIMESTAMP, which converts on read using the session timezone while DATETIME never converts. Storage and the API's ISO-8601 contract agreed only because the app timezone happens to be fixed. Converted. Production now reports zero datetime columns.
  • G3 — scheduling tools under Analytics. Vendor calendar and blocked dates moved into the vendor panel beside pricing and gallery. Boost stayed in Analytics, beside the numbers that justify buying it.
  • G6 — weak recommendations. Not a sparse-data artifact: candidates were drawn from every visible vendor, and 7 of the 15 in production sit in the home and cars domains, so a cleaning company was a legitimate suggestion for a wedding. Now scoped to the events domain, with the heuristic ranking the categories that suit the event type first. Verified live — a wedding returns venues → catering → photography → decor → flowers → entertainment, and no home-services vendor at all.
  • G7 — unaudited settings. Settings decide commission, deposit, payout hold and whether registrations are open, and changed with no record of who moved them. Now audited with before/after values; an identical resubmit is correctly not recorded as a change.

7. Fixed this session

15 commits. The ones that mattered:

Would have blocked real users

  • Saved addresses never loaded. Address.fromJson cast latitude with as num?; MySQL decimals serialize as strings, so it threw on every address. The list showed "no saved addresses" forever, checkout requires an address, so no direct booking could be completed by anyone. The throw also happened after a successful POST and wasn't an ApiException, so it surfaced as "could not reach the server" — every retry silently created a duplicate.
  • Event date and guest count did not exist in the mobile app, though the API stored both and the planner derives every checklist deadline from the date.
  • Event bookings were unreachable. EventBookingsScreen was referenced only when super_app_enabled is false; production has it true, so an accepted quote produced a booking with a deposit owing and no list view to reach it.
  • Availability slots were three hours off for Kuwait, hiding bookable evening slots and offering ones already gone.
  • Service ratings and booking counts were never computed. Three columns shipped defaulted to 0 with nothing writing them, while catalog discovery sorted on exactly those columns.

Security

  • Session token moved from a plaintext file in the cache directory to the Android Keystore / iOS Keychain, with a one-time migration. Verified on device: nothing token-shaped remains in app storage.
  • allowBackup disabled with explicit data-extraction rules.
  • Catalog no longer lists direct-booking services that have no package.

Localisation

  • All 25+ notification types were English-only regardless of locale. Titles now resolve per-reader, retroactively for rows already stored.
  • Vendor pricing advice was English-only; the competitor line uses trans_choice because Arabic inflects at 1, 2, 3–10 and 11+.
  • The network-error fallback behind ~20 screens was a hardcoded English string.

UX

  • Sign-up rebuilt (it self-confirmed the password, so a typo created an unusable account, and had no invite-code field — referrals were dead on mobile).
  • Pending vendors now see their application status, and rejected ones see the reviewer's notes.
  • Unrated listings say "جديد / New" rather than "0.0 ★".

8. Roadmap

Now — close the launch blockers

  1. Live payment merchant account; set PAYMENT_DRIVER, live keys, webhook secret; enable ONLINE_PAYMENT in the mobile build (B1).
  2. Wipe load-test and audit data from production (B2).
  3. Vendor photography and brand artwork, including a correct adaptive-icon foreground (B3, G4).
  4. Decide the pre-review default sort (B4).

Next — first real supply and demand

  1. Onboard and verify the first real vendors; the approval queue and status banner are ready.
  2. WhatsApp Business API (G2) — the highest-leverage channel in this market.
  3. Re-check recommendation quality once the catalogue is real. The scoping bug is fixed; what remains is whether the affinity ordering matches how Kuwaiti customers actually shop.

Then — scale

  1. Android to Play, which forces real release signing (G5).
  2. Vendor subscription billing, once payments are live.
  3. Escrow milestones — the code path exists and is tested, but is unexercised without real payments.

9. How to re-run this audit

# deployed version + container health
ssh root@89.167.17.109 'cd /opt/yam3at && git rev-parse --short HEAD && \
  docker compose -f docker-compose.prod.yml ps'

# data snapshot
ssh root@89.167.17.109 'cd /opt/yam3at && docker compose -f docker-compose.prod.yml \
  exec -T api php artisan tinker --execute="echo DB::table(\"users\")->count();"'

# test suite
cd apps/api && php artisan test

# mobile, against production
cd apps/mobile && flutter build apk --debug \
  --dart-define=API_URL=https://api.yam3at.com/api/v1

Deployment procedure and rollback: DEPLOYMENT.md and operations/runbook.md.

Caution when scripting against production from Windows. Passing Arabic through a Git Bash curl -d string transcodes it to the ANSI codepage and stores ????. Write the JSON to a file with explicit UTF-8 and use curl --data-binary @file. Two records were corrupted this way during the audit and had to be repaired.