# تطبيق SaaS متعدد المستأجرين بـ Go وPostgres RLS: كيف بُنيت مهام على ToGO

> دراسة حالة لـ Mahaam، نظام إدارة مشاريع مفتوح المصدر مبني على ToGO: العزل بين المستأجرين بأمان مستوى الصف في Postgres، وخادم MCP بـ 119 أداة، وأرقام حقيقية، وحدود الإطار التي اصطدمنا بها.

*2026-10-19T12:49:05Z*

![تطبيق SaaS متعدد المستأجرين على ToGO](https://to-go.dev/uploads/sites/c797a67b-97b7-4ed5-8459-baafb6981307/media/mahaam-togo-case-study-ar.png)

# تطبيق SaaS متعدد المستأجرين بـ Go وPostgres RLS: كيف بُنيت مهام على ToGO

[مهام](https://mahaam.app) (Mahaam) نظام مفتوح المصدر لإدارة المشاريع في الوكالات: مشاريع ومهام وتتبّع وقت وفواتير وبوابة للعملاء، مع خادم MCP يتيح لوكلاء الذكاء الاصطناعي العمل على اللوحة. هو تطبيق SaaS متعدد المستأجرين مكتوب بـ Go على [ToGO](https://github.com/togo-framework/togo)، ويعتمد على أمان مستوى الصف في Postgres (RLS) كخط دفاع أخير للعزل بين المستأجرين. في هذه الدراسة أشرح كيف رُكّب النظام، وأعرض الأرقام التي أستطيع عدّها من المستودع، والمواضع التي عطّلنا فيها ToGO.

**الخلاصة:** وفّر ToGO النواة ونظام الإضافات وREST وGraphQL وبنية المشروع الجاهزة لـ MCP. أما العزل بين المستأجرين فليس من مهام ToGO، لذلك أضافته مهام بدورين في Postgres، وسياسة لكل جدول يخص مستأجرًا، وغلاف للاتصال يضبط مساحة العمل مع كل استعلام.

## 1. المشكلة

لسنوات ظلت مشاريع الاستوديو وجداول الوقت والفواتير موزعة على ثلاث أدوات لا تتفق مع بعضها. الهدف مجموعة بيانات واحدة: تُسجَّل الساعات على ميزانية المشروع وتُنشأ الفاتورة من السجلات نفسها دون إعادة كتابة. وحكم البناء ثلاثة قيود:

- **مساحات عمل كثيرة في قاعدة بيانات واحدة.** كل عميل مساحة عمل، ولا يجوز لخطأ واحد أن يسرّب صفوفًا من مساحة إلى أخرى.
- **البشر والوكلاء بالقواعد نفسها.** حين يبدأ وكيل ذكاء اصطناعي مؤقّتًا فهو يمر بمسار الصلاحيات ذاته الذي يمر به المتصفح.
- **العربية والإنجليزية**، بما فيها الاتجاه من اليمين إلى اليسار.

## 2. لماذا ToGO

أردت واجهة خلفية بـ Go تكون الأجزاء المملة فيها جاهزة: نواة، وإعدادات، وسجل إضافات، وREST وGraphQL مولَّدان، وطابور، ومجدوِل، وبريد. يقدّم ToGO هذه كلها إضافات تُستورد في الكود، فيصير التطبيق في معظمه كود مجاله الخاص. مهام أحد المنتجات التي أبنيها عليه، والإطار مفتوح المصدر على [github.com/togo-framework/togo](https://github.com/togo-framework/togo).

## 3. البنية

**وحدات ToGO.** يثبّت `go.mod` الإصدار `github.com/togo-framework/togo v0.21.0` مع 15 وحدة إضافة: `auth` و`auth-oauth` و`cache` و`db-postgres` و`faker` و`i18n` و`import-export` و`mail` و`orm` و`queue` و`ratelimit` و`realtime` و`scheduler` و`storage` و`validation`. اثنتا عشرة منها تُستورد في `internal/plugins/plugins.gen.go`.

**حول ToGO.** طبقة HTTP هي chi مع Huma (REST وOpenAPI)، وGraphQL عبر gqlgen، وSQL عبر sqlc فوق Postgres. تطبيق الويب Next.js 16 بتخطيط عربي وإنجليزي. وفي `apps/` عملاء منها تطبيق Electron في شريط النظام، وإضافة لـ Chrome، وتطبيقات جوّال بـ Expo.

**طبقة مجال واحدة.** تضم `internal/work` قواعد العمل. تستدعي معالجات REST تحت `/api/w/*` وأدوات MCP الدوال نفسها، فيتشارك المتصفح والوكيل مسار صلاحيات واحدًا.

**مجمّعا اتصال في Postgres.**

- **مجمّع التطبيق** يتصل بدور لا يملك شيئًا، وليس مشرفًا عامًا، ولا يملك `BYPASSRLS`. كل استعلام مرتبط بطلب يمر عبره.
- **مجمّع المالك** يُستخدم فقط لتحديد المستأجر وتسجيل الدخول والبحث عن الرموز وخادم MCP الإداري.

**خادما MCP.** خادم المستخدم على `/api/mcp` والخادم الإداري على `/api/admin/mcp`. تعلن كل أداة نطاق صلاحية (scope)، ويفشل اختبار في CI إن غاب.

## 4. كود حقيقي

**سياج العزل سياسة لكل جدول.** من `internal/db/post/20_rls.sql`:

```sql
ALTER TABLE workspace_members ENABLE ROW LEVEL SECURITY;
ALTER TABLE workspace_members FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON workspace_members
    USING (workspace_id = current_setting('app.workspace_id', true))
    WITH CHECK (workspace_id = current_setting('app.workspace_id', true));
```

إن لم توجد مساحة عمل في السياق يكون الإعداد فارغًا فتصير كل السياسات خاطئة. أي أن الأصل هو المنع.

**تُضبط مساحة العمل مع كل استعلام.** يمسك ORM في ToGO بكائن `*sql.DB` فعلي، فلم يكن ممكنًا استخدام `SET LOCAL` داخل معاملة لكل طلب مع الكود المولَّد. لذلك غلّفت مهام المشغّل نفسه (`internal/tenant/rlsdb.go`):

```go
const setScopeSQL = "SELECT set_config('app.workspace_id', $1, false)"
```

ينفّذ كل اتصال هذا قبل كل استعلام، على الاتصال الفعلي نفسه وبسياق الاستعلام نفسه. الكلفة رحلة إضافية إلى قاعدة البيانات لكل استعلام، وتعليق الملف نفسه يقول إن علينا إعادة النظر حين يحصل ORM على واجهة منفِّذ.

**حارس في CI يُبقي الاستعلامات منضبطة.** يُفشل `internal/guards/sqlc_guard_test.go` أي استعلام sqlc على جدول مستأجر لا يقيّد `workspace_id`، فتبقى RLS خط دفاع أخيرًا لا السياج الوحيد. والاستعلام العابر للمستأجرين عمدًا يجب أن يصرّح بذلك بتعليق `-- guard:cross-tenant <reason>` يراه المراجع.

**أداة MCP بنية ومعها نطاق صلاحية.** من `internal/mcp/time_tools.go`:

```go
{
    Name:        "get_running_timer",
    Scope:       "time:read",
    InputSchema: schema(nil, map[string]any{"user": prop("string", "Email, user id or \"me\" (someone else needs time:manage)")}),
    Run: func(ctx context.Context, a *app.App, raw json.RawMessage) (string, error) {
        // ...calls work.New(a).RunningTimers(...)
    },
},
```

## 5. أرقام حقيقية

كلها معدودة من فرع `master` في [fadymondy/mahaam](https://github.com/fadymondy/mahaam) بتاريخ 2026-10-19.

| العنصر | العدد | كيف عُدّ |
|---|---|---|
| وحدات إضافات ToGO في `go.mod` | 15 (إضافة إلى النواة v0.21.0) | متطلبات `togo-framework/` المباشرة دون `// indirect` |
| الإضافات المستوردة | 12 | `internal/plugins/plugins.gen.go` |
| أدوات MCP للمستخدم | 119 | مدخلات `Name:` المميزة في `internal/mcp/*_tools.go` دون `admin_*` |
| أدوات MCP الإدارية | 24 | مدخلات `Scope:` في `internal/mcp/admin_*.go` |
| نطاقات صلاحية المستخدم | 34 | مدخلات `UserScopes` في `internal/mcp/scopes.go` |
| ملفات المخطط | 57 | `internal/db/schema/*.sql` |
| ملفات القيود وRLS وتصحيح البيانات | 45 | `internal/db/post/*.sql` |
| ملفات استعلامات sqlc | 28 | `internal/db/queries/*.sql` |
| سياسات RLS | 21 | `CREATE POLICY` في `internal/db/post/*.sql` |
| ملفات الاختبار / دوال الاختبار | 89 / 167 | `*_test.go` و`func Test` تحت `internal` و`cmd` |
| الإيداعات منذ الأول (2026-09-14) | 254 | `git log master` |

لا أذكر أعداد مستخدمين أو إيرادات أو أرقام أداء، لأنني لا أملك منها ما يمكن توثيقه.

## 6. ما الذي كان صعبًا، وبماذا ساعد ToGO

**ما ساعد:** نموذج الإضافات. وصلت إلينا البريد والطابور والمجدوِل وتحديد المعدّل والتخزين والترجمة بوصفها اعتماديات، فصار المستودع في معظمه كود مجال مهام. وأتاحت النواة وهيكلة REST وGraphQL تشغيل أول مخطط بسرعة.

**ما كان صعبًا، بصراحة:** يحتفظ README بقائمة مرقّمة من ملاحظات الإطار من مرحلتي P0 وP5 (CLI 0.2.31 وToGO v0.21.0). أكثرها كلفة في الوقت:

- **لا خطّاف للعزل بين المستأجرين.** لأن ORM يمسك `*sql.DB` فعليًا، لا تستطيع المعالجات المولَّدة العمل داخل معاملة لكل طلب. وغلاف الاتصال أعلاه هو الحل البديل.
- **افتراضات في القالب لا تناسب تطبيقًا متعدد المستأجرين.** يثبّت `togo new` الإضافة `auth-dev` التي تعيد جلسة مسؤول لأي طلب POST فارغ في أي بيئة غير إنتاجية، ولوحة تحكم عامة فوق CRUD غير مقيّد. ولا يوجد أمر لإزالة إضافة، فعُدّل `plugins.gen.go` يدويًا.
- **إضافات تفتح مسارات بلا مصادقة.** تفتح `import-export` المسارين `/api/export/{resource}` و`/api/import/{resource}`، وتفتح `settings` (تأتي عبر `queue`) المسار `/api/settings/{scope}/{key}`، كلها دون مصادقة أو عزل. وفي مهام حارس يحجبها.
- **الوقت الفعلي ليس لكل مستخدم.** ينشر وسيط SSE للجميع، فلا يصلح لنقل بيانات خاصة بمستخدم كما هو.
- **قد يفشل توليد الكود بصمت.** طبع `togo generate` عبارة «Codegen complete» وخرج بالرمز 0 حين فشل gqlgen أو بوابة OpenAPI، فصرنا نفحص مخرجاته بالبحث.
- **وسيط `ratelimit`** يردّ بنص عادي لا يفهمه عميل MCP. فتستدعي مهام `Allow` وتكتب ردّ JSON-RPC 429 بنفسها.

قد يكون بعض هذه قد تغيّر في إصدارات ToGO الأحدث؛ القائمة كما وجدناها على v0.21.0.

## 7. أسئلة شائعة

**هل يوفّر ToGO العزل بين المستأجرين؟** لا. نفّذته مهام: دوران في Postgres، وسياسات `FORCE ROW LEVEL SECURITY`، وغلاف `set_config` لكل استعلام، وحارس في CI على الاستعلامات.

**لماذا لا `SET LOCAL` داخل معاملة؟** لأن ORM يأخذ `*sql.DB` فعليًا، فلا سبيل لتمرير معاملة خاصة بالطلب إلى الكود المولَّد. والغلاف يكلّف رحلة إضافية لكل استعلام.

**هل مهام مفتوح المصدر؟** نعم. المستودع عام على GitHub برخصة AGPL-3.0.

**كيف تتصل الوكلاء؟** عبر نقطة MCP على `/api/mcp` برمز محدّد النطاقات يُنشأ من التطبيق. وتُضيَّق صلاحيات الوكيل الفعلية إلى دور صاحبه في مساحة العمل.

## 8. روابط

- مقال المنتج: [خادم MCP لإدارة المشاريع لوكلاء الذكاء الاصطناعي](https://mahaam.app/ar/blog/project-management-mcp-server-for-ai-agents)
- مقال المؤسس: [لماذا بنيت مهام](https://fadymondy.com/ar/blog/why-i-built-mahaam)
- مهام: [mahaam.app](https://mahaam.app) · [GitHub](https://github.com/fadymondy/mahaam) · [Product Hunt](https://www.producthunt.com/products/mahaam) · [Discord](https://discord.gg/a59RzxnB2H)
- ToGO: [github.com/togo-framework/togo](https://github.com/togo-framework/togo) · [المعرض](https://to-go.dev/ar/showcase)

---

Source: https://to-go.dev/ar/blog/mahaam-togo-case-study
