---
title: Postgres Row-Level Security لتطبيقات الـ Multi-Tenant (2026)
description: 'تنفيذ أمن مستوى الصفوف (RLS) في Postgres للتطبيقات متعددة المستأجرين باستخدام node-postgres: تفعيل RLS، وكتابة سياسات current_setting، وتجنب فخ تسريب الـ pool.'
url: https://nerdleveltech.com/ar/postgres-row-level-security-multi-tenant-nodejs-tutorial
language: ar
date: '2026-06-27'
category: backend
tags:
  - postgres
  - row-level security
  - multi-tenant
  - node-postgres
  - rls
  - backend
  - security
markdown_url: https://nerdleveltech.com/ar/postgres-row-level-security-multi-tenant-nodejs-tutorial.md
attribution: 'Source: NerdLevelTech — https://nerdleveltech.com/ar/postgres-row-level-security-multi-tenant-nodejs-tutorial'
terms: https://nerdleveltech.com/llms.txt
---

# Postgres Row-Level Security لتطبيقات الـ Multi-Tenant (2026)

تفرض سياسة أمان مستوى الصف (RLS) في Postgres عزل المستأجرين المتعددين (multi-tenant) في قاعدة البيانات: قم بتمكين RLS على كل جدول، واكتب سياسة تقارن `tenant_id` بمتغير جلسة خاص بكل طلب، وقم بتوصيل node-postgres كدور بأقل الصلاحيات، واضبط المستأجر باستخدام `set_config(..., true)` داخل معاملة (transaction) حتى لا يتسرب أبدًا عبر الاتصالات المجمعة (pooled connections).

## ملخص

تحول سياسة أمان مستوى الصف في Postgres عزل المستأجر إلى ضمانة من قاعدة البيانات بدلاً من قاعدة يجب أن تتذكرها في كل استعلام. ستقوم ببناء جدول `documents` معزول للمستأجرين على **PostgreSQL 18** باستخدام **node-postgres (`pg`) 8.21.0**. سياسة `tenant_isolation` واحدة تجعل قاعدة البيانات — وليس جمل `WHERE` الخاصة بك — هي الشيء الذي يمنع مستأجرًا من قراءة أو كتابة صفوف مستأجر آخر. يستغرق البناء بالكامل حوالي 30 دقيقة، وقد تم تنفيذ كل جملة SQL هنا مقابل Postgres 18 قبل النشر — جنبًا إلى جنب مع مساعد `withTenant` ومجموعة الاختبارات — والجزء الأكثر تعقيدًا (تسرب في مجمع الاتصالات الذي يبطل RLS بصمت) يحصل على إصلاح من سطر واحد.

## ما ستتعلمه

- كيف تفرض سياسة أمان مستوى الصف عزل المستأجرين المتعددين في طبقة قاعدة البيانات
- كيفية تمكين RLS وكتابة سياسة `USING` مرتبطة بـ `tenant_id`
- لماذا تبدو RLS "متجاهلة" لمالكي الجداول والمستخدمين الخارقين (superusers)، والدور ذو الصلاحيات الأقل الذي يصلح ذلك
- كيفية تمرير المستأجر الحالي من node-postgres باستخدام `current_setting` و `set_config`
- تسرب مجمع الاتصالات الذي يكسر RLS البسيطة، والإصلاح المحلي للمعاملة (transaction-local)
- كيف يمنع `WITH CHECK` المستأجر من الكتابة في صفوف مستأجر آخر
- متى تلجأ إلى سياسات `FORCE ROW LEVEL SECURITY` و `RESTRICTIVE`
- كيفية اختبار عزل المستأجر تلقائيًا حتى لا تؤدي عملية ترحيل (migration) لاحقة إلى كسره بصمت

## المتطلبات الأساسية

- **PostgreSQL 18** يعمل محليًا (أي نسخة يمكن الوصول إليها تفي بالغرض). RLS نفسها ليست جديدة — لقد كانت ميزة أساسية منذ PostgreSQL 9.5 (2016)، لذا فإن السياسات أدناه تعمل أيضًا على 9.5+.[^since95]
- **Node.js 20.6+** (أي إصدار LTS حالي؛ `pg` نفسها تحتاج فقط إلى Node 16+، لكن مجموعة الاختبار الاختيارية في النهاية تستخدم `node --test --import`، التي أضيفت في 20.6) و **node-postgres `pg` 8.21.0**.[^pg]
- الإلمام بأساسيات SQL (`CREATE TABLE`، `GRANT`) و `async/await`.

ثبّت إصدار التعريف (driver) ليكون بناؤك قابلاً لإعادة الإنتاج:

```bash
npm install pg@8.21.0
npm install -D TypeScript@6.0.3 tsx@4.22.4 @types/pg@8.20.0 @types/node@26.0.1
```

أمان مستوى الصف هو نصف جانب قاعدة البيانات في تعدد المستأجرين. إذا كنت تقوم أيضًا بتخزين بيانات المستأجر مؤقتًا في تطبيقك، فقم بدمج ذلك مع مفاتيح تخزين مؤقت لكل مستأجر حتى لا يتم التراجع عن خطأ عزل في طبقة واحدة بواسطة الطبقة الأخرى — راجع الدليل المصاحب حول [مفاتيح التخزين المؤقت متعددة المستأجرين في Next.js](https://nerdleveltech.com/nextjs-16-cache-components-multi-tenant-cache-keys-tutorial).

## الخطوة 1 — نمذجة المستأجرين باستخدام عمود `tenant_id`

عادةً ما تختار التطبيقات متعددة المستأجرين أحد نماذج العزل الثلاثة: قاعدة بيانات منفصلة لكل مستأجر (أقوى عزل، وأصعب في التشغيل)، أو مخطط (schema) لكل مستأجر (حل وسط يصبح صعبًا بعد بضع مئات من المستأجرين)، أو **مخطط مشترك** حيث تعيش صفوف كل مستأجر في نفس الجداول المميزة بـ `tenant_id`. يتوسع نموذج المخطط المشترك ليشمل آلاف المستأجرين في قاعدة بيانات واحدة وهو الأكثر شيوعًا — ولكنه عادةً ما يضع عبء العزل بالكامل على ضبط فلتر `tenant_id` بشكل صحيح في كل استعلام. يزيل أمان مستوى الصف هذا العبء عن طريق نقل الفلتر إلى قاعدة البيانات نفسها، حيث لا يمكن لجملة `WHERE` منسية أن تكشف بيانات مستأجر آخر.

ابدأ بالمخطط. كل جدول مخصص للمستأجرين يحمل عمود `tenant_id`. قم بتشغيل هذا كدور ترحيل متميز (مالك الجدول):

```sql
CREATE TABLE documents (
  id          bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  tenant_id   bigint      NOT NULL,
  title       text        NOT NULL,
  created_at  timestamptz NOT NULL DEFAULT now()
);

INSERT INTO documents (tenant_id, title) VALUES
  (1, 'Acme roadmap'),
  (1, 'Acme invoices'),
  (2, 'Globex memo');
```

استخدام `GENERATED ALWAYS AS IDENTITY` بدلاً من `serial` مهم لاحقًا: أعمدة الهوية مملوكة للجدول، لذا لا يحتاج دور تطبيقك إلى منح تسلسل منفصل للإدراج.

أضف فهرسًا يبدأ بـ `tenant_id`. نظرًا لأن كل استعلام تمت تصفيته بواسطة RLS يكتسب فعليًا مسند `tenant_id = <current tenant>`، فإن الفهرس الذي يبدأ بـ `tenant_id` يحافظ على سرعة تلك الاستعلامات:

```sql
CREATE INDEX documents_tenant_id_idx ON documents (tenant_id, created_at DESC);
```

## الخطوة 2 — قم بتشغيل أمان مستوى الصف واكتب سياستك الأولى

يؤدي تمكين RLS إلى تحويل الجدول من "أي شخص لديه امتيازات الجدول يرى كل صف" إلى **الرفض الافتراضي**: بمجرد تمكين أمان الصف، إذا لم تمنح أي سياسة حق الوصول، فلن تكون أي صفوف مرئية أو قابلة للتعديل على الإطلاق.[^rls]

```sql
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation ON documents
  FOR ALL
  USING      (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::bigint)
  WITH CHECK (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::bigint);
```

تؤدي تعبيرتان وظيفتين. تقوم `USING` بتصفية الصفوف **الموجودة** التي تكون مرئية لعمليات القراءة والتحديث والحذف؛ الصفوف التي تعيد فيها قيمة false أو null يتم تخطيها بصمت — دون خطأ.[^createpolicy] تقوم `WITH CHECK` بالتحقق من صحة الصفوف **الجديدة** عند `INSERT`/`UPDATE`؛ عندما تعيد false أو null يتم إلغاء الأمر بالكامل مع ظهور خطأ.[^createpolicy] سنعتمد على كليهما.

يقرأ استدعاء `current_setting('app.tenant_id', true)` متغيرًا خاصًا بكل جلسة يضبطه تطبيقك في كل طلب — وهو الجسر بين طبقة المصادقة وقاعدة البيانات. الوسيط الثاني، `true` (`missing_ok`)، يعني "إرجاع NULL بدلاً من إظهار خطأ إذا لم يتم ضبط المتغير أبدًا."[^setconfig] غلاف `NULLIF(..., '')` ليس للزينة؛ توضح الخطوة 5 العطل الدقيق الذي يمنعه.

### كيف تقوم السياسة بإعادة كتابة استعلاماتك

لا تقوم RLS بتشغيل فحص أذونات منفصل بعد عودة استعلامك — بل تعيد كتابة الاستعلام قبل تشغيله. مع تمكين أمان الصف، يضيف Postgres تعبير `USING` الخاص بالسياسة كمسند ضمني ويقوم بتقييمه لكل صف قبل أي شروط تأتي من استعلامك الخاص.[^rls] استعلام `SELECT * FROM documents` عادي صادر عن `app_user` يتصرف كما لو كنت قد كتبت `SELECT * FROM documents WHERE tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::bigint`. هذا هو الهدف بالكامل — وهو أيضًا السبب في أن تعبير السياسة يجب أن يظل رخيصًا وصديقًا للفهرس، لأنه يعمل على كل صف من كل استعلام مقابل الجدول.

## الخطوة 3 — لماذا تبدو سياستك متجاهلة (المالكون والمستخدمون الخارقون يتجاوزون RLS)

إليك مفاجأة شائعة "RLS لا تعمل". اتصل كمستخدم خارق (أو كمالك للجدول) واستعلم عن الجدول:

```sql
-- متصل كمستخدم خارق / مالك الجدول
SELECT count(*) FROM documents;   -- يعيد 3، وليس 0
```

هذا سلوك صحيح، وليس خطأ. **المستخدمون الخارقون والأدوار التي تمتلك سمة `BYPASSRLS` يتجاوزون دائمًا أمان الصف، ومالك الجدول يتجاوزه أيضًا** ما لم تفرضه صراحةً.[^rls] عادةً ما يكون دور الترحيل وجلسة psql الخاصة بك متميزين، لذا فهم يرون كل شيء — وهذا هو بالضبط السبب في وجوب ممارسة RLS بواسطة دور غير متميز.

قم بإنشاء دور تطبيق مخصص ليس مستخدمًا خارقًا ولا مالكًا للجدول، وامنحه فقط امتيازات البيانات التي يحتاجها:

```sql
CREATE ROLE app_user NOSUPERUSER NOINHERIT LOGIN PASSWORD 'change-me';
GRANT USAGE ON SCHEMA public TO app_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_user;

-- لضمان تغطية الجداول المستقبلية تلقائياً
ALTER DEFAULT PRIVILEGES IN SCHEMA public
  GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_user;
```

يتصل تطبيقك كـ `app_user`. ولأن هذا الدور لا يملك `documents` وليس superuser، فإن سياسة `tenant_isolation` تنطبق عليه بالفعل. قم بتشغيل تطبيقك تحت دور بأقل صلاحيات ممكنة في كل بيئة — فقوة RLS تعتمد كلياً على الدور الذي تُنفذ به استعلاماتك فعلياً.

## الخطوة 4 — ربط الطلب بقاعدة البيانات باستخدام متغير جلسة

الآن قم بربط node-postgres بالسياسة. يتصل التطبيق ببيانات اعتماد `app_user`، ومع كل طلب، يخبر Postgres أي مستأجر (tenant) هو النشط عن طريق تعيين متغير `app.tenant_id` الذي تقرأه السياسة.

```ts
// db.ts
import { Pool } from 'pg';

export const pool = new Pool({
  connectionString: process.env.DATABASE_URL, // postgres://app_user:change-me@localhost:5432/app
  max: 10,
});
```

لا تقم بتشغيل `SET app.tenant_id = ...` مباشرة على اتصال مجمع (pooled connection) (الخطوة 5 تشرح السبب). بدلاً من ذلك، قم بتغليف كل وحدة عمل داخل عملية (transaction) واستخدم `set_config(name, value, true)`. الوسيط الثالث، `is_local = true`، يجعل نطاق الإعداد مقتصرًا على **العملية الحالية** — وهو الشكل الوظيفي لـ `SET LOCAL`، ويتم إعادة ضبطه تلقائياً عند `COMMIT` أو `ROLLBACK`.[^setconfig]

```ts
// with-tenant.ts
import type { Pool, PoolClient } from 'pg';

export async function withTenant<T>(
  pool: Pool,
  tenantId: number,
  fn: (client: PoolClient) => Promise<T>,
): Promise<T> {
  const client = await pool.connect();
  try {
    await client.query('BEGIN');
    // محلي للعملية: لا يمكن أن يتسرب للطلب التالي على هذا الاتصال
    await client.query('SELECT set_config($1, $2, true)', ['app.tenant_id', String(tenantId)]);
    const result = await fn(client);
    await client.query('COMMIT');
    return result;
  } catch (err) {
    await client.query('ROLLBACK');
    throw err;
  } finally {
    client.release();
  }
}
```

كل استعلام بنطاق المستأجر يمر الآن عبر `withTenant`، وتقوم قاعدة البيانات بتصفية الصفوف نيابة عنك:

```ts
// معالج المسار (بأسلوب Express)
app.get('/documents', async (req, res) => {
  const tenantId = req.tenant.id; // يتم استخراجه من JWT / الجلسة الخاصة بك، وليس من جسم طلب العميل أبداً
  const docs = await withTenant(pool, tenantId, (client) =>
    client.query('SELECT id, title, created_at FROM documents ORDER BY id'),
  );
  res.json(docs.rows);
});
```

لاحظ ما هو *مفقود*: لا يوجد `WHERE tenant_id = $1`. السياسة تضيفه لكل استعلام ضد `documents`، لذا فإن نسيان عامل تصفية لم يعد يعني اختراقاً للبيانات بين المستأجرين. تعيين المستأجر من سياق المصادقة الموثوق به — وليس أبداً من بارامتر الطلب — هو ما يجعل ذلك آمناً.

## الخطوة 5 — فخ مجمع الاتصالات (والحل في سطر واحد)

هذا هو الخطأ الذي يحول RLS إلى شعور زائف بالأمان، وتتجاهله العديد من الأدلة. تقوم مجمعات الاتصالات (Connection pools) بإعادة استخدام الاتصالات المادية عبر الطلبات. استخدام `SET` العادي (بدون `LOCAL`) يغير الإعداد لـ **الجلسة** بأكملها، لذا فإنه يظل موجوداً بعد `client.release()` ويتسرب إلى من يستخدم هذا الاتصال بعد ذلك.

إليك التسريب، تمت محاكاته على Postgres 18 مع مجمع اتصال واحد لفرض إعادة الاستخدام:

```ts
// نمط سيء — لا تفعل هذا
const a = await pool.connect();
await a.query("SET app.tenant_id = '2'");                 // نطاق الجلسة
await a.query('SELECT count(*) FROM documents');          // يرى صفوف المستأجر 2
a.release();                                               // تمت إعادته بدون إعادة ضبط

const b = await pool.connect();                            // نفس الاتصال المادي
await b.query('SELECT count(*) FROM documents');          // لا يزال يرى المستأجر 2 — تسريب
```

الطلب الثاني لم يقم بتعيين مستأجر أبداً، ومع ذلك لا يزال `current_setting('app.tenant_id')` يقرأ `2`، لذا فإنه يرى بيانات مستأجر آخر. المساعد `withTenant` من الخطوة 4 يحل هذه المشكلة تلقائياً: `set_config(..., true)` محلي للعملية، لذا تختفي القيمة بمجرد انتهاء العملية ولا يمكن للاتصال المسرب حملها للأمام.

هناك تفصيل دقيق في عملية التراجع هذه. بعد تراجع الإعداد المحلي للعملية، لا يصبح المتغير NULL — في الاتصال الذي سبق تعيينه فيه، يعيد `current_setting('app.tenant_id', true)` **سلسلة فارغة**:

```ts
// نفس الاتصال المادي، مباشرة بعد إتمام عملية withTenant()
const { rows } = await client.query("SELECT current_setting('app.tenant_id', true) AS raw");
console.log(JSON.stringify(rows[0].raw)); // ""  (سلسلة فارغة، وليس null)
```

إذا كان تحويل النوع في سياستك هو البسيط `current_setting('app.tenant_id', true)::bigint`، فإن تلك السلسلة الفارغة ستؤدي لخطأ في المرة القادمة التي يتم فيها استخدام الاتصال خارج سياق المستأجر:

```text
error: invalid input syntax for type bigint: ""
```

هذا هو بالضبط السبب في أن السياسة في الخطوة 2 تغلف القيمة بـ `NULLIF(current_setting('app.tenant_id', true), '')::bigint`. يقوم `NULLIF` بتحويل كل من "لم يتم تعيينه أبداً" (NULL) و "تم تعيينه ثم تراجع" (سلسلة فارغة) إلى NULL، و `tenant_id = NULL` لا يطابق أي صفوف — لذا فإن السياق غير المعين يفشل بأمان إلى صفر صفوف بدلاً من إلقاء خطأ. هذا الاحتراز الوحيد هو الفرق بين سياسة قوية وأخرى تنهار بشكل متقطع تحت الضغط.

## الخطوة 6 — منع الكتابة عبر المستأجرين باستخدام `WITH CHECK`

تحمي `USING` القراءات. بدون `WITH CHECK`، لا يزال بإمكان المستأجر القيام بـ `INSERT` لصف مختوم بـ `tenant_id` الخاص بشخص آخر. ولأن سياستنا هي `FOR ALL` مع `WITH CHECK` صريح، يتم رفض تلك الكتابة. نجاح المستأجر 1 في إدراج صفه الخاص؛ وفشل المستأجر 1 في إدراج صف للمستأجر 2:

```ts
// داخل withTenant(pool, 1, ...)
await client.query("INSERT INTO documents (tenant_id, title) VALUES (1, 'Acme Q3 plan')"); // ناجح

await client.query("INSERT INTO documents (tenant_id, title) VALUES (2, 'sneaky')");
// يلقي خطأ: new row violates row-level security policy for table "documents"
```

يمكنك جعل الوقوع في هذا الخطأ مستحيلاً عن طريق إعطاء `tenant_id` قيمة افتراضية مستمدة من نفس متغير الجلسة، بحيث لا يقوم كود التطبيق بتعيينها يدوياً أبداً:

```sql
ALTER TABLE documents
  ALTER COLUMN tenant_id
  SET DEFAULT NULLIF(current_setting('app.tenant_id', true), '')::bigint;
```

الآن، أي عملية إدراج تغفل `tenant_id` يتم ختمها بالمستأجر الحالي تلقائياً:

```ts
// داخل withTenant(pool, 2, ...)
const { rows } = await client.query(
  "INSERT INTO documents (title) VALUES ('Globex plan') RETURNING id, tenant_id",
);
console.log(rows[0]); // مثلاً { id: '6', tenant_id: '2' } — تم ملء tenant_id تلقائياً من GUC
```

تنطبق نفس الحماية على `UPDATE` و `DELETE`: عامل تصفية `USING` الخاص بهما يعني أن المستأجر ببساطة لا يمكنه رؤية صفوف مستأجر آخر لتعديلها، لذا فإن عملية `UPDATE` عبر المستأجرين ستؤثر على صفر صفوف بدلاً من إثارة خطأ.

## الخطوة 7 — الدفاع المتعمق: سياسات `FORCE` و `RESTRICTIVE`

أداتان إضافيتان لتقوية الإعداد.

**`FORCE ROW LEVEL SECURITY`** تجعل السياسة تنطبق حتى على مالك الجدول:[^rls]

```sql
ALTER TABLE documents FORCE ROW LEVEL SECURITY;
```

هذا يهم فقط إذا كان الدور الذي يستخدمه تطبيقك هو أيضاً *مالك* الجداول — وهو إعداد يجب تجنبه، ولكن `FORCE` هي شبكة الأمان عندما لا تستطيع ذلك. تنبيه واحد: `FORCE` **لا** تؤثر على الـ superusers أو الأدوار التي تملك صلاحية `BYPASSRLS`. لا شيء سوى سحب تلك الامتيازات سيخضعهم لـ RLS، وهذا هو السبب الحقيقي لإبقاء دور التطبيق بدون امتيازات.

**السياسات التقييدية (`RESTRICTIVE`)** تجتمع بشكل مختلف عن الافتراضي. السياسات السماحية (permissive) المتعددة يتم دمجها باستخدام OR (أي واحدة يمكنها منح الوصول)؛ أما السياسات التقييدية فيتم دمجها باستخدام AND، ويجب أن تنجح *كل* واحدة منها.[^createpolicy] وهذا يجعل السياسة التقييدية مثالية كحاجز حماية غير قابل للتفاوض — على سبيل المثال، "رفض كل الوصول ما لم يتم تعيين مستأجر بالفعل":

```sql
CREATE POLICY require_tenant ON documents
  AS RESTRICTIVE
  USING (NULLIF(current_setting('app.tenant_id', true), '') IS NOT NULL);
```

مع وجود هذا، فإن أي استعلام نسي تعيين `app.tenant_id` سيتم حظره بواسطة السياسة التقييدية بالإضافة إلى عدم مطابقة أي صفوف من خلال السياسة السماحية. تذكر قاعدة الترتيب: يجب أن تكون هناك سياسة سماحية واحدة على الأقل تمنح الوصول قبل أن تتمكن السياسات التقييدية من تضييقه بشكل مفيد؛ السياسات التقييدية وحدها تمنع كل شيء.[^createpolicy]

## التحقق من العملية بالكامل

أضمن طريقة للوثوق بقاعدة عزل هي مراقبتها وهي تعمل من دور غير متميز. في `psql`، انتقل إلى `app_user` وقم بتعيين مستأجر لكل عملية. (قوائم الصفوف أدناه تفترض بيانات البداية من الخطوة 1؛ إذا قمت أيضاً بتشغيل عمليات الإدراج في الخطوة 6، فسيكون لكل مستأجر صف إضافي واحد.)

```sql
-- كدور متميز أولاً، امنح دور الاختبار سياق المستأجر
BEGIN;
SET LOCAL ROLE app_user;
SELECT set_config('app.tenant_id', '1', true);
SELECT tenant_id, title FROM documents ORDER BY id;
--  tenant_id |     title
-- -----------+---------------
--          1 | Acme roadmap
--          1 | Acme invoices
COMMIT;

BEGIN;
SET LOCAL ROLE app_user;
SELECT set_config('app.tenant_id', '2', true);
SELECT tenant_id, title FROM documents ORDER BY id;
--  tenant_id |    title
-- -----------+-------------
--          2 | Globex memo
COMMIT;
```

يرى المستأجر 1 صفوف Acme فقط؛ ويرى المستأجر 2 صفوف Globex فقط. جرب كتابة عبر المستأجرين وشاهد كيف ترفضها `WITH CHECK`:

```sql
BEGIN;
SET LOCAL ROLE app_user;
SELECT set_config('app.tenant_id', '1', true);
INSERT INTO documents (tenant_id, title) VALUES (2, 'sneaky');
-- ERROR:  new row violates row-level security policy for table "documents"
ROLLBACK;
```

من Node، يتم تشغيل نفس الفحوصات عبر `withTenant`. تعيين `tenant_id` إلى `1` ثم `2` يعيد مجموعات صفوف منفصلة، وعملية الإدراج عبر المستأجرين تثير خطأ — وهذا هو العقد الكامل للميزة، تم التحقق منه.

## اختبار عزل المستأجر في CI

العزل هو خاصية تريد لاختبار أن يدافع عنها، لأن أي هجرة مستقبلية — جدول جديد بدون تفعيل RLS، أو سياسة تم حذفها أثناء إعادة الهيكلة — يمكن أن تفتح الباب بهدوء مرة أخرى. يغطي اختباران صغيران باستخدام مشغل الاختبارات المدمج في Node هذا العقد. يتصلان كـ `app_user` من خلال نفس المساعد `withTenant` الذي يستخدمه تطبيقك، لذا فهما يختبران السياسة الحقيقية، وليس نموذجاً (mock):

يؤكد الاختبار الأول أن كل صف يقرأه المستأجر ينتمي إلى ذلك المستأجر؛ بينما يؤكد الثاني أن الكتابة عبر المستأجرين يتم رفضها بواسطة `WITH CHECK`. قم بربط `node --test` بمسار العمل الخاص بك، وستتحول السياسة المفقودة إلى فشل في البناء (red build) بدلاً من اختراق صامت. تفصيل واحد يستحق الاستيعاب: تعيد مكتبة node-postgres أعمدة `bigint` كسلاسل نصية (strings) لتجنب فقدان الدقة، وهذا هو سبب مقارنة التأكيد بـ `'1'` بدلاً من الرقم `1`.

## استكشاف الأخطاء وإصلاحها

**"RLS لا يقوم بتصفية أي شيء."** أنت متصل كمستخدم خارق (superuser) أو مالك الجدول؛ كلاهما يتجاوز أمان الصفوف.[^rls] اتصل باستخدام دور `app_user` غير المتميز (الخطوة 3)، أو قم بتطبيق `FORCE ROW LEVEL SECURITY` إذا كان دور التطبيق يمتلك الجدول.

**"كل استعلام يعيد صفر صفوف."** إما أن RLS مفعل بدون سياسة مطبقة (الرفض الافتراضي[^createpolicy])، أو أنك نسيت تعيين `app.tenant_id`، لذا فإن `NULLIF(...)` يكون NULL ولا يتطابق شيء. تأكد من المتغير داخل نفس المعاملة (transaction): `SELECT current_setting('app.tenant_id', true)`.

**`invalid input syntax for type bigint: ""`.** اتصال مجمع (pooled connection) معاد استخدامه يحمل قيمة سلسلة فارغة من إعداد محلي لمعاملة سابقة. قم بالتحويل عبر `NULLIF(current_setting('app.tenant_id', true), '')::bigint` كما هو موضح في الخطوة 5.

**"بيانات المستأجر تتسرب بين الطلبات."** لقد استخدمت `SET` على مستوى الجلسة بدلاً من `set_config(..., true)` / `SET LOCAL` المحلي للمعاملة. قم بلف كل طلب في معاملة وقم بتعيين المتغير هناك (الخطوة 4).

**خطأ في المفتاح الفريد أو المفتاح الخارجي يكشف عن صف مخفي.** تتجاوز فحوصات السلامة المرجعية (Referential-integrity) نظام RLS حسب التصميم، لذا فإن فشل إدراج فريد يمكن أن يكشف عن وجود قيمة في صف لا يمكنك رؤيته.[^rls] حيثما يهم ذلك، استخدم مفاتيح بديلة غامضة بدلاً من مفاتيح ذات معنى خارجي — على سبيل المثال، [مفاتيح UUIDv7 الأساسية](https://nerdleveltech.com/postgres-18-uuidv7-primary-keys).

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

لديك الآن أمان مستوى الصف في Postgres يفرض تعدد المستأجرين من خلال node-postgres: نموذج `tenant_id`، وسياسة `tenant_isolation`، ودور بأقل الامتيازات، وجسر مستأجر محلي للمعاملة، و `WITH CHECK` في عمليات الكتابة — وكلها تم التحقق منها مقابل Postgres 18.

من هنا، هناك أمران يستحقان الاهتمام. أولاً، التجميع (pooling): النمط المحلي للمعاملة هو بالضبط ما يجعل RLS آمناً خلف PgBouncer أو Supavisor في وضع المعاملة، وهو ما تمت تغطيته في [دليل تجميع اتصالات Postgres للإنتاج](https://nerdleveltech.com/production-postgres-pooling-pgbouncer-supavisor-tutorial). ثانياً، الأداء: اجعل `tenant_id` هو العمود الرائد في فهارسك النشطة (الخطوة 1) وتأكد باستخدام `EXPLAIN` من أن شرط السياسة يستخدمها، وهو نفس انضباط مجموعة المفاتيح المستخدم في [التنقل بين الصفحات بالمؤشر (cursor pagination) مع node-postgres](https://nerdleveltech.com/cursor-pagination-postgres-nodejs-tutorial).

[^since95]: PostgreSQL 9.5 release announcement — "UPSERT, Row Level Security, and Big Data" (released 2016-01-07). <https://www.postgresql.org/about/news/postgresql-95-upsert-row-level-security-and-big-data-1636/>

[^pg]: node-postgres (`pg`) — npm package. <https://www.npmjs.com/package/pg>

[^rls]: PostgreSQL 18 Documentation — "5.9. Row Security Policies." <https://www.postgresql.org/docs/18/ddl-rowsecurity.html>

[^createpolicy]: PostgreSQL 18 Documentation — "CREATE POLICY." <https://www.postgresql.org/docs/18/sql-createpolicy.html>

[^setconfig]: PostgreSQL 18 Documentation — "9.28.1. Configuration Settings Functions" (`current_setting`, `set_config`). <https://www.postgresql.org/docs/18/functions-admin.html>
