العودة للأعمال

سفرة — منصة حجز السائقين في جورجيا

منصة ثلاثية الأطراف بثلاث لغات تجعل حجز سائق محلي موثوق في جورجيا بسهولة حجز فندق.

Full-StackFrontendBackendMobileDevOps
العميل
سفرة
الدور
مهندس ومعماري النظام (Full-Stack)
المجال
السفر والسياحة · منصات وساطة
المدة
٥ أشهر · أبريل–سبتمبر ٢٠٢٦
الخدمات
تصميم المنتج وبنية النظام · تطوير متكامل (Full-Stack) · تطوير تطبيقات الموبايل · تصميم قاعدة البيانات والأمان · نظام التصميم وواجهات المستخدم · التوطين ودعم الاتجاه من اليمين إلى اليسار · DevOps وهندسة التكلفة
الأدوات
Next.js 16 (App Router) · React 19 · TypeScript · Supabase (Auth · Postgres · Realtime · Storage) · PostgreSQL + Row Level Security · Drizzle ORM · TanStack Query v5 · Tailwind CSS 4 · shadcn/ui · next-intl · Payload CMS v3 · Flutter / Dart · PayMob · Resend · Twilio · Google Cloud Translate · Vercel · GitHub Actions
Safrah home page — search for a trusted local driver in Georgia
٦٦جدول في Postgres تحكمها ~٢٣٢ سياسة أمان على مستوى الصف
٣٤٩نقطة REST — واحدة لكل دالة خادم، للويب والموبايل
٣لغات بـ ٦٧١٩ مفتاحًا لكل منها، دون أي ترجمة ناقصة
١١٥شاشة تطبيق موزعة على خمس بوابات حسب الدور
~٥٩٢ ألفسطر برمجي بين تطبيق الويب وتطبيق Flutter
٦٧×تجاوز في استهلاك البيانات جرى تشخيصه وتفسيره وخفضه هندسيًا
زيارة الموقع

سفرة منصة لحجز السائقين للسياحة في جورجيا. يبحث المسافرون — ومعظمهم قادم من الخليج — عن سائقين محليين موثّقين حسب المطار والتواريخ ونوع المركبة واللغات، يتفقون على السعر في المحادثة، ثم يحجزون. يدفعون عربونًا صغيرًا بالبطاقة ويسوّون الباقي نقدًا مع السائق يومًا بيوم، وهي الطريقة التي يعمل بها هذا السوق فعلًا.

وهو منتج ثلاثي الأطراف. العملاء يتصفحون ويحجزون؛ والسائقون يديرون تسجيلهم ومستنداتهم وتوفرهم وأرباحهم؛ والإدارة تعتمد السائقين وتفصل في النزاعات وتدير دفتر المحفظة. تعمل الأطراف الثلاثة بالعربية والإنجليزية والجورجية مع دعم كامل للاتجاه من اليمين إلى اليسار — وتخدمها جميعًا واجهة برمجية واحدة يستهلكها تطبيق Flutter أصلي إلى جانب عميل الويب.

قدت تصميم البنية وبنيتها على مدى خمسة أشهر: تطبيق ويب بـ Next.js 16 فوق Supabase Postgres حيث الأمان على مستوى الصف هو الحد الأمني، ونظام Payload لإدارة المحتوى التسويقي، وعميل Flutter لنظامي iOS وأندرويد.

أبرز المزايا

  • سوق للسائقين مع فلاتر للمطار وتواريخ الرحلة ونوع المركبة واللغات والسعر — إلى جانب التقييمات وعدد الرحلات ونبذات السائقين المترجمة تلقائيًا
  • آلة حالة للحجز تغطي الطلب ← عرض السعر ← التأكيد ← لقاء يوم الرحلة ← التسوية، مع عربون بالبطاقة مقدمًا والباقي نقدًا للسائق يومًا بيوم
  • محادثة لحظية بين العميل والسائق مع ترجمة فورية للرسائل، ليتحدث المسافر العربي والسائق الجورجي دون لغة مشتركة
  • محفظة السائق — دفتر حسابات كامل يشمل الغرامات والعروض الترويجية وتسوية المستحقات
  • تأمين سفر اختياري يُباع داخل مسار الحجز، مع إصدار وثائق PDF تلقائيًا
  • لوحة إدارة خلفية: ٤٣ شاشة لاعتماد السائقين والمستندات والحجوزات والنزاعات والمحفظة وسجل التدقيق
  • بوابة خدمة ذاتية للسائق: التسجيل ورفع المستندات وتقويم التوفر وتعديل الأسعار والأرباح
  • منطقة العميل: الحجوزات والمفضلة وعمليات البحث المحفوظة وخطط الرحلات والفواتير
  • ثلاث لغات — العربية والإنجليزية والجورجية — مع دعم كامل للاتجاه من اليمين إلى اليسار
  • تطبيق Flutter أصلي لنظامي iOS وأندرويد يشارك الويب نفس واجهة REST
  • Payload CMS يدير صفحات التسويق والفنادق والبرامج السياحية والمدن والأسئلة الشائعة

المشكلة

التنقل في جورجيا كعائلة زائرة أصعب مما ينبغي. المواصلات العامة لا تصل حيث يريد السياح، وتطبيقات النقل تتلاشى سريعًا خارج تبليسي، والسوق غير الرسمي — سائق يرشّحه ابن عم أحدهم عبر واتساب — بلا أسعار قابلة للمقارنة ولا مساءلة ولا لغة مشتركة. وفي المقابل هناك عرض حقيقي من سائقين محليين محترفين بلا وسيلة للوصول إلى المسافرين الباحثين عنهم تحديدًا.

وُجدت سفرة لتجعل هذا السوق مقروءًا: سائقون موثّقون، وأسعار يومية شفافة، وتقييمات يمكن قراءتها، وحجز يمكن محاسبة أحد عليه.

البنية

تطبيق الويب مبني بـ Next.js 16 على App Router مع React 19. ويوفر Supabase أربعة أشياء من مزوّد واحد — Postgres والمصادقة واللحظية والتخزين — والقرار الذي يشكّل كل ما عداه هو أن الأمان على مستوى الصف داخل Postgres هو الحد الأمني، لا شيفرة التطبيق. تعمل استعلامات الميزات عبر Drizzle داخل غلاف يبدّل الاتصال إلى دور `authenticated` أو `anon` ويحقن مطالبات JWT لكل معاملة، فيصبح التحقق من الملكية خاصية في قاعدة البيانات لا أمرًا على المطور تذكّره.

الشيفرة مقسّمة إلى شرائح رأسية. التوجيه في `app/`، ومنطق الأعمال في شرائح تحت `features/`، وواجهات المستخدم العامة في `components/`، والبنية التقنية في `lib/`. ويُمنع استيراد الشرائح من بعضها؛ وحين تحتاج شريحتان الشيء نفسه يُرفع إلى طبقة أعلى بدل مشاركته جانبيًا. وهناك قائمة قصيرة من الشرائح العابرة المعتمدة صراحةً — يسجّل كل مدخل فيها تاريخ إضافته وسببها، لأن قاعدة معمارية تتآكل بصمت أسوأ من قاعدة لم تُكتب أصلًا.

  • ٢١ شريحة ميزات رئيسية، و٩٧ مجلدًا عند احتساب الشرائح الفرعية
  • ١١٥ مسار صفحة عبر خمس بوابات حسب الدور: العام والمصادقة والعميل والسائق والإدارة
  • ٣٤٩ معالج مسار REST — الواجهة الرسمية للويب والموبايل معًا
  • ٦٦ جدولًا في Postgres و٥٢ نوعًا معدودًا، تحكمها نحو ٢٣٢ سياسة أمان على مستوى الصف
  • ٢٢١ ترحيل SQL، وهي المصدر الوحيد للحقيقة في المخطط والسياسات والمشغّلات

واجهة واحدة، عميلان

تطبيق Flutter ليس غلافًا حول الموقع — بل عميل أصلي يخاطب واجهة REST نفسها. وهذا مفروض بقاعدة لا بحسن النية: كل دالة خادم يجب أن تكون قابلة للوصول عبر معالج مسار HTTP، والميزة لا تكتمل قبل وجود تلك النقطة. تبقى Server Actions لسهولة الويب، لكن المنطق المشترك يعيش في وحدات خدمة تستقبل مقبض قاعدة البيانات كوسيط، فيستدعي كلٌّ من الإجراء والمعالج الدالة ذاتها بمقبض مبني من كوكي أو من رمز Bearer على التوالي.

والنتيجة أن الويب والموبايل لا يمكن أن يتباعدا. يحمل عميل الموبايل نحو ١٠٠ ألف سطر Dart مكتوبة يدويًا عبر ٢٢ شريحة، وفيه تعيش مجموعة الاختبارات الآلية للمشروع كاملة — ٨٨ ملف اختبار بـ ٧٦٣ حالة و١٥٢٥ تأكيدًا، يشغّلها التكامل المستمر مع كل دفعة. أما في الويب فيقوم بهذا الدور ٢٤ سكربت حراسة مخصصًا، يعمل ٢٢ منها في التكامل المستمر إلى جانب الفحص والأنواع والبناء، مؤكدةً ما لا يستطيع نظام الأنواع تأكيده: أن صلاحيات قاعدة البيانات مطابقة للمتوقع، وأن لا جدول شُحن بلا أمان على مستوى الصف، وأن مجمّع الاتصالات مضبوط بشكل صحيح.

ثلاث لغات بلا ثغرات

العربية والإنجليزية والجورجية كلها من الدرجة الأولى. تحمل كل حزمة لغة ٦٧١٩ مفتاحًا بالضبط، وفرق مسارات المفاتيح بين الإنجليزية وأي لغة أخرى يساوي صفرًا — فلا مسار احتياطي يرى فيه المستخدم مفتاحًا خامًا. وتقود العربية اتجاهًا كاملًا من اليمين إلى اليسار، منفّذًا بخصائص منطقية بدل نسخة أنماط معكوسة، فيصير الاتجاه خاصية تخطيط لا تصميمًا ثانيًا يجب صيانته. وتغلق المحادثة الفجوة المتبقية وقت التشغيل: تُترجم الرسائل بين مسافر عربي وسائق جورجي تلقائيًا فور وصولها.

الكلفة بوصفها مسألة هندسية

بعد شهرين تحولت فاتورة البنية التحتية إلى تمرين في التشخيص، وتبيّن أن نصفيها معماريان لا مسألة ضبط إعدادات.

كانت قاعدة البيانات ترسل ٣٥٤٫٩٤ جيجابايت شهريًا لـ ٧٦٣ مستخدمًا نشطًا — نحو ٤٦٥ ميجابايت لكل مستخدم مقابل قاعدة حجمها ١٥٤ ميجابايت، أي أن البيانات كلها تغادر المنصة ٦٧ مرة تقريبًا. والسبب أن الصور كانت في مخزن خاص خلف روابط يعاد توقيعها كل ساعة، فكان كل طلب إخفاقًا في التخزين المؤقت بحكم التصميم. نقلها إلى مخزن عام وإنشاء نسخ WebP بعرض ٨٠٠ بكسل أصغر بنحو ٩ أضعاف مع تعبئة رجعية للمكتبة عالج معظم المشكلة؛ وأتمّ الباقي تقليصُ حجم الصفوف ودمجُ عمليات الاستطلاع.

وبشكل منفصل، كشف بيان البناء عن ٥ مسارات ثابتة فقط من أصل ١٢٦ — كانت كل صفحة تُعرض في المصدر، فتنفخ أربعة عدّادات فوترة دفعة واحدة. والمسؤول ثلاثة استدعاءات مستقلة لـ `headers()` يتساهل معها البناء بصمت. وأكثرها إفادةً صفحة «غير موجود» الجذرية وهي تقرأ الترويسات لتحديد اللغة: يُدرج Next هذا الحد في شجرة عرض كل مسار، فألغى استدعاءٌ واحد في ملف لا يفكر فيه أحد تحسينَ الموقع بأكمله.

لم تكن أي من المشكلتين ظاهرة من سلوك التطبيق. اكتُشفت كلتاهما بقراءة فاتورة سطرًا سطرًا والسؤال عمّا يجب أن يكون صحيحًا فيزيائيًا كي يظهر ذلك الرقم.

التحديات والحلول

التحدي

بلغ استهلاك البيانات الصادرة من Supabase ٣٥٤٫٩٤ جيجابايت في شهر واحد لـ ٧٦٣ مستخدمًا نشطًا فقط — أي نحو ٤٦٥ ميجابايت لكل مستخدم مقابل قاعدة بيانات حجمها ١٥٤ ميجابايت. كانت البيانات كلها تغادر المنصة ٦٧ مرة تقريبًا، والتجاوز مرشح للتضاعف مع كل مستخدم جديد.

الحل

تتبّعت السبب إلى أمرين: روابط صور في مخزن خاص يُعاد توقيعها كل ساعة (فلا يمكن تخزينها في شبكة التوصيل أبدًا)، وقراءات صفوف بلا حدود. نقلت صور السائقين والأماكن إلى مخزن عام قابل للتخزين المؤقت، وأنشأت لكل أصل نسخة WebP بعرض ٨٠٠ بكسل أصغر بنحو ٩ أضعاف مع تعبئة رجعية للمكتبة كاملة، ثم قلّصت حجم الصفوف وعمليات الاستطلاع. انخفض المعدل من ~١١٫٨ جيجابايت يوميًا نحو هدف ٨ جيجابايت — كما أغلقت مراجعةٌ أمنية أُجريت في الطريق ثغرة تعداد للزوار المجهولين لم تكن في الخطة أصلًا.

التحدي

أظهرت فاتورة الاستضافة أن لا شيء تقريبًا قابل للتخزين في شبكة التوصيل: ٥ مسارات ثابتة فقط من أصل ١٢٦ مسارًا. أربعة عدّادات مختلفة — معالجة البناء والذاكرة المخصصة والمعالجة النشطة ونقل البيانات من المصدر — كانت كلها منتفخة بالسبب الجذري نفسه، دون أي تحذير من عملية البناء.

الحل

ثلاثة استدعاءات مستقلة لـ `headers()` يتساهل معها Next.js بصمت، وكلٌّ منها كافٍ وحده لإلغاء التحسين. أسوأها صفحة «غير موجود» الجذرية وهي تستشعر اللغة: يُدرج Next هذا الحد في شجرة عرض كل مسار، فأفسد استدعاء واحد الموقع بأكمله. استبدلتها بهيكل ثابت مع إعادة توجيه من العميل، ومرّرت اللغة كخاصية إلزامية إلى الترويسة والتذييل بدل استخلاصها من ذاكرة الطلب الضمنية في next-intl.

التحدي

عشر شارات في الشريط الجانبي للإدارة، كل واحدة تستطلع نقطة نهاية خاصة بها كل ١٢٠ ثانية. ولأن كل واحدة نقطة مستقلة كانت طلبًا مستقلًا، فلم تستطع ذاكرة المصادقة توحيدها — بكلفة تسع رحلات مصادقة إضافية كل دقيقتين لكل تبويب إدارة مفتوح.

الحل

كان دمجها يتطلب مفتاح استعلام واحدًا تتشاركه شارات تعيش في ثماني شرائح مختلفة — وهي شرائح تمنع البنية استيرادها من بعضها. وبدل كسر القاعدة بهدوء، رقّيت الخطاف إلى شريحة عابرة معتمدة تحتوي الأنواع والخطافات ومساعدات التخزين فقط، ونقلت التجميع من جهة الخادم إلى معالج مسار: الطبقة الوحيدة المخوّلة بالاستيراد من عدة شرائح. نجت القاعدة من الاستثناء، وهو موثّق في ملف البنية.

التحدي

كان على المنتج أن يخدم تطبيق Flutter إلى جانب واجهة الويب، لكن Server Actions في Next.js داخلية لمسار العرض في React. أي دالة توجد كـ Server Action فقط تكون غير مرئية لعميل موبايل أو أداة سطر أوامر أو أي تكامل خارجي.

الحل

جعلت وجود معالج مسار إلزاميًا لكل دالة خادم — فالميزة لا تكتمل قبل وجود نقطتها. يعيش المنطق المشترك في وحدات خدمة تستقبل مقبض قاعدة البيانات كوسيط؛ ويبني كلٌّ من Server Action ومعالج المسار ذلك المقبض بنفسه (مسار الكوكيز للويب، ورمز Bearer للموبايل) ثم يستدعي الدالة ذاتها. يتصرف الويب والموبايل بشكل متطابق بحكم التصميم، عبر ٣٤٩ نقطة نهاية.

التحدي

مع اعتماد الأمان على مستوى الصف كحد أمني، كان استعلام واحد عبر اتصال Postgres الخطأ كفيلًا بإعادة كل الصفوف لكل المستخدمين بصمت — فرابط الاتصال الخام يصادق كمالك الجداول، متجاوزًا الأمان تمامًا. والأسوأ أن صلاحيات GRANT لا تظهر في نسخ المخطط، فيولد أي جدول جديد بالإعدادات المتساهلة الافتراضية دون أن يكتشفه شيء.

الحل

يمر كل استعلام عبر غلاف يبدّل الدور إلى `authenticated` أو `anon` ويحقن مطالبات JWT لكل معاملة — وهو نفس نمط PostgREST. ويعيش مقبض الإدارة المتجاوز للأمان في وحدة `server-only` منفصلة، فيفشل البناء إن استوردها مكوّن عميل. ولأن الصلاحيات لا تُلتقط في النسخ، يتحقق فحص في التكامل المستمر من حالتها المتوقعة عند كل طلب دمج.

التحدي

ثلاث لغات — العربية والإنجليزية والجورجية — تحتاج العربية فيها اتجاهًا كاملًا من اليمين إلى اليسار، وليس للجورجية جذر مشترك مع أي منهما. أي تغطية جزئية كانت ستترك مستخدمين حقيقيين أمام مفاتيح ترجمة خام في صفحة دفعوا مقابل استخدامها.

الحل

يعيش كل نص في حزم اللغات، وتحمل كل لغة المفاتيح الـ ٦٧١٩ كاملة: فرق مسارات المفاتيح بين الإنجليزية وكل لغة أخرى يساوي صفرًا. وتستخدم المسافات الاتجاهية خصائص منطقية بدل يمين/يسار، فيصير الاتجاه خاصية تخطيط لا نسخة منفصلة من الأنماط، وتحمل حزمة الموبايل العربية فئات الجمع الإضافية التي تتطلبها قواعد العربية.

الخلاصة

سفرة أكبر نظام أخذته من مستودع فارغ إلى الإنتاج على بنية من تصميمي: نحو ٥٩٢ ألف سطر بين تطبيق ويب وعميل موبايل أصلي، و١٩١٠ إسهامات على مدى خمسة أشهر، يخدم ثلاث لغات وثلاثة أدوار مستخدمين مختلفة.

والدرس الباقي لديّ منه أن القرارات التي تستحق العناء هي تلك المكلفة في التراجع عنها. وضعُ الحد الأمني في قاعدة البيانات بدل شيفرة التطبيق جعل كل ميزة لاحقة ترث تفويضها مجانًا. وجعلُ نقطة REST إلزامية لكل دالة خادم بدا عبئًا في الشهر الأول، وهو السبب الوحيد لإمكان إطلاق تطبيق موبايل أصلي على الواجهة نفسها في الشهر الرابع. وتدوينُ سبب منح كل استثناء معماري — بتاريخه وسببه — هو ما منع القواعد من الذوبان بصمت تحت ضغط المواعيد.

The marketplace: verified drivers filtered by airport, dates, vehicle type and spoken languages.
A driver profile — transparent per-day pricing, trip count, reviews and an auto-translated bio.
The same product in Arabic. Right-to-left is a layout property here, not a second stylesheet.
Search in Arabic — every one of the 6,719 message keys is present in all three locales.
City guides, driven by Payload CMS alongside hotels, programs and the FAQ.
Curated travel programs a customer can attach to a booking.
Optional travel insurance, sold in-flow with generated PDF policy documents.
The mobile web experience. A native Flutter client ships the same features against the same REST API.
Arabic on mobile, with full RTL.