TorbatYar/docs/architecture.md
Mortezakoohjani 800b0ba2c5 Deploy TorbatYar for torbatyar.ir with nginx multi-tenant routing.
Wire production domain, CORS for tenant subdomains, celery volume mounts, and nginx reverse proxy configs for apex, API, identity, auth, and wildcard tenants.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 21:43:33 +03:30

20 KiB
Raw Blame History

معماری کلان پلتفرم SuperApp SaaS

۱. اهداف

ساخت یک SuperApp SaaS چندمستأجری (Multi-tenant)، ماژولار، API-first و microservice-ready که ابتدا روی VPS و در آینده روی زیرساخت مقیاس‌پذیر اجرا شود.

فاز ۱ تنها Core Platform را می‌سازد؛ اما ساختار پروژه به‌گونه‌ای است که افزودن سرویس‌های بعدی بدون بازنویسی معماری ممکن باشد.

۲. سبک معماری

  • Service-oriented / Microservice-ready: هر قابلیت بزرگ یک سرویس مستقل است.
  • Database-per-service: هر سرویس فقط دیتابیس خودش را می‌شناسد.
  • API-first: همه تعامل‌ها از طریق API/Event تعریف‌شده انجام می‌شوند.
  • Multi-tenancy: همه جداول بیزینسی ستون tenant_id دارند.

سرویس‌های اصلی (فعلی و آینده)

Core Platform (فاز ۱)، Identity & Access، Subscription & Entitlement، Accounting، CRM، Ecommerce، Website Builder، Live Chat، AI Assistant، Smart Messenger، SMS Panel، Link Shortener، Notification، File Storage، Restaurant و ماژول‌های آینده Marketplace.

در فاز ۱ فقط Core Platform پیاده شده است. بقیه سرویس‌ها در پوشه backend/services/ به‌صورت placeholder با README مسئولیت‌ها موجودند.

۲.۱ Frontend & Backend Separation (اجباری)

قانون معماری (سخت‌گیرانه)

پروژه باید از معماری کاملاً جدا (decoupled) پیروی کند. Backend و Frontend دو اپلیکیشن کاملاً مستقل هستند و هرگز نباید با هم مخلوط شوند.

Backend

تمام کد منبع backend فقط داخل پوشه backend/ قرار می‌گیرد (Core Service و سرویس‌های آینده).

مسئولیت‌ها:

  • FastAPI
  • SQLAlchemy
  • Alembic
  • Business Logic
  • Authentication
  • Authorization
  • Database
  • Background Workers
  • APIs
  • Event Processing

Backend هرگز نباید شامل موارد زیر باشد:

  • React / Next.js
  • Tailwind
  • UI Components
  • صفحات HTML
  • Frontend assets

Frontend

Frontend یک اپلیکیشن Next.js کاملاً جدا در پوشه frontend/ است.

مسئولیت‌ها:

  • UI
  • Dashboard
  • Forms
  • Pages
  • Components
  • Layouts
  • State Management
  • API Client
  • Theme

Frontend هرگز نباید شامل موارد زیر باشد:

  • کد FastAPI
  • دسترسی مستقیم به دیتابیس
  • مدل‌های SQLAlchemy
  • Alembic
  • Business Logic
  • کوئری مستقیم دیتابیس

ارتباط (Communication)

Frontend فقط از طریق REST API نسخه‌دار (و در آینده WebSocket در صورت نیاز) با backend ارتباط برقرار می‌کند.

هیچ کد منبع مشترکی بین frontend و backend وجود ندارد، به‌جز:

  • قراردادهای API
  • اسکیماهای مشترک
  • SDKهای تولیدشده
  • تعاریف نوع (type definitions) مشترک

ساختار پوشه‌ها (اجباری)

superapp-platform/
├── backend/
│   ├── core-service/
│   ├── shared-lib/
│   └── services/
│
├── frontend/
│   ├── app/
│   ├── components/
│   ├── lib/
│   ├── hooks/
│   ├── styles/
│   └── public/
│
├── docs/
├── docker-compose.yml
├── .env.example
└── README.md

این جداسازی در کل پروژه اجباری است. فایل‌های frontend هرگز نباید به پوشه‌های backend منتقل شوند و بالعکس — حتی برای راحتی. هر قابلیت جدید باید این معماری را حفظ کند.

۳. تصمیم معماری دیتابیس

  • الگوی Database-per-service.
  • ارتباط مستقیم بین دیتابیس سرویس‌ها ممنوع است.
  • ارتباط بین سرویس‌ها فقط از طریق:
    • REST API
    • Webhook
    • Async Event
    • الگوی Outbox/Inbox
  • در فاز ۱ فقط دیتابیس core_platform_db ساخته می‌شود.
  • طراحی اولیه دیتابیس سرویس‌های آینده در database_schema.md آمده اما migration واقعی آن‌ها ساخته نشده است.

۴. Core Platform Service

مسئول مفاهیم مشترک و مرکزی پلتفرم:

  • مدیریت Tenant و Domain
  • مدیریت Plan / Feature / Subscription و بررسی دسترسی (Entitlement)
  • Service Registry و Module Registry
  • توکن‌های داخلی سرویس (Internal Service Tokens)
  • الگوی Outbox/Inbox و Audit Log

لایه‌بندی داخلی سرویس هسته

API (routers)  →  Services (business logic)  →  Repositories  →  Models (DB)
                         ↑
                     Schemas (Pydantic)
  • core/: زیرساخت (config, database, cache, security, logging)
  • middlewares/: تشخیص tenant
  • workers/: Celery و taskها

۵. Multi-tenancy و Tenant Resolution

ترتیب تشخیص tenant در middleware:

  1. هدر X-Tenant-ID
  2. هدر X-Tenant-Slug
  3. Subdomain (از روی Host و base_domain)
  4. Custom domain (از روی Host)

نتیجه در request.state.tenant_id و request.state.tenant_slug قرار می‌گیرد. برای endpointهای tenant-aware از dependency require_tenant استفاده می‌شود که در نبود tenant خطای tenant_not_resolved برمی‌گرداند.

۶. Entitlement (بررسی دسترسی قابلیت)

تابع مرکزی EntitlementService.check_feature_access(tenant_id, feature_key):

  1. ابتدا Redis cache بررسی می‌شود.
  2. در نبود cache، از دیتابیس محاسبه می‌شود.
  3. نتیجه با TTL در Redis ذخیره می‌گردد.

قواعد: اگر tenant غیرفعال باشد، اشتراک غیرفعال باشد، یا قابلیت در پلن نباشد → دسترسی false. دسترسی سفارشی (custom access) بالاترین اولویت را دارد.

۷. رویدادها (Outbox/Inbox)

  • رویدادهای خروجی ابتدا در جدول outbox_events و در همان تراکنش عملیات بیزینسی ذخیره می‌شوند (تضمین atomicity).
  • یک task دوره‌ای Celery (process_outbox_events) رویدادهای pending را پردازش و وضعیت آن‌ها را به‌روزرسانی می‌کند.
  • جدول inbox_events برای idempotency رویدادهای ورودی است.

۸. امنیت و SSO (فاز ۲ و ۳)

اصل یکپارچگی: شماره موبایل الزامی است

در سامانه یکپارچه TorbatYar، شماره موبایل برای همه کاربران واجب است و با OTP پیامکی تأیید می‌شود.

لایه‌های هویت (تفکیک اجباری)

لایه چه کسی SSO مرکزی Keycloak
هسته TorbatYar مدیر پلتفرم، صاحب/کارمند tenant اجباری
زیرسیستم‌ها (staff) پنل کافه، CRM، … اجباری — همان JWT
مشتری tenant سفارش‌دهنده منوی دیجیتال اختیاری — auth محلی tenant
  • frontend/lib/optional-sso.ts — برای UIs زیرسیستم: اگر session مرکزی هست، login دوباره لازم نیست.
  • مشتری end-user در restaurant_db / crm_db ذخیره می‌شود، نه Keycloak مرکزی.

ورود مرکزی فقط از Keycloak (تم torbatyar) با دو تب «رمز عبور» و «موبایل»؛ frontend فقط redirect می‌کند.

┌─────────────────────────────────────────────────────────────────┐
│     F) ورود مرکزی Keycloak — تنها نقطه ورود کاربر              │
├─────────────────────────────────────────────────────────────────┤
│  Keycloak /realms/superapp/.../auth (تم torbatyar)             │
│    تب «رمز عبور»: نام کاربری / ایمیل / موبایل + رمز            │
│    تب «موبایل»: OTP + ثبت‌نام inline (mobile-auth.js)          │
│    → Identity /auth/mobile/* → handoff → /auth/callback        │
│  Frontend /login و /register → redirect به Keycloak            │
│  Admin: همان SSO — /admin/login → Keycloak → /admin/tenants       │
└─────────────────────────────────────────────────────────────────┘

احراز هویت OTP (Core Service)

کاربر → Frontend → POST /api/v1/auth/otp/request?context=public|admin
                → Payamak (pattern 245189)
                → POST /api/v1/auth/otp/verify
                → JWT محلی (HS256)
  • context=public: نقش پیش‌فرض user (ثبت‌نام عمومی)
  • context=admin: نقش pending_tenant_admin (پنل tenant)
  • PLATFORM_ADMIN_MOBILES: ارتقا به platform_admin

جدول users در Core: mobile (unique)، mobile_verified، keycloak_sub (لینک SSO).

معماری SSO مرکزی

کاربر → Frontend → Keycloak (OIDC) → JWT
                                      ↓
              Identity /auth/me → requires_mobile_verification?
                                      ↓
              همه سرویس‌ها JWT را validate می‌کنند

Identity & Access Service

  • دیتابیس: identity_access_db
  • جدول user_profiles: mobile, mobile_verified, core_user_id
  • APIهای جدید:
    • POST /api/v1/auth/mobile/start — تشخیص login/register + ارسال OTP
    • POST /api/v1/auth/mobile/complete — تأیید OTP + صدور توکن SSO
    • POST /api/v1/auth/session/redeem — handoff از Keycloak theme
    • POST /api/v1/auth/register (legacy)
    • POST /api/v1/auth/register/verify-mobile
    • POST /api/v1/auth/register/mobile
    • POST /api/v1/auth/register/mobile/verify
    • POST /api/v1/auth/mobile/request (کاربر SSO لاگین‌شده)
    • POST /api/v1/auth/mobile/verify
  • OTP از طریق Core Service (CoreOtpClient) — بدون تکرار منطق SMS

Keycloak

  • تم torbatyar: ورود مرکزی — تب رمز عبور + تب موبایل (OTP)
  • theme.properties: identityApiUrl, frontendCallbackUrl
  • attribute کاربر: mobile در Admin API
  • Realm: superapp — زبان پیش‌فرض fa

اعتبارسنجی JWT

  • کتابخانه مشترک: shared/auth/jwt.py
  • نرمال‌سازی موبایل مشترک: shared/phone.py

۹. زیرساخت

  • Docker و docker-compose (postgres, redis, keycloak, core-service, identity-access-service, frontend, celery-worker, celery-beat).
  • همه تنظیمات از .env خوانده می‌شوند؛ هیچ چیز hardcode نمی‌شود.
  • Nginx/Traefik در فازهای بعدی به‌عنوان reverse proxy اضافه می‌شوند.

۱۰. White-label

برند (نام، رنگ، لوگو، ایمیل پشتیبانی) از env/config/دیتابیس خوانده می‌شود. Frontend در frontend/ رنگ‌ها را از طریق CSS Variables و public/theme.config.json اعمال می‌کند تا تغییر برند بدون build مجدد ممکن باشد.

۱۱. فاز ۴ — Tenant Onboarding و Workspace Activation

در بریف پروژه این کار با عنوان «Phase 3: operationalizing tenant onboarding and workspace activation» معرفی شده است؛ چون در شماره‌گذاری داخلی مستندات پیش‌تر «فاز ۳» به OTP Login + Tenant Management اختصاص یافته بود (نگاه کنید به progress.md)، این کار به‌عنوان فاز ۴ پروژه ثبت می‌شود تا تاریخچه پیاده‌سازی واقعی حفظ شود. محتوای این فاز دقیقاً همان چیزی است که در بریف «Phase 3» خوانده می‌شود.

هدف

سیستم را از «هویت + لیست ادمین» به یک workspace عملیاتی چندمستأجری واقعی تبدیل می‌کند: کاربر OTP/SSO می‌زند، در نبود tenant به onboarding هدایت می‌شود، یک workspace (tenant) می‌سازد، مالک آن می‌شود، پلن پیش‌فرض و دامنه اولیه می‌گیرد، برندینگ را تنظیم می‌کند و در پایان وارد یک داشبورد واقعی tenant می‌شود (نه فقط صفحات لیست ادمین).

مدل عضویت Tenant (Tenant Membership)

جدول جدید tenant_memberships در core_platform_db رابطهٔ واقعی کاربر↔tenant را نگه می‌دارد (مستقل از جدول هم‌نام و ساده‌تر tenant_memberships در identity_access_db که برای مدیریت اعضای هویتی استفاده می‌شود؛ این دو جدول در دو دیتابیس/سرویس متفاوت‌اند و هیچ ارتباط مستقیمی ندارند).

نقش‌ها: platform_admin, tenant_owner, tenant_admin, tenant_editor, tenant_viewer. وضعیت عضویت: active / invited / disabled. هر tenant باید حداقل یک tenant_owner فعال داشته باشد (بررسی در زمان POST /onboarding/tenant/{id}/complete). یک کاربر می‌تواند در آینده عضو چند tenant باشد (current_tenant_id روی users مشخص می‌کند کدام tenant «جاری» است).

چرخهٔ عمر Tenant (Activation Lifecycle)

tenant.status اکنون این مقادیر عملیاتی را پشتیبانی می‌کند:

draft → pending_activation → active → suspended / archived
  • tenant تازه‌ساخته‌شده از مسیر onboarding با pending_activation شروع می‌شود.
  • با تکمیل onboarding (POST .../complete) و اعتبارسنجی (نام/slug موجود و حداقل یک owner فعال) به active تغییر می‌کند.
  • suspended/archived فقط توسط پنل ادمین (فاز‌های قبلی، PATCH /admin/tenants/{id}) قابل تنظیم‌اند.
  • مقادیر قدیمی inactive/deleted برای سازگاری با داده‌های فاز ۱ حفظ شده‌اند.

پروفایل و برندینگ Tenant

ستون‌های برندینگ/تنظیمات مستقیماً روی جدول tenants اضافه شده‌اند (بدون جدول جدا، چون حجم کم و همیشه ۱به۱ با tenant است): business_type, default_locale, timezone, primary_color, secondary_color, logo_url, favicon_url, onboarding_completed.

پلن / اشتراک (بدون درگاه پرداخت)

ساختار plans و tenant_subscriptions از فاز ۱ موجود بود؛ در این فاز:

  • یک پلن پیش‌فرض «FREE» (و «STARTER» برای آینده) از طریق migration seed می‌شود (PlanService.ensure_default_plan idempotent است).
  • هنگام POST /onboarding/tenant، به‌صورت خودکار یک tenant_subscriptions با plan=FREE و status=active ساخته می‌شود.
  • درگاه پرداخت پیاده‌سازی نشده — فقط ساختار provisioning است.

نگاشت دامنه (Tenant Domain Mapping)

جدول domains (فاز ۱) با دو ستون جدید تقویت شده: is_primary (دامنهٔ اصلی/پیش‌فرض tenant) و verification_status (pending/verified/failed). هنگام POST /onboarding/tenant، اگر PLATFORM_BASE_DOMAIN تنظیم شده باشد، زیردامنهٔ {slug}.{PLATFORM_BASE_DOMAIN} به‌صورت خودکار و از پیش verified ساخته می‌شود. دامنهٔ اختصاصی (custom) از طریق PATCH /onboarding/tenant/{id}/domain با وضعیت pending اضافه می‌شود و تأیید واقعی آن به فازهای بعدی موکول شده است.

Tenant Resolution و Current Tenant Context

علاوه بر middleware قبلی (X-Tenant-IDX-Tenant-Slug → subdomain → custom domain)، این dependencyهای جدید اضافه شدند:

  • get_current_core_user / get_optional_core_user: رکورد واقعی users را چه برای JWT محلی (OTP) و چه برای JWT کیکلوک (SSO، بر اساس keycloak_sub) resolve می‌کنند.
  • get_tenant_resolution: اول هدر/دامنه (middleware)، در نبود آن current_tenant_id کاربر جاری را برمی‌گرداند؛ endpointهای platform-admin فعلی تحت تأثیر قرار نمی‌گیرند.
  • TenantContextService.resolve_current_tenant: برای GET /tenant/current استفاده می‌شود (بر اساس current_tenant_id یا اولین عضویت کاربر).

رابطهٔ ورود OTP/SSO با provisioning workspace

هر دو مسیر احراز هویت (JWT محلی OTP فاز ۱ صادرشده از Core، و JWT کیکلوک SSO فاز ۲ صادرشده از Identity) در نهایت باید به یک رکورد users در core_platform_db متصل شوند تا onboarding معنا پیدا کند:

JWT محلی (HS256)  → users.id  ──┐
                                 ├─→ UserService.resolve_current() → User
JWT کیکلوک (RS256) → users.keycloak_sub ──┘

اگر کاربر SSO هنوز به هیچ رکورد Core لینک نشده باشد (یعنی هرگز از مسیر OTP محلی Core عبور نکرده)، resolve_current خطای forbidden برمی‌گرداند — به این معنا که JIT provisioning کامل کاربر Core از JWT کیکلوک فعلاً در محدودهٔ این فاز پیاده نشده و به فاز بعدی موکول شده (نگاه کنید به last_step.md).

APIهای Onboarding

جزئیات کامل در services_contracts.md؛ خلاصه: GET /me, GET /me/tenants, POST /onboarding/tenant, PATCH /onboarding/tenant/{id}/branding, PATCH /onboarding/tenant/{id}/domain, POST /onboarding/tenant/{id}/complete, GET /tenant/current, POST /tenant/switch.

Authorization

MembershipService.ensure_role بررسی می‌کند که کاربر جاری روی tenant مقصد نقش tenant_owner یا tenant_admin داشته باشد (پیش‌فرض برای مدیریت برندینگ/دامنه/تکمیل onboarding)؛ platform_admin همیشه bypass می‌شود. endpointهای onboarding فقط نیاز به کاربر احرازهویت‌شده دارند (بدون بررسی نقش برای ساخت tenant جدید، چون هر کاربر می‌تواند workspace خودش را بسازد).

Frontend

  • Bootstrap: پس از ورود، hooks/useMe.ts نتیجهٔ GET /api/v1/me را می‌گیرد. صفحهٔ /dashboard اگر onboarding_required=true باشد کاربر را به /onboarding هدایت می‌کند.
  • Onboarding Wizard: صفحهٔ تک‌فایلی app/onboarding/page.tsx با ۴ گام (اطلاعات کسب‌وکار → برندینگ → دامنه → بازبینی) که به‌ترتیب APIهای onboarding را صدا می‌زند و در پایان به /dashboard هدایت می‌کند.
  • Tenant Dashboard: app/dashboard/page.tsx اطلاعات tenant جاری (GET /tenant/current) را نمایش می‌دهد: نام، وضعیت، پلن، دامنه، وضعیت onboarding و نقش کاربر.
  • Tenant Switcher: components/TenantSwitcher.tsx — فقط وقتی کاربر بیش از یک عضویت داشته باشد نمایش داده می‌شود و از POST /tenant/switch استفاده می‌کند.