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>
390 lines
20 KiB
Markdown
390 lines
20 KiB
Markdown
# معماری کلان پلتفرم 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-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`
|
||
استفاده میکند.
|