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>
20 KiB
معماری کلان پلتفرم 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:
- هدر
X-Tenant-ID - هدر
X-Tenant-Slug - Subdomain (از روی Host و
base_domain) - 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):
- ابتدا Redis cache بررسی میشود.
- در نبود cache، از دیتابیس محاسبه میشود.
- نتیجه با 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 + ارسال OTPPOST /api/v1/auth/mobile/complete— تأیید OTP + صدور توکن SSOPOST /api/v1/auth/session/redeem— handoff از Keycloak themePOST /api/v1/auth/register(legacy)POST /api/v1/auth/register/verify-mobilePOST /api/v1/auth/register/mobilePOST /api/v1/auth/register/mobile/verifyPOST /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_planidempotent است). - هنگام
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-ID → X-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استفاده میکند.