A full two-sided booking marketplace (Airbnb / Gathern-style) for short-term rentals in Saudi Arabia — guest and host web app, a native Flutter iOS app sharing one JSON API, and a full guest/host mode switch.
Architecture sketch
Swipe sideways to see the whole sketch →
nuzul.sa
nuzul.sa/search
nuzul.sa/property
Guest — browse & book
Host — manage properties
Web appLaravel, Inertia.js, React 19 + TS, Tailwind v4
Mobile appNative Flutter (iOS), shared JSON API
Booking engineRace-safe date holds, auto refunds, post-stay reviews
The payment callback and the webhook can land at the same moment — the row lock lets exactly one of them confirm the booking. · app/Actions/ConfirmBooking.php
DB::transaction(function () use ($booking, $payment, $valid): void {
// Serialize the callback and the webhook: both can arrive at once.
$fresh = Booking::query()->whereKey($booking->getKey())->lockForUpdate()->first();
if ($fresh === null || $fresh->status !== BookingStatus::PendingPayment) {
return; // another request got here first
}
$this->record($booking, $payment, $valid ? PaymentStatus::Paid : PaymentStatus::Failed);
// …
});
بنيت بمفردي منصة حجز عقارات ثنائية الاتجاه (على غرار Airbnb أو Gathern) للإيجار قصير المدى في السعودية — تطبيق ويب للضيوف والمضيفين، وتطبيق جوال أصلي بـ Flutter لنظام iOS يشارك واجهة برمجية (API) واحدة مع تطبيق الويب، مع إمكانية التبديل الكامل بين وضع الضيف ووضع المضيف.
مخطط البنية
← اسحب أفقياً لرؤية المخطط كاملاً
nuzul.sa
nuzul.sa/search
nuzul.sa/property
ضيف — تصفّح واحجز
مضيف — إدارة العقارات
تطبيق الويبLaravel وInertia.js وReact 19 مع TypeScript وTailwind v4
تطبيق الجوالFlutter أصلي لنظام iOS، بنفس الـ API
محرك الحجوزاتحجز آمن للتواريخ، استرداد تلقائي، تقييمات بعد الإقامة
الدفعMoyasar، تأكيد عبر Webhook، معالجة آمنة تمنع التكرار
المحادثاتمحادثات فورية ضيف/مضيف، دعم منفصل، خصوصية صارمة
رد بوابة الدفع والـ Webhook قد يصلان في اللحظة نفسها — قفل الصف يسمح لواحد فقط منهما بتأكيد الحجز. · app/Actions/ConfirmBooking.php
DB::transaction(function () use ($booking, $payment, $valid): void {
// Serialize the callback and the webhook: both can arrive at once.
$fresh = Booking::query()->whereKey($booking->getKey())->lockForUpdate()->first();
if ($fresh === null || $fresh->status !== BookingStatus::PendingPayment) {
return; // another request got here first
}
$this->record($booking, $payment, $valid ? PaymentStatus::Paid : PaymentStatus::Failed);
// …
});