SaaS مخفف Software as a Service است و به مدلی از ارائه نرمافزار گفته میشود که کاربران بهجای نصب و نگهداری مستقل برنامه، از طریق اینترنت از آن استفاده میکنند. نرمافزارهای مدیریت پروژه، سیستمهای حسابداری آنلاین، ابزارهای اتوماسیون، سامانههای CRM و سرویسهای مدیریت محتوا میتوانند با این مدل ارائه شوند.
Laravel به دلیل داشتن ابزارهایی برای مدیریت کاربران، پایگاه داده، احراز هویت، صفها، کش و ارتباط با سرویسهای خارجی، گزینه مناسبی برای ساخت بسیاری از محصولات SaaS است.
البته ساخت یک SaaS فقط به ایجاد پنل کاربری و دریافت پرداخت محدود نمیشود. باید معماری نرمافزار، جداسازی اطلاعات مشتریان، مدیریت اشتراکها، کنترل دسترسی، محدودیت مصرف و فرایند تمدید یا لغو اشتراک نیز بهدرستی طراحی شوند.
در این مقاله مراحل اصلی طراحی یک SaaS با Laravel را بررسی میکنیم و نمونههایی از ساختار دیتابیس و کدهای موردنیاز را میبینیم.
۱. SaaS چه تفاوتی با یک وبسایت معمولی دارد؟
در یک وبسایت معمولی ممکن است همه کاربران به مجموعهای از صفحات و امکانات مشترک دسترسی داشته باشند. اما در یک SaaS، هر مشتری معمولاً حساب، تنظیمات، اطلاعات و سطح دسترسی مخصوص خود را دارد.
برای مثال، در یک نرمافزار مدیریت پروژه:
- هر شرکت میتواند یک فضای کاری مستقل داشته باشد.
- هر فضای کاری اعضای خودش را دارد.
- پروژهها و وظایف هر شرکت باید از شرکتهای دیگر جدا باشند.
- امکانات قابل استفاده ممکن است به پلن اشتراک وابسته باشند.
- مصرف منابع میتواند بر اساس پلن محدود شود.
- تمدید یا پایان اشتراک باید در دسترسی کاربران اثر بگذارد.
بنابراین، از ابتدای پروژه باید به چند موضوع مهم توجه کنید:
- مدیریت مشتریان یا سازمانها
- جداسازی اطلاعات هر مشتری
- احراز هویت و مجوزهای دسترسی
- مدیریت پلنها و اشتراکها
- دریافت و تأیید پرداخت
- اندازهگیری مصرف و اعمال محدودیتها
- پشتیبانی، گزارشگیری و ثبت رویدادها
۲. انتخاب معماری مناسب برای SaaS
یکی از تصمیمهای مهم، انتخاب معماری چندمستاجری یا Multi-Tenancy است.
در این معماری، یک نرمافزار میتواند به چند مشتری مستقل خدمترسانی کند. هر مشتری که از سامانه استفاده میکند، یک Tenant محسوب میشود.
سه رویکرد رایج برای نگهداری اطلاعات وجود دارد.
| معماری | توضیح | مزیت | چالش |
|---|---|---|---|
| دیتابیس مشترک و جداول مشترک | اطلاعات همه مشتریان در جداول یکسان ذخیره میشود | هزینه و پیچیدگی عملیاتی کمتر | نیاز به کنترل دقیق جداسازی دادهها |
| دیتابیس مشترک با Schema جداگانه | هر مشتری Schema مستقل دارد؛ در دیتابیسهای پشتیبان این معماری | جداسازی بیشتر نسبت به جداول مشترک | مدیریت و Migration پیچیدهتر |
| دیتابیس مستقل برای هر مشتری | هر مشتری دیتابیس جداگانه دارد | جداسازی قویتر و امکان مدیریت مستقل | هزینه و پیچیدگی عملیاتی بیشتر |
برای بسیاری از SaaSهای کوچک و متوسط، شروع با دیتابیس مشترک و جداول مشترک میتواند سادهتر باشد؛ به شرط آنکه جداسازی دادهها از ابتدا اصولی طراحی شود.
اگر محصول به الزامات جدی جداسازی داده، پشتیبانگیری مستقل یا استقرار اختصاصی برای مشتریان نیاز دارد، معماری دیتابیس جداگانه نیز میتواند مناسب باشد.
نکته مهم: هیچ معماریای بهصورت خودکار امنیت اطلاعات مشتریان را تضمین نمیکند. نحوه کنترل دسترسی، ساختار جداول، فرایندهای عملیاتی و آزمونهای امنیتی اهمیت زیادی دارند.
۳. ایجاد پروژه Laravel
ابتدا یک پروژه جدید ایجاد کنید:
composer create-project laravel/laravel saas-platform
سپس وارد پوشه پروژه شوید:
cd saas-platform
تنظیمات اتصال به پایگاه داده را در فایل .env انجام دهید. برای نمونه:
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=saas_platform
DB_USERNAME=saas_user
DB_PASSWORD=your_secure_password
مقادیر بالا نمونه هستند و باید با اطلاعات واقعی محیط توسعه جایگزین شوند.
پس از تنظیم پایگاه داده، Migrationهای اولیه را اجرا کنید:
php artisan migrate
نسخه Laravel و روش احراز هویت را متناسب با نیاز پروژه انتخاب کنید. در این مقاله، تمرکز بر طراحی ساختار SaaS است و بخشهایی مانند پرداخت واقعی، ثبتنام و ارسال ایمیل باید با سرویسها و تنظیمات محیط شما تکمیل شوند.
۴. طراحی ساختار Tenantها
فرض کنیم میخواهیم SaaSای بسازیم که شرکتها بتوانند در آن حساب ایجاد کنند و اعضای خود را به فضای کاری اضافه کنند.
در این مدل، جدول tenants نماینده شرکت یا فضای کاری است.
برای ایجاد Migration:
php artisan make:model Tenant -m
در فایل Migration میتوانیم ساختاری مشابه زیر تعریف کنیم:
Schema::create('tenants', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->string('status')->default('active');
$table->timestamps();
});
در این ساختار:
nameنام فضای کاری یا شرکت را نگهداری میکند.slugشناسه متنی یکتایی برای آدرس یا شناسایی فضای کاری است.statusوضعیت کلی Tenant را نشان میدهد.
در پروژه واقعی، بهتر است وضعیتها بهصورت محدود و مشخص تعریف شوند؛ برای مثال، فعال، تعلیقشده یا در انتظار راهاندازی.
پس از تعریف Migration:
php artisan migrate
مدل Tenant
در مدل Tenant میتوان رابطه با کاربران را تعریف کرد. اگر هر کاربر فقط عضو یک Tenant باشد، رابطه ساده است؛ اما اگر کاربران بتوانند در چند فضای کاری عضو شوند، به جدول واسط نیاز داریم.
برای محصولی که احتمال توسعه آن وجود دارد، عضویت چندگانه معمولاً انعطاف بیشتری فراهم میکند.
۵. طراحی رابطه کاربران و فضاهای کاری
در بسیاری از محصولات SaaS، یک کاربر میتواند عضو چند سازمان باشد و در هر سازمان نقش متفاوتی داشته باشد.
برای مثال، یک نفر ممکن است در سازمان اول مدیر و در سازمان دوم کاربر عادی باشد.
برای پشتیبانی از این مدل، میتوان از جدول واسط tenant_user استفاده کرد.
Schema::create('tenant_user', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')
->constrained()
->cascadeOnDelete();
$table->foreignId('user_id')
->constrained()
->cascadeOnDelete();
$table->string('role')->default('member');
$table->timestamps();
$table->unique(['tenant_id', 'user_id']);
});
این جدول مشخص میکند هر کاربر عضو کدام فضای کاری است و در آن چه نقشی دارد.
در مدل Tenant:
public function users()
{
return $this->belongsToMany(User::class)
->withPivot('role')
->withTimestamps();
}
در مدل User نیز:
public function tenants()
{
return $this->belongsToMany(Tenant::class)
->withPivot('role')
->withTimestamps();
}
به این ترتیب میتوان فضاهای کاری مرتبط با هر کاربر را دریافت کرد.
نکته امنیتی: نقش ذخیرهشده در جدول واسط نباید صرفاً برای نمایش در رابط کاربری استفاده شود. هر عملیات حساس باید در سمت سرور بررسی کند که کاربر در فضای کاری موردنظر مجوز لازم را دارد.
۶. جداسازی اطلاعات مشتریان
فرض کنید SaaS ما امکان مدیریت پروژهها را فراهم میکند. هر پروژه باید متعلق به یک فضای کاری باشد.
برای ایجاد جدول پروژهها، میتوانیم از ساختاری مانند زیر استفاده کنیم:
Schema::create('projects', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')
->constrained()
->cascadeOnDelete();
$table->string('name');
$table->text('description')->nullable();
$table->timestamps();
$table->index(['tenant_id', 'created_at']);
});
در این ساختار، هر پروژه به یک Tenant متصل است.
هنگام دریافت اطلاعات، باید Tenant فعال و مجاز کاربر مشخص باشد:
$projects = Project::query()
->where('tenant_id', $tenant->id)
->latest()
->paginate(20);
این نمونه فیلتر اولیه اطلاعات بر اساس Tenant را نشان میدهد؛ اما نباید تمام امنیت برنامه را به فراموش نکردن این شرط در هر کوئری وابسته کنید.
در یک محصول واقعی، بهتر است جداسازی دادهها را با سیاستهای معماری مشخص، Scopeهای مناسب یا سازوکار چندمستاجری معتبر پیادهسازی کنید و دسترسی به هر منبع را نیز از نظر مجوز بررسی کنید.
چرا جداسازی Tenantها حیاتی است؟
اگر کاربر یک شرکت بتواند با تغییر شناسه در URL یا درخواست API به اطلاعات شرکت دیگری دسترسی پیدا کند، یک آسیبپذیری جدی رخ داده است.
برای جلوگیری از چنین مشکلی:
- Tenant فعال باید از نشست یا سازوکار معتبر انتخاب فضای کاری تعیین شود.
- مالکیت هر منبع باید در سمت سرور بررسی شود.
- شناسه Tenant ارسالی از کاربر نباید بدون اعتبارسنجی پذیرفته شود.
- آزمونهای خودکار باید دسترسی بین دو Tenant را بررسی کنند.
- عملیات پسزمینه نیز باید Tenant مربوط به هر Job را بهدرستی حفظ کند.
۷. طراحی پلنهای اشتراک
بسیاری از SaaSها امکانات خود را بر اساس پلن ارائه میکنند. برای مثال:
- پلن رایگان با محدودیت تعداد پروژهها
- پلن حرفهای با امکانات بیشتر
- پلن سازمانی با تعداد اعضای بالاتر و امکانات مدیریتی
برای نگهداری پلنها، میتوان جدول plans ایجاد کرد:
Schema::create('plans', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('code')->unique();
$table->unsignedBigInteger('price');
$table->string('billing_interval')->default('monthly');
$table->json('features')->nullable();
$table->boolean('is_active')->default(true);
$table->timestamps();
});
در این مثال، price مبلغ را در کوچکترین واحد پولی مورد توافق سیستم ذخیره میکند. در محصولی که با چند واحد پولی کار میکند، باید واحد پول نیز بهصورت روشن ثبت شود.
فیلد features میتواند امکانات یا محدودیتهای پلن را بهشکل JSON نگهداری کند؛ بااینحال، برای گزارشگیری پیچیده یا تحلیل دقیق مصرف، ممکن است ساختارهای نرمالشده مناسبتر باشند.
۸. طراحی جدول اشتراکها
پلن و اشتراک یک مفهوم نیستند.
پلن مشخص میکند چه امکاناتی ارائه میشود؛ اشتراک مشخص میکند یک Tenant در چه بازهای و با چه وضعیتی از آن پلن استفاده میکند.
نمونه ساختار جدول اشتراک:
Schema::create('subscriptions', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')
->constrained()
->cascadeOnDelete();
$table->foreignId('plan_id')
->constrained();
$table->string('status')->default('active');
$table->timestamp('starts_at');
$table->timestamp('ends_at')->nullable();
$table->timestamp('cancelled_at')->nullable();
$table->timestamps();
$table->index(['tenant_id', 'status']);
});
در این مدل، میتوان وضعیت اشتراک و بازه اعتبار آن را بررسی کرد.
البته وضعیت اشتراک در پروژه واقعی باید با قوانین مشخص مدیریت شود؛ برای مثال، باید معلوم باشد که اشتراک منقضیشده، لغوشده یا در مهلت تمدید چه دسترسیهایی دارد.
همچنین اگر تغییر پلن در طول زمان اهمیت دارد، بهتر است مبلغ و مشخصات پلن در زمان خرید نیز در رکورد اشتراک یا فاکتور ثبت شوند تا تغییرات آینده پلن، سوابق مالی گذشته را تغییر ندهند.
۹. کنترل دسترسی بر اساس پلن
یکی از قابلیتهای مهم SaaS، محدود کردن امکانات بر اساس پلن اشتراک است.
برای مثال، فرض کنیم پلن رایگان اجازه ایجاد حداکثر سه پروژه را میدهد.
ابتدا باید تعداد پروژههای متعلق به Tenant فعال را بررسی کنیم:
$currentCount = Project::query()
->where('tenant_id', $tenant->id)
->count();
سپس سقف مجاز را از تنظیمات معتبر پلن دریافت کنیم:
$projectLimit = $plan->project_limit;
اگر تعداد پروژهها به سقف مجاز رسیده باشد، برنامه نباید اجازه ایجاد پروژه جدید بدهد.
if ($currentCount >= $projectLimit) {
abort(403, 'Project limit reached.');
}
کد بالا یک نمونه ساده است و فرض میکند فیلد project_limit در مدل پلن وجود دارد. در طراحی واقعی، باید رفتار پلن نامحدود، مقدارهای نامعتبر، دسترسی مدیر و پاسخ مناسب API نیز مشخص شوند.
همچنین کنترل محدودیت باید در سمت سرور اجرا شود؛ مخفی کردن دکمه ایجاد پروژه در رابط کاربری، جایگزین کنترل دسترسی نیست.
جلوگیری از عبور همزمان از محدودیت
اگر دو درخواست همزمان برسند، ممکن است هر دو تعداد پروژههای فعلی را کمتر از سقف ببینند و سپس هر دو پروژه جدید بسازند.
برای محدودیتهای مهم، بهویژه در سیستمهای پرترافیک، باید عملیات بررسی و ایجاد منبع با راهکار مناسب همزمانی طراحی شود. بسته به مدل داده، این راهکار ممکن است شامل تراکنش دیتابیس، قفلگذاری یا سازوکار رزرو سهمیه باشد.
۱۰. اتصال سیستم پرداخت به اشتراک
برای فعال کردن پلن پولی، SaaS معمولاً به یک سیستم پرداخت نیاز دارد.
فرایند کلی میتواند به این شکل باشد:
۱. کاربر پلن موردنظر را انتخاب میکند.
۲. سرور قیمت و وضعیت پلن را از دیتابیس بررسی میکند.
۳. یک رکورد سفارش یا تراکنش با وضعیت در انتظار ایجاد میشود.
۴. کاربر به درگاه پرداخت هدایت میشود.
۵. پس از بازگشت از درگاه، سرور نتیجه پرداخت را با API رسمی درگاه تأیید میکند.
۶. در صورت تأیید، تراکنش ثبت و اشتراک فعال یا تمدید میشود.
۷. نتیجه عملیات بهصورت قابل پیگیری ذخیره میشود.
نباید صرفاً بر اساس پارامترهایی که مرورگر پس از پرداخت ارسال میکند، اشتراک را فعال کنید. وضعیت پرداخت باید از طریق فرایند تأیید معتبر سمت سرور مشخص شود.
همچنین عملیات تأیید باید در برابر درخواستهای تکراری مقاوم باشد؛ یعنی اگر Callback چند بار ارسال شد، نباید اشتراک دوبار تمدید یا یک پرداخت چند بار ثبت شود.
۱۱. تمدید خودکار و مدیریت انقضای اشتراک
اگر محصول از تمدید خودکار پشتیبانی میکند، باید زمان انقضا و وضعیت پرداخت را بهصورت قابل اتکا مدیریت کند.
Laravel امکان اجرای وظایف زمانبندیشده را فراهم میکند. برای مثال، میتوان Job یا Commandای ایجاد کرد که اشتراکهای نزدیک به انقضا را بررسی و اعلان مناسب ارسال کند.
فرایند پیشنهادی:
- شناسایی اشتراکهای نزدیک به انقضا
- ارسال اعلان به مدیر فضای کاری
- تلاش برای تمدید، در صورت فعال بودن تمدید خودکار
- ثبت نتیجه پرداخت
- تمدید تاریخ اشتراک پس از تأیید پرداخت
- محدود کردن امکانات در صورت پایان اعتبار، طبق سیاست محصول
برای اجرای عملیات زمانبندیشده، باید Scheduler لاراول نیز در سرور بهدرستی تنظیم شده باشد.
نکته: ارسال اعلان، تلاش برای پرداخت و فعالسازی اشتراک سه عملیات جداگانهاند. بهتر است هرکدام وضعیت مشخص و قابلیت ثبت خطا داشته باشند تا مشکلات تمدید قابل پیگیری باشند.
۱۲. ثبت مصرف منابع و اعمال سهمیهها
در SaaSهای مبتنی بر مصرف، فقط تاریخ انقضای اشتراک اهمیت ندارد. ممکن است لازم باشد میزان استفاده از منابع نیز ثبت شود.
برای مثال:
- تعداد درخواستهای API
- تعداد پروژههای ایجادشده
- حجم فایلهای ذخیرهشده
- تعداد اجرای Workflow
- تعداد پیامکهای ارسالشده
- تعداد پردازشهای انجامشده
در این مدل، باید تعریف کنید مصرف چگونه محاسبه میشود، چه زمانی سهمیه بازنشانی میشود و آیا مصرف در لحظه کنترل میشود یا بهصورت دورهای.
برای سهمیههای ساده، میتوان از شمارندههای ذخیرهشده استفاده کرد. برای گزارشگیری مالی یا محاسبه دقیق مصرف، نگهداری رویدادهای مصرف بهصورت مستقل نیز ممکن است ضروری باشد.
اگر مصرف روی هزینه واقعی سرویس اثر میگذارد، باید از ثبت تکراری رویدادها جلوگیری کنید و امکان تطبیق دادهها با صورتحسابها را داشته باشید.
۱۳. استفاده از Queue برای عملیات پسزمینه
در یک SaaS، ممکن است عملیات سنگینی مانند تولید گزارش، پردازش فایل، ارسال اعلان و همگامسازی با سرویسهای خارجی وجود داشته باشد.
اجرای همه این کارها در درخواست مستقیم کاربر میتواند زمان پاسخ را افزایش دهد. Laravel Queue به شما امکان میدهد چنین پردازشهایی را در پسزمینه انجام دهید.
برای مثال:
php artisan make:job GenerateTenantReport
سپس میتوانید Job را به صف ارسال کنید:
GenerateTenantReport::dispatch($tenant->id);
این مثال صرفاً شناسه Tenant را به Job ارسال میکند. هنگام اجرای Job، باید بررسی شود که Tenant هنوز معتبر است و عملیات در محدوده مجاز آن انجام میشود.
برای کارهای حساس، مانند پردازش مالی یا مصرف سهمیه، مدیریت خطا، Retry و جلوگیری از اجرای تکراری را جدی بگیرید.
۱۴. امنیت در معماری چندمستاجری
در یک SaaS، خطای امنیتی ممکن است اطلاعات یک مشتری را در اختیار مشتری دیگری قرار دهد. به همین دلیل، جداسازی دادهها باید یکی از اصول اصلی طراحی باشد.
اقدامات مهم عبارتاند از:
- بررسی مجوز دسترسی برای هر منبع
- جلوگیری از دسترسی به اطلاعات Tenant دیگر با تغییر شناسه
- اعتبارسنجی عضویت کاربر در فضای کاری فعال
- محدود کردن دسترسی به فایلها و گزارشها
- ثبت رویدادهای حساس و تغییرات اشتراک
- محافظت از کلیدهای API و اطلاعات پرداخت
- استفاده از HTTPS
- کنترل دسترسی عملیات مدیریتی
- آزمایش سناریوهای دسترسی بین Tenantها
اگر از صفها یا Cache مشترک استفاده میکنید، کلیدهای Cache و دادههای Jobها نیز باید طوری طراحی شوند که اطلاعات مشتریان با یکدیگر اشتباه نشوند.
۱۵. چه زمانی به پکیج Multi-Tenancy نیاز داریم؟
برای پروژهای کوچک با ساختار ساده، میتوان معماری چندمستاجری را با مدلها، رابطهها و سیاستهای دسترسی Laravel پیادهسازی کرد.
اما اگر محصول به امکاناتی مانند تشخیص Tenant بر اساس دامنه، اتصال دیتابیس جداگانه، Migration مستقل یا مدیریت پیچیده فضای کاری نیاز داشته باشد، استفاده از یک پکیج تخصصی میتواند بخشی از کار را سادهتر کند.
پیش از انتخاب پکیج، این موارد را بررسی کنید:
- سازگاری با نسخه Laravel پروژه
- پشتیبانی از نوع معماری موردنظر
- نحوه جداسازی دادهها
- سازگاری با Queue و Cache
- وضعیت نگهداری و بهروزرسانی پروژه
- مستندات و آزمونهای امنیتی
- امکان مهاجرت یا تغییر معماری در آینده
استفاده از پکیج جایگزین طراحی درست نیست. حتی با پکیج چندمستاجری نیز باید دسترسیها و سناریوهای جداسازی داده را آزمایش کنید.
جمعبندی
ساخت SaaS با Laravel نیازمند طراحی دقیقتر از یک وبسایت معمولی است. در کنار قابلیتهای اصلی محصول، باید از ابتدا به مدیریت Tenantها، جداسازی اطلاعات، پلنهای اشتراک، پرداخت، محدودیت مصرف و امنیت توجه کرد.
برای شروع، یک معماری ساده و قابل نگهداری انتخاب کنید، مرزهای دسترسی مشتریان را مشخص کنید و سپس امکانات اشتراک و پرداخت را مرحلهبهمرحله توسعه دهید.
پس از شکلگیری نسخه اولیه محصول، میتوانید قابلیتهایی مانند تمدید خودکار، گزارش مصرف، Queue، Redis و مدیریت پیشرفته پلنها را بر اساس نیاز واقعی اضافه کنید.





