داده و چندمستاجری
هر Building مرز Tenant است، اما Account سراسری باقی میماند تا یک فرد بتواند با یک شماره موبایل در چند ساختمان و چند نقش عضو باشد. جداسازی Tenant در Application اجباری و Row-Level Security در PostgreSQL لایه دفاعی دوم است.
مدل Tenant
| مفهوم | دامنه |
|---|---|
accounts |
سراسری؛ یک هویت برای هر موبایل نرمالشده |
buildings |
ریشه Tenant |
units |
وابسته به building_id |
memberships |
پیوند Account با Building و در صورت نیاز Unit |
roles و role_assignments |
Template سراسری یا نمونه Scoped به Building |
| داده عملیاتی و مالی | همیشه وابسته به building_id |
| تنظیمات پلتفرم و Provider | سراسری و فقط برای Platform Admin |
قید Unique برای داده Tenant باید building_id را شامل شود؛ برای نمونه شماره واحد با (building_id, normalized_number) یکتا است، نه در کل سامانه.
قواعد Isolation
building_idاز Path، Session فعال یا Resource والد استخراج و با Membership معتبر تطبیق داده میشود؛ مقدار Client بهتنهایی قابل اعتماد نیست.- هر Use Case پیش از دسترسی به داده، Actor، Building فعال و Permission لازم را میسازد.
- Repositoryهای Tenant-scoped بدون
building_idدر Signature مجاز نیستند. - Query سراسری فقط در Package و Role جداگانه Platform Admin اجرا میشود.
- Application Role پایگاه داده مالک Table، Superuser یا دارای
BYPASSRLSنیست. - روی Tableهای Tenant-scoped، RLS فعال و در صورت تناسب
FORCE ROW LEVEL SECURITYمیشود. - Policy بهشکل Deny-by-default است و هم
USINGو همWITH CHECKرا پوشش میدهد.
Tenant Context با SET LOCAL app.current_building_id = ... فقط داخل Transaction تنظیم میشود تا Connection Pool باعث نشت Context بین درخواستها نشود. هر Query Tenant-scoped داخل همان Transaction اجرا میشود. آزمون Integration باید تلاش برای خواندن و نوشتن Cross-tenant را رد کند.
RLS جای Authorization دامنهای را نمیگیرد؛ فقط مانع نهایی دسترسی اشتباه به Row Tenant دیگر است.
قرارداد داده
- شناسه عمومی UUIDv7 است تا قابلیت حدس کم و Locality ایندکس بهتر از UUID تصادفی داشته باشد. در PostgreSQL 18 تابع
uuidv7()مرجع تولید است. - زمان با
timestamptzو UTC ذخیره میشود. تقویم شمسی فقط در Client یا Presentation ساخته میشود. - مبلغ با
bigintو واحد ریال ذخیره میشود؛floatبرای پول ممنوع است. - نرخ یا درصد با عدد صحیح مقیاسدار یا
numeric(precision, scale)و Precision صریح نگهداری میشود. - شماره موبایل ایران پس از Validation در قالب E.164 مانند
+989121234567ذخیره میشود. - Statusهای دامنه با Check Constraint یا Lookup کنترلشده و Transition در Domain محافظت میشوند.
- JSONB فقط برای داده واقعا نیمهساختیافته، Snapshot قرارداد یا metadata کمQuery استفاده میشود؛ جای مدل رابطهای را نمیگیرد.
- حذف نرم پیشفرض نیست. برای تاریخچه کسبوکار از Status یا رکورد immutable استفاده میشود و
deleted_atفقط با نیاز مشخص اضافه میشود.
Schema و Constraint
- نام Table، Column، Index و Constraint به
snake_caseاست. NOT NULL، Foreign Key، Unique و Check Constraint نزدیک داده تعریف میشوند؛ Validation برنامه بهتنهایی کافی نیست.- رفتار
ON DELETEصریح است؛ Cascade برای داده مالی، Audit و Membership بدون بررسی ممنوع است. created_atوupdated_atبرای Entityهای قابل تغییر وجود دارد؛ زمان کسبوکار مانندpaid_atجای آنها را نمیگیرد.- Version عددی برای Optimistic Concurrency روی Entityهای مستعد ویرایش همزمان استفاده میشود.
- Audit مالی و رویدادهای حساس Append-only هستند و اصلاح با رکورد جبرانی انجام میشود.
Query و Index
- SQL کنار Feature و بهصورت ثابتها و تابعهای Query دستی روی
pgx/v5نگهداری میشود. پارامتر، Row وScanباید صریح، کوچک و قابل Review باشند؛ Code Generator و ORM عمومی مجاز نیست. SELECT *در Query محصول ممنوع است؛ ستونهای لازم صریح انتخاب میشوند.- هر Index باید Query یا Constraint مشخص داشته باشد و با
EXPLAIN (ANALYZE, BUFFERS)روی داده نزدیک Production ارزیابی شود. - Indexهای Tenant-scoped معمولا با
building_idآغاز میشوند و ترتیب ستونها بر اساس Filter، Sort و Selectivity واقعی تعیین میشود. - Pagination بر پایه Cursor و Sort پایدار است؛ Offset بزرگ برای Feed و لیستهای در حال رشد مجاز نیست.
- Query بیش از 100ms در Production بهعنوان Slow Query ثبت و بررسی میشود؛ متن کامل دارای PII Log نمیشود.
pg_stat_statementsدر Production فعال و Queryهای پرتکرار، پرهزینه و دارای Variance بالا پایش میشوند.
Partitioning فقط وقتی حجم، Retention یا Maintenance آن را با Metric توجیه کند اضافه میشود. ایجاد Partition زودهنگام برای Tableهای کوچک ممنوع است.
Connection Pool و Transaction
- Pool با
pgxpoolساخته میشود و مجموع Connection تمام Replicaها از ظرفیت PostgreSQL کمتر میماند. - مقدارهای
MaxConns،MinConns، Lifetime و Idle Time بر اساس Load Test و ظرفیت Server تنظیم میشوند، نه Copy/Paste. - Transaction کوتاه است و در زمان تماس شبکه، Upload یا انتظار کاربر باز نمیماند.
- Isolation پیشفرض
READ COMMITTEDاست؛REPEATABLE READیاSERIALIZABLEفقط برای Invariant مشخص و با Retry خطای Serialization استفاده میشود. - Lock با ترتیب ثابت گرفته میشود و Deadlock یا Serialization Failure با Retry محدود و Jitter مدیریت میشود.
- Side Effect بیرونی با Transactional Outbox از Commit داده جدا میشود.
Migration
- Migrationها SQL، ترتیبی، immutable و در
db/migrationsنگهداری میشوند. - فایل Mergeشده و اجراشده ویرایش نمیشود؛ اصلاح آن Migration جدید است.
- تغییر سازگار با راهبرد Expand/Contract انجام میشود: افزودن ساختار سازگار، Deploy کد Dual-compatible، Backfill، Cutover و سپس حذف قدیمی در Release جدا.
- تغییر مخرب، Rewrite بزرگ Table یا ساخت Index بدون Concurrent Strategy باید Lock Impact، زمان، Rollback و فضای موقت را پیش از Production مشخص کند.
- Migration در Pipeline یکبار و پیش از تغییر Traffic اجرا میشود؛ Replicaهای App Migration اجرا نمیکنند.
- Down Migration برای داده مخرب، تضمین بازیابی نیست. Rollback کد باید با Schema جدید سازگار باشد و بازیابی داده از Backup انجام شود.
- CI پایگاه داده خالی را تا آخرین نسخه Migrate و مسیر ارتقا از Snapshot نسخه قبلی را آزمایش میکند.
Backup و بازیابی
- Backup شامل Base Backup زمانبندیشده و آرشیو پیوسته WAL برای Point-in-Time Recovery است.
- Backup پیش از خروج از Server رمز میشود و Key آن جدا از Object Storage نگهداری میشود.
- نسخه Backup در Bucket خصوصی و ترجیحا در Failure Domain جدا از VPS اصلی نگهداری میشود.
- هدف اولیه Production برابر
RPO <= 5mوRTO <= 60mاست؛ این اعداد تعهد نهایی نیستند تا Restore Test روی توپولوژی انتخابشده آنها را اثبات کند. - Restore خودکار حداقل ماهانه و تمرین بازیابی انتهابهانتها حداقل فصلی اجرا میشود.
- موفق بودن Upload Backup کافی نیست؛ Age آخرین WAL، قابلیت Decrypt، Integrity و Restore واقعی Alert دارند.
- Retention باید با نیاز حقوقی و مالی تصویب شود؛ تا آن زمان حداقل 7 نسخه روزانه، 4 نسخه هفتگی و 6 نسخه ماهانه نقطه شروع است.
معیار پذیرش
- آزمون Cross-tenant برای تمام Repositoryهای Tenant-scoped وجود دارد.
- Application Role امکان Bypass کردن RLS یا مالکیت Table ندارد.
- همه مبلغها ریال و Integer هستند و همه زمانها UTC دارند.
- Migration روی پایگاه خالی و نسخه قبلی بدون Downtime ناسازگار آزمایش شده است.
- Restore Test ثبتشده، RPO و RTO هدف را اندازهگیری میکند.