# Go متعدد المستأجرين مع Postgres Row-Level Security: كيف بُنيت حوسبة على ToGO

> حوسبة منصة نشر ذاتية الاستضافة مكتوبة بلغة Go على ToGO. تعرض دراسة الحالة هذه المعمارية الفعلية: الإضافات، وPostgres Row-Level Security، والعمليات الموقّعة للوكيل، وخادم MCP، مع كود وأرقام من المستودع.

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

# Go متعدد المستأجرين مع Postgres Row-Level Security: كيف بُنيت حوسبة على ToGO

حوسبة (Hosbah) منصة نشر ذاتية الاستضافة: تربط سيرفراتك، وتشير إلى مستودع git، فيُبنى كل push ويُنشر بعد فحص الصحة. وتحت هذا كله تطبيق Go متعدد المستأجرين. وبناء تطبيق Go متعدد المستأجرين مع Postgres Row-Level Security هو معظم ما تدور حوله دراسة الحالة هذه.

حوسبة مبنية على [ToGO](https://github.com/togo-framework/togo)، إطار Go + sqlc + Atlas + React. كل ما يلي مأخوذ من المستودع العام [github.com/fadymondy/hosbah](https://github.com/fadymondy/hosbah)، وكل رقم أذكره أوضّح كيف عددته.

![غلاف دراسة حالة حوسبة على ToGO](/uploads/sites/c797a67b-97b7-4ed5-8459-baafb6981307/media/hosbah-togo-case-study-ar.png)

## 1. المشكلة

حوسبة تحتفظ بأسرار الآخرين وتشغّل أوامر على سيرفراتهم. وهذا يرفع المطلوب في ثلاثة أمور:

- **عزل المستأجرين.** يجب ألا يرى أي فريق سيرفرات فريق آخر أو تطبيقاته أو متغيرات بيئته أو سجلاته، حتى لو نسي أحد الـhandlers شرط `WHERE`.
- **الثقة بين لوحة التحكم والسيرفرات التي تديرها.** لوحة التحكم تخبر الوكيل (agent) على سيرفر العميل بما يُنفَّذ، ولا بد أن يقدر الوكيل على رفض أي أمر لم يتوقعه.
- **وصول وكلاء الذكاء الاصطناعي.** يريد الناس أن يسألوا وكيلًا ذكيًا «هل النشر سليم؟» ويتركوه يقرأ السجلات أو يطلب رجوعًا إلى نسخة سابقة، دون أن يحصل على صلاحية root.

## 2. لماذا ToGO

احتاجت حوسبة إلى خلفية Go فيها مصادقة وتفويض وسجل تدقيق وطابور وزمن حقيقي (realtime) وmigrations، مع طريقة مضبوطة الأنواع لتوليد كود قاعدة البيانات. يقدّم ToGO هذه كإضافات (plugins) وأداة `togo` تشغّل `sqlc` و`gqlgen` وتصدير OpenAPI في خطوة واحدة (`togo generate`).

وأبني منتجات أخرى على ToGO، فتنتقل الاصطلاحات نفسها (الإضافات، وملف `togo.yaml`، وشجرة `.claude` للوكلاء) من مشروع إلى آخر. هذا سبب شخصي وليس حجة عامة.

## 3. المعمارية

**الإضافات.** يذكر `togo.yaml` ثلاثًا: `auth` و`audit` و`authz`. أما الملف `internal/plugins/plugins.gen.go` فيستورد تسعًا:

```go
// internal/plugins/plugins.gen.go
import (
	_ "github.com/togo-framework/audit"
	_ "github.com/togo-framework/auth"
	_ "github.com/togo-framework/authz"
	_ "github.com/togo-framework/cache"
	_ "github.com/togo-framework/db-postgres"
	_ "github.com/togo-framework/i18n"
	_ "github.com/togo-framework/queue"
	_ "github.com/togo-framework/realtime"
	_ "github.com/togo-framework/storage"
)
```

كما يطلب `go.mod` الحزم `orm` و`faker` و`validation` من المؤسسة نفسها.

**ثلاثة ملفات تنفيذية وتطبيق ويب.** `cmd/api` هو لوحة التحكم (REST على `/api`، والتوثيق على `/docs`، وMCP على `/api/mcp`). و`cmd/agent` هو `hosbah-agent`، ملف Go ثابت يعمل تحت systemd على كل سيرفر مُدار. و`cmd/hosbah` أداة سطر أوامر. وواجهة الويب بـNext.js في `web/` بالعربية (RTL) والإنجليزية.

**تعدد المستأجرين.** كل صف يخص مستأجرًا فيه `team_id`. ومسارات الفريق محددة بالمسار (`/api/teams/{team}/...`)، والفريق الذي لست عضوًا فيه يرد بـ404 تمامًا مثل فريق غير موجود. وكل استعلام على بيانات المستأجر يمر عبر دالة واحدة هي `tenancy.Tx` تثبّت المستخدم والفريق على المعاملة (transaction)، ثم يأتي Postgres Row-Level Security سندًا لكل `WHERE team_id`.

**الوكلاء.** يتصل الوكيل بلوحة التحكم من جهته عبر websocket ويجيب عن تحدٍّ موقّع. توقّع لوحة التحكم كل عملية بـed25519، ويتحقق الوكيل منها بالمفتاح الذي ثبّته عند التسجيل، ويرفض العمليات المعدّلة أو غير الموقّعة أو المنتهية أو الموجّهة لغيره أو من نوع غير معروف. وأنواع العمليات محددة وليست نصوص shell.

**MCP.** تقبل نقطة MCP الخاصة بالفريق رموزًا مرتبطة بفريق واحد. وكل أداة تعلن نطاقًا (read وdeploy وwrite وsecrets:read وdestructive)، وإذا غاب النطاق عن أداة يفشل اختبار.

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

دالة المعاملة، من `internal/tenancy/tx.go`:

```go
func Tx(ctx context.Context, sqlDB *sql.DB, s Scope, fn func(q *db.Queries, tx *sql.Tx) error) error {
	tx, err := sqlDB.BeginTx(ctx, nil)
	if err != nil {
		return err
	}
	defer tx.Rollback() //nolint:errcheck // no-op after Commit

	if err := apply(ctx, tx, s); err != nil {
		return err
	}
	if err := fn(db.New(tx), tx); err != nil {
		return err
	}
	return tx.Commit()
}
```

وفي الملف نفسه، توجيه معاملة مفتوحة إلى فريق يستخدم `set_config` وحجته الثالثة `true`، وهي الصيغة ذات المعاملات (parameterised) من `SET LOCAL`:

```go
_, err := tx.ExecContext(ctx, `SELECT set_config('app.team_id', $1, true)`, teamID)
```

يزول هذا الإعداد بانتهاء المعاملة، فلا يمكن لاتصال من مجمّع الاتصالات (pool) أن ينقل سياق فريق إلى الطلب التالي.

أدوار الفريق، من `internal/tenancy/roles.go`:

```go
const (
	RoleOwner    = "owner"
	RoleAdmin    = "admin"
	RoleDeployer = "deployer"
	RoleViewer   = "viewer"
)
```

ترتبط هذه الأدوار بإضافة `authz` في ToGO، وهي ترفض افتراضيًا: أي صلاحية لم تُمنح تُرفض.

وتتبع طبقات المسارات في `internal/server/authorization.go` الفكرة نفسها. كل ما تحت `/api` مغلق ما لم يُذكر بالاسم، في خمس طبقات: محجوب، وعام، وtoken (لـMCP)، وعضو، ومدير المنصة هو الافتراضي لأي مسار غير مدرج. فلا يصل إلى مسار مُولَّد حديثًا غير مدير المنصة حتى يقرر أحدهم غير ذلك.

## 5. الأرقام

عددتها على فرع `main` في المستودع بتاريخ 2026-10-19:

| ماذا | العدد | كيف عددته |
|---|---|---|
| migrations موثّقة بإصدارات | 124 | ملفات `.sql` في `db/atlas/migrations` |
| أجزاء المخطط (schema fragments) | 103 | ملفات `internal/db/schema` |
| إضافات ToGO المحمّلة | 9 | الاستيرادات الفارغة في `internal/plugins/plugins.gen.go` |
| أدوات MCP في خادم الفريق | 282 | تعريفات `Name: "...", Scope:` في `internal/mcp/*.go` (دون الاختبارات) |
| ملفات الاختبار | 262 | ملفات `*_test.go` |
| أدوار / صلاحيات الفريق | 4 / 13 | الثوابت في `internal/tenancy/roles.go` |

يشمل عدد MCP الأدوات المعلنة في سطر واحد بهذا النمط فقط. أما خادم مدير المنصة فمُعلن بطريقة مختلفة وليس ضمن الـ282، فاعتبر الرقم «على الأقل».

ليست لدي أرقام أداء أو مستخدمين، ولذلك لا أذكر أيًّا منها.

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

**الـCRUD المُولَّد لا يعرف الفِرق.** يستطيع ToGO توليد REST وGraphQL لمورد ما. وفي منتج متعدد المستأجرين هذا خطر، لأن المعالجات المُولَّدة لا تعرف الفريق. حوسبة لا تركّبها لجداول المستأجرين؛ فبيانات المستأجر لا تُبلَغ إلا عبر معالجات مكتوبة يدويًا في `internal/api`. وهذا عمل تنفذه بنفسك.

**إزالة إضافة تعديل يدوي.** يقول رأس `plugins.gen.go` ذلك: أُزيلت `auth-dev` و`dashboard` يدويًا لأن الأداة ليس فيها أمر إزالة. و`auth-dev` تسهيل لتسجيل الدخول أثناء التطوير لا يُشحن للإنتاج. كما تركت `auth-mfa` عمدًا بعد مراجعتها، والملف يسجّل السبب. فإن استخدمت الإضافات فاقرأ مساراتها.

**SQLite افتراضيًا وPostgres في الواقع.** المشغّل المدمج في الإطار هو SQLite. وحوسبة تستخدم `db-postgres`، وسير العمل في README هو توليد جزء مخطط بـ`togo make:resource` ثم تحويله إلى Postgres يدويًا ثم `togo migrate:diff`. وسياسات Row-Level Security هي migrations مكتوبة يدويًا (`*_row_level_security.sql`) لأن المولّد لا يعرفها.

**أين ساعد ToGO.** نواة واحدة وملف إعداد واحد وقائمة إضافات يعني أن المصادقة والتدقيق والتفويض والطابور والزمن الحقيقي والتخزين موجودة أصلًا، وأن `togo generate` يبقي مخرجات sqlc وgqlgen وOpenAPI متزامنة. وسجل migrations (`schema_migrations` مع checksum لكل ملف) يوقف التشغيل إذا عُدّل ملف طُبّق من قبل.

## 7. الروابط

- المنتج: [hosbah.com](https://hosbah.com)
- مقال المنتج: [منصة PaaS ذاتية الاستضافة على Proxmox](https://hosbah.com/ar/blog/self-hosted-paas-proxmox-push-to-deploy)
- مقال المؤسس: [لماذا بنيت حوسبة](https://fadymondy.com/ar/blog/why-i-built-hosbah)
- GitHub: [fadymondy/hosbah](https://github.com/fadymondy/hosbah)
- Product Hunt: [Hosbah](https://www.producthunt.com/products/hosbah)
- ToGO: [github.com/togo-framework/togo](https://github.com/togo-framework/togo)
- المعرض: [to-go.dev/ar/showcase](https://to-go.dev/ar/showcase)

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

### كيف تعزل حوسبة الفِرق عن بعضها؟
كل صف يخص مستأجرًا فيه `team_id`. ويجري كل استعلام داخل `tenancy.Tx` التي تضبط `app.user_id` و`app.team_id` للمعاملة، ويفرض Postgres Row-Level Security السياسات (FORCE مع دور ليس superuser). والمعالجات محددة بالفريق في المسار أيضًا، فهناك طبقتان.

### هل يولّد ToGO الواجهة البرمجية الواعية بالمستأجر؟
لا. الـCRUD المُولَّد لا يعرف الفريق، ولذلك لا تركّبه حوسبة لجداول المستأجرين.

### أي إضافات ToGO تحمّلها حوسبة؟
تسع: audit وauth وauthz وcache وdb-postgres وi18n وqueue وrealtime وstorage.

### هل حوسبة مفتوحة المصدر؟
نعم. المستودع عام، وتذكر GitHub أن رخصته AGPL-3.0.

## الخطوات التالية

- اقرأ [مستودع ToGO](https://github.com/togo-framework/togo) و[المعرض](https://to-go.dev/ar/showcase).
- تعرّف على ما تقدمه حوسبة لمستخدميها في [مقال المنتج](https://hosbah.com/ar/blog/self-hosted-paas-proxmox-push-to-deploy).
- اقرأ الكود: [`internal/tenancy`](https://github.com/fadymondy/hosbah/tree/main/internal/tenancy).

---

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