> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# Supabase على سيرفرك: بديل Firebase فوق PostgreSQL
- URL: https://arabroot.io/articles/تثبيت-supabase-على-سيرفرك/
- Published: 2026-10-08T09:15:00.000Z
- Updated: 2026-10-09T13:02:11.000Z
- Description: كيف تثبت Supabase على سيرفرك وتبني عليه تطبيقاً صغيراً بمستخدمين وصلاحيات وملفات، وما الذي ينقص النسخة المستضافة عن السحابية.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, الاستضافة الذاتية والتطبيقات, قواعد البيانات, DevOps وCI/CD, Docker والحاويات, الاستضافة الذاتية

في مقال [ما هو Backend as a Service؟](https://arabroot.io/articles/%D9%85%D8%A7-%D9%87%D9%88-backend-as-a-service-%D9%88%D8%A8%D8%AF%D8%A7%D8%A6%D9%84-firebase/) ذكرنا أن لكل خدمة بناء في Firebase بديلاً مفتوح المصدر، وأن التطبيق في هذا النموذج يكلم قاعدة البيانات مباشرة فتصبح قواعد الوصول هي الحارس الوحيد. وفي هذا الدليل سوف ننتقل من الفكرة إلى سيرفر يعمل، فنثبت Supabase بنسخته المستضافة ذاتياً Self-hosted بإصدارات مثبتة، ثم نبني عليه تطبيق ملاحظات صغيراً حقيقياً فيه تسجيل دخول لمستخدمين وجدول محمي بسياسات الصفوف Row-Level Security ومجلد ملفات خاص بكل مستخدم وتحديث فوري ودالة على السيرفر، ونرى بالمخرجات ماذا يحدث عندما يحاول مستخدم أن يقرأ بيانات غيره.

وSupabase منصة مبنية فوق PostgreSQL، فقاعدة البيانات عنده قاعدة علائقية كاملة وليست قاعدة مستندات كما في Firestore، والمصادقة والتخزين والتحديث الفوري كلها خدمات صغيرة تحفظ حالتها في القاعدة نفسها. وسوف نناقش في هذا الدليل ما يلي:

- ما الذي يعمل داخل Supabase المستضاف: أحد عشر Container خلف بوابة واحدة، وترخيص كل واحد منها.
- التثبيت من الإصدار الموسوم `self-hosted/v0.8.2`، وتوليد المفاتيح، وتقييد المنافذ على 127.0.0.1، وأول خطأ ستراه عند التشغيل.
- المفاتيح: أيها يوضع في التطبيق وأيها لا يخرج من السيرفر أبداً.
- قاعدة البيانات وسياسات RLS، وما يحدث لجدول نسيت أن تحميه.
- التخزين بمجلد لكل مستخدم، والتحديث الفوري Realtime، والدوال Edge Functions.
- تطبيق صغير كامل بمكتبة supabase-js ومخرجاته الحقيقية.
- ما الذي ينقص النسخة المستضافة مقارنة بالنسخة السحابية، بصراحة.
- ما تحتاجه قبل الإنتاج، ثم حذف المختبر.

## ما الذي يعمل داخل Supabase المستضاف؟

قد يبدو Supabase من الخارج منتجاً واحداً، لكنه في الواقع مجموعة من المشاريع المستقلة، أغلبها مكتوب بلغات مختلفة، تجمعها بوابة API Gateway تستقبل كل الطلبات على منفذ واحد ثم توجه كل مسار إلى الخدمة المسؤولة عنه. والصورة التالية تبين ما شغلناه في المختبر:

![تطبيق جوال ومتصفح المدير يتصلان ببوابة api-gw المبنية على Envoy على المنفذ 127.0.0.1:28000، والبوابة توجه المسارات auth/v1 وrest/v1 وrealtime/v1 وstorage/v1 وfunctions/v1 والمسار الرئيسي إلى خدمات auth وrest وrealtime وstorage وfunctions وstudio، وكلها تتصل بقاعدة PostgreSQL 17.6، وبجانبها Supavisor على المنفذ 25432](https://arabroot.io/content/images/2026/10/supabase-01-architecture.webp)

كل خدمة تحفظ حالتها في PostgreSQL، والبوابة هي المنفذ الوحيد الذي يصل إليه التطبيق

والخدمات كما في ملف `docker-compose.yml` للإصدار الذي شغلناه، مع ترخيص كل مشروع كما يظهر في مستودعه:

| الخدمة       | ما تفعله                                                                     | Docker Image                                                           | الترخيص                                                                                                                                     |
| ------------ | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| api-gw       | البوابة: تتحقق من وجود المفتاح وتوجه المسارات، وتحمي لوحة الإدارة بكلمة مرور | envoyproxy/envoy:v1.39.1                                               | [Apache-2.0](https://github.com/envoyproxy/envoy?ref=arabroot.io)                                                                           |
| auth         | المصادقة: التسجيل والدخول وإصدار JWT، واسمها القديم GoTrue                   | supabase/gotrue:v2.196.0                                               | [MIT](https://github.com/supabase/auth?ref=arabroot.io)                                                                                     |
| rest         | PostgREST: يحول جداول قاعدة البيانات إلى واجهة REST                          | postgrest/postgrest:v14.17                                             | [MIT](https://github.com/PostgREST/postgrest?ref=arabroot.io)                                                                               |
| realtime     | يرسل تغييرات الجداول إلى المشتركين عبر WebSocket                             | supabase/realtime:v2.134.10                                            | [Apache-2.0](https://github.com/supabase/realtime?ref=arabroot.io)                                                                          |
| storage      | رفع الملفات وتنزيلها، وصلاحياتها في PostgreSQL                               | supabase/storage-api:v1.74.0                                           | [Apache-2.0](https://github.com/supabase/storage?ref=arabroot.io)                                                                           |
| imgproxy     | تصغير الصور وتحويلها عند الطلب                                               | darthsim/imgproxy:v3.31.4                                              | [Apache-2.0](https://github.com/imgproxy/imgproxy?ref=arabroot.io)                                                                          |
| functions    | يشغل الدوال المكتوبة ب TypeScript على Deno                                   | supabase/edge-runtime:v1.76.2                                          | [MIT](https://github.com/supabase/edge-runtime?ref=arabroot.io)                                                                             |
| studio وmeta | لوحة الإدارة، وواجهة تقرأ بنية قاعدة البيانات للوحة                          | supabase/studio:2026.09.07-sha-7996410 وsupabase/postgres-meta:v0.99.0 | [Apache-2.0](https://github.com/supabase/supabase?ref=arabroot.io) و[Apache-2.0](https://github.com/supabase/postgres-meta?ref=arabroot.io) |
| db           | PostgreSQL 17 مع الإضافات التي تحتاجها بقية الخدمات                          | supabase/postgres:17.6.1.136                                           | [PostgreSQL](https://github.com/supabase/postgres?ref=arabroot.io)                                                                          |
| supavisor    | مجمع الاتصالات Connection Pooler أمام قاعدة البيانات                         | supabase/supavisor:2.9.12                                              | [Apache-2.0](https://github.com/supabase/supavisor?ref=arabroot.io)                                                                         |

ولاحظ أن خدمتي السجلات Logflare وVector لم تعودا جزءاً من التشغيل الافتراضي، وإنما ملف إضافي اسمه `docker-compose.logs.yml` تفعله إذا أردت، والسبب كما يذكر [دليل التثبيت الرسمي](https://supabase.com/docs/guides/self-hosting/docker?ref=arabroot.io) هو تقليل استهلاك الذاكرة. ومعنى المجمع Pooler وسبب وجوده شرحناه في [تجميع الاتصالات Connection Pooling](https://arabroot.io/articles/%D8%AA%D8%AC%D9%85%D9%8A%D8%B9-%D8%A7%D9%84%D8%A7%D8%AA%D8%B5%D8%A7%D9%84%D8%A7%D8%AA-connection-pooling-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/).

## ما الذي تحتاجه قبل أن تبدأ؟

- **سيرفر Linux مع Docker Engine وDocker Compose:** كما في [تثبيت Docker على Ubuntu](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-docker-%D8%B9%D9%84%D9%89-ubuntu/). وقد شغلنا المختبر على Docker Engine 29.8.0.
- **الذاكرة:** قسنا الاستهلاك بعد التشغيل ودون أي حمل فكان قرابة 1.2 جيجابايت للخدمات الإحدى عشرة مجتمعة، فسيرفر بذاكرة 4 جيجابايت هو أقل ما نقترحه لمشروع صغير، والقاعدة نفسها ستكبر مع بياناتك.
- **القرص:** حجم ال Images بعد التنزيل قرابة 9 جيجابايت، وأكبرها Studio وقاعدة البيانات بقرابة 1.7 جيجابايت لكل منهما، ثم بياناتك وملفاتك المرفوعة.
- **نطاق وReverse Proxy مع شهادة HTTPS:** فالبوابة تعمل ب HTTP فقط، وسوف نجعلها تستمع على 127.0.0.1 ونضع أمامها Caddy أو Nginx، كما شرحنا في [تثبيت Caddy](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-caddy-reverse-proxy/) و[تطبيقك خلف Reverse Proxy](https://arabroot.io/articles/%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D9%83-%D8%AE%D9%84%D9%81-reverse-proxy/).
- **سيرفر بريد SMTP:** لرسائل تأكيد التسجيل واستعادة كلمة المرور، فالإعداد الافتراضي يشير إلى سيرفر بريد وهمي، كما سيأتي.

## التثبيت من إصدار موسوم

الحل البسيط هو أن تنسخ المستودع من الفرع الرئيسي وتشغله، وهذا غير مناسب للإنتاج، والسبب أن الفرع الرئيسي يتغير كل يوم، فقد تنسخه اليوم بإصدار وغداً بإصدار آخر دون أن تعرف ما الذي تغير. والطريقة الصحيحة هي أن تنسخ إصداراً موسوماً Tag من إصدارات النسخة المستضافة، وكل إصدار منها يثبت أرقام ال Images كلها، وآخرها عند كتابة هذا الدليل هو [self-hosted/v0.8.2](https://github.com/supabase/supabase/releases/tag/self-hosted/v0.8.2?ref=arabroot.io) بتاريخ 23 سبتمبر 2026، وسجل الأرقام في ملف [versions.md](https://github.com/supabase/supabase/blob/self-hosted/v0.8.2/docker/versions.md?ref=arabroot.io). والخطوات كما في الدليل الرسمي:

```bash
git clone --depth 1 --branch self-hosted/v0.8.2 https://github.com/supabase/supabase
mkdir supabase-project
cp -rf supabase/docker/. supabase-project
cd supabase-project
cp .env.example .env
printf 'ref=self-hosted/v0.8.2\n' > .supabase-version
```

ولاحظ ملف `.supabase-version`، فهو يسجل الإصدار الذي بدأت منه، ويستخدمه سكربت `update.sh` لاحقاً ليعرف ما الذي تغير بين إصدارك والإصدار الجديد ويدمج التغييرات مع تعديلاتك، كما في [دليل التحديث](https://supabase.com/docs/guides/self-hosting/updating?ref=arabroot.io).

### توليد الأسرار والمفاتيح

ملف `.env.example` يأتي بكلمات مرور ومفاتيح تجريبية معروفة لكل من قرأ المستودع، وأول ما تفعله هو استبدالها، وفي المستودع سكربتان لذلك، الأول يولد الأسرار وكلمات المرور، والثاني يولد المفاتيح الجديدة وزوج المفاتيح غير المتماثلة Asymmetric Keys الذي توقع به التوكنات:

```bash
sh utils/generate-keys.sh --update-env
sh utils/add-new-auth-keys.sh --update-env
```

والمخرج سوف يكون كما يلي، وقد حذفنا القيم نفسها:

```text
JWT_SECRET=<redacted>

ANON_KEY=<redacted>
SERVICE_ROLE_KEY=<redacted>

SECRET_KEY_BASE=<redacted>
REALTIME_DB_ENC_KEY=<redacted>
VAULT_ENC_KEY=<redacted>
PG_META_CRYPTO_KEY=<redacted>
LOGFLARE_PUBLIC_ACCESS_TOKEN=<redacted>
LOGFLARE_PRIVATE_ACCESS_TOKEN=<redacted>
S3_PROTOCOL_ACCESS_KEY_ID=<redacted>
S3_PROTOCOL_ACCESS_KEY_SECRET=<redacted>
MINIO_ROOT_PASSWORD=<redacted>

POSTGRES_PASSWORD=<redacted>
DASHBOARD_PASSWORD=<redacted>

Updating .env...
```

والسكربت الثاني يحتاج Node.js 16 أو أحدث على السيرفر، وإذا لم يجده شغل Container مؤقتاً من `node:22-alpine`، ولاحظ أنه لا يعدل ملف `.env` فقط، وإنما يعدل `docker-compose.yml` أيضاً، فيفعل الأسطر التي تمرر المفاتيح العامة JWKS إلى خدمات المصادقة والتحديث الفوري والتخزين والدوال، وهذه أول تعديلاتك على ملف ال Compose، فانتبه لها عند التحديث.

### الإعدادات التي غيرناها

بعد ذلك نعدل العناوين في `.env`، ففي المختبر تعمل البوابة على المنفذ 28000 والتطبيق على 28080، وفي الإنتاج تضع هنا نطاقك ب HTTPS:

```ini
SUPABASE_PUBLIC_URL=http://127.0.0.1:28000
API_EXTERNAL_URL=http://127.0.0.1:28000/auth/v1
SITE_URL=http://127.0.0.1:28080
API_GW_HTTP_PORT=28000
ENABLE_EMAIL_AUTOCONFIRM=true
POOLER_TENANT_ID=lab-baas
```

في الإعداد أعلاه لاحظ التالي:

- `SUPABASE_PUBLIC_URL` هو العنوان الذي يصل منه التطبيق إلى البوابة، و`SITE_URL` هو عنوان تطبيقك الذي تعيد إليه روابط التأكيد واستعادة كلمة المرور.
- `ENABLE_EMAIL_AUTOCONFIRM=true` يجعل الحساب مؤكداً دون رسالة بريد، وفعلناه في المختبر فقط لأننا لم نربط سيرفر بريد، أما في الإنتاج فاتركه `false` واضبط إعدادات `SMTP_HOST` وأخواتها، والقيمة الافتراضية `SMTP_HOST=supabase-mail` سيرفر وهمي للتطوير. وإذا لم يكن عندك سيرفر بريد فقد شرحنا [البريد التفاعلي Transactional Email](https://arabroot.io/articles/%D8%A7%D9%84%D8%A8%D8%B1%D9%8A%D8%AF-%D8%A7%D9%84%D8%AA%D9%81%D8%A7%D8%B9%D9%84%D9%8A-transactional-email/) و[الإرسال عبر Amazon SES](https://arabroot.io/articles/%D8%A7%D8%B1%D8%B3%D8%A7%D9%84-%D8%A8%D8%B1%D9%8A%D8%AF%D9%83-%D8%B9%D8%A8%D8%B1-amazon-ses/).
- إذا كان التطبيق داخلياً لا يجوز لأي شخص أن يسجل فيه، فاجعل `DISABLE_SIGNUP=true` وأنشئ الحسابات من لوحة الإدارة.

أما في `docker-compose.yml` فغيرنا منفذ البوابة ومنافذ المجمع لتستمع على 127.0.0.1 فقط، والسبب أن Docker يفتح المنفذ المنشور على كل الواجهات ويتجاوز جدار الحماية UFW، كما شرحنا في [تأمين خادم VPS](https://arabroot.io/articles/%D8%AA%D8%A3%D9%85%D9%8A%D9%86-%D8%AE%D8%A7%D8%AF%D9%85-vps-%D9%85%D9%86-%D8%A3%D9%88%D9%84-%D8%AF%D8%AE%D9%88%D9%84/)، والوصول من الخارج يجب أن يكون عبر ال Reverse Proxy ب HTTPS فقط:

```yaml
  api-gw:
    ports:
      - 127.0.0.1:${API_GW_HTTP_PORT}:8000/tcp
  supavisor:
    ports:
      - 127.0.0.1:25432:5432
      - 127.0.0.1:26543:6543
```

ويجدر الإشارة هنا إلى أننا غيرنا أيضاً أسماء ال Containers لتبدأ ب `lab-baas-` حتى نحذفها بالاسم بعد التجربة، وجعلنا بيانات القاعدة والتخزين في Volumes مسماة بدل مجلدات داخل المشروع، وهذان تعديلان خاصان بمختبرنا ولا تحتاجهما على سيرفرك.

### التشغيل، وأول خطأ ستراه

الآن نقوم بتنزيل ال Images ثم التشغيل:

```bash
docker compose pull
docker compose up -d
```

وعند التشغيل الأول سوف تجد أن الأمر قد ينتهي بخطأ كهذا:

```text
 Container lab-baas-db Error dependency db failed to start
dependency failed to start: container lab-baas-db is unhealthy
```

وقد يبدو من الوهلة الأولى أن قاعدة البيانات معطلة، لكن السجل يقول غير ذلك، فقاعدة البيانات في التشغيل الأول تنفذ سكربتات التهيئة كلها، أي إنشاء المخططات Schemas والأدوار Roles والإضافات، وهذا أخذ عندنا وقتاً أطول من مهلة فحص الصحة Healthcheck، فاعتبرها Docker غير سليمة وأوقف الخدمات التي تعتمد عليها:

```bash
docker logs lab-baas-db 2>&1 | tail -4
```

```text
waiting for server to shut down.... 2026-10-07 15:16:07.615 UTC [84] LOG:  pg_cron scheduler shutting down
.................. done
server stopped

PostgreSQL init process complete; ready for start up.
```

والحل أن تنتظر حتى تكتمل التهيئة ثم تعيد الأمر نفسه، فيكمل تشغيل الخدمات المتبقية، ولا تقم بحذف ال Volume والبدء من جديد، لأن التهيئة الثانية ستأخذ الوقت نفسه. وبعد دقيقة تقريباً:

```bash
docker compose up -d
docker compose ps --format '{{.Name}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}'
```

والمخرج سوف يكون كما يلي:

```text
lab-baas-auth            supabase/gotrue:v2.196.0                 Up About a minute (healthy)
lab-baas-db              supabase/postgres:17.6.1.136             Up 2 minutes (healthy)   5432/tcp
lab-baas-edge-functions  supabase/edge-runtime:v1.76.2            Up 2 minutes (healthy)
lab-baas-envoy           envoyproxy/envoy:v1.39.1                 Up 2 minutes (healthy)   127.0.0.1:28000->8000/tcp
lab-baas-imgproxy        darthsim/imgproxy:v3.31.4                Up 3 minutes (healthy)   8080/tcp
lab-baas-meta            supabase/postgres-meta:v0.99.0           Up 57 seconds (healthy)  8080/tcp
lab-baas-pooler          supabase/supavisor:2.9.12                Up About a minute (healthy)  127.0.0.1:25432->5432/tcp, 127.0.0.1:26543->6543/tcp
lab-baas-realtime        supabase/realtime:v2.134.10              Up About a minute (healthy)
lab-baas-rest            postgrest/postgrest:v14.17               Up About a minute (healthy)  3000/tcp
lab-baas-storage         supabase/storage-api:v1.74.0             Up 54 seconds (healthy)  5000/tcp
lab-baas-studio          supabase/studio:2026.09.07-sha-7996410   Up 3 minutes (healthy)   3000/tcp
```

واستهلاك الذاكرة بعد التشغيل ودون أي حمل:

```bash
docker stats --no-stream --format '{{.Name}}\t{{.MemUsage}}' | grep lab-baas
```

```text
lab-baas-auth            13.07MiB / 31.29GiB
lab-baas-storage         224.7MiB / 31.29GiB
lab-baas-edge-functions  28.92MiB / 31.29GiB
lab-baas-meta            120.9MiB / 31.29GiB
lab-baas-realtime        231MiB / 31.29GiB
lab-baas-pooler          190.8MiB / 31.29GiB
lab-baas-rest            33.15MiB / 31.29GiB
lab-baas-envoy           22.26MiB / 31.29GiB
lab-baas-db              114.4MiB / 31.29GiB
lab-baas-studio          223.9MiB / 31.29GiB
lab-baas-imgproxy        19.91MiB / 31.29GiB
```

ثم نتأكد أن البوابة تعمل وأن خدمة المصادقة ترد، وأن الطلب دون مفتاح يرفض من البوابة قبل أن يصل إلى أي خدمة:

```bash
curl -s http://127.0.0.1:28000/auth/v1/health -H "apikey: $SUPABASE_PUBLISHABLE_KEY"
curl -s -i http://127.0.0.1:28000/rest/v1/notes | head -1
```

```text
{"version":"v2.196.0","name":"GoTrue","description":"GoTrue is a user registration and authentication API"}
HTTP/1.1 401 Unauthorized
```

## المفاتيح: ما الذي يوضع في التطبيق، وما الذي لا يخرج من السيرفر؟

بعد تشغيل السكربتين أصبح في ملف `.env` نوعان من المفاتيح، القديمة وهي توكنات JWT موقعة بسر مشترك، والجديدة وهي مفاتيح نصية تبدأ ببادئة تدل على نوعها، كما يشرح [توثيق مفاتيح API](https://supabase.com/docs/guides/api/api-keys?ref=arabroot.io):

```bash
grep -E '^(ANON_KEY|SUPABASE_PUBLISHABLE_KEY|SUPABASE_SECRET_KEY|JWT_KEYS)=' .env | sed -E 's/=(.{14}).*/=\1…/'
```

```text
ANON_KEY=eyJhbGciOiJIUz…
SUPABASE_PUBLISHABLE_KEY=sb_publishable…
SUPABASE_SECRET_KEY=sb_secret_qEMK…
JWT_KEYS=[{"kty":"EC","…
```

| المفتاح                                          | مكانه                       | ما يستطيعه                                                    |
| ------------------------------------------------ | --------------------------- | ------------------------------------------------------------- |
| SUPABASE\_PUBLISHABLE\_KEY والقديم ANON\_KEY     | كود التطبيق والمتصفح        | يعرف المشروع فقط، والصلاحيات تحددها سياسات RLS وتوكن المستخدم |
| SUPABASE\_SECRET\_KEY والقديم SERVICE\_ROLE\_KEY | السيرفر والدوال فقط         | يتجاوز سياسات RLS كلها                                        |
| JWT\_KEYS وJWT\_SECRET                           | ملف .env فقط                | توقيع توكنات المستخدمين، ومن يملكه يصنع توكن أي مستخدم        |
| POSTGRES\_PASSWORD وDASHBOARD\_PASSWORD          | ملف .env ومدير كلمات المرور | قاعدة البيانات ولوحة الإدارة كاملة                            |

## لوحة الإدارة Studio

لوحة الإدارة على العنوان نفسه، أي `http://127.0.0.1:28000/`، وتحميها البوابة بكلمة مرور من نوع HTTP Basic Auth، فالطلب دونها يرد ب 401، واسم المستخدم وكلمة المرور هما `DASHBOARD_USERNAME` و`DASHBOARD_PASSWORD`. والصورة التالية من لوحتنا بعد إنشاء الجدول وسياساته في القسم التالي:

![لوحة Supabase Studio المستضافة على صفحة Policies، وفيها الجدول notes وأربع سياسات: delete own notes وinsert own notes وread own notes وupdate own notes، وكلها مطبقة على الدور authenticated، وزر Disable RLS](https://arabroot.io/content/images/2026/10/supabase-03-studio-policies.webp)

سياسات جدول الملاحظات كما تظهر في Studio على سيرفرنا

ولاحظ أن قائمة المصادقة في النسخة المستضافة فيها المستخدمون والسياسات فقط، كما في الصورة التالية، أما مزودو الدخول ورسائل البريد وعناوين إعادة التوجيه، والتي تضبطها من اللوحة في النسخة السحابية، فتضبطها هنا من ملف `.env` ثم تعيد تشغيل خدمة المصادقة:

![صفحة Users في Studio المستضاف وفيها مستخدمان omar@example.com وsara@example.com مع معرف كل منهما ومزود الدخول Email، وفي القائمة الجانبية Users وPolicies فقط](https://arabroot.io/content/images/2026/10/supabase-04-studio-users.webp)

إدارة المستخدمين من اللوحة، والإعدادات من ملف .env

## قاعدة البيانات وسياسات الصفوف RLS

تطبيقنا تطبيق ملاحظات، ولكل ملاحظة مالك، ولا يرى أي مستخدم إلا ملاحظاته. وقد شرحنا سياسات الصفوف بالتفصيل في [سياسات الصفوف Row-Level Security خطوة بخطوة](https://arabroot.io/articles/%D8%A8%D9%86%D8%A7%D8%A1-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82-saas-%D9%85%D8%AA%D8%B9%D8%AF%D8%AF-%D8%A7%D9%84%D9%85%D8%B3%D8%AA%D8%A7%D8%AC%D8%B1%D9%8A%D9%86-%D8%B9%D9%85%D9%84%D9%8A%D8%A7/#rls)، فلن نعيد شرحها هنا، والجديد في Supabase أن الدالة `auth.uid()` تقرأ معرف المستخدم من التوكن الذي أرسله التطبيق، وأن الدور `authenticated` هو الدور الذي يعمل به كل طلب يحمل توكن مستخدم مسجل. والملف التالي ينشئ الجدول والسياسات ويضيف الجدول إلى التحديث الفوري وينشئ مجلد التخزين وسياساته:

```sql
-- notes: every row belongs to the user who created it
create table public.notes (
  id         bigint generated always as identity primary key,
  owner_id   uuid not null default auth.uid() references auth.users (id) on delete cascade,
  title      text not null check (char_length(title) between 1 and 200),
  body       text not null default '',
  created_at timestamptz not null default now()
);
create index notes_owner_idx on public.notes (owner_id);

alter table public.notes enable row level security;

create policy "read own notes"   on public.notes for select to authenticated
  using ((select auth.uid()) = owner_id);
create policy "insert own notes" on public.notes for insert to authenticated
  with check ((select auth.uid()) = owner_id);
create policy "update own notes" on public.notes for update to authenticated
  using ((select auth.uid()) = owner_id) with check ((select auth.uid()) = owner_id);
create policy "delete own notes" on public.notes for delete to authenticated
  using ((select auth.uid()) = owner_id);

-- realtime: send changes of this table to subscribed clients
alter publication supabase_realtime add table public.notes;

-- storage: a private bucket, each user writes and reads only inside a folder named with his id
insert into storage.buckets (id, name, public) values ('attachments', 'attachments', false);

create policy "upload to own folder" on storage.objects for insert to authenticated
  with check (bucket_id = 'attachments' and (storage.foldername(name))[1] = (select auth.uid())::text);
create policy "read own folder" on storage.objects for select to authenticated
  using (bucket_id = 'attachments' and (storage.foldername(name))[1] = (select auth.uid())::text);
```

```bash
docker exec -i lab-baas-db psql -U postgres -v ON_ERROR_STOP=1 < schema.sql
```

```text
CREATE TABLE
CREATE INDEX
ALTER TABLE
CREATE POLICY
CREATE POLICY
CREATE POLICY
CREATE POLICY
ALTER PUBLICATION
INSERT 0 1
CREATE POLICY
CREATE POLICY
```

في الإعداد أعلاه لاحظ التالي:

- العمود `owner_id` قيمته الافتراضية `auth.uid()`، فالتطبيق لا يرسل المالك أصلاً، والقاعدة تأخذه من التوكن، وسياسة الإضافة تمنع من يحاول أن يرسل مالكاً غيره، كما سنرى في المخرجات.
- كتبنا `(select auth.uid())` داخل `select` وليس `auth.uid()` وحدها، والسبب أن PostgreSQL ينفذها مرة واحدة للاستعلام بدل مرة لكل صف، وهذه من [توصيات الأداء في توثيق Supabase](https://supabase.com/docs/guides/database/postgres/row-level-security?ref=arabroot.io#rls-performance-recommendations)، ومعها الفهرس على `owner_id`.
- السياسات مكتوبة للدور `authenticated` فقط، فالزائر غير المسجل، ودوره `anon`، لا توجد له سياسة، فلا يرى شيئاً.
- سياسات التخزين تستخدم الدالة `storage.foldername()` لتأخذ اسم المجلد الأول في مسار الملف، كما في [دوال التخزين المساعدة](https://supabase.com/docs/guides/storage/schema/helper-functions?ref=arabroot.io)، فالمستخدم يكتب ويقرأ داخل مجلد اسمه معرفه فقط.

والصورة التالية تبين ما يحدث لطلب سارة من التطبيق حتى يعود بصفوفها:

![ثلاث خطوات: هاتف سارة يرسل GET /rest/v1/notes مع المفتاح العام وتوكن JWT، ثم PostgREST ينفذ SET ROLE authenticated ويضع معرف سارة في claims، ثم داخل PostgreSQL تطبق السياسة read own notes فيمر صفان مالكهما سارة ويستبعد صف مالكه عمر، ويعود الرد بصفين](https://arabroot.io/content/images/2026/10/supabase-02-rls-request.webp)

التصفية تحدث داخل قاعدة البيانات، فلا يصل إلى التطبيق صف لا يملكه المستخدم

### ماذا يحدث لجدول نسيت أن تحميه؟

الآن سوف نكسر الإعداد عن قصد، فننشئ جدول عملاء دون أن نفعل عليه RLS، ثم نطلبه بالمفتاح العام فقط، أي بالمفتاح الموجود في كود كل نسخة من تطبيقك، ودون أي تسجيل دخول:

```bash
docker exec lab-baas-db psql -U postgres -c "create table public.customers (id int primary key, name text, phone text); insert into public.customers values (1,'شركة الأفق','0500000001'),(2,'مؤسسة النخبة','0500000002');"
curl -s "http://127.0.0.1:28000/rest/v1/customers?select=*" -H "apikey: $SUPABASE_PUBLISHABLE_KEY"
```

```text
CREATE TABLE
INSERT 0 2
[{"id":1,"name":"شركة الأفق","phone":"0500000001"},
 {"id":2,"name":"مؤسسة النخبة","phone":"0500000002"}]
```

وعند تشغيل الأمر سوف تجد أن الجدول كله عاد لزائر مجهول، والسبب أن الجداول الجديدة في المخطط `public` تصبح جزءاً من الواجهة البرمجية تلقائياً، وغياب RLS يعني غياب أي تصفية. والآن نفعل RLS دون أن نكتب أي سياسة:

```bash
docker exec lab-baas-db psql -U postgres -c "alter table public.customers enable row level security;"
curl -s "http://127.0.0.1:28000/rest/v1/customers?select=*" -H "apikey: $SUPABASE_PUBLISHABLE_KEY"
```

```text
ALTER TABLE
[]
```

فالجدول المفعل عليه RLS دون سياسات مغلق للجميع إلا المفتاح السري، ثم تفتح منه ما تحتاجه بسياسة. ولهذا تعرض لوحة Studio في صفحة السياسات اقتراحاً بإنشاء Trigger يفعل RLS تلقائياً على كل جدول جديد، كما ظهر في الصورة السابقة، وهو اقتراح يستحق أن تقبله. ولاحظ أيضاً أن الطلب الأول بعد إنشاء الجدول مباشرة قد يرد بأن الجدول غير موجود في ذاكرة PostgREST:

```text
{"code":"PGRST205","details":null,"hint":null,"message":"Could not find the table 'public.customers' in the schema cache"}
```

وهذا ليس خطأ في الصلاحيات، وإنما PostgREST لم يحدث نسخته من بنية القاعدة بعد، وثوان قليلة تكفي.

## تطبيق صغير كامل: المصادقة والبيانات والتخزين والتحديث الفوري والدوال

قبل التطبيق نحتاج الدالة التي سيستدعيها، وفي النسخة المستضافة لا يوجد أمر نشر للدوال كما في النسخة السحابية، وإنما تضع مجلد الدالة داخل `volumes/functions`، وخدمة الدوال تحمله عند أول طلب. والدالة التالية تحسب عدد ملاحظات المستخدم وكلماتها، وتعمل بتوكن المستخدم نفسه وليس بالمفتاح السري، فتبقى سياسات RLS مطبقة عليها:

```ts
// note-stats: counts the caller's notes and words, with the caller's own token, so RLS still applies
import { createClient } from "npm:@supabase/supabase-js@2.117.3"

Deno.serve(async (req) => {
  const auth = req.headers.get("Authorization") ?? ""
  const supabase = createClient(
    Deno.env.get("SUPABASE_URL")!,
    Deno.env.get("SUPABASE_ANON_KEY")!,
    { global: { headers: { Authorization: auth } } },
  )
  const { data: { user }, error: userError } = await supabase.auth.getUser()
  if (userError || !user) return Response.json({ error: "not signed in" }, { status: 401 })

  const { data, error } = await supabase.from("notes").select("body")
  if (error) return Response.json({ error: error.message }, { status: 500 })

  const words = data.reduce((n, r) => n + r.body.split(/\s+/).filter(Boolean).length, 0)
  return Response.json({ user: user.email, notes: data.length, words })
})
```

ويجدر الإشارة هنا إلى أن التحقق من التوكن قبل الوصول إلى الدالة يضبطه في النسخة المستضافة متغير واحد هو `FUNCTIONS_VERIFY_JWT` لكل الدوال معاً، وفي ملف ال Compose نفسه تعليق يقول إن ضبطه لكل دالة على حدة ما زال مهمة لم تنفذ، وقيمته الافتراضية `false`، ولذلك تتحقق دالتنا من المستخدم بنفسها عبر `auth.getUser()` وترد ب 401 إذا لم تجده.

والآن التطبيق نفسه، وهو سكربت Node.js يمثل ما يفعله تطبيق الويب أو الجوال، ويستخدم المفتاح العام فقط، ويمر على كل الخدمات بالترتيب:

```js
// A small notes app against our self-hosted Supabase: auth, RLS, storage, realtime and an edge function
import { createClient } from '@supabase/supabase-js'

const URL = 'http://127.0.0.1:28000'
const KEY = process.env.SUPABASE_PUBLISHABLE_KEY          // the public key, safe to ship in a web or mobile app
const client = () => createClient(URL, KEY, { auth: { persistSession: false } })
const sleep = (ms) => new Promise(r => setTimeout(r, ms))

// 1) two users sign up
const sara = client(), omar = client()
const s = await sara.auth.signUp({ email: 'sara@example.com', password: 'Lab-Pass-2026!' })
const o = await omar.auth.signUp({ email: 'omar@example.com', password: 'Lab-Pass-2026!' })
const saraId = s.data.user.id, omarId = o.data.user.id
console.log('signup  sara:', saraId, ' omar:', omarId)

// 2) realtime: omar listens to every change on notes
const events = []
const channel = omar.channel('notes-feed')
  .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'notes' }, (p) => events.push(p.new.title))
// wait until Realtime confirms it is reading changes from PostgreSQL, not only that the socket joined
const ready = new Promise(r => channel.on('system', {}, (m) => m.extension === 'postgres_changes' && m.status === 'ok' && r(m.message)))
channel.subscribe()
console.log('realtime omar:', await ready)

// 3) each user writes notes; owner_id comes from auth.uid() in the database, not from the client
await sara.from('notes').insert([{ title: 'خطة الإطلاق', body: 'نراجع الخطة يوم الأحد مع الفريق' }, { title: 'موردون', body: 'قائمة الموردين المعتمدين' }])
await omar.from('notes').insert({ title: 'ملاحظة عمر', body: 'تجربة' })
await sleep(1500)

const sNotes = await sara.from('notes').select('id,title')
const oNotes = await omar.from('notes').select('id,title')
console.log('select  sara sees:', sNotes.data.map(n => n.title))
console.log('select  omar sees:', oNotes.data.map(n => n.title))
console.log('realtime omar received:', events)

// 4) omar tries to change sara's note, and an anonymous visitor tries to read
const target = sNotes.data[0].id
const upd = await omar.from('notes').update({ title: 'hacked' }).eq('id', target).select()
console.log('update  omar on sara note', target, '->', upd.error?.message ?? `${upd.data.length} rows`)
const forged = await omar.from('notes').insert({ title: 'forged', owner_id: saraId })
console.log('insert  omar as sara ->', forged.error?.message)
const anon = await client().from('notes').select('*')
console.log('select  anonymous ->', anon.data)

// 5) storage: private bucket, a folder per user
const up1 = await sara.storage.from('attachments').upload(`${saraId}/plan.txt`, new Blob(['خطة الإطلاق']), { contentType: 'text/plain' })
console.log('upload  sara -> own folder:', up1.error?.message ?? up1.data.path)
const up2 = await sara.storage.from('attachments').upload(`${omarId}/x.txt`, new Blob(['x']))
console.log('upload  sara -> omar folder:', up2.error?.message)
const dl = await omar.storage.from('attachments').download(`${saraId}/plan.txt`)
console.log('download omar <- sara file:', dl.error ? `${dl.error.status} ${dl.error.message}` : 'OK (unexpected)')
const signed = await sara.storage.from('attachments').createSignedUrl(`${saraId}/plan.txt`, 60)
console.log('signed  url for 60s:', signed.data.signedUrl.replace(/token=.*/, 'token=…'))

// 6) edge function, called with sara's session token
const fn = await sara.functions.invoke('note-stats')
console.log('function sara:', fn.data)
const raw = await fetch(`${URL}/functions/v1/note-stats`, { headers: { apikey: KEY } })
console.log('function without login:', raw.status, await raw.text())

await omar.removeChannel(channel)
process.exit(0)
```

```bash
npm i @supabase/supabase-js@2.117.3
SUPABASE_PUBLISHABLE_KEY=$(grep '^SUPABASE_PUBLISHABLE_KEY=' .env | cut -d= -f2) node app.mjs
```

والمخرج سوف يكون كما يلي:

```text
signup  sara: 938ffcd5-c202-4931-9b2a-23d090715411  omar: a88f14d8-aed3-4e9d-acf5-6686ccf40b53
realtime omar: Subscribed to PostgreSQL
select  sara sees: [ 'خطة الإطلاق', 'موردون' ]
select  omar sees: [ 'ملاحظة عمر' ]
realtime omar received: [ 'ملاحظة عمر' ]
update  omar on sara note 6 -> 0 rows
insert  omar as sara -> new row violates row-level security policy for table "notes"
select  anonymous -> []
upload  sara -> own folder: 938ffcd5-c202-4931-9b2a-23d090715411/plan.txt
upload  sara -> omar folder: new row violates row-level security policy
download omar <- sara file: 400 Object not found
signed  url for 60s: http://127.0.0.1:28000/storage/v1/object/sign/attachments/938ffcd5-c202-4931-9b2a-23d090715411/plan.txt?token=…
function sara: { user: 'sara@example.com', notes: 2, words: 9 }
function without login: 401 {"error":"not signed in"}
```

في الإعداد أعلاه لاحظ التالي:

- كل مستخدم رأى ملاحظاته فقط، مع أن الاستعلام نفسه `select('id,title')` لا يحمل أي شرط، فالشرط في السياسة داخل القاعدة.
- محاولة عمر تعديل ملاحظة سارة لم ترجع خطأ، وإنما رجعت `0 rows`، والسبب أن سياسة التعديل تخفي الصف عنه أصلاً، فلا يجد ما يعدله، وهذا أفضل من رسالة خطأ تؤكد له أن الصف موجود.
- محاولة عمر أن يضيف ملاحظة باسم سارة رفضتها سياسة الإضافة `with check`.
- عمر لم يستطع تنزيل ملف سارة، والرد كان `Object not found` وليس رفض صلاحية، للسبب نفسه: الملف غير موجود بالنسبة له. وإذا أرادت سارة أن تشارك الملف فالطريقة الصحيحة هي الرابط الموقع Signed URL لمدة محددة.
- الدالة عملت بتوكن سارة فحسبت ملاحظتيها فقط، أي تسع كلمات، وبدون تسجيل دخول ردت ب 401.

### التحديث الفوري Realtime، والخطأ الذي وقعنا فيه أول مرة

عمر اشترك في كل إضافة على جدول الملاحظات، ومع ذلك وصله حدث ملاحظته هو فقط، والسبب أن خدمة Realtime تتحقق لكل مشترك هل تسمح له سياسة القراءة برؤية هذا الصف قبل أن ترسله، كما يذكر [توثيق Postgres Changes](https://supabase.com/docs/guides/realtime/postgres-changes?ref=arabroot.io)، والصورة التالية تبين ذلك:

![عمر وسارة يضيفان ملاحظات إلى PostgreSQL، والتغيير يكتب في سجل WAL ويخرج عبر publication اسمها supabase_realtime إلى خدمة Realtime، والخدمة تسأل لكل مشترك هل يسمح له RLS برؤية الصف، فتصل ملاحظة عمر إلى عمر ولا تصل ملاحظة سارة إليه](https://arabroot.io/content/images/2026/10/supabase-05-realtime.webp)

الاشتراك في كل التغييرات لا يتجاوز سياسات القراءة

لكن في التشغيل الأول للتطبيق كانت قائمة الأحداث فارغة تماماً، أي أن عمر لم يستلم حتى ملاحظته:

```text
realtime omar subscribed
...
realtime omar received: []
```

وكان الكود الأول ينتظر حالة `SUBSCRIBED` ثم يضيف الملاحظات مباشرة، وهذه الحالة تعني أن قناة WebSocket انضمت فقط، أما قراءة التغييرات من قاعدة البيانات فتبدأ بعد لحظات، وتعلن عنها الخدمة برسالة نظام System Message مستقلة، كما ظهر عندما طبعنا كل الرسائل في سكربت صغير:

```text
STATUS SUBSCRIBED
SYSTEM {"message":"Subscribed to PostgreSQL","status":"ok","extension":"postgres_changes","channel":"t"}
EVENT INSERT rt test
```

ولذلك ينتظر التطبيق في نسخته النهائية رسالة `Subscribed to PostgreSQL` قبل أن يعتمد على الأحداث، وهذا مهم في الاختبارات الآلية خصوصاً، أما في تطبيق حقيقي فالمستخدم عادة لا يضيف شيئاً في أول ثانية، لكن لا تقم بالاعتماد على Realtime وحده لحالة لا تحتمل فقد حدث، والسبب أن الاتصال قد ينقطع، فاقرأ البيانات من الجدول عند فتح الشاشة وعند عودة الاتصال، واجعل Realtime لتحديث الشاشة بين ذلك.

### حذف الملفات من قاعدة البيانات مباشرة

عندما أردنا تفريغ المختبر لإعادة التجربة حاولنا حذف صفوف الملفات من جدول `storage.objects` مباشرة، فرفضت القاعدة:

```text
ERROR:  Direct deletion from storage tables is not allowed. Use the Storage API instead.
HINT:  This prevents accidental data loss from orphaned objects.
CONTEXT:  PL/pgSQL function storage.protect_delete() line 5 at RAISE
```

والسبب أن الجدول يحفظ وصف الملف فقط، والملف نفسه في التخزين، فحذف الصف وحده يترك الملف دون سجل يدل عليه، ولذلك يحذف الملف عبر واجهة التخزين، بالمفتاح السري من السيرفر إذا كان الحذف إدارياً.

## ما الذي ينقص النسخة المستضافة مقارنة بالسحابية؟

صفحة [الميزات](https://supabase.com/docs/guides/getting-started/features?ref=arabroot.io) في Supabase فيها خيار لعرض ميزات النسخة المستضافة فقط، والبيانات نفسها في ملف [features.tsx](https://github.com/supabase/supabase/blob/self-hosted/v0.8.2/apps/www/data/features.tsx?ref=arabroot.io) في المستودع، وعند الإصدار الذي ثبتناه كانت الميزات المعلمة بأنها غير متاحة للنسخة المستضافة اثنتي عشرة ميزة: النطاقات المخصصة Custom Domains، والفروع Branching لبيئات المعاينة، ومزود Terraform، ونسخ القراءة Read Replicas، وخطوط Supabase Pipelines، ومخازن التحليلات Analytics Buckets ومخازن المتجهات Vector Buckets، وتشغيل الدوال في مناطق متعددة Regional Invocations، وواجهة الإدارة Management API، وصلاحيات أعضاء الفريق RBAC، وشهادة SOC 2، والربط الخاص PrivateLink.

وبجانب هذه القائمة لاحظنا في المختبر ما يلي:

- **مشروع واحد:** اللوحة تعرض مشروعاً واحداً اسمه Default Project، فإذا أردت عدة مشاريع منفصلة فكل مشروع تثبيت مستقل بقاعدة ومنافذ خاصة به.
- **الإعدادات من ملف `.env`:** مزودو الدخول وقوالب البريد وعناوين إعادة التوجيه لا تظهر في اللوحة، وتغييرها يحتاج تعديل الملف وإعادة تشغيل الخدمة.
- **النسخ الاحتياطي عليك:** صفحة الميزات تعد النسخ الاحتياطي متاحاً للنسخة المستضافة لكن بأداة تجهزها أنت مثل wal-g، فلا توجد نسخ يومية تلقائية ولا استعادة لنقطة زمنية Point-in-Time Recovery إلا إذا بنيتها، وأقل ما تفعله هو `pg_dump` مجدول مع نسخة من ملفات التخزين و`.env`، ومراحل النسخ الاحتياطي في [التشغيل والصيانة](https://arabroot.io/start/#stage-operate).
- **السجلات اختيارية:** بدون ملف `docker-compose.logs.yml` لا توجد صفحة سجلات في اللوحة، وتقرأ السجلات بالأمر `docker logs`.
- **الدوال:** لا يوجد أمر نشر، والتحقق من التوكن إعداد واحد لكل الدوال كما ذكرنا.
- **الدعم:** النسخة المستضافة يدعمها المجتمع، كما يقول ملف README في مجلد docker، فلا يوجد عقد دعم من Supabase لها.

وكل هذا لا يغير أن الجزء الأهم، أي المصادقة وقاعدة البيانات والسياسات والتخزين والتحديث الفوري والدوال، يعمل كاملاً على سيرفرك بترخيص مفتوح، وأن مكتبة supabase-js نفسها تعمل مع النسختين دون تغيير في الكود.

## قبل الإنتاج

- ضع البوابة خلف Reverse Proxy بشهادة HTTPS، واجعل `SUPABASE_PUBLIC_URL` و`API_EXTERNAL_URL` و`SITE_URL` عناوين HTTPS الحقيقية.
- لا تنشر منفذ المجمع 5432 على الإنترنت، واتصل بقاعدة البيانات من سيرفراتك عبر شبكة خاصة أو نفق، كما في [WireGuard](https://arabroot.io/articles/%D8%B4%D8%A8%D9%83%D8%A9-%D8%AE%D8%A7%D8%B5%D8%A9-%D9%85%D8%B9-wireguard-%D9%88-wg-easy/).
- اضبط سيرفر بريد حقيقياً، واجعل `ENABLE_EMAIL_AUTOCONFIRM=false`.
- فعل RLS على كل جدول في `public`، واختبر كل جدول بطلب مجهول وبطلب من مستخدم آخر كما فعلنا.
- جدول النسخ الاحتياطي وجرب الاستعادة على سيرفر آخر قبل أن تحتاجها.
- احفظ ملف `.env` في مدير أسرار أو مدير كلمات مرور، فضياعه يعني ضياع مفاتيح توقيع التوكنات.
- حدث عبر `update.sh` من إصدار موسوم إلى إصدار موسوم، بعد نسخة احتياطية، وراجع ملف CHANGELOG لأن بعض الإصدارات تغير ملفات التهيئة.

## حذف المختبر

حذفنا ما أنشأناه فقط، بالاسم:

```bash
docker compose -p lab-baas down -v
```

وهذا الأمر يحذف ال Containers والشبكة وال Volumes التابعة لمشروع `lab-baas` فقط، ولا تقم بتشغيل `docker system prune` أو `docker volume prune` على سيرفر عليه خدمات أخرى، والسبب أنها تحذف كل ما لا يعمل في تلك اللحظة، ولو كان لمشروع آخر.

## الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

- Supabase المستضاف أحد عشر Container خلف بوابة Envoy واحدة، كلها مفتوحة المصدر بتراخيص Apache-2.0 وMIT وPostgreSQL، وتحفظ حالتها في PostgreSQL 17.
- ثبت من إصدار موسوم مثل `self-hosted/v0.8.2`، وولد المفاتيح بالسكربتين، واجعل المنافذ على 127.0.0.1 خلف Reverse Proxy.
- المفتاح العام مكانه التطبيق، والمفتاح السري ومفاتيح التوقيع لا تخرج من السيرفر.
- جدول دون RLS مفتوح لكل من يملك المفتاح العام، وجدول عليه RLS دون سياسات مغلق للجميع، والسياسات تطبق على القراءة والكتابة والتخزين والتحديث الفوري معاً.
- انتظر رسالة `Subscribed to PostgreSQL` قبل الاعتماد على Realtime، واقرأ البيانات من الجدول عند عودة الاتصال.
- النسخة المستضافة ينقصها الفروع ونسخ القراءة وواجهة الإدارة وتعدد المشاريع والنسخ الاحتياطي التلقائي، فخطط لها قبل أن تعتمد عليها.

[![](https://arabroot.io/content/images/2026/10/baas-basics-cover-cover.webp)دليلما هو Backend as a Service؟ وما بديل كل خدمة في Firebaseما الذي يقدمه ال Backend الجاهز لتطبيقك، ولماذا تصبح قواعد الوصول هي الحارس الوحيد لبياناتك، وما البديل المفتوح لكل خدمة في Firebase.](https://arabroot.io/articles/%D9%85%D8%A7-%D9%87%D9%88-backend-as-a-service-%D9%88%D8%A8%D8%AF%D8%A7%D8%A6%D9%84-firebase/)[![](https://arabroot.io/content/images/2026/10/multi-tenancy-practice-cover-cover.webp)دليلتطبيق SaaS متعدد المستأجرين عملياً مع Row-Level Securityنبني على PostgreSQL تطبيق SaaS يعزل كل مستأجر Tenant عن غيره بسياسات Row-Level Security، ونرى أين تتسرب البيانات رغم ذلك.](https://arabroot.io/articles/%D8%A8%D9%86%D8%A7%D8%A1-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82-saas-%D9%85%D8%AA%D8%B9%D8%AF%D8%AF-%D8%A7%D9%84%D9%85%D8%B3%D8%AA%D8%A7%D8%AC%D8%B1%D9%8A%D9%86-%D8%B9%D9%85%D9%84%D9%8A%D8%A7/)[![](https://arabroot.io/content/images/2026/10/postgres-for-everything-cover-cover.webp)دليلPostgreSQL لكل شيء: متى يكفيك ومتى تحتاج غيره؟كم عملاً يستطيع PostgreSQL واحد أن يقوم به في شركتك الناشئة قبل أن تحتاج Redis أو Elasticsearch، وما الإشارة التي تقول لك إن الوقت قد حان.](https://arabroot.io/articles/postgresql-%D9%84%D9%83%D9%84-%D8%B4%D9%8A%D8%A1-%D9%85%D8%AA%D9%89-%D8%AA%D9%83%D9%81%D9%8A%D9%83-%D9%82%D8%A7%D8%B9%D8%AF%D8%A9-%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA-%D9%88%D8%A7%D8%AD%D8%AF%D8%A9/)[![](https://arabroot.io/content/images/2026/10/connection-pooling-cover-cover.webp)دليلConnection Pooling: لماذا الاتصال بقاعدة البيانات مكلفلماذا تظهر رسالة too many clients عندما تزيد نسخ تطبيقك، وكيف يحل مجمع الاتصالات وPgBouncer هذه المشكلة.](https://arabroot.io/articles/%D8%AA%D8%AC%D9%85%D9%8A%D8%B9-%D8%A7%D9%84%D8%A7%D8%AA%D8%B5%D8%A7%D9%84%D8%A7%D8%AA-connection-pooling-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)

## سجل التحديثات

- أكتوبر 2026: كتابة الدليل، وتنفيذ المختبر على Supabase self-hosted v0.8.2 مع PostgreSQL 17.6 وsupabase-js 2.117.3 وNode.js 22.14 على Docker Engine 29.8.0.