> ## 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.

# Cal.com على سيرفرك: بديل Calendly لحجز الاجتماعات
- URL: https://arabroot.io/articles/حجز-المواعيد-والاجتماعات-مع-cal-com/
- Published: 2026-10-12T15:15:00.000Z
- Updated: 2026-10-11T22:50:55.000Z
- Description: ما الذي تستطيع استضافته من Cal.com بعد تحوله إلى الكود المغلق، وكيف تشغله وتربطه بتقويمك ونظامك.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, الاستضافة الذاتية والتطبيقات, تطبيقات لفريقك, الاستضافة الذاتية, Cal.com

ترسل لعميل أو مرشح لوظيفة رسالة تقول «متى يناسبك؟»، فيرد بوقتين لا يناسبانك، فترد بثلاثة أوقات أخرى، وبعد يومين من الرسائل تتفقان على موعد، وهذه هي المشكلة التي تحلها خدمة Calendly: رابط واحد يرى منه الطرف الآخر أوقاتك الفارغة فقط، ويختار منها، فيصل الموعد إلى تقويمك ومعه رابط الاجتماع. وفي [دليل اختيار أداة الحجز](https://arabroot.io/articles/%D8%A7%D9%8A-%D8%A7%D8%AF%D8%A7%D8%A9-%D8%AD%D8%AC%D8%B2-%D9%85%D9%88%D8%A7%D8%B9%D9%8A%D8%AF-%D8%AA%D9%86%D8%A7%D8%B3%D8%A8%D9%83/) قلنا إن Cal.com كان البديل المفتوح الأشهر لهذه الحالة، وإن شيئاً كبيراً تغير فيه في 2026\. وفي هذا المقال من مرحلة [المستندات والنماذج والتوقيع والحجز](https://arabroot.io/start/#stage-office) سوف نبدأ بما تغير بالضبط، وما الذي تستطيع استضافته منه اليوم، ثم نشغله على جهازنا ونبني نوع اجتماع وجدول توفر، ونربطه بتقويم CalDAV على Radicale وب Webhook، ونحاول أن نحجز الوقت نفسه من عشرة أشخاص في اللحظة نفسها.

وسوف نناقش في هذا المقال ما يلي:

- ماذا حدث لكود Cal.com في أبريل 2026، والفرق بين Cal.com وCal.diy والترخيص كما في المستودع.
- التثبيت بال Docker Compose بآخر Image رسمية، وكم يستغرق أول تشغيل.
- الإعداد الأول واختيار الترخيص، ثم نوع الاجتماع Event Type وجدول التوفر Availability.
- ربط تقويم CalDAV، ولماذا لا يكفي الربط وحده لمنع التعارض.
- مسار الحجز كاملاً: البريد، والموعد في التقويم، وال Webhook وتوقيعه.
- الواجهة العربية، والحجز المتزامن، وما الذي تغيره قبل الإنتاج.

## ماذا حدث لكود Cal.com؟

حتى بداية 2026 كان كود Cal.com في المستودع العام `calcom/cal.com`، وملف [LICENSE](https://github.com/calcom/cal.diy/blob/v6.2.0/LICENSE?ref=arabroot.io) عند الإصدار v6.2.0 يقسمه كما يلي: المجلدان `packages/features/ee` و`apps/api/v2/src/ee` تحت [ترخيص تجاري](https://github.com/calcom/cal.diy/blob/v6.2.0/packages/features/ee/LICENSE?ref=arabroot.io) لا يسمح باستخدامهما في الإنتاج دون اشتراك Enterprise، ويسمح بنسخهما وتعديلهما للتطوير والاختبار، وكل ما سواهما تحت AGPLv3\. ثم في [15 أبريل 2026](https://cal.com/blog/cal-diy-open-source-to-closed-source?ref=arabroot.io) أعلنت الشركة أنها نقلت كود الخدمة إلى مستودع خاص، وأن المستودع العام أصبح باسم [Cal.diy](https://github.com/calcom/cal.diy?ref=arabroot.io) بترخيص MIT، بعد حذف ميزات المؤسسات منه، والصورة التالية تلخص ذلك:

![مخطط من ثلاثة أجزاء: على اليمين مستودع cal.com حتى أبريل 2026 بنواة AGPLv3 ومجلد ee تجاري وآخر Image هي v6.2.0، وفي الوسط Cal.com اليوم مغلق المصدر في مستودع خاص، وعلى اليسار Cal.diy بترخيص MIT دون ميزات الفرق وسير العمل وSSO، ولا يوجد له أي وسم على Docker Hub](https://arabroot.io/content/images/2026/10/cal-com-01-split.webp)

Cal.com أصبح مغلقاً، وCal.diy هو المستودع العام القديم بترخيص MIT دون ميزات المؤسسات

وبحسب إعلان الشركة فقد بقي في Cal.diy محرك الجدولة ومتجر التطبيقات App Store ومسارات الحجز وال API v2، وحذفت منه المنظمات والفرق، ونماذج التوجيه Routing Forms، وسير العمل Workflows، والحجز الفوري، والإحصاءات Insights، وSSO وSAML، وال API v1، وسجل تدقيق الحجوزات. ويقول ملف README في Cal.diy صراحة إن المشروع للاستخدام الشخصي وغير الإنتاجي، وإن من يريد بنية جدولة تجارية فعليه بنسخة Cal.com المستضافة، أو بعقد استضافة داخلية مع الشركة.

### أين ال Docker Image؟

هنا المشكلة العملية التي سوف تواجهها في أول خطوة، فقد قرأنا سجل Docker Hub في أكتوبر 2026 فوجدنا ما يلي:

- صفحة `calcom/cal.diy` على Docker Hub ليس فيها أي وسم Tag، مع أن ملف README يشير إليها.
- آخر وسم في المستودع هو v6.2.0 بتاريخ 1 مارس 2026، أي قبل التحويل، ولم يصدر بعده أي إصدار.
- آخر Image منشورة هي `calcom/cal.com:v6.2.0` بتاريخ 2 مارس 2026، وهي من الكود قبل التحويل، أي AGPLv3 مع مجلد `ee`.
- التذكرة [28995](https://github.com/calcom/cal.diy/issues/28995?ref=arabroot.io) عن غياب ال Image مفتوحة منذ أبريل، وفيها مستخدم يذكر في 30 سبتمبر أن آخر خمس محاولات لبناء ال Image في GitHub Actions فشلت، وأن بناءها من الكود استغرق نحو 36 دقيقة وبلغ استهلاك الذاكرة 6.5 GB على سيرفر بذاكرة 8 GB.

لذلك أمامك طريقان: أن تشغل `calcom/cal.com:v6.2.0` كما فعلنا في هذا المقال، وهي Image رسمية جاهزة ولكنها لن تحصل على أي إصلاح أمني، أو أن تبني Cal.diy من الفرع الرئيسي بنفسك بحسب قسم [Building from source with Docker](https://github.com/calcom/cal.diy?ref=arabroot.io#building-from-source-with-docker)، وتتحمل أنت اختيار الإصدار وبناءه واختباره عند كل تحديث. وقد يتساءل البعض: أيهما أفضل للإنتاج؟ والإجابة أن الاثنين ليسا مريحين، فالأول قديم ولن يتحدث، والثاني يقول عنه أصحابه إنه ليس للإنتاج، لذلك لا ننصح بوضع أي منهما على الإنترنت لعملائك دون أن تقرر من سوف يتابع تحديثاته.

## التثبيت بال Docker Compose

ملف Compose في مستودع Cal.diy يبني ال Image من الكود ويستخدم `postgres` و`redis:latest` دون إصدار محدد، وينشر المنفذين 6379 و5555 على كل الواجهات، والثاني هو Prisma Studio الذي يفتح قاعدة البيانات كاملة في المتصفح، لذلك كتبنا ملفاً أصغر: PostgreSQL وتطبيق Cal.com، ومعهما Mailpit لالتقاط البريد، وRadicale 3.8.3 كسيرفر CalDAV، وContainer صغير يطبع ال Webhooks:

```yaml
name: lab-book-cal
services:
  db:
    image: postgres:17.6-alpine
    container_name: lab-book-cal-db
    restart: unless-stopped
    environment:
      POSTGRES_USER: calcom
      POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
      POSTGRES_DB: calcom
    volumes:
      - db:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U calcom"]
      interval: 5s
      retries: 60
      start_period: 300s
  calcom:
    image: calcom/cal.com:v6.2.0
    container_name: lab-book-cal-app
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    ports:
      - "127.0.0.1:28440:3000"
    environment:
      DATABASE_URL: "postgresql://calcom:${POSTGRES_PASSWORD}@db:5432/calcom"
      DATABASE_DIRECT_URL: "postgresql://calcom:${POSTGRES_PASSWORD}@db:5432/calcom"
      NEXT_PUBLIC_WEBAPP_URL: "http://127.0.0.1:28440"
      NEXTAUTH_URL: "http://127.0.0.1:28440/api/auth"
      NEXTAUTH_SECRET: "${NEXTAUTH_SECRET}"
      CALENDSO_ENCRYPTION_KEY: "${CALENDSO_ENCRYPTION_KEY}"
      CALCOM_TELEMETRY_DISABLED: "1"
      EMAIL_FROM: "meetings@example.com"
      EMAIL_FROM_NAME: "Lab Cal"
      EMAIL_SERVER_HOST: "mailpit"
      EMAIL_SERVER_PORT: "1025"
  mailpit:
    image: axllent/mailpit:v1.31.5
    container_name: lab-book-cal-mailpit
    ports:
      - "127.0.0.1:28441:8025"
  radicale:
    image: kozea/radicale:3.8.3
    container_name: lab-book-cal-radicale
    ports:
      - "127.0.0.1:28442:5232"
    volumes:
      - ./radicale-config:/etc/radicale/config:ro
      - ./radicale-users:/etc/radicale/users:ro
      - radicale:/var/lib/radicale
  hook:
    image: python:3.13-alpine
    container_name: lab-book-cal-hook
    command: ["python", "-u", "/hook.py"]
    volumes:
      - ./hook.py:/hook.py:ro
volumes:
  db:
  radicale:
```

والأسرار في ملف `.env`، بالطول الذي يطلبه README، فالمفتاح `CALENDSO_ENCRYPTION_KEY` يجب أن يكون 32 بايت لأنه مفتاح AES-256 يشفر بيانات الدخول إلى التقويمات المربوطة:

```bash
umask 077
printf "POSTGRES_PASSWORD=%s\nNEXTAUTH_SECRET=%s\nCALENDSO_ENCRYPTION_KEY=%s\n" \
  $(openssl rand -hex 16) $(openssl rand -base64 32) $(openssl rand -base64 24) > .env
docker compose up -d
```

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

- إعدادات Radicale والمستخدمين كما في [دليل Radicale](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-radicale-%D9%84%D9%84%D8%AA%D9%82%D9%88%D9%8A%D9%85-%D9%88%D8%AC%D9%87%D8%A7%D8%AA-%D8%A7%D9%84%D8%A7%D8%AA%D8%B5%D8%A7%D9%84/)، وملف `hook.py` سيرفر HTTP من عشرين سطراً يطبع ما يصله، ونعرضه في قسم ال Webhooks.
- إذا فقدت `CALENDSO_ENCRYPTION_KEY` فلن يستطيع التطبيق قراءة كلمات مرور التقويمات المربوطة، لذلك يحفظ مع النسخة الاحتياطية.
- لم نضع خدمة `calcom-api`، وهي ال API v2، لأنها خدمة منفصلة تحتاج Redis، ولم نحتجها في التجربة.

### ربع ساعة قبل أول صفحة

ال Image مبنية بالعنوان `http://localhost:3000`، فإذا اختلف `NEXT_PUBLIC_WEBAPP_URL` عنه فإن سكربت البداية يستبدله في كل الملفات المبنية قبل التشغيل، وهذا ما رأيناه في السجل مع الوقت:

```bash
docker logs -t lab-book-cal-app 2>&1 | grep -E "Replacing|migrate deploy|migrations found|seed-app-store|Ready in"
```

```text
2026-10-11T21:27:47.835486241Z Replacing all statically built instances of http://localhost:3000 with http://127.0.0.1:28440.
2026-10-11T21:39:30.345358543Z + npx prisma migrate deploy --schema /calcom/packages/prisma/schema.prisma
2026-10-11T21:40:15.331095666Z 588 migrations found in prisma/migrations
2026-10-11T21:41:56.414232378Z + npx ts-node --transpile-only /calcom/scripts/seed-app-store.ts
2026-10-11T21:43:45.076703948Z @calcom/web:start: ✓ Ready in 9.9s
```

أي نحو 12 دقيقة لاستبدال العنوان، ودقيقتين لتطبيق 588 Migration على قاعدة فارغة، ثم تجهيز متجر التطبيقات، فالمجموع 16 دقيقة على جهاز التجربة وقرصه مشغول بتجارب أخرى. ويتكرر الاستبدال عند كل إعادة إنشاء لل Container، فلا تفسر الصمت الطويل في السجل على أنه تعطل. وحجم ال Image على القرص 8.05 GB، وقد استهلك التطبيق 1.7 GB من الذاكرة بعد التجربة، وقاعدة البيانات 54 MB، فلا ننصح بسيرفر بذاكرة أقل من 4 GB.

## الإعداد الأول واختيار الترخيص

أول زيارة تفتح معالج الإعداد `/auth/setup`، وفيه ثلاث خطوات: إنشاء المدير، ثم اختيار الترخيص، ثم التطبيقات. وكلمة مرور المدير يجب أن تكون 15 حرفاً على الأقل، فيها حروف كبيرة وصغيرة ورقم، والخطوة الثانية تبين التقسيم الذي قرأناه في ملف الترخيص:

![خطوة اختيار الترخيص في معالج إعداد Cal.com: بطاقة AGPLv3 مجانية دون ميزات المؤسسات، وبطاقة إدخال مفتاح ترخيص يبدأ من 15 دولاراً للمستخدم شهرياً](https://arabroot.io/content/images/2026/10/cal-com-03-license-step.webp)

AGPLv3 مجاناً دون ميزات المؤسسات، أو مفتاح ترخيص لكل الميزات

تركنا الاختيار على AGPLv3، ثم أكملنا خطوات التهيئة Onboarding: الاستخدام الشخصي، والاسم ورابط الصفحة، ثم ربط التقويم، وفيها هذه الخيارات:

![خطوة ربط التقويم في Cal.com، وفيها Apple Calendar وCalDav بيتا وNotion Calendar وMicrosoft Exchange وICS Feed، دون Google Calendar](https://arabroot.io/content/images/2026/10/cal-com-04-connect-calendar.webp)

خيارات ربط التقويم دون إعداد إضافي، وGoogle غير موجود هنا

ولاحظ أن Google Calendar غير موجود في القائمة، والسبب أنه يحتاج عميل OAuth من Google Cloud تضعه في المتغير `GOOGLE_API_CREDENTIALS` بحسب قسم [Obtaining the Google API Credentials](https://github.com/calcom/cal.diy?ref=arabroot.io#obtaining-the-google-api-credentials)، ومعه عنوان رجوع على نطاقك العام، وكذلك Microsoft 365\. وبعد الدخول تظهر في القائمة الجانبية روابط Teams وRouting وWorkflows وInsights، وهي ميزات تحتاج مفتاح الترخيص في v6.2.0، وغير موجودة في Cal.diy أصلاً. ويظهر شريط برتقالي في أعلى كل صفحة يقول إن صلاحيات المدير لا تعمل حتى تكون كلمة المرور 15 حرفاً أو يفعل التحقق بخطوتين 2FA، وقد ظهر عندنا مع أن كلمة المرور 16 حرفاً، فاعتبره تذكيراً بتفعيل 2FA لحساب المدير.

## نوع الاجتماع وجدول التوفر

جدول التوفر Availability هو ساعات العمل التي يقدم منها Cal.com الأوقات، وهو منفصل عن نوع الاجتماع، فيمكن أن يستخدم عدة أنواع الجدول نفسه. أنشأنا جدولاً اسمه Clinic hours من الأحد إلى الخميس من 9:00 إلى 17:00 بالمنطقة Asia/Riyadh، ولاحظ أن الجدول الافتراضي عند الإنشاء من الإثنين إلى الجمعة، فغيرنا الأحد والجمعة:

![صفحة جدول التوفر Clinic hours في Cal.com، فيها الأحد إلى الخميس من 9:00 صباحاً إلى 5:00 مساء، والجمعة والسبت مغلقة، والمنطقة الزمنية Asia/Riyadh، وقسم استثناءات التواريخ](https://arabroot.io/content/images/2026/10/cal-com-05-availability.webp)

جدول التوفر: الأيام والساعات والمنطقة الزمنية، وتحتها استثناءات التواريخ Date Overrides للإجازات

وتحت الأيام قسم Date overrides، وهو المكان الذي تضع فيه إجازة أو يوماً بساعات مختلفة. وقد ظهر لنا عند أول دخول تنبيه يقول إن منطقة الحساب تختلف عن منطقة المتصفح Asia/Riyadh ويسأل إن كنا نريد تحديثها، وهذا تنبيه مفيد، والسبب أن الحساب يبدأ بمنطقة السيرفر، وقد رأينا في [دليل Easy!Appointments](https://arabroot.io/articles/easy-appointments-%D8%AD%D8%AC%D8%B2-%D9%85%D9%88%D8%A7%D8%B9%D9%8A%D8%AF-%D8%A7%D9%84%D8%B9%D9%8A%D8%A7%D8%AF%D8%A7%D8%AA-%D9%88%D9%85%D8%B1%D8%A7%D9%83%D8%B2-%D8%A7%D9%84%D8%AE%D8%AF%D9%85%D8%A9/#break-timezone) ماذا يحدث عندما تختلف المنطقة دون أن ينبهك أحد.

ثم أنشأنا نوع اجتماع باسم «مكالمة تعارف» مدته 30 دقيقة، ورابطه `/owner/intro-30`، والصورة التالية تبين صفحته، ولاحظ في القائمة اليسرى الإعدادات الأخرى: الحدود Limits مثل أقل مهلة قبل الحجز Minimum notice، والمتقدم Advanced مثل طلب التأكيد قبل قبول الحجز Requires confirmation، والتكرار، وال Webhooks الخاصة بهذا النوع:

![صفحة نوع الاجتماع مكالمة تعارف في Cal.com، فيها العنوان والوصف والرابط owner/intro-30 والمدة 30 دقيقة والمكان، وقائمة جانبية بالتوفر والحدود والإعدادات المتقدمة والتكرار والتطبيقات وسير العمل وال Webhooks](https://arabroot.io/content/images/2026/10/cal-com-06-event-type.webp)

نوع الاجتماع: العنوان والرابط والمدة والمكان، والإعدادات الأخرى في القائمة الجانبية

والمكان الافتراضي للاجتماع Cal Video، وهو يعتمد على خدمة Daily.co ويحتاج مفتاحها في المتغير `DAILY_API_KEY`، فدونه يصل في البريد أن المكان Cal Video ولا يعمل الرابط، لذلك اختر مكاناً حضورياً أو تطبيق Jitsi من متجر التطبيقات، أو اربط سيرفر الاجتماعات الخاص بك.

## ربط تقويم CalDAV

أنشأنا تقويماً في Radicale بالطلب `MKCALENDAR`، ثم من Apps اخترنا CalDav (Beta) وأدخلنا العنوان `http://radicale:5232/sara/meetings/` واسم المستخدم وكلمة المرور، فاتصل مباشرة، وفي سجل Radicale طلبات PROPFIND ناجحة من التطبيق، بخلاف Easy!Appointments الذي يرفض سيرفر CalDAV على عنوان خاص. وبعد الربط تظهر صفحة التقويمات كما يلي:

![صفحة التقويمات المثبتة في Cal.com: قسم إضافة الأحداث إلى تقويم sara/meetings، وقسم فحص التعارض فيه تقويم CalDav والمفتاح الخاص به مغلق](https://arabroot.io/content/images/2026/10/cal-com-07-calendars.webp)

قسمان منفصلان: أين يضاف الموعد، وأي التقويمات تفحص للتعارض، والمفتاح الثاني مغلق افتراضياً

ولاحظ في الصورة أن الصفحة فيها قسمان: «Add to calendar» وهو التقويم الذي تكتب فيه المواعيد الجديدة، و«Check for conflicts» وهي التقويمات التي يقرأ منها الأوقات المشغولة، ومفتاح التقويم في القسم الثاني مغلق بعد الربط. ولنرى أثر ذلك أضفنا في Radicale موعداً خاصاً يوم الثلاثاء 13 أكتوبر من 10:00 إلى 11:00 بتوقيت الرياض:

```text
BEGIN:VEVENT
UID:lab-busy-1
DTSTART:20261013T070000Z
DTEND:20261013T080000Z
SUMMARY:Dentist (private)
END:VEVENT
```

```bash
curl -s -u 'sara:********' -X PUT -H 'Content-Type: text/calendar' --data-binary @busy.ics \
  http://127.0.0.1:28442/sara/meetings/lab-busy-1.ics -o /dev/null -w "PUT %{http_code}\n"
```

```text
PUT 201
```

ثم قرأنا الأوقات المعروضة في صفحة الحجز لذلك اليوم، قبل تفعيل المفتاح وبعده:

```text
before: 9:00am 9:30am 10:00am 10:30am 11:00am 11:30am 12:00pm 12:30pm 1:00pm 1:30pm 2:00pm 2:30pm 3:00pm 3:30pm 4:00pm 4:30pm
after:  9:00am 9:30am 11:00am 11:30am 12:00pm 12:30pm 1:00pm 1:30pm 2:00pm 2:30pm 3:00pm 3:30pm 4:00pm 4:30pm
```

أي أن الربط وحده يكتب المواعيد في تقويمك ولا يمنع التعارض، فقبل تفعيل المفتاح عرض Cal.com الوقتين 10:00 و10:30 مع أن التقويم مشغول، وبعد تفعيله اختفيا. لذلك بعد ربط أي تقويم افتح هذه الصفحة وفعل فحص التعارض لكل تقويم فيه مواعيدك، وليس فقط التقويم الذي يكتب فيه Cal.com.

## مسار الحجز كاملاً

الصورة التالية تبين ما يحدث من فتح الرابط إلى وصول ال Webhook، والخطوتان 2 و6 لا تحدثان إلا مع تقويم مربوط:

![مخطط مرقم لحجز في Cal.com: المدعو يفتح الرابط، ثم يقرأ التطبيق الأوقات المشغولة من تقويم CalDAV، ثم يعرض الأوقات الفارغة، ثم يختار المدعو ويؤكد، ثم يحفظ الحجز في PostgreSQL، ويكتب الموعد في التقويم، ويرسل البريد، ويرسل Webhook باسم BOOKING_CREATED إلى نظامك](https://arabroot.io/content/images/2026/10/cal-com-02-booking-flow.webp)

ثماني خطوات لحجز واحد، أربع منها اتصالات تخرج من Cal.com

فتحنا صفحة الحجز كمدعو من متصفح منطقته Asia/Riyadh، واخترنا يوم 13 أكتوبر الساعة 11:00، وكتبنا الاسم والبريد، فظهرت صفحة التأكيد:

![صفحة تأكيد الحجز في Cal.com: This meeting is scheduled، مع العنوان مكالمة تعارف بين Clinic Owner وFahad Alsalem، والوقت الثلاثاء 13 أكتوبر 11:00 بتوقيت Arabian Standard Time، ومكان الاجتماع Cal Video](https://arabroot.io/content/images/2026/10/cal-com-08-done.webp)

صفحة التأكيد بعد الحجز، والوقت بمنطقة المدعو

وبعد ثوان وجدنا النتائج الثلاث، أولها البريد في Mailpit:

```text
fahad@example.com | مكالمة تعارف between Clinic Owner and Fahad Alsalem
owner@example.com | مكالمة تعارف between Clinic Owner and Fahad Alsalem
owner@example.com | [Action Required] Confirmed: intro-30 with Fahad Alsalem at 11:00am - 11:30am, Tuesday, October 13, 2026
```

وثانيها الموعد في تقويم Radicale، بجوار الموعد الخاص الذي أضفناه:

```text
<href>/sara/meetings/tmJ6ReYK982ZYfkWhVgeP5.ics</href>
<href>/sara/meetings/lab-busy-1.ics</href>
```

وثالثها ال Webhook كما في القسم التالي. ولاحظ في عنوان الرسالة أن اسم الموعد بالعربية والباقي بالإنجليزية، لأن قالب اسم الحدث الافتراضي في ملف الترجمة الإنجليزية هو «{{eventName}} between {{host}} and {{attendeeName}}»، ويحفظ العنوان مع الحجز عند إنشائه، وقد كانت لغة الحساب الإنجليزية عند حجوزاتنا فبقيت العناوين كذلك بعد التحويل إلى العربية، فإذا أردته عربياً كاملاً فاكتب الاسم بنفسك في خيار Calendar event name في الإعدادات المتقدمة لنوع الاجتماع.

## ال Webhooks وتوقيعها

من Settings ثم Developer ثم Webhooks أضفنا Webhook إلى `http://hook:8080/cal` مع سر Secret هو `lab-cal-secret`، ويمكن أن تختار الأحداث التي تطلقه، مثل إنشاء الحجز وإلغائه وتغيير موعده. وهذا ملف `hook.py` الذي يستقبله ويتحقق من التوقيع:

```python
# Prints every webhook it receives and checks the Cal.com HMAC signature.
import http.server, json, sys, hmac, hashlib
class H(http.server.BaseHTTPRequestHandler):
    def do_POST(self):
        body = self.rfile.read(int(self.headers.get('Content-Length', 0)))
        print('POST', self.path, flush=True)
        for k, v in self.headers.items():
            if k.lower().startswith('x-') or k.lower() in ('content-type', 'user-agent'): print(f'{k}: {v}', flush=True)
        sig = self.headers.get('X-Cal-Signature-256')
        if sig: print('hmac ok:', hmac.compare_digest(sig, hmac.new(b'lab-cal-secret', body, hashlib.sha256).hexdigest()), flush=True)
        try: print(json.dumps(json.loads(body), ensure_ascii=False, indent=2)[:3000], flush=True)
        except Exception: print(body[:2000], flush=True)
        self.send_response(200); self.end_headers(); self.wfile.write(b'ok')
    def log_message(self, *a): pass
http.server.HTTPServer(('0.0.0.0', 8080), H).serve_forever()
```

وهذا جزء مما وصل عند حجز:

```text
POST /cal
Content-Type: application/json
X-Cal-Signature-256: 1b93e4ca3222bdb81c3ab9b06909a7f990994b6fb8fae293086530712c41b451
X-Cal-Webhook-Version: 2021-10-20
user-agent: node
{
  "triggerEvent": "BOOKING_CREATED",
  "createdAt": "2026-10-11T22:16:40.562Z",
  "payload": {
    "title": "مكالمة تعارف between Clinic Owner and Fahad Alsalem",
    "startTime": "2026-10-13T08:00:00+00:00",
    "endTime": "2026-10-13T08:30:00+00:00",
    "type": "intro-30",
    "organizer": {
      "email": "owner@example.com",
      "timeZone": "Asia/Riyadh",
      ...
    },
    "attendees": [
      {
        "email": "fahad@example.com",
        "name": "Fahad Alsalem",
        "timeZone": "Asia/Riyadh",
        ...
```

وبعد أن أضفنا التحقق إلى السكربت وحجزنا موعداً آخر طبع السطر التالي:

```text
hmac ok: True
```

في المخرج أعلاه لاحظ التالي:

- الترويسة `X-Cal-Signature-256` هي HMAC-SHA256 لمحتوى الطلب كما وصل، بالسر الذي أدخلته، فاحسبها من البايتات الخام قبل تحويلها إلى JSON، وارفض الطلب إذا لم تتطابق، والسبب أن عنوان ال Webhook قد يعرفه غيرك.
- الوقت بتوقيت UTC ومعه منطقة كل طرف، فيستطيع نظامك أن يعرض الوقت لكل شخص بمنطقته دون تخمين.
- ال Webhook يمكن أن يكون عاماً لكل أنواع الاجتماعات من الإعدادات، أو خاصاً بنوع واحد من صفحة النوع نفسه.

## الواجهة العربية

ملف الترجمة العربية في الإصدار v6.2.0 فيه 4630 نصاً من أصل 4647 في الملف الإنجليزي، أي ينقصه 18 نصاً، وهو من 44 لغة في المجلد `packages/i18n/locales`. وصفحة الحجز تأخذ لغة المتصفح، فإذا كانت العربية صارت الصفحة كلها من اليمين إلى اليسار، كما في الصورة التالية:

![صفحة حجز مكالمة تعارف بالعربية من اليمين إلى اليسار: معلومات الاجتماع على اليمين، والتقويم يبدأ بالأحد من اليمين، والأوقات المتاحة على اليسار بنظام 24 ساعة](https://arabroot.io/content/images/2026/10/booking-tools-03-cal-com.webp)

صفحة الحجز بالعربية: الاتجاه والأيام والأوقات صحيحة

وكذلك لوحة الإدارة بعد أن تختار العربية لغة لحسابك من الإعدادات، والصورة التالية تبين قائمة الحجوزات، ولاحظ أن عناوين المواعيد نصف إنجليزية بسبب القالب الذي ذكرناه، وأن صيغة الوقت am وpm في هذه الصفحة:

![قائمة الحجوزات القادمة في Cal.com بالعربية من اليمين إلى اليسار، فيها عناوين مثل مكالمة تعارف between Clinic Owner and Fahad Alsalem، وحجزان متداخلان يوم الأحد 18 أكتوبر من 10:00 ومن 10:15](https://arabroot.io/content/images/2026/10/cal-com-11-bookings-ar.webp)

لوحة الإدارة بالعربية، ويومها الأحد 18 أكتوبر فيه حجزان متداخلان من تجربة القسم التالي

وهذا أفضل دعم للعربية بين أدوات الحجز الأربع التي شغلناها، فهو الوحيد الذي يقلب الاتجاه فعلاً.

## اكسرها: عشرة أشخاص على الوقت نفسه

كررنا هنا تجربة الحجز المتزامن التي قبل فيها Easy!Appointments سبعة من عشرة، فأرسلنا عشرة طلبات في اللحظة نفسها إلى نقطة الحجز العامة `/api/book/event` التي تستخدمها صفحة الحجز، لموعد الساعة 10:00 يوم 15 أكتوبر:

```python
# race.py: N invitees try to book the same Cal.com slot at the same moment
import sys, json, threading, urllib.request
BASE = 'http://127.0.0.1:28440'
N = int(sys.argv[1]); START, END = sys.argv[2], sys.argv[3]

def worker(i, barrier, results):
    body = {'responses': {'name': f'Guest {i}', 'email': f'guest{i}@example.com',
                          'location': {'value': 'integrations:daily', 'optionValue': ''}},
            'start': START, 'end': END, 'eventTypeId': 4, 'eventTypeSlug': 'intro-30', 'user': 'owner',
            'timeZone': 'Asia/Riyadh', 'language': 'en', 'metadata': {}}
    req = urllib.request.Request(BASE + '/api/book/event', data=json.dumps(body).encode(),
                                 headers={'Content-Type': 'application/json'})
    barrier.wait()
    try:
        with urllib.request.urlopen(req, timeout=120) as r: results[i] = (r.status, json.loads(r.read()).get('uid'))
    except urllib.error.HTTPError as e:
        results[i] = (e.code, e.read().decode()[:160])

barrier = threading.Barrier(N); results = {}
ts = [threading.Thread(target=worker, args=(i, barrier, results)) for i in range(N)]
[t.start() for t in ts]; [t.join() for t in ts]
for i in sorted(results): print(i, *results[i])
print('accepted:', sum(1 for s, _ in results.values() if s == 200))
```

```bash
python3 race.py 10 2026-10-15T07:00:00.000Z 2026-10-15T07:30:00.000Z
```

```text
0 400 {"message":"An error occurred while querying the database.","data":{"traceId":"trace_8u8bb_3b0BWX70ruMApis"}}
1 400 {"message":"An error occurred while querying the database.","data":{"traceId":"trace_xSealLZs3BBOePEySOWUU"}}
2 400 {"message":"An error occurred while querying the database.","data":{"traceId":"trace_zxDu0W0RT7YiO0VLxm-_k"}}
3 400 {"message":"An error occurred while querying the database.","data":{"traceId":"trace_ma07YHqDERkh1CYb4DSsn"}}
4 200 jJzE3JgwPVUyZXwZHUTVcN
5 400 {"message":"An error occurred while querying the database.","data":{"traceId":"trace__scF1oZO5NtL1lbwwRDej"}}
6 400 {"message":"An error occurred while querying the database.","data":{"traceId":"trace_CtyvpPQ8oivk4qNXNB2Jq"}}
7 400 {"message":"An error occurred while querying the database.","data":{"traceId":"trace_TAgO2rQUnZfgqjdRccGUv"}}
8 400 {"message":"An error occurred while querying the database.","data":{"traceId":"trace_eZ1z2v5ZJgp7sprhr6qEm"}}
9 400 {"message":"An error occurred while querying the database.","data":{"traceId":"trace_t7oudpYFrrNvBv8i6iRhN"}}
accepted: 1
```

حجز واحد فقط، والتسعة الباقية رفضتها قاعدة البيانات، وفي سجل التطبيق الخطأ P2002 من Prisma على القيد `Booking_idempotencyKey_key`. وعند البحث في الكود وجدنا أن Cal.com يضيف إلى كل حجز مقبول مفتاحاً اسمه `idempotencyKey`، يحسبه من وقت البداية ووقت النهاية ورقم المستخدم، وعليه قيد فريد في الجدول:

```bash
docker exec lab-book-cal-db psql -U calcom -d calcom -c '\d "Booking"' | grep -i unique
```

```text
    "Booking_idempotencyKey_key" UNIQUE, btree ("idempotencyKey")
    "Booking_oneTimePassword_key" UNIQUE, btree ("oneTimePassword")
    "Booking_uid_key" UNIQUE, btree (uid)
```

وهذا هو خط الدفاع الأخير الذي شرحناه في [مقال نظام حجز التذاكر](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%A8%D9%86%D9%8A-%D9%86%D8%B8%D8%A7%D9%85-%D8%AD%D8%AC%D8%B2-%D8%AA%D8%B0%D8%A7%D9%83%D8%B1-%D9%8A%D8%AA%D8%AD%D9%85%D9%84-%D9%84%D8%AD%D8%B8%D8%A9-%D9%81%D8%AA%D8%AD-%D8%A7%D9%84%D8%A8%D9%8A%D8%B9/#unique-constraint). ولكن لاحظ أن المفتاح يحسب من البداية والنهاية بالضبط، فهو يمنع الحجز المكرر للوقت نفسه، ولا يمنع حجزين متداخلين بأوقات مختلفة. فأرسلنا طلبين في اللحظة نفسها: «مكالمة تعارف» 30 دقيقة من 10:00، ونوع آخر مدته 15 دقيقة من 10:15، مع المستخدم نفسه، وكررنا ذلك ثلاث مرات:

```text
== 7
intro-30 2026-10-18T07:00:00.000Z 200
15min 2026-10-18T07:15:00.000Z 200
== 8
intro-30 2026-10-18T08:00:00.000Z 200
15min 2026-10-18T08:15:00.000Z 200
== 10
intro-30 2026-10-18T10:00:00.000Z 200
15min 2026-10-18T10:15:00.000Z 200
```

قبل الطلبان في المرات الثلاث، وقد رأيت الحجزين المتداخلين في صورة قائمة الحجوزات. أما إذا وصل الطلب الثاني بعد الأول، فيرفضه Cal.com لأنه يرى الوقت مشغولاً:

```text
409 {"message":"no_available_users_found_error","data":{"traceId":"trace_sbaUMcXM97aH16yONkSt1"}}
```

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

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

1. **قرر أي كود تشغل:** v6.2.0 الجاهزة دون تحديثات، أو Cal.diy تبنيه بنفسك، واكتب من المسؤول عن متابعة الثغرات، وإذا لم يوجد فانظر إلى [Easy!Appointments](https://arabroot.io/articles/easy-appointments-%D8%AD%D8%AC%D8%B2-%D9%85%D9%88%D8%A7%D8%B9%D9%8A%D8%AF-%D8%A7%D9%84%D8%B9%D9%8A%D8%A7%D8%AF%D8%A7%D8%AA-%D9%88%D9%85%D8%B1%D8%A7%D9%83%D8%B2-%D8%A7%D9%84%D8%AE%D8%AF%D9%85%D8%A9/) أو إلى صفحات الحجز في تقويمك الحالي.
2. **لا تنشر Prisma Studio ولا Redis:** ملف Compose الرسمي ينشر المنفذين 5555 و6379، فاحذفهما أو اربطهما ب 127.0.0.1.
3. **HTTPS:** اجعل `NEXT_PUBLIC_WEBAPP_URL` و`NEXTAUTH_URL` عنوان HTTPS العام، وضع التطبيق خلف [Caddy](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-caddy-reverse-proxy/) أو غيره، وتذكر أن تغيير العنوان يعيد الاستبدال الطويل عند التشغيل.
4. **حساب المدير:** فعل 2FA، وأغلق التسجيل العام بالمتغير `NEXT_PUBLIC_DISABLE_SIGNUP` إذا كان السيرفر لفريقك فقط.
5. **التقويمات:** فعل فحص التعارض لكل تقويم، وغير مكان الاجتماع الافتراضي إذا لم يكن لديك مفتاح Daily.
6. **البريد:** اضبط SMTP حقيقياً كما في [مقال 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/).
7. **النسخ الاحتياطي:** قاعدة PostgreSQL مع ملف `.env`، وخاصة `CALENDSO_ENCRYPTION_KEY`، واختبر الاستعادة كما في [قاعدة 3-2-1](https://arabroot.io/articles/%D8%A7%D9%84%D9%86%D8%B3%D8%AE-%D8%A7%D9%84%D8%A7%D8%AD%D8%AA%D9%8A%D8%A7%D8%B7%D9%8A-%D8%A8%D9%82%D8%A7%D8%B9%D8%AF%D8%A9-3-2-1-%D9%88%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1-%D8%A7%D9%84%D8%A7%D8%B3%D8%AA%D8%B9%D8%A7%D8%AF%D8%A9/).

## الخلاصة

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

- منذ 15 أبريل 2026 أصبح Cal.com مغلق المصدر، والمستودع العام Cal.diy بترخيص MIT دون الفرق وسير العمل وSSO والإحصاءات، ويقول عن نفسه إنه ليس للإنتاج.
- لا يوجد Docker Image لـ Cal.diy، وآخر Image رسمية هي `calcom/cal.com:v6.2.0` من مارس 2026، وحجمها 8 GB على القرص، وأول تشغيل بعنوان مختلف استغرق 16 دقيقة عندنا.
- ربط CalDAV يعمل مع سيرفر داخلي، ولكن فحص التعارض مغلق افتراضياً، فدونه عرض Cal.com أوقاتاً مشغولة في تقويمنا.
- ال Webhooks موقعة بال HMAC-SHA256، والأوقات فيها بتوقيت UTC مع منطقة كل طرف.
- العربية مكتملة تقريباً وباتجاه RTL صحيح، وقالب اسم الموعد يبقى بالإنجليزية حتى تغيره.
- قيد فريد يمنع حجز الوقت نفسه مرتين، ولكنه لا يمنع حجزين متداخلين بمدد مختلفة في اللحظة نفسها.

[![](https://arabroot.io/content/images/2026/10/booking-tools-cover-cover.webp)دليلأي أداة حجز مواعيد تناسب عيادتك أو فريقك؟أربع حالات حجز مختلفة، من العيادة إلى قاعة الاجتماعات، والأداة مفتوحة المصدر التي تناسب كل حالة منها.](https://arabroot.io/articles/%D8%A7%D9%8A-%D8%A7%D8%AF%D8%A7%D8%A9-%D8%AD%D8%AC%D8%B2-%D9%85%D9%88%D8%A7%D8%B9%D9%8A%D8%AF-%D8%AA%D9%86%D8%A7%D8%B3%D8%A8%D9%83/)[![](https://arabroot.io/content/images/2026/10/easy-appointments-cover-cover.webp)دليلEasy!Appointments: حجز مواعيد العيادات ومراكز الخدمةكيف تشغل Easy!Appointments على سيرفرك لحجز الخدمات مع مقدميها، وما الذي يحتاج انتباهك في العربية والمنطقة الزمنية.](https://arabroot.io/articles/easy-appointments-%D8%AD%D8%AC%D8%B2-%D9%85%D9%88%D8%A7%D8%B9%D9%8A%D8%AF-%D8%A7%D9%84%D8%B9%D9%8A%D8%A7%D8%AF%D8%A7%D8%AA-%D9%88%D9%85%D8%B1%D8%A7%D9%83%D8%B2-%D8%A7%D9%84%D8%AE%D8%AF%D9%85%D8%A9/)[![](https://arabroot.io/content/images/2026/10/calendar-contacts-cover-cover.webp)دليلالتقويم وجهات الاتصال على سيرفرك مع CalDAV وCardDAVكيف تبقى مواعيدك وجهات اتصالك متطابقة بين الهاتف والحاسوب على سيرفرك، وما الذي يعمل مع Outlook وما الذي لا يعمل.](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D9%82%D9%88%D9%8A%D9%85-%D9%88%D8%AC%D9%87%D8%A7%D8%AA-%D8%A7%D9%84%D8%A7%D8%AA%D8%B5%D8%A7%D9%84-caldav-%D9%88-carddav/)[![](https://arabroot.io/content/images/2026/10/radicale-cover-cover.webp)دليلRadicale: سيرفر تقويم وجهات اتصال خفيف لفريقككيف تشغل Radicale خلف Reverse Proxy بكلمات مرور bcrypt وتقويم مشترك للفريق، وتحفظ كل تغيير في git لتسترجعه عند الخطأ.](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-radicale-%D9%84%D9%84%D8%AA%D9%82%D9%88%D9%8A%D9%85-%D9%88%D8%AC%D9%87%D8%A7%D8%AA-%D8%A7%D9%84%D8%A7%D8%AA%D8%B5%D8%A7%D9%84/)

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

- أكتوبر 2026: كتابة المقال، والتجربة العملية على calcom/cal.com:v6.2.0 مع PostgreSQL 17.6 وRadicale 3.8.3 وMailpit v1.31.5 على Docker Engine 29.8.0، وقراءة ملفات الترخيص في Cal.diy عند الوسم v6.2.0 والفرع الرئيسي.