الـ Object Storage والـ Media: presigned URLs و uploads مباشرة

جارٍ التحميل…

01 / ملخص الدرس

الـ Object Storage والـ Media: presigned URLs و uploads مباشرة

الـ users بيرفعوا صور وفيديوهات، والـ server بيستلم كل byte في الـ memory، ويحفظها على الـ disk بتاعه، وبالاسم اللي الـ user اختاره، وأي حد يعرف الـ URL يشوفها.

02 / شرح الدرس مكتوبًا

نص شرح المعلم والكود الذي كتبه ونتيجة تشغيله، بنفس ترتيب الدرس.

أهلًا بيك. الـ chat بتاعنا بقى فيه صور. بص على upload dot naive dot js. الـ client بيبعت الصورة base64 في JSON، والـ server بيحفظها في public uploads، ويرجّع الـ URL. وده code بتلاقيه في tutorials كتير. الـ file كله بيعدّي على الـ server، وفي الـ memory، و base64 كمان يعني أكبر بالتلت. عشرين user بيرفعوا فيديو، والـ server وقع. الـ disk بتاع instance واحد. الـ instance التاني مش شايف الصورة، ولما تعمل deploy، الـ container الجديد فاضي. الاسم من الـ user زي ما هو. يبعت dot dot slash، ويكتب فوق ملفات الـ app. ده path traversal. النوع ما اتشافش. يرفع HTML فيه script ويقول إنها صورة، وأي حد يفتحها الـ script يشتغل على الـ domain بتاعك. مفيش حد للحجم، والـ resize جُوّه الـ request. والـ public folder: أي حد يعرف الـ URL يشوف صورة البطاقة بتاعتك.

الحل الأول: الـ files ما تتحفظش على الـ server خالص. بتتحفظ في object storage: S3 من AWS، أو R2 من Cloudflare، أو GCS، أو MinIO لو عايز تشغّله بنفسك. فيه buckets، وجُوّه كل bucket فيه objects. كل object ليه key، زي uploads slash sara slash id، والـ bytes، و metadata زي الـ content type. مفيش folders بجد. الـ slash دي جزء من الاسم، بس تقدر تعمل list بـ prefix. والـ object ما بيتعدّلش في النص. يا تكتبه كله، يا تكتب واحد جديد. وكل الـ instances بتشوف نفس الـ storage، والـ deploy ما بيلمسوش، والـ storage بيعمل نسخ لوحده، وبيستحمل أي حجم.

طب إزاي الـ client يرفع للـ storage من غير ما ياخد الـ credentials بتاعتنا؟ بالـ presigned URL. الـ server بيكتب تصريح: PUT، على الـ key ده بالضبط، بالـ content type ده، بالحجم ده بالكتير، لحد ساعة معيّنة. وبيعمل signature للتصريح ده بـ HMAC بالـ secret اللي هو والـ storage بس يعرفوه. وبيحط التصريح والـ signature في الـ URL. الـ client بيعمل PUT على الـ URL ده مباشرة. والـ storage بيحسب الـ signature تاني، ولو أي حاجة اتغيرت، الـ key أو الحجم أو الوقت، الـ signature مش هتطابق، والـ request يترفض. كده الـ server ما شافش ولا byte من الـ file، والـ client ما خدش أي secret، والتصريح ده بيخلص بعد خمس دقايق، وما ينفعش يتستخدم لـ key تاني. في S3 الحقيقي، الـ signature اسمها SigV4، وأعقد شوية، بس نفس الفكرة بالضبط. وفيه حاجتين لازم تعرفهم: الـ presigned PUT في S3 ما بيقدرش يحدّد حجم أقصى. لو عايز تحدّد الحجم، بتستخدم presigned POST بـ policy فيها content-length-range. أو تتأكد بعد الـ upload، وده اللي هنعمله كمان. والـ files الكبيرة، زي فيديو جيجا، ما بتترفعش في request واحد. بتستخدم multipart upload: بتبدأ upload، وتاخد presigned URL لكل part، خمسة ميجا، والـ client يرفع الـ parts بالتوازي، ولو part وقع يعيده هو بس، وفي الآخر complete بيجمعهم. وكل multipart ما خلصش بياخد مساحة وانت بتدفع عليها، فلازم lifecycle rule تمسحه.

الـ client رفع. نصدّقه؟ لأ. الـ client بيقول للـ server خلصت، يعني complete. والـ server بيعمل: head على الـ object: موجود فعلًا؟ حجمه زي اللي اتقال؟ ويقرا أول كام byte، اللي اسمهم magic bytes. كل نوع file بيبدأ ببصمة: PNG بيبدأ بـ تمن bytes معروفين، و JPEG بـ تلاتة. الـ content type اللي الـ client قاله مجرد كلام، إنما الـ bytes دي هي الحقيقة. لو مش مطابقين، امسح الـ object، وارفض. وبعد كده الـ processing: resize، و thumbnails، و virus scan، وشيل الـ EXIF اللي فيه مكان الصورة. ده تقيل، فبيتحط في queue، والـ worker يعمله في background، والـ status يبقى processing لحد ما يخلص. والـ complete لازم يبقى idempotent: لو الـ client بعته مرتين، ما يتحطش في الـ queue مرتين.

والـ download. الـ bucket يفضل private. محدش يقدر يقرا منه من غير signature. ولما صاحب الصورة يطلبها، الـ server يتأكد إنها بتاعته، وإنها ready، ويعمل presigned GET لمدة دقيقة. ولو الصور public بجد، زي صور المنتجات، حط CDN قدام الـ bucket، والـ CDN يعمل cache قريب من الـ users. وفيه CDNs بتعمل signed URLs برضه، و image resizing وقت الـ request. وخلّي بالك من الـ content-disposition: الـ files اللي الـ users رفعوها تتعرض من domain تاني غير الـ app، علشان لو حاجة عدّت، ما توصلش للـ cookies بتاعتك. وآخر حاجة، الـ cleanup. الـ client خد URL ورفع ومشي، أو ما رفعش أصلًا. الـ records دي بتفضل pending، والـ objects بتاخد مساحة. job بيعدّي كل شوية، وأي pending عدّى عليه وقت كبير يتمسح. وده بيتعمل كمان بـ lifecycle rules في الـ bucket نفسه.

التحدي. عندك storage dot js، object storage صغير على HTTP زي S3، بيتأكد من الـ presigned URLs بنفسه، ومعاه functions للـ server زي الـ SDK. و lab dot js بيمشي الـ flow كله، وبيجرّب محاولات غلط. والمطلوب media dot js. كل التفاصيل في الكومنت: presign بنفس طريقة الـ storage بالضبط، و sniff و pngSize و safeFilename، و createMediaService بالـ createUpload والـ completeUpload والـ processNext والـ downloadUrl والـ cleanup. اقرا storage dot js الأول علشان تفهم بيتأكد من إيه، وبعدين الكومنت، ونشغّل test dot js لحد ما تبقى أربعتاشر من أربعتاشر.

تمرين

اكتب media.js: MediaError، و presign بـ HMAC-SHA256 بنفس الـ string والـ query اللي storage.js بيتأكد منهم، و sniff و pngSize و safeFilename، و createMediaService بـ createUpload و completeUpload (head والحجم والـ magic bytes) و processNext (meta.json والـ retries) و downloadUrl و cleanup و get و pending. كل التفاصيل في الكومنت، وشغّل node test.js.

الحل. presign: الـ string بالترتيب، و HMAC، و URLSearchParams بالترتيب، و encodeURIComponent لكل جزء من الـ key. الـ key من الـ user والـ id بس، والـ filename بيتخزّن في الـ record للعرض، متنضّف. الـ complete: head، والحجم، وأول ستاشر byte، ولو مش مطابق delete. الـ processNext: لو وقع، يرجع لآخر الـ queue لحد تلات مرات. نشغّل lab dot js.

media.js
// media.js: الـ uploads بتروح للـ storage مباشرة بـ presigned URLs، والـ server بيتأكد ويعمل processing في background
import { createHmac, createHash, randomUUID } from "node:crypto";

export class MediaError extends Error {
  constructor(code, message = code) {
    super(message);
    this.code = code;
  }
}

export function presign({ base, bucket, secret, method, key, ttlMs, contentType = null, maxBytes = null, now = Date.now }) {
  const expires = now() + ttlMs;
  const toSign = [method, bucket, key, expires, contentType ?? "", maxBytes ?? ""].join("\n");
  const sig = createHmac("sha256", secret).update(toSign).digest("hex");
  const q = new URLSearchParams({ "X-Method": method, "X-Expires": String(expires) });
  if (contentType) q.set("X-Content-Type", contentType);
  if (maxBytes != null) q.set("X-Max-Bytes", String(maxBytes));
  q.set("X-Signature", sig);
  const path = key.split("/").map(encodeURIComponent).join("/");
  return { url: `${base}/${bucket}/${path}?${q}`, expiresAt: expires };
}

const MAGIC = {
  "image/png": [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a],
  "image/jpeg": [0xff, 0xd8, 0xff],
};

export function sniff(buf) {
  for (const [type, sig] of Object.entries(MAGIC)) if (sig.every((b, i) => buf[i] === b)) return type;
  return null;
}

export function pngSize(buf) {
  if (sniff(buf) !== "image/png" || buf.length < 24) return null;
  return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
}

export function safeFilename(name) {
  const base = String(name ?? "").split(/[\\/]/).pop().normalize("NFKC");
  const clean = base.replace(/[^\p{L}\p{N}._-]+/gu, "_").replace(/^\.+/, "").slice(0, 100);
  return clean || "file";
}

export function createMediaService({
  storage, bucket, secret, now = Date.now,
  maxBytes = 5 * 1024 * 1024, allowed = ["image/png", "image/jpeg"],
  uploadTtl = 300000, downloadTtl = 60000,
}) {
  const records = new Map();
  const queue = [];
  const sign = (method, key, ttlMs, extra = {}) => presign({ base: storage.url, bucket, secret, method, key, ttlMs, now, ...extra });

  function mine(userId, id) {
    const r = records.get(id);
    if (!r || r.userId !== userId) throw new MediaError("NOT_FOUND");
    return r;
  }

  function createUpload(userId, { filename, contentType, size } = {}) {
    if (!allowed.includes(contentType)) throw new MediaError("UNSUPPORTED_TYPE");
    if (!Number.isInteger(size) || size <= 0 || size > maxBytes) throw new MediaError("TOO_LARGE");
    const id = randomUUID();
    const key = `uploads/${userId}/${id}`;
    const rec = { id, userId, key, filename: safeFilename(filename), contentType, size, status: "pending", createdAt: now() };
    records.set(id, rec);
    const { url, expiresAt } = sign("PUT", key, uploadTtl, { contentType, maxBytes: size });
    return { id, key, url, expiresAt };
  }

  async function completeUpload(userId, id) {
    const rec = mine(userId, id);
    if (rec.status !== "pending") return { ...rec };
    const head = await stora
…

المخرجات

$ node lab.js
— 1) الـ client بيطلب upload، والـ file بيروح للـ storage على طول —
  الـ server رجّع key: uploads/sara/bdef5ca7-5472-46fc-9379-cbbefa4a6506
  والـ URL صالح لحد: 14:51:35 وفيه: X-Method X-Expires X-Content-Type X-Max-Bytes X-Signature
  PUT للـ storage: 200 تمام
  الـ server استلم كام byte من الـ file؟ صفر. الـ storage استلم: 150000

— 2) محاولات غلط —
  نفس الـ URL على key تاني: 403 SignatureDoesNotMatch
  file أكبر من اللي اتقال: 413 EntityTooLarge
  content-type تاني: 403 ContentTypeMismatch
  بعد ما الـ URL انتهى: 403 RequestExpired
  HTML على إنه PNG، الـ PUT نفسه: 200 تمام
  بس الـ complete: BAD_CONTENT ، والـ object اتمسح: true

— 3) complete، والـ processing في background —
  بعد الـ complete: processing ، والـ filename المتنضّف: صورة_البروفايل.png
  download قبل ما يخلص: NOT_READY
  الـ worker خلّص: ready 1080x720 sha256 2e269eef766d
  الـ objects: [
  'derived/bdef5ca7-5472-46fc-9379-cbbefa4a6506/meta.json',
  'uploads/sara/bdef5ca7-5472-46fc-9379-cbbefa4a6506'
]

— 4) الـ download: private، و URL قصير العمر —
  GET: 200 image/png 150000 bytes
  omar بيحاول ياخد URL لصورة سارة: NOT_FOUND
  الـ path من غير signature: 403
  نفس الـ URL بعد دقيقة: 403

— 5) upl
…

الأول: الـ key ما فيهوش الاسم اللي فيه dot dot slash، والـ URL فيه التصريح كله والـ signature، والـ storage استلم الـ bytes، والـ server صفر. التاني: key تاني، أو حجم أكبر، أو نوع تاني، أو بعد الوقت، كله اترفض. والـ HTML عدّى من الـ PUT، لأن الـ storage ما بيفتحش الـ file، بس الـ complete مسكه بالـ magic bytes ومسحه. التالت: processing، والـ download قبل ما يخلص اترفض، والـ worker طلّع الأبعاد و meta dot json. الرابع: الـ download بـ URL صاحبه بس، والـ path من غير signature اترفض، ونفس الـ URL بعد دقيقة اترفض. الخامس: الـ cleanup مسح الاتنين اللي اتسابوا. ونشغّل test dot js.

media.js

المخرجات

$ node test.js
✅ presign: الـ storage بيقبل الـ URL الصح، وبيرفض لو حرف واحد اتغير في الـ signature {"r1":{"status":200,"error":null},"r1b":{"status":403,"error":"SignatureDoesNotMatch"},"p":{"url":"http://s/b/x/y?X-Method=GET&X-Expires=6000&X-Signature=a1523dc06c016257f3eec5c6d8d65af4de565a08c023b1ac3bfe709c3454d9d1","expiresAt":6000}}
✅ URL انتهى ← RequestExpired، و URL للـ PUT ما ينفعش GET، و content-type غير المتسجّل ← رفض {"r2":{"status":403,"error":"RequestExpired"},"r3a":{"error":"MethodNotAllowed"},"r3b":{"status":403,"error":"ContentTypeMismatch"}}
✅ أكبر من X-Max-Bytes ← 413 EntityTooLarge {"status":413,"error":"EntityTooLarge"}
✅ الـ signature مربوط بالـ key والحجم: ما ينفعش تغيّر الـ path ولا X-Max-Bytes {"r4b":{"status":403,"error":"SignatureDoesNotMatch"},"r4c":{"status":403,"error":"SignatureDoesNotMatch"},"s":{"puts":1,"gets":0,"rejected":7}}
✅ sniff و pngSize و safeFilename ["صورة_العيد.png","hidden"]
✅ createUpload: الـ validation، والـ key من الـ user والـ id بس، والـ filename متنضّف {"e6":["UNSUPPORTED_TYPE","TOO_LARGE","TOO_LARGE","TOO_LARGE"],"up":"uploads/u1/a37975cd-ceb0-4dab-bc9a-52cb9e3dee66"}
✅ الـ flow كله: PUT مباشرة للـ storage، و complete ← processing
…

كله نجح. الصورة بقت ready. وبعدين؟ نبعت إيميل، ونعمل notification، ونحطها في الـ search. الدرس الجاي: الـ integrations مع الـ email providers والـ push notifications والـ search engines، والـ webhooks بتاعتهم، والـ retries، وإزاي تخلّي الـ index ماشي مع الـ database.

upload.naive.js
// upload.naive.js: الـ file بيعدّي على الـ server، وبيتحفظ على الـ disk بتاعه
import { createServer } from "node:http";
import { writeFileSync, mkdirSync } from "node:fs";

mkdirSync("public/uploads", { recursive: true });
createServer(async (req, res) => {
  if (req.method !== "POST" || req.url !== "/upload") return res.writeHead(404).end();
  let raw = "";
  for await (const c of req) raw += c; // الـ file كله في الـ memory، وكمان base64 يعني أكبر بالتلت
  const { filename, data } = JSON.parse(raw);
  // الاسم من الـ user زي ما هو: ../../server.js ؟
  writeFileSync(`public/uploads/${filename}`, Buffer.from(data, "base64"));
  // وبعدين resize هنا، والـ request مستني
  res.end(JSON.stringify({ url: `/uploads/${filename}` })); // public لأي حد يعرف الاسم
}).listen(3000);
// والمشاكل:
// - كل upload واخد memory و bandwidth و connection من الـ server طول ما هو بيترفع
// - الـ disk بتاع instance واحد: الـ instance التاني مش شايف الـ file، والـ deploy بيمسحه
// - الاسم من الـ user ← path traversal، والنوع ما اتشافش ← HTML متخبّي على إنه صورة
// - مفيش حد للحجم، والـ processing جُوّه الـ request
storage.js
// storage.js: object storage صغير زي S3، في الـ memory. ما تعدّلش فيه.
// - HTTP: PUT و GET على /bucket/key بـ presigned URL بس، وبيتأكد من الـ signature والوقت والـ method والـ content-type والحجم
// - وللـ server نفسه (زي الـ SDK بالـ credentials): head و get و put و delete و list
import { createServer } from "node:http";
import { createHmac, createHash, timingSafeEqual } from "node:crypto";

export async function startStorage({ secret, now = Date.now } = {}) {
  const objects = new Map(); // "bucket/key" -> { body, contentType, size, etag }
  const stats = { puts: 0, gets: 0, rejected: 0 };
  const save = (bucket, key, body, contentType) => {
    const etag = createHash("md5").update(body).digest("hex");
    objects.set(`${bucket}/${key}`, { body, contentType, size: body.length, etag });
    return etag;
  };
  const deny = (res, status, code) => {
    stats.rejected++;
    res.writeHead(status, { "content-type": "application/json" }).end(JSON.stringify({ error: code }));
  };

  const server = createServer(async (req, res) => {
    const url = new URL(req.url, "http://x");
    const [, bucket, ...rest] = url.pathname.split("/");
    const key = decodeURIComponent(rest.join("/"));
    const q = Object.fromEntries(url.searchParams);
    const toSign = [q["X-Method"], bucket, key, q["X-Expires"], q["X-Content-Type"] ?? "", q["X-Max-Bytes"] ?? ""].join("\n");
    const want = createHmac("sha256", secret).update(toSign).digest();
    const got = Buffer.from(q["X-Signature"] ?? "", "hex");
    if (got.length !== want.length || !timingSafeEqual(got, want)) return deny(res, 403, "SignatureDoesNotMatch");
    if (Number(q["X-Expires"]) <= now()) return deny(res, 403, "RequestExpired");
    if (q["X-Method"] !== req.method) return deny(res, 403, "MethodNotAllowed");

    if (req.method === "PUT") {
      const ct = req.headers["content-type"] ?? "";
      if (q["X-Content-Type"] && ct !== q["X-Content-Type"]) return deny(res, 403, "ContentTypeMismatch");
      const max = q["X-Max-Bytes"] ? Number(q["X-Max-Bytes"]) : Infinity;
      const chunks = [];
      let size = 0;
      for await (const c of req) {
        size += c.length;
        if (size > max) return deny(res, 413, "EntityTooLarge");
        chunks.push(c);
      }
      stats.puts++;
      const etag = save(bucket, key, Buffer.concat(chunks), ct);
      return res.writeHead(200, { etag }).end();
    }
    if (req.method === "GET") {
      const o = objects.get(`${bucket}/${key}`);
      if (!o) return deny(res, 404, "NoSuchKey");
      stats.gets++;
      return res.writeHead(200, { "content-type": o.contentType, "content-length": o.size, etag: o.etag }).end(o.body);
    }
    deny(res, 405, "MethodNotAllowed");
  });
  await new Promise((r) => server.listen(0, "127.0.0.1", r));

  return {
    url: `http://127.0.0.1:${server.address().port}`,
    stats,
    async head(bucket, key) {
      const o = objects.get(`${bucket}/${key}`);
      return o ? { size: o.size, contentType: o.contentTyp
…
03 / دروس المسار (122)
  1. 01أهلًا بيك: يعني إيه backend، والكورس ده ماشي إزاي
  2. 02الملفات والفولدرات والـ terminal: تتحرك على جهازك زي المبرمجين
  3. 03Node وأول برنامج: تشغّل وتقرا رسالة الخطأ
  4. 04القيم والمتغيرات والأنواع: const و let و typeof
  5. 05القرارات والتكرار: if و === والـ truthy والـ loops
  6. 06الـ functions: parameters و return والـ arrow functions والـ scope
  7. 07الـ objects والـ arrays: destructuring و spread و map و filter و reduce
  8. 08JSON والنصوص والتواريخ: اللغة اللي الـ backend بيتكلم بيها
  9. 09الـ callbacks والـ closures وأول لمسة async: setTimeout
  10. 10الـ classes والـ errors: throw و try/catch و stack trace
  11. 11git: تحفَظ كل خطوة وترجعلها، وما تسرّبش أسرارك
  12. 12ESM و CommonJS و package exports: Node بيلاقي الـ modules إزاي
  13. 13الـ Promises و async/await: sequential و parallel والأخطاء والإلغاء
  14. 14الـ event loop والـ microtasks: Node بيعمل كل حاجة بـ thread واحد إزاي
  15. 15الـ Streams والـ Backpressure: ملفات أكبر من الـ memory
  16. 16الـ File System والـ paths: قراية وكتابة صح، ومن غير path traversal
  17. 17الـ Buffers والبيانات الـ binary: الـ bytes من جُوّه
  18. 18Crypto APIs والأسرار: random و hash و HMAC و AES-GCM
  19. 19الـ Worker Threads: شغل CPU تقيل من غير ما توقف الـ server
  20. 20الـ Errors على مستوى الـ process: operational و programmer و crash-only
  21. 21الـ Node test runner والـ debugging: tests من غير مكتبات
  22. 22HTTP من جُوّه: الـ methods والـ status codes والـ headers
  23. 23Express والـ Routing: الـ params والـ query والترتيب والـ Router
  24. 24Express 5: إيه اللي اتغيّر، وإزاي تنقل مشروع قديم
  25. 25الـ Middleware ودورة حياة الـ request جُوّه Express
  26. 26Validation للمدخلات بـ Zod: ما تصدّقش أي حاجة جاية من برة
  27. 27تصنيف الأخطاء و Problem Details: شكل واحد لكل الـ errors
  28. 28الـ CORS والـ Cookies: المتصفح بيحمي المستخدم إزاي
  29. 29رفع الملفات وحدود الحجم: multer من غير ثغرات
  30. 30الـ Realtime: Server-Sent Events و WebSockets
  31. 31OpenAPI والـ API Versioning: عقد مكتوب، وتغيير من غير ما تكسر حد
  32. 32PostgreSQL: الـ DDL وأنواع البيانات الصح لكل عمود
  33. 33الـ Constraints: PK و FK و UNIQUE و CHECK — الـ database بتحمي نفسها
  34. 34الـ Normalization والـ ERD: تقسّم البيانات صح من أول يوم
  35. 35الـ JOINs والتجميع: INNER و LEFT و GROUP BY و HAVING و window functions
  36. 36JSONB في PostgreSQL: إمتى تستخدمه، وإزاي تسأله وتعمله index
  37. 37الـ Indexes من جُوّه: B-tree و GIN و BRIN
  38. 38Partial و Covering indexes: indexes أصغر وأسرع
  39. 39الـ Pagination بالـ Keyset: ليه OFFSET بيبطّأ وبيكرر
  40. 40Full Text Search في Postgres: بحث بالكلمات، بالعربي والإنجليزي
  41. 41EXPLAIN ANALYZE والـ Planner: اقرا الـ plan وصلّح الـ query
  42. 42الـ Transactions بـ node-postgres: كله أو ولا حاجة
  43. 43الـ MVCC ومستويات الـ Isolation: transactions كتير في نفس الوقت
  44. 44الـ Locks والـ Deadlocks: مين مستني مين
  45. 45SKIP LOCKED و Job Leasing: Postgres كـ job queue
  46. 46Row Level Security والـ Multi-tenancy: كل عميل يشوف بياناته بس
  47. 47Migrations من غير downtime: expand و backfill و contract
  48. 48الـ Replication والـ Read Replicas: نسخ حية من الـ database
  49. 49الـ Connection Pooling و PgBouncer: connections أقل، requests أكتر
  50. 50الـ Partitioning والـ VACUUM والمراقبة: Postgres على المدى الطويل
  51. 51Redis وأنواع البيانات: strings و hashes و lists و sets و sorted sets
  52. 52Cache-aside والـ TTL: Redis قدام الـ database
  53. 53الـ Cache invalidation والـ Stampede: لما الـ key الشهير يموت
  54. 54Lua والـ Atomicity في Redis: كام أمر كأنهم أمر واحد
  55. 55الـ Rate Limiting في Redis: fixed window و sliding window و token bucket
  56. 56الـ Sessions في Redis: تسجيل دخول تقدر تلغيه في أي وقت
  57. 57Redis Streams والـ Consumer Groups: log بيتوزع على workers
  58. 58Pub/Sub و BullMQ: إشارات بين الـ instances، وطوابير شغل محترمة
  59. 59Redis لما يقع: الـ persistence (RDB و AOF) والـ replication
  60. 60Redis في الإنتاج: الـ ACLs والـ Cluster والمراقبة
  61. 61تخزين الـ Passwords بـ Argon2id: صح من أول مرة
  62. 62Session Fixation والـ Rotation: id جديد مع كل صلاحية جديدة
  63. 63الـ Cookie Flags والـ CSRF: المتصفح بيبعت الـ cookie، فلازم تتأكد مين اللي طلب
  64. 64الـ XSS والـ Content Security Policy: ما تخليش النص يتحوّل لـ code
  65. 65الـ OAuth و OpenID Connect و PKCE: login بـ Google من غير ما تشوف الـ password
  66. 66التأكد من الـ JWT صح: التوقيع والـ claims والـ scopes
  67. 67الـ BOLA و BFLA: الـ token السليم مش معناه إن الطلب مسموح
  68. 68الـ RBAC و ABAC والملكية: policy واحدة بدل ifs متفرقة
  69. 69الـ MFA والـ recovery: التاني factor اللي بجد
  70. 70الـ Passkeys و WebAuthn: login من غير password وضد الـ phishing
  71. 71الـ Threat Modeling بـ STRIDE: فكّر زي المهاجم قبل ما تكتب code
  72. 72الـ SQL Injection والـ Parameterization: الـ data ما تبقاش code
  73. 73الـ SSRF والـ Egress: ما تخليش الـ server بتاعك يزور الشبكة الداخلية
  74. 74الـ Security Headers والـ TLS: كل response يقول للـ browser يحمي المستخدم إزاي
  75. 75توقيع الـ Webhooks والـ Replay: الـ webhook ده جاي فعلًا من Stripe؟
  76. 76تغيير الأسرار والـ HMAC: إزاي تغيّر مفتاح من غير ما كل حاجة تقع
  77. 77سلسلة التوريد والـ SBOM: إنت واثق في ألف package ما تعرفهمش؟
  78. 78الـ Audit والـ Privacy Logs: سجّل مين عمل إيه، من غير ما تسجّل الأسرار
  79. 79اختبار الأمان و OWASP ASVS: الأمن جُوّه الـ CI، مش audit آخر السنة
  80. 80الـ Prototype Pollution والـ Resource DoS: request واحد يخلّي الكل admin أو يوقّع الـ server
  81. 81الـ SLI والـ SLO والـ Error Budget: إمتى تصحّي الـ on-call وإمتى تسيبه نايم
  82. 82الـ Event Loop Delay والـ ELU: لما Node نفسه يتخنق
  83. 83الـ Worker Threads والـ CPU Profiling: الشغل التقيل بره الـ event loop، والوقت راح فين
  84. 84الـ Memory والـ GC والـ Leaks: الـ process اللي بيكبر لحد ما يقع
  85. 85الـ N+1 Queries والـ DataLoader: صفحة واحدة بمِيّتين query
  86. 86الـ HTTP Caching والـ Compression: أسرع request هو اللي ما بيوصلش
  87. 87الـ Load Testing والـ Soak Testing: الـ server بيستحمل قد إيه بجد
  88. 88الـ Concurrency Limits والـ Bulkheads: dependency بطيئة ما توقعش الـ app كله
  89. 89الـ Timeouts والـ Retries والـ Circuit Breakers: إزاي تتعامل مع dependency بطيئة أو واقعة
  90. 90OpenTelemetry والـ Distributed Tracing: الـ request راح فين وضاع وقته فين
  91. 91Docker Layers والـ Build Context: ليه الـ build بياخد دقيقة وفيه الـ .env بتاعك
  92. 92Multi-stage Builds و Non-root: image صغيرة ومن غير root
  93. 93Docker Compose والـ Networks: الـ services بتلاقي بعض إزاي
  94. 94Docker Volumes: الـ data بتتحفَظ فين
  95. 95Healthcheck و Restart و Resource Limits: running مش معناها شغّال
  96. 96BuildKit والـ Build Secrets: الـ token ما يدخلش الـ image
  97. 97Reverse Proxy و TLS: اللي واقف قدام الـ app
  98. 98Multi-arch Images والـ Native Modules: اشتغلت عندي
  99. 99CI/CD و Rollback و Canary: الـ deploy يبقى ممل
  100. 100Kubernetes من الأول: Pods و Deployments و Services و Probes
  101. 101Modular Monolith و DDD: حدود واضحة من غير network
  102. 102Transactional Outbox و Inbox: الـ event ما يضيعش وما يتنفذش مرتين
  103. 103Sagas: عملية واحدة على تلات services من غير transaction واحدة
  104. 104CQRS و Event Sourcing: الـ events هي الحقيقة
  105. 105الـ Queues والـ Brokers: BullMQ و RabbitMQ و Kafka و SQS
  106. 106Realtime على أكتر من instance: Redis pub/sub و presence و resume
  107. 107الـ Object Storage والـ Media: presigned URLs و uploads مباشرةانت هنا
  108. 108التكامل مع الـ Email والـ Search: retries و webhooks و indexes
  109. 109GraphQL و gRPC في الإنتاج: contracts و DataLoader و limits و deadlines
  110. 110Kafka على أكتر من zone و region: الـ replication والـ ISR والـ acks والـ MirrorMaker والـ failover
  111. 111TypeScript و runtime boundaries: الـ types بتختفي، والـ schemas لأ
  112. 112ORM ولا SQL: query builders و repositories و migrations و transactions
  113. 113الـ Serverless من جُوّه: cold starts و freeze و timeouts و connections
  114. 114الـ Testing Pyramid: unit و integration و e2e على Postgres حقيقية
  115. 115Contract Testing و Property-Based Testing بـ fast-check
  116. 116الـ Payments: idempotency و webhooks و refunds و reconciliation
  117. 117الـ Timezones والـ Internationalization: UTC و DST والـ locales والـ plurals والفلوس
  118. 118الـ Privacy والـ Data Retention: consent و export و erasure و crypto shredding
  119. 119الـ ADRs والـ Feature Flags: القرارات المكتوبة والـ rollout من غير deploy
  120. 120Multi-region active-active: حل الـ conflicts بـ HLC و CRDTs
  121. 121Readiness و Liveness و Startup Probes: الـ pod جاهز ولا عايش؟
  122. 122Incident Runbooks: من الـ alert للـ postmortem