Motoshub · Technical Assessment & Product Blueprint

موتوشاب: وضعیت فعلی، معماری هدف و نقشه‌راه محصول

گزارشی که بعد از خواندن کامل سورس هر دو مخزن (پلتفرم PHP/Oxwall و پروتوتایپ React) نوشته شده — از «چه چیزی امروز آماده است» تا «چطور این را به یک موتور SaaS چندمشتری تبدیل کنیم».

پلتفرم: Oxwall fork · v2.0.0 · build 11233 Runtime: PHP 7.4 · MariaDB 10.5 Gateway: iismobilesupport (~168 API مستند) Frontend: React 19 · Vite 8 (mock) تاریخ: ۱۴۰۵/۰۴/۲۶ — 2026-07-17

01 وضعیت فعلی در یک نگاه

دو مخزن مستقل، دو دنیای جدا که هنوز به هم وصل نشده‌اند.

motoshub-web — محصول واقعی

یک فورک Oxwall با ~۱۴۱ پلاگین (اکثراً سفارشیِ iis*). کل سطح API در یک پلاگین به‌نام iismobilesupport جمع شده که همان «Gateway» است. روی گیت‌لب داخلی، جریان MR-محور با ~۲۵۰ برنچ.

API اجتماعی آماده بدون tenant/license

motoshub-prototype — نمای محصول

React 19 + Vite 8، روی demo.shub.ir دیپلوی می‌شود. همه‌ی داده‌ها mock است؛ تنها نقطه‌ی اتصال واقعی، فایل src/lib/api.ts است که هنوز پیاده نشده. ماژول‌های سازمانی (پروژه/قرارداد/صندوق) فقط اینجا وجود دارند.

داکس /docs آماده به API واقعی وصل نیست

گزاره‌ی مرکزی: بک‌اندِ «شبکه‌ی اجتماعی» (کاربر، گروه، فید، چت، رسانه…) کامل و به‌صورت API آماده است. اما ماژول‌های سازمانی‌ای که پروتوتایپ نشان می‌دهد — پروژه، قرارداد، صندوق، فرصت پژوهشی، گزارش‌ساز — هیچ API بک‌اندی ندارند و باید از صفر (روی Django) ساخته شوند. شکاف اصلی محصول دقیقاً همین‌جاست.

02 چه چیزی از موتوشاب فعلی API دارد؟

استخراج‌شده از dispatcher واقعی (web_service_general.php) و انوتیشن‌های @OA.

دامنهوضعیتتعداد opتوضیح
Auth / Sessionآماده~12login/logout/join، فراموشی رمز، کد تأیید، CAPTCHA، کد ملی
Users / Profileآماده42پروفایل، ویرایش، آواتار/کاور، follow/block، sessionها
Groups (+files)آماده28CRUD گروه، عضویت، دعوت، مدیران، فایل/پوشه‌ی گروه
Messaging / Chatآماده13پیام، فوروارد، mute، رسانه‌ی چت، جستجو
Newsfeedآماده9داشبورد، پست، لایک، فوروارد، privacy
Events / Forum / Blogs / Newsآماده35CRUD کامل هر چهار دامنه
Media (photo/video)آماده11آلبوم، عکس، ویدیو
Comments / Friends / Notifications / Privacy / Searchآماده~18سطح پایه‌ی اجتماعی
آپلود فایل (upload_single_file)آماده1چندنوعی، با اسکن آنتی‌ویروس ClamAV
iispors (پرسمان‌ساز/کوییز)فعال، بدون داکس~82در dispatcher هست ولی صفر انوتیشن @OA
iispors_judgement (داوری)فعال، بدون داکس~0 docCRUD داوری، معیار، امتیازدهی
iislandingcreator (لندینگ)فعال، بدون داکس~10ساخت صفحه/سکشن
پروژه / قرارداد / صندوق / پژوهش / گزارشAPI ندارد0ماژول‌های سازمانی — فقط در پروتوتایپ mock هستند
iisgrant، iiscfp، iiscompetition، iisticketing…بیرون از Gateway0پلاگین‌های دامنه‌ای که به لایه‌ی API وصل نشده‌اند
سه عدد که باید یکی شوند: کد واقعی ۱۶۸ عملیاتِ انوتیشن‌دار دارد؛ فایل openapi.json در پروتوتایپ ۱۵۶ عملیات دارد (کمی قدیمی)؛ و رجیستری دستی مدعی ۲۷۱ عملیاتِ «قابل‌فراخوانی» است (شامل iispors/لندینگ بدون schema). قدم اول داکس: بازتولید openapi.json از سورس فعلی تا این اعداد هم‌تراز شوند و tagها اضافه شوند (الان خالی‌اند و Swagger UI دسته‌بندی ندارد).

03 معماری فعلی

یک مونولیت Oxwall به‌ازای هر مشتری؛ همه‌چیز روی یک دیسک، یک دیتابیس.

flowchart TB
  M["Mobile apps
iOS / Android (FCM)"]:::client W["Web browsers"]:::client M --> IMS W --> IMS subgraph OX["Oxwall PHP 7.4 monolith — ONE per customer"] IMS["iismobilesupport
= API gateway
mobile/services/{information,action}/:type"]:::acc PL["~141 iis* plugins
(social + domain + SMS + SSO)"] CORE["Oxwall core
ow_base_config (settings)"] IMS --> PL --> CORE end CORE --> DB[("MariaDB 10.5
ow_* tables")]:::store CORE --> FS[("ow_userfiles/
local disk, per-plugin")]:::store classDef client fill:#eef1f4,stroke:#9aa6b4,color:#1b2431; classDef acc fill:#b45e28,stroke:#8a4620,color:#fff; classDef store fill:#e3f3ec,stroke:#2e9e6b,color:#14432f;

Fig 1 — امروز: هر مشتری = یک کپی کامل از کد + یک دیتابیس + یک پوشه‌ی فایل، به‌صورت دستی نصب می‌شود.

04 معماری هدف — «موتور» چندمشتری

یک frontend، یک Gateway، و دو بک‌اند (قدیمی PHP + جدید Django) پشت یک هویت واحد.

flowchart TB
  FE["New React frontend
(from prototype)"]:::acc FE --> GW["API Gateway / BFF
یک host، JWT auth واحد"]:::acc IDP["Identity Provider
Keycloak / OIDC"]:::idp IDP -. validate JWT .- GW GW --> BR{"Router / Bridge"} BR -->|"social: users, groups,
feed, chat, media, forum"| LEG["Legacy Oxwall API
iismobilesupport"] BR -->|"projects, contracts, funds,
research, reports,
settings, storage"| DJ["Django + DRF
services (new)"]:::new LEG --> LDB[("tenant MariaDB")]:::store DJ --> PGDB[("tenant PostgreSQL")]:::store DJ --> OBJ[("Object storage
MinIO / S3")]:::store classDef acc fill:#b45e28,stroke:#8a4620,color:#fff; classDef new fill:#2e9e6b,stroke:#1f6f4b,color:#fff; classDef idp fill:#3e6cb0,stroke:#2c4d80,color:#fff; classDef store fill:#e3f3ec,stroke:#2e9e6b,color:#14432f;

Fig 2 — هدف: Gateway تصمیم می‌گیرد هر درخواست به دنیای قدیمی برود یا جدید؛ فرانت فقط یک API می‌بیند.

چرا Gateway/BFF؟

تا فرانت هیچ‌وقت نفهمد کدام قابلیت از PHP می‌آید و کدام از Django. این یعنی می‌توانی فیچرها را تک‌به‌تک از PHP به Django مهاجرت دهی بدون اینکه فرانت عوض شود — دقیقاً همان استراتژی «Strangler Fig».

چرا Django برای موارد جدید؟

دامنه‌های سازمانی (قرارداد، صندوق…) state-machine و approval-flow سنگین دارند. Django/DRF + مدل چندمشتری، RBAC و Celery برای این‌ها بسیار مناسب‌تر از افزودن پلاگین به Oxwall PHP 7.4 است.

05 اتصال Front Gateway Back و جایگاه Git

پاسخ مستقیم به سؤالت: فرانت چطور به بک وصل می‌شود، bridge چیست، git کجاست.

flowchart LR
  subgraph SRC["Source control"]
    GH["GitHub
frontend + docs
demo.shub.ir"]:::git GL["GitLab (internal)
motoshub-web
+ Django (new repo)"]:::git end GH -->|"GitHub Actions"| FEBUILD["Frontend build
(static SPA)"] GL -->|"GitLab CI"| IMG["Versioned images
+ registry"] FEBUILD --> RUNFE["Frontend (served)"] RUNFE -->|"HTTPS /api/*"| GW["API Gateway"] GW -->|"REST"| DJ["Django services"] GW -->|"HTTP form-POST
access_token bridge"| LEG["Oxwall iismobilesupport"] IMG --> DJ IMG --> LEG classDef git fill:#3e6cb0,stroke:#2c4d80,color:#fff;

Fig 3 — Git دو جا زندگی می‌کند: فرانت روی GitHub، بک‌اندها روی GitLab. CI هر کدام artifact نسخه‌دار می‌سازد.

Bridge دقیقاً چیست؟

یک آداپتور کوچک داخل Gateway که درخواست REST مدرن فرانت را به قرارداد قدیمی Oxwall ترجمه می‌کند: فرانت GET /api/social/feed می‌فرستد؛ bridge آن را به POST mobile/services/information/get_dashboard با فیلد access_token تبدیل می‌کند، JWT کاربر را به session/کوکی اکسوال map می‌کند، و جواب JSON را نرمال برمی‌گرداند. فرانت از وجود این ترجمه بی‌خبر است.

06 SSO و لاگین — پیشنهاد

همین حالا سه SSO در کد هست؛ باید یکی‌شان را «منبع حقیقت هویت» کنیم.

در سورس این‌ها را داری: iishajsso (OpenID Connect کامل، با کلاینت Jumbojett)، iissso (تیکتی/CAS با single-logout)، و iisgmailconnect (OAuth گوگل). یعنی زیرساخت OIDC از قبل وجود دارد.

پیشنهاد: یک Identity Provider مرکزی (Keycloak) در نظر گرفته شود تا همه‌ی محصول (فرانت، Gateway، Django، و حتی Oxwall) توکن JWT را از آن دریافت و اعتبارسنجی کنند. iishajsso که الان OIDC است، به‌عنوان یک realm/federation به Keycloak وصل می‌شود. این هم SSO واقعی بین دنیای قدیم و جدید می‌دهد، هم برای مدل چندمشتری (هر tenant یک realm) تمیز است.
sequenceDiagram
  participant U as User
  participant FE as Frontend
  participant GW as Gateway
  participant IDP as Keycloak (OIDC)
  participant OX as Oxwall API
  U->>FE: open app
  FE->>IDP: OIDC login
  IDP-->>FE: id_token + access (JWT)
  FE->>GW: request + Bearer JWT
  GW->>GW: validate JWT (per-tenant realm)
  GW->>OX: bridge  map to ow_login session
  OX-->>GW: data
  GW-->>FE: normalized JSON
  

Fig 4 — لاگین یک‌بار در Keycloak؛ همان توکن هم برای Django و هم (از طریق bridge) برای Oxwall کار می‌کند.

07 چندمشتری بودن و فروش فیچر

قلب مدل کسب‌وکار تو: یک تیم maintain، N مشتری، فروش فیچر به همه.

واقعیت سخت از سورس: هیچ مفهوم tenant/license/feature-flag در کد نیست. اکسوال ذاتاً single-tenant است (یک DB، یک config.php، یک پوشه‌ی فایل به‌ازای هر نصب). پس دو مسیر داری:

مدلایزولاسیونمزیتهزینه
Instance-per-tenant (پیشنهادی)کانتینر + DB مجزا برای هر مشتریایزولاسیون کامل، بکاپ/بیلینگ ساده، منطبق با واقعیت اکسوال، ریسک پاییناورکستریشن و منابع بیشتر
Shared-app + tenant columnیک اپ، ستون tenant در DBمصرف منابع کمترنیازمند بازنویسی عمیق اکسوال — ریسک بالا

توصیه: مدل Instance-per-tenant با یک Control Plane مرکزی. اکسوال را دست‌نخورده نگه می‌داری و multi-tenancy را در لایه‌ی اورکستریشن حل می‌کنی، نه داخل کد قدیمی.

flowchart TB
  GL["GitLab: یک codebase
یک تیم maintenance"]:::git --> REG["Image registry
نسخه‌های immutable (semver)"] CP["Control Plane
tenants · licenses · enabled features"]:::acc REG --> PROV["Provisioner
(scripted / IaC)"] CP --> PROV PROV --> T1["Tenant A
app + db + files + settings"]:::t PROV --> T2["Tenant B
features: {social, projects}"]:::t PROV --> T3["Tenant C
features: {social, funds, SSO}"]:::t classDef acc fill:#b45e28,stroke:#8a4620,color:#fff; classDef git fill:#3e6cb0,stroke:#2c4d80,color:#fff; classDef t fill:#e3f3ec,stroke:#2e9e6b,color:#14432f;

Fig 5 — فیچر جدید یک‌بار روی GitLab merge می‌شود یک image نسخه‌دار Control Plane تعیین می‌کند کدام مشتری کدام فیچر را (طبق لایسنسی که خریده) روشن دارد.

«فروش فیچر به مشتری دیگر» یعنی صرفاً یک feature-flag/entitlement در Control Plane روشن شود — نه یک دیپلوی جدید. برای این کار باید یک لایه‌ی سبک tenant + license + entitlement ساخته شود (در Django، به‌عنوان بخشی از فاز ۳).

08 تنظیمات پویا و مدیریت دیسک از داشبورد

دو خواسته‌ی مشخص تو — و اینکه در سورس کجا قلاب می‌شوند.

الف) متغیرهای سیستمی قابل‌مدیریت (upload size و…)

مشکل فعلی: سقف واقعی آپلود در php-fpm.conf/www.conf/nginx روی ۲۰M هاردکد است. راه‌حل حرفه‌ای:

  1. در سطح image، محدودیت php/nginx با حاشیه‌ی اطمینان کافی تنظیم شود (برای نمونه 512M) تا سقف سخت‌گیرانه نباشد.
  2. لیمیت مؤثر را به جدول ow_base_config (که از قبل زیرساخت admin-editable دارد) منتقل کن و در کد آپلود، به‌جای ini_get() این مقدار DB را بخوان.
  3. در پروتوتایپ، بخش SystemSection (که الان mock است) را به این API واقعی وصل کن: GET/PUT /api/core/settings. متغیرهایی که خودت طراحی کرده‌ای (upload_max_mb, chunk_size_mb, user_quota_gb, session_hours…) دقیقاً همین‌ها هستند.

ب) مدیریت مرتب فایل‌ها روی دیسک + داشبورد حجم

الان فایل‌ها زیر ow_userfiles/<plugin>/… ذخیره می‌شوند (لوکال، cloudfiles خاموش)، ولی هیچ quota/آمار/پاکسازی مرکزی نیست. برای داشبوردی که در پروتوتایپ (StorageSection) طراحی کرده‌ای:

09 نقشه‌راه قدم‌به‌قدم

پنج فاز؛ هر فاز یک خروجی قابل‌عرضه دارد. تخمین برای یک تیم کوچک بک‌اند.

فاز ۰
۲–۳ هفته

پایه و شفافیت

بازتولید openapi.json از سورس فعلی (۱۶۸ op + tagها) و کامل‌کردن داکس برای تیم فرانت. افزودن Sentry برای دیدن خطاهای ساکت (مثل باگ فایل‌منیجر). راه‌اندازی registry ایمیج + تگ semver در CI. دیسیپلین dev/staging/prod.

فاز ۱
۳–۵ هفته

Gateway + هویت واحد + اتصال فرانت به API اجتماعی

بالا آوردن API Gateway/BFF و Keycloak. پیاده‌سازی bridge به Oxwall. جایگزینی src/lib/api.ts پروتوتایپ با fetch واقعی. خروجی: فرانت واقعی روی داده‌ی زنده‌ی اجتماعی.

فاز ۲
۶–۱۰ هفته

سرویس Django برای دامنه‌های جدید

ساخت پروژه Django/DRF: هسته + Auth (JWT/Keycloak)، تنظیمات سیستم، سرویس فایل/دیسک، سپس ماژول‌های سازمانی به‌ترتیب: پروژه قرارداد صندوق پژوهش گزارش‌ساز. این‌ها همان‌هایی‌اند که هیچ API ندارند.

فاز ۳
۴–۶ هفته

Control Plane چندمشتری

مدل Tenant/License/Entitlement، اتوماسیون provisioning، داشبورد per-tenant تنظیمات و storage، و مکانیزم «فروش فیچر = روشن‌کردن flag».

فاز ۴
مستمر

Bridge اعلان‌ها + مهاجرت تدریجی

رویدادهای Django (تسک اساین شد، قسط سررسید شد) فید/اعلان اکسوال. سپس مهاجرت آرام باقی فیچرها از PHP به Django با الگوی Strangler، بدون تغییر فرانت.

10 لیست API برای تحویل به فرانت

دو گروه: آماده‌ها (مستندسازی/اتصال) و ساختنی‌ها (Django جدید).

گروه A — از قبل روی Gateway هست (فقط داکس + اتصال)

حوزهمسیر نمونه (قرارداد فعلی)اقدام
Auth/Users/Groups/Feed/Chat/Media/Forum/Blog/News/EventsPOST mobile/services/{information,action}/:typeمستندسازی کامل + wrapper REST در Gateway
iispors / iispors_judgement / landing(بدون @OA)افزودن انوتیشن + داکس

گروه B — باید ساخته شوند (Django، REST)

ماژولEndpointهای کلیدیاولویت
Core / SettingsGET/PUT /core/settings · /core/me۱
Files / StoragePOST /files (chunked) · GET /storage/stats · POST /storage/cleanup۲
Projects/projects · /projects/:id/tasks · /milestones · /board۳
Contracts/contracts · /:id/stages · /payments · /approvals۴
Funds/funds · /:id/reviews · /installments · /kpi۵
Research/research · /:id/applicants · /outputs۶
ReportsPOST /reports/aggregate · /reports/saved۷
Tenant / License/tenants · /licenses · /entitlements۸

11 تسک‌های بک‌اند به‌ترتیب + زمان پیشنهادی

تخمین «مهندسی خالص» برای یک بک‌اند سینیور؛ تست/ریویو جداست.

#تسکفازتخمین
1بازتولید openapi.json (۱۶۸ op) + tag + انتشار داکس۰۲–۳ روز
2Sentry + registry ایمیج + تگ semver در CI۰۳–۴ روز
3راه‌اندازی Gateway/BFF (اسکلت + routing)۱۴–۵ روز
4Keycloak + اعتبارسنجی JWT در Gateway۱۴–۶ روز
5Bridge اکسوال (map JWTow_login، نرمال‌سازی JSON)۱۵–۸ روز
6اتصال پروتوتایپ به API واقعی (جایگزینی api.ts)۱۵–۷ روز
7اسکلت Django/DRF + Auth + Tenant model پایه۲۵–۷ روز
8سرویس Settings (DB-backed) + وصل به SystemSection۲۳–۴ روز
9سرویس File/Storage + داشبورد حجم (StorageSection)۲۶–۹ روز
10ماژول Projects (board/task/milestone)۲۸–۱۲ روز
11ماژول Contracts (state-machine + approval)۲۱۰–۱۴ روز
12ماژول Funds + Research (روی الگوی قرارداد)۲۱۰–۱۴ روز
13گزارش‌ساز (aggregate + saved + خروجی)۲۶–۹ روز
14Control Plane: License/Entitlement + provisioning۳۱۰–۱۵ روز
15Notification bridge (Django events فید اکسوال)۴۵–۷ روز

جمع تقریبی تا پایان فاز ۳: ~۴ تا ۵ ماه کار مهندسی خالص برای یک نفر؛ با تیم دو-سه‌نفره و موازی‌سازی کمتر.

12 نسخه‌گذاری، بکاپ و مدیریت منابع

آنچه امروز شکسته است و فرایند حرفه‌ای پیشنهادی.

نسخه‌گذاری

الان: نسخه در ow_version.xml (2.0.0/build 11233) + تگ‌های گیت متروکه (۲۰۲۰) + CI بدون تگ. پراکنده و غیرقابل‌اتکا.

پیشنهاد: semver واقعی روی هر release، ایمیج immutable در registry به‌ازای هر نسخه، جریان staging prod با promotion، changelog خودکار. هر مشتری نسخه‌ی پین‌شده دارد؛ آپدیت = rollout کنترل‌شده‌ی همان image.

بکاپ

الان: فقط پلاگین iisdatabackup، بدون سیاست منظم.

پیشنهاد: به‌ازای هر tenant: dump زمان‌بندی‌شده‌ی DB + snapshot از ow_userfiles، ذخیره‌ی offsite (S3)، retention مشخص (مثلاً روزانه×۷ + هفتگی×۴)، و تست بازیابی دوره‌ای. با مهاجرت به object storage، فایل‌ها ذاتاً نسخه‌دار/replicated می‌شوند.

مدیریت منابع

در مدل instance-per-tenant، هر کانتینر limit CPU/RAM دارد (staging الان 1cpu/1G). یک لایه‌ی مانیتورینگ (Prometheus/Grafana) مصرف هر tenant را می‌بیند؛ همین داده مبنای بیلینگ maintenance و ظرفیت‌سنجی می‌شود. quota فایل هر مشتری از همان سرویس Storage اعمال می‌شود.

ریسک‌های فوری که در سورس دیدم و باید زود رفع شوند: pepper مشترک هاردکد (motoshub_salt_SK750) بین همه‌ی نصب‌ها؛ اجرای کانتینر staging با کاربر root؛ پیش‌فرض دیتای MySQL روی /tmp؛ و دو استک داکر واگرا (docker/Dockerfile در برابر docker/php/Dockerfile).

Motoshub Technical Blueprint · تهیه‌شده پس از مطالعه‌ی کامل سورس motoshub-web و motoshub-prototype · نسخه‌ی زنده و قابل‌ویرایش