- CDN תמונות חי: מקור יחיד ב-R2, URL אחד עם
/cdn-cgi/image/שמחזיר אותה תמונה בכל גודל שתבקש, עם WebP/AVIF אוטומטי ו-edge cache מלא — ב-$0. - Edge Screenshot API חי: Worker שמקבל
?url=, פותח את הדף ב-Puppeteer, מצלם, שומר ל-R2, ומחזיר URL — ובקריאה חוזרת מגיש מ-cache ב-KV בלי לשרוף עוד browser-minutes. - וידאו HLS מתנגן מ-R2: קובצי
.m3u8+.tsמ-ffmpeg, מוגשים עם ה-content-type הנכון, מתנגנים adaptively בדפדפן ב-$0 egress. - חדר וידאו real-time: Cloudflare Realtime SFU + Worker signaling — שני peers רואים זה את זה, וה-App Secret לעולם לא מגיע לדפדפן.
- שתי מסגרות החלטה: מתי Stream/Images המנוהל שווה את הכסף מול R2 self-host, ואיך לבחור בין Image Transforms ל-Media Transformations.
- טבלת תקציב media משולב: לכל מוצר media — מה חינם, מה הקיר, ואיזה limit נופל ראשון באפליקציה האמיתית שלך.
wrangler.tomlמורחב: אותו קובץ מפרק 2, עכשיו עם טבלת[browser]עליונה ל-Browser Run בנוסף ל-bindings של R2 ו-KV.
- לבנות CDN תמונות חינמי: לאחסן מקור ב-R2 (10GB) ולהשתמש ב-Image Transforms (5,000/month) ל-resize ו-WebP on-the-fly דרך URL בלבד.
- לבנות screenshot/scraper/PDF API עם Browser Run (Puppeteer) בתוך תקציב 10 דקות browser/day, עם cache ב-KV שלא שורף את המכסה פעמיים.
- להשוות Stream המנוהל ($5/1,000 דקות) מול R2 + HLS self-hosting, ולנמק בעצמך מתי כל אחד שווה.
- לבנות חדר וידאו עם Cloudflare Realtime (1,000 GB egress/month ≈ 4,400 שעות) + Worker signaling שמחזיק את ה-Secret בצד שרת.
- לזהות איזה limit במחסנית ה-media נופל ראשון באפליקציה שלך, ולתכנן סביבו.
- סיימת את פרק 1 (פרסת Worker והכרת את מטריצת ה-free-tier) ואת פרק 2 (R2, KV, bindings ב-
wrangler.toml, ו-wrangler secret put). - יש לך Worker פרוס אחד לפחות ו-bucket R2 קיים. כאן לא נלמד מחדש איך יוצרים bucket או binding בסיסי — נבנה מעליהם.
- מותקן
wranglerעדכני ו-Node.js. נתקין בנוסף@cloudflare/puppeteerו-ffmpegמקומי לתרגיל הווידאו. - zone אחד מחובר לחשבון Cloudflare שלך — Image/Media Transforms דורשים zone שבו הם enabled (Images → Transformations).
בפרק 2 פרסת Worker אמיתי והכרת את R2, KV ו-wrangler.toml. בפרק 3 הוספת מעליהם שכבת AI עם Workers AI ו-RAG. פרק 4 מוסיף את שכבת ה-media: אותו Worker ואותו bucket R2 — עכשיו מגישים תמונות, מצלמים screenshots, מזרימים וידאו ומריצים video chat. בסוף הפרק יהיו לך ארבעה רכיבי media חיים על אותה תשתית. בפרק 5 (Auth, Email, Analytics) נעטוף אותם בהגנות: Turnstile מול ה-screenshot API שלא ינוצל לרעה, Zero Trust Access לגדר חדר וידאו פרטי, ו-Analytics Engine לספור צפיות ו-screenshots. כל מה שתבנה כאן — שם מקבל שכבת אבטחה ומדידה.
| מונח | בעברית | הסבר |
|---|---|---|
| Image Transforms | המרת תמונה בזמן אמת | resize/crop/quality/format on-the-fly דרך prefix של /cdn-cgi/image/ ב-URL; 5,000 transformations ייחודיים/חודש חינם בכל הטיירים. |
| Cloudflare Images | מוצר ניהול תמונות | מוצר תמונות מלא עם storage ו-variants מובנים; ה-storage וה-delivery בתשלום בלבד — רק ה-Transforms חינמיים. |
| Cloudflare Stream | מוצר וידאו מנוהל | וידאו עם transcoding, HLS ladder אוטומטי ו-player מובנה; ללא free tier — $5/1,000 דקות מאוחסנות + $1/1,000 דקות delivery. |
| Media Transformations | המרת וידאו בזמן אמת | clip/resize/thumbnail לוידאו ממקור R2 דרך prefix של /cdn-cgi/media/; 5,000 operations ייחודיים/חודש חינם — הקצאה נפרדת מ-Image Transforms. |
| HLS | HTTP Live Streaming | פרוטוקול streaming שמפצל וידאו ל-playlist (.m3u8) וקטעים (.ts/.m4s); מאפשר self-hosting על R2 ב-$0 egress כחלופה ל-Stream. |
| Browser Run (Puppeteer) | דפדפן ב-edge | Chrome headless מנוהל ב-edge, נשלט דרך @cloudflare/puppeteer/Playwright/CDP ל-screenshots/PDF/scraping; 10 דקות browser/יום + 3 concurrent חינם, דורש wrangler dev --remote. |
| Cloudflare Realtime | real-time WebRTC | מוצר real-time (לשעבר Cloudflare Calls) — SFU+TURN ל-WebRTC; inbound חינם, 1,000 GB egress/חודש חינם (~4,400 שעות @500kbps), $0.05/GB מעבר. |
| egress-GB billing | חיוב לפי נתונים יוצאים | חיוב לפי כמות הדאטה שיוצאת מ-Cloudflare החוצה; ב-R2 ה-egress תמיד $0, וב-Realtime מחויב רק ה-outbound מעבר ל-1,000 GB/חודש. |
| SFU | Selective Forwarding Unit | שרת שמקבל זרם וידאו אחד מכל משתתף ומעביר אותו לאחרים, במקום שכל זוג ידבר ישירות — מה שמאפשר חדרים עם הרבה משתתפים בלי שכל דפדפן יקרוס. |
מפת ה-media ב-$0: מה באמת חינמי ומה רק נראה ככה
media זה השדה שבו אפליקציות שורפות הכי הרבה כסף בלי לשים לב. תמונה אחת שלא מכווצת היא 4MB; וידאו של חמש דקות הוא כמה מאות מגה; וכל אלה צריכים להיות מאוחסנים, מומרים ומוגשים שוב ושוב. אצל ספקים מסורתיים, ה-egress — הדאטה שיוצאת מהשרת אל הגולש — היא הרוצח השקט: אתה משלם על כל בייט שנשלח החוצה, וזה מצטבר מהר. לכן השאלה הראשונה בכל פרויקט media היא לא "איזה כלי הכי טוב", אלא "איפה מתחיל החיוב".
ב-Cloudflare התמונה שונה ומבלבלת בו-זמנית. יש כאן חמישה מוצרי media שונים, וההבחנה הקריטית היא בין אחסון/הגשה (storage/delivery) לבין עיבוד (transforms). העיבוד — לקחת תמונה קיימת ולשנות לה גודל, או לחתוך frame מסרטון — הוא חינמי בנדיבות. האחסון וההגשה של media מנוהל — Stream ו-Cloudflare Images — הם דווקא לא. זו בדיוק הנקודה שבה vibe coders נופלים: רואים "Cloudflare Images, 5,000 חינם" ומניחים שהכול חינם, ואז מגלים שה-5,000 מתייחס רק ל-transforms, וה-storage מחויב מהתמונה הראשונה.
בוא נסדר את זה אחת ולתמיד. R2 הוא ה-storage שלך — 10GB חינם, ו-egress תמיד $0, מה שהופך אותו לבסיס של כל workaround media חינמי. Image Transforms (ה-prefix /cdn-cgi/image/) נותן 5,000 המרות ייחודיות בחודש חינם בכל הטיירים. Media Transformations (ה-prefix /cdn-cgi/media/) נותן 5,000 פעולות ייחודיות בחודש לוידאו — הקצאה נפרדת לגמרי. Browser Run נותן 10 דקות דפדפן ביום. ו-Realtime נותן 1,000 GB egress בחודש. כל אלה — חינם.
ומה לא חינם? Cloudflare Stream — אין לו free tier בכלל. אתה משלם $5 לכל 1,000 דקות מאוחסנות, ועוד $1 לכל 1,000 דקות שמוגשות, גם אם אף אחד לא צפה. Cloudflare Images storage — paid-only: $5 לכל 100,000 תמונות מאוחסנות, $1 לכל 100,000 שמוגשות. שני אלה הם מוצרים מנוהלים שמרוויחים מהנוחות שלהם — ובהמשך הפרק נראה בדיוק מתי הנוחות הזו שווה את הכסף ומתי R2+ffmpeg חוסכים לך הכול.
הכלל המנחה לכל הפרק, אם תזכור רק משפט אחד: אחסן את המקורות ב-R2, עבד אותם דרך Transform URLs, ושמור את ה-media המנוהל (Stream/Images storage) רק כשאתה באמת צריך את ה-pipeline המנוהל ומוכן לשלם עליו. זו לא קמצנות — זו ארכיטקטורה. כשה-egress הוא $0 והעיבוד נספר רק על קומבינציות ייחודיות, אתה יכול לבנות CDN תמונות שמשרת מיליוני בקשות בלי לשלם, כל עוד אתה לא חורג מ-5,000 וריאציות שונות.
שים לב גם להבחנה דקה אחת שתחזור: ה-free tier של ה-transforms סופר קומבינציות ייחודיות, לא בקשות. אם 100,000 גולשים מבקשים את אותה תמונה ברוחב 400 פיקסל, זו המרה אחת שנספרת פעם אחת; שאר הבקשות מוגשות מה-cache בחינם. לעומת זאת, אם תבקש את אותה תמונה ב-50 רוחבים שונים, אלה 50 המרות. זה משנה לחלוטין איך אתה מתכנן וריאנטים, ונחזור לזה בסקשן הבא.
למה זה כל כך משחרר ל-vibe coder? כי זה מסיר את החרדה הגדולה ביותר של media — "מה יקרה אם הפרויקט שלי יתפוצץ ויקבל מיליון גולשים?". בארכיטקטורות מסורתיות, הצלחה היא בעיה: יותר תנועה = יותר egress = חשבון גדול יותר. כאן ההפך — ה-egress של R2 הוא $0 בכל קנה מידה, וה-transforms נספרים פעם אחת לכל וריאנט ולא לכל גולש. כלומר אפליקציה עם עשרה גולשים ואפליקציה עם עשרה מיליון גולשים משלמות אותו דבר על אותה תמונה: כלום. זה מה שהופך את ה-stack הזה למתאים בדיוק ל-vibe coder שלא יודע מראש אם הפרויקט יתרומם — אתה לא משלם על ההימור, רק על מה שבאמת בנית.
המספרים בפרק נכונים ל-מאי 2026. תמחור ושמות מוצרים ב-Cloudflare משתנים (Calls הפך ל-Realtime, חיוב Media Transformations התחיל ב-1 בנובמבר 2025) — לפני שאתה מתכנן תקציב production, אמת מול דף ה-pricing הרשמי של כל מוצר.
פתח את הדאשבורד של Cloudflare → Images → Transformations וודא שהוא enabled על ה-zone שלך. אם אין לך zone מחובר, רשום לעצמך פתק: "להפעיל Transformations לפני התרגיל הראשון". בלי זה, URL של /cdn-cgi/image/ פשוט יחזיר את התמונה המקורית או שגיאה — וזו מלכודת ה-"למה זה לא עושה resize" הנפוצה ביותר.
Image Transforms — resize, WebP/AVIF ו-crop דרך URL בלבד
Image Transforms הוא הפיצ'ר היחיד שאתה צריך כדי לבנות CDN תמונות מקצועי, והיופי בו הוא שאין בו שום קוד. אתה לא מתקין SDK, לא כותב Worker, לא מגדיר pipeline. אתה בונה URL. כל הלוגיקה של resize, חיתוך, שינוי איכות והמרת פורמט יושבת בתוך פרמטרים שאתה מצרף ל-URL, ו-Cloudflare מבצע את ההמרה ב-edge הקרוב לגולש ושומר את התוצאה ב-cache.
המבנה תמיד זהה: https://<ZONE>/cdn-cgi/image/<OPTIONS>/<SOURCE-IMAGE>. ה-OPTIONS הוא רשימה מופרדת בפסיקים — לפחות אופציה אחת חובה — וה-SOURCE-IMAGE יכול להיות נתיב יחסי על אותו zone, או URL מלא לתמונה במקום אחר (למשל ה-URL הציבורי של bucket R2). הנה דוגמה אמיתית:
https://your-zone.com/cdn-cgi/image/width=400,quality=85,format=auto/https://pub-xxxx.r2.dev/uploads/photo.jpg
# format=auto => AVIF/WebP לפי כותרת Accept. fit=cover,gravity=face ל-avatars:
https://your-zone.com/cdn-cgi/image/fit=cover,width=200,height=200,gravity=face/avatars/u123.png
בוא נפרק את האופציות החשובות. width (או w) ו-height (h) קובעים את הממדים בפיקסלים — או auto לרוחב שמתאים את עצמו. quality (q) הוא 1 עד 100; ערך 80–85 הוא נקודת המתיקות שבה העין כמעט לא רואה הבדל אבל המשקל צונח. format (f) קובע את פורמט הפלט, ובו טמון הקסם הגדול ביותר.
כשאתה כותב format=auto, Cloudflare בודק את כותרת ה-Accept של הדפדפן ומגיש את הפורמט המודרני ביותר שהוא תומך בו: AVIF לדפדפנים החדשים (קטן בערך ב-50% מ-JPEG באותה איכות), WebP לדפדפנים מעט ישנים יותר (קטן ב-25–35%), ו-JPEG כ-fallback. זה אומר שגולש בכרום מקבל AVIF זעיר וגולש בדפדפן ישן מקבל JPEG תקין — מאותו URL בדיוק, בלי שתכתוב שורת קוד אחת. הסתייגות חשובה: זה עובד רק כשהדפדפן שולח Accept מתאים; אל תבטיח ללקוח AVIF באופן גורף, כי דפדפנים מסוימים פשוט לא יבקשו אותו ויקבלו fallback.
ה-fit שולט באיך התמונה מתאימה את עצמה למסגרת: scale-down (לא מגדיל מעבר למקור), contain (כל התמונה נכנסת), cover (ממלא את המסגרת וחותך עודף), crop ו-pad. בשילוב עם gravity (למשל gravity=face שמזהה פנים, או gravity=auto שבוחר את האזור המעניין ביותר) אתה מקבל avatars מושלמים בלי לגעת ב-Photoshop. נקודה קריטית לתקציב: כל זה — שילוב של מספר אופציות + מקור — נספר כהמרה ייחודית אחת, לא כמה.
למה זה מפתה: "5,000 בחודש" נשמע מעט נורא לאתר עם תנועה. מתחילים מניחים שכל גולש ששואב תמונה שורף יחידה מהמכסה, ומיד בורחים למוצר בתשלום.
למה זה טעות: ה-free tier סופר קומבינציות ייחודיות (combo של אופציות + מקור), לא בקשות. אותה תמונה ברוחב 400 שמוגשת למיליון גולשים = המרה אחת שנספרה פעם אחת; כל השאר מ-cache, בחינם.
מה לעשות במקום: תכנן סט קבוע ומצומצם של וריאנטים (למשל 4 רוחבים: 200/400/800/1600). 1,000 תמונות × 4 רוחבים = 4,000 המרות — מתחת ל-5,000, ומשרת תנועה אינסופית. הימנע מרוחב דינמי לכל גולש, שמפוצץ את הספירה.
קח תמונה ציבורית כלשהי והרכב בכתובת הדפדפן URL בצורת https://<zone>/cdn-cgi/image/width=300,format=auto/<image-url>. טען אותה ובדוק שהיא חוזרת קטנה יותר מהמקור. זה ה-"hello world" של Image Transforms — אם זה עובד, ה-zone שלך מוכן.
פתח DevTools → Network, טען את אותו URL ובדוק את ה-content-type בתגובה. בכרום עדכני תראה image/avif או image/webp — זו ההוכחה ש-format=auto עובד ושאתה מגיש פורמט מודרני בלי לכתוב קוד.
CDN תמונות חינמי בפועל: R2 כמקור + Transform URLs
עכשיו נחבר את שני החלקים: R2 כמקור האחסון, ו-Image Transforms כשכבת העיבוד. התוצאה היא CDN תמונות מלא — מקור יחיד, אינסוף גדלים, פורמטים מודרניים ו-edge caching — בעלות אפס. זו אחת הדוגמאות הנקיות ביותר ל"ארכיטקטורת $0" של Cloudflare, כי כל חלק בשרשרת חינמי: ה-storage (10GB ב-R2), ה-egress ($0 ב-R2), והעיבוד (5,000 המרות ייחודיות).
הזרימה היא כזו: אתה מעלה את התמונה פעם אחת ב-resolution הכי גבוה ל-R2. מפרסם את ה-bucket כ-public — או דרך subdomain של r2.dev (טוב ל-dev, לא ל-production) או דרך custom domain (production). ואז בונה URL של /cdn-cgi/image/ שמצביע על ה-URL הציבורי של R2. ה-edge מבצע את ההמרה בבקשה הראשונה, שומר את התוצאה ב-cache, ומכאן והלאה כל בקשה לאותו גודל מוגשת מיד מה-edge הקרוב לגולש.
בוא נעלה תמונה. עם wrangler זה שורה אחת — שים לב ל---content-type המפורש, שיהיה חשוב מאוד גם בהמשך כשנעבוד עם HLS:
wrangler r2 object put my-bucket/photo.jpg --file ./photo.jpg --content-type image/jpeg
אם אתה רוצה שליטה מלאה — למשל להגביל איזה objects חשופים, להוסיף headers משלך, או להגיש מ-bucket פרטי — אתה כותב Worker קטן שמגיש מ-R2. החלק הקריטי כאן הוא writeHttpMetadata: הוא לוקח את ה-content-type ושאר ה-metadata שנשמרו ב-upload ומדביק אותם על התגובה, כך שהדפדפן מקבל בדיוק את ה-content-type הנכון:
// מגיש כל object מ-R2 עם ה-content-type הנכון (מה-metadata שנשמר ב-upload)
export default {
async fetch(request, env) {
const url = new URL(request.url);
const key = url.pathname.slice(1); // מסיר את ה-/ הראשון
if (request.method !== "GET") return new Response("Method Not Allowed", { status: 405 });
const object = await env.MY_BUCKET.get(key);
if (object === null) return new Response("Object Not Found", { status: 404 });
const headers = new Headers();
object.writeHttpMetadata(headers); // קובע content-type וכו' מה-metadata השמור
headers.set("etag", object.httpEtag);
return new Response(object.body, { headers });
},
};
ה-binding ב-wrangler.toml הוא בדיוק זה שכבר הכרת מפרק 2 — אנחנו רק נשתמש בו שוב. נציג כאן את הצורה המלאה שתשרת אותנו לאורך הפרק, כולל הטבלה ל-Browser Run שנגיע אליה בקרוב:
[[r2_buckets]]
binding = "MY_BUCKET"
bucket_name = "my-bucket"
# binding ל-Browser Run (נפרד, טבלה עליונה — נשתמש בו בסקשן הבא):
[browser]
binding = "MYBROWSER"
שים לב למילה edge caching כאן — זה לא רק "מהיר יותר", זו הסיבה שזה חינם בקנה מידה. אחרי שהבקשה הראשונה הפעילה את ההמרה ושמרה אותה, כל הבקשות הבאות לאותו URL מוגשות מה-cache בלי להפעיל המרה חדשה ובלי לגעת ב-R2. כך אתה משלם (במונחי מכסה) פעם אחת לכל וריאנט, ומשרת תנועה בלתי מוגבלת. תוכל לאמת את זה ב-DevTools: בקשה ראשונה תראה cf-cache-status: MISS, והשנייה HIT.
שאלה 1: זו תמונה, ואתה רק צריך resize/format/crop ממקור קיים?
- כן → Image Transforms (
/cdn-cgi/image/, 5,000/חודש חינם). אין storage נוסף, אין קוד.
שאלה 2: זה וידאו ב-R2 שצריך clip/thumbnail/resize?
- כן → Media Transformations (
/cdn-cgi/media/, 5,000/חודש חינם — הקצאה נפרדת!). אותו רעיון, prefix אחר.
שאלה 3: צריך ניהול תמונות מלא עם storage + variants מובנה, ויש תקציב?
- כן → Cloudflare Images (storage בתשלום). שלם רק כשהנוחות שווה את זה.
המפתח: שני ה-Transforms הם prefixים שונים עם הקצאות נפרדות. לעולם אל תניח שהמכסה משותפת — יש לך 5,000 לתמונות ועוד 5,000 לוידאו.
- ודא ש-Transformations enabled על ה-zone (Images → Transformations).
- צור bucket R2 ופרסם אותו public — דרך
r2.dev(dev) או custom domain (production). - העלה תמונת מקור:
wrangler r2 object put bkt/photo.jpg --file ./photo.jpg --content-type image/jpeg - בנה URL:
https://<zone>/cdn-cgi/image/width=400,quality=85,format=auto/<r2-public-url> - טען בדפדפן ואמת resize, ובדוק ב-Network שה-content-type הוא
image/webpאוimage/avif. - טען שנית ואמת
cf-cache-status: HIT— אותו transform לא נספר שוב. - חזור על שלב 4 עם שני רוחבים נוספים (200 ו-800) — עכשיו יש לך שלושה גדלים ממקור יחיד.
פלט נראה לעין: URL חי שמחזיר את אותה תמונה בשלושה גדלים שונים מתוך מקור יחיד ב-R2, עם WebP/AVIF אוטומטי ו-edge cache — הכל ב-$0.
Browser Run — Chrome headless ב-Edge עם Puppeteer
עכשיו נעבור ממדיה סטטית ל-media שאתה מייצר. Browser Run (השם הרשמי החדש למה שהיה Browser Rendering) הוא Chrome מלא, headless, שרץ ב-edge של Cloudflare. אתה שולט בו דרך Puppeteer — אותה ספרייה שמשתמשים בה לאוטומציית דפדפן בכל מקום — רק שכאן הדפדפן לא רץ על השרת שלך אלא ברשת של Cloudflare. עם זה אתה מצלם screenshots, מייצר PDF מדפים, וגורף תוכן מאתרים שדורשים JavaScript לרינדור.
למה vibe coder צריך את זה? כי המון משימות "פשוטות" דורשות דפדפן אמיתי. רוצה תמונת preview של URL ל-card ברשת חברתית? צריך לפתוח את הדף ולצלם. רוצה לייצר חשבונית PDF מתבנית HTML? אותו דבר. רוצה לבדוק שאתר עלה כמו שצריך? לפתוח ולצלם. בלי Browser Run היית צריך שרת עם Chrome מותקן, ניהול memory, ו-headless flags — כאן זה binding אחד, בלי שום תשתית משלך.
מתחילים בהתקנת החבילה. Cloudflare מספקת fork מותאם של Puppeteer:
npm i @cloudflare/puppeteer
# חלופה: npm i @cloudflare/playwright — אם אתה מעדיף Playwright API
עכשיו ה-binding. כאן יש תיקון חשוב שמדריכים ישנים מפספסים: הצורה המודרנית היא טבלה עליונה בשם [browser] עם binding = "MYBROWSER" — לא הצורה הישנה של browser = {} inline. אם תעתיק קוד ישן עם הצורה ה-inline, ה-binding פשוט לא יעבוד, וזה אחד מ-debugging-ים הכי מתסכלים כי הקוד "נראה נכון".
[browser]
binding = "MYBROWSER"
והנה ה-Worker הבסיסי שמצלם screenshot. שים לב לזרימה: משיגים את ה-URL מ-query string, מפעילים את הדפדפן דרך ה-binding, פותחים עמוד, מנווטים אליו, מצלמים, וסוגרים. הסגירה (browser.close()) קריטית — דפדפן פתוח שורף מהתקציב היומי שלך, ובהמשך נראה כמה מהר:
// wrangler.toml:
// [browser]
// binding = "MYBROWSER"
//
// src/index.js:
import puppeteer from "@cloudflare/puppeteer";
export default {
async fetch(request, env) {
const { searchParams } = new URL(request.url);
const target = searchParams.get("url");
if (!target) return new Response("Add ?url=https://example.com", { status: 400 });
const browser = await puppeteer.launch(env.MYBROWSER);
const page = await browser.newPage();
await page.goto(target, { waitUntil: "networkidle0" });
const img = await page.screenshot({ type: "png" });
await browser.close();
return new Response(img, { headers: { "content-type": "image/png" } });
},
};
שתי נקודות עדינות בקוד. ה-waitUntil: "networkidle0" אומר ל-Puppeteer לחכות עד שהרשת נרגעה (אין בקשות פעילות) לפני שהוא מצלם — כך אתה מקבל את הדף המלא, לא שלד חצי-טעון. וה-type: "png" נותן איכות מלאה; אם אתה רוצה משקל קטן יותר אפשר type: "jpeg" עם quality. מעבר ל-Puppeteer, Browser Run תומך גם ב-Playwright וב-CDP (Chrome DevTools Protocol) ישיר — אבל ל-vibe coder, Puppeteer הוא הברירת מחדל הנוחה ביותר, ובו נישאר.
wrangler dev הרגיל
למה זה מפתה: כל פיתוח Workers אחר רץ מקומית ב-Miniflare עם wrangler dev, מהר ובלי רשת. אז הנטייה היא להריץ גם את ה-screenshot Worker ככה.
למה זה טעות: Browser Run לא רץ ב-Miniflare המקומי. אין Chrome בתוך ה-emulator. wrangler dev רגיל ייכשל או יחזיר שגיאת binding. בנוסף, הוא משתמש בטבלת ה-binding העליונה [browser] ולא בצורה ה-inline הישנה.
מה לעשות במקום: הרץ תמיד wrangler dev --remote לבדיקת Browser Run — זה מריץ מול ה-edge האמיתי. שים לב: גם ה-dev הזה שורף מתוך 10 הדקות היומיות, אז אל תשאיר אותו פתוח בלי לחשוב.
התקן את החבילה: npm i @cloudflare/puppeteer, ואז הוסף ל-wrangler.toml את הטבלה העליונה [browser] עם binding = "MYBROWSER". זה ה-setup שכל פרויקט Browser Run יושב עליו — שתי פעולות, ואתה מוכן.
מגבלת 10 הדקות/יום: מה Browser Run כן ולא נועד אליו
עכשיו לחלק שמפריד בין מי שבונה דמו מגניב למי שמקבל הפתעה לא נעימה ב-production. ה-free tier של Browser Run הוא 10 דקות browser ביום ועד 3 דפדפנים במקביל. זה לא 10 דקות לכל בקשה — זה 10 דקות סך הכל ביום, מתאפס ב-00:00 UTC. וזה משנה לחלוטין את מה שאפשר לבנות.
בוא נעשה את החשבון, כי הוא מפכח. טעינה וצילום של דף ממוצע לוקחים בערך 5–10 שניות (תלוי במשקל הדף וב-networkidle0). ניקח 7 שניות כממוצע. 10 דקות = 600 שניות. 600 ÷ 7 ≈ 85 ניווטים ביום. בקצה האופטימי (5 שניות לדף) תקבל ~120, ובקצה הפסימי (10 שניות) ~60. אז התקציב היומי האמיתי שלך הוא בערך 60 עד 120 צילומים ביום, לא יותר. זכור גם שכברירת מחדל session נשאר פתוח עד 10 דקות של חוסר פעילות — אז אם שכחת browser.close(), אתה ממשיך לשרוף בשקט.
זה מספיק לגמרי בשביל: דמו, כלי פנימי, screenshot API אישי, או פיצ'ר preview באפליקציה קטנה. זה לא מספיק בשביל: scraping של production בקנה מידה, שירות screenshot ציבורי עם תנועה אמיתית, או כל דבר שצריך מאות-אלפי צילומים. אם תנסה — אחרי שהתקציב נגמר, הקריאות פשוט נכשלות עד reset, וזה ייראה כמו באג אקראי שמופיע "בערך באמצע היום".
אז מה הפתרון? cache. זו התובנה המרכזית של הסקשן: ברגע שצילמת URL מסוים, שמור את התוצאה ב-KV (או R2) ממופתחת ב-hash של ה-URL. בקריאה הבאה לאותו URL, בדוק קודם את ה-cache — אם הצילום קיים, החזר אותו בלי לפתוח דפדפן בכלל. ככה אתה הופך 85 ניווטים יקרים ל-85 צילומים ייחודיים, וכל שאר התנועה מוגשת מ-cache בחינם. זה בדיוק מה שנבנה בתרגיל הבא, וזו תבנית שתשרת אותך הרבה מעבר ל-screenshots.
שווה להכיר גם מה הגרסה בתשלום נותנת, כדי שתדע מתי לשדרג. ה-paid tier (חלק מ-Workers Paid, $5/חודש) מעלה ל-10 שעות browser בחודש ו-10 דפדפנים במקביל, ומוסיף שני פיצ'רים שהוצגו ב-Agents Week 2026: Live View (לצפות ב-session של הדפדפן בזמן אמת, מצוין ל-debugging של agent) ו-Human in the Loop (להשהות את ה-agent עד שאדם מאשר/מזין משהו — קריטי לזרימות שדורשות login או CAPTCHA). באותו עדכון הוכפלה גם ה-concurrency פי 4. כל אלה בתשלום, אבל הם הופכים את Browser Run מכלי screenshot לכלי automation אמיתי.
למה זה מפתה: "יש לי screenshot API שעובד מצוין בדמו — בוא נפתח אותו לציבור". הקוד זהה, אז למה לא?
למה זה טעות: 10 דקות ביום ≈ 60–120 ניווטים. שירות ציבורי יחצה את זה בדקות, ואז כל הקריאות נכשלות עד 00:00 UTC. הגרוע מכול — זה ייכשל בשקט באמצע היום בלי אזהרה ברורה, ותחפש את הבאג במקום הלא נכון.
מה לעשות במקום: השתמש ב-Browser Run לפיתוח, כלים פנימיים ודמו בלבד. cache תוצאות ב-KV/R2 לפי hash של ה-URL כדי לא לרנדר אותו דף פעמיים. לעומסי production אמיתיים — עבור ל-paid (10 שעות/חודש) או לשירות screenshot חיצוני.
חשב בעצמך: אם כל ניווט לוקח ~7 שניות, כמה ניווטים מקבל יום אחד מתוך 10 הדקות? רשום את המספר — זה התקציב היומי האמיתי שלך, והוא יקבע אם אתה צריך cache (רמז: כן) ומתי תצטרך לשקול paid.
פרויקט: Edge Screenshot API עם cache ב-KV
זה הפרויקט שמרכיב את כל מה שלמדנו עד כה: Browser Run לצילום, R2 לאחסון, ו-KV ל-cache. התוצאה היא API חי שמקבל ?url=, מחזיר screenshot, ובקריאה חוזרת מגיש מ-cache בלי לשרוף עוד מהתקציב היקר של 10 הדקות. זה הופך את ~85 הניווטים היומיים ל-85 URLs ייחודיים — מעבר לזה, הכול מ-cache.
הלוגיקה בנויה כך: כשמגיעה בקשה, מחשבים מפתח cache מ-hash של ה-URL המבוקש. בודקים את KV — אם המפתח קיים, יש כבר צילום שמור, אז מחזירים מיד את ה-URL של R2 (או את הצילום עצמו) בלי לגעת בדפדפן. רק אם המפתח לא קיים, פותחים דפדפן, מצלמים, שומרים ל-R2 עם content-type נכון, רושמים את המפתח ב-KV, ומחזירים. ככה כל URL ייחודי עולה צילום אחד, ולא יותר.
שים לב ל-httpMetadata: { contentType: "image/png" } ב-put ל-R2 — בלעדיו, R2 ישמור את הקובץ עם content-type גנרי, וכשתגיש אותו מאוחר יותר הדפדפן לא ידע שזו תמונה. זו אותה מלכודת content-type שתחזור בענק בסקשן ה-HLS, אז כדאי להפנים אותה כבר עכשיו: תמיד קבע content-type מפורש ב-put ל-R2.
// wrangler.toml צריך: [browser] + [[r2_buckets]] (SHOTS) + [[kv_namespaces]] (CACHE)
import puppeteer from "@cloudflare/puppeteer";
export default {
async fetch(request, env) {
const target = new URL(request.url).searchParams.get("url");
if (!target) return new Response("Add ?url=https://example.com", { status: 400 });
// מפתח cache מ-hash של ה-URL
const key = "shots/" + btoa(target).replace(/[^a-z0-9]/gi, "").slice(0, 40) + ".png";
// 1) בדוק KV — אם קיים, החזר מ-R2 בלי דפדפן
const cached = await env.CACHE.get(key);
if (cached) {
const obj = await env.SHOTS.get(key);
if (obj) {
const h = new Headers(); obj.writeHttpMetadata(h);
return new Response(obj.body, { headers: h }); // HIT — 0 browser-minutes
}
}
// 2) MISS — צלם בפועל
const browser = await puppeteer.launch(env.MYBROWSER);
const page = await browser.newPage();
await page.goto(target, { waitUntil: "networkidle0" });
const img = await page.screenshot({ type: "png" });
await browser.close();
// 3) שמור ל-R2 עם content-type, ורשום ב-KV
await env.SHOTS.put(key, img, { httpMetadata: { contentType: "image/png" } });
await env.CACHE.put(key, "1", { expirationTtl: 86400 }); // תוקף יום
return new Response(img, { headers: { "content-type": "image/png" } });
},
};
ה-expirationTtl: 86400 נותן ל-cache תוקף של יממה — אחרי זה, URL שביקשו שוב ירונדר מחדש (שימושי אם תוכן הדף משתנה). אתה יכול להאריך או לקצר לפי כמה "טרי" אתה צריך את הצילום. הגישה הזו — cache לפי hash, החזרה מ-store אם קיים — היא דפוס שתשתמש בו שוב ושוב בכל פעם שיש לך פעולה יקרה (browser-minutes, neurons מפרק 3, קריאת API חיצוני) שאתה לא רוצה לחזור עליה.
npm i @cloudflare/puppeteerוהוסף ל-wrangler.toml:[browser]+[[r2_buckets]]+[[kv_namespaces]].- חשב מפתח cache מ-hash של ה-URL; בדוק KV — אם קיים, החזר את הצילום מ-R2 בלי דפדפן.
- אם לא קיים:
puppeteer.launch(env.MYBROWSER)→newPage→goto(waitUntil: networkidle0)→screenshot. - שמור ל-R2 עם
httpMetadata.contentType: "image/png", ורשום את המפתח ב-KV. - החזר את ה-CDN URL (או הצילום עצמו).
- הרץ
wrangler dev --remote— חובה: Browser Run לא רץ ב-Miniflare המקומי;wrangler devרגיל ייכשל על ה-binding. בדוק?url=https://example.com, ואז בדוק שקריאה שנייה חוזרת מ-cache בלי לשרוף browser-minutes.
פלט נראה לעין: Worker חי שמקבל ?url=, מחזיר screenshot PNG מ-R2, ובקריאה חוזרת מגיש מ-KV cache בלי ניווט דפדפן נוסף — ואתה רואה בעיניים את ההבדל בזמן התגובה.
Cloudflare Stream — למה אין כאן free tier בכלל
עד כה הכול היה $0. עכשיו נכיר את המוצר היחיד בפרק שאין לו free tier בכלל — Cloudflare Stream — דווקא כדי שתבין למה ה-workaround של R2+HLS בסקשן הבא שווה את המאמץ. Stream הוא מוצר וידאו מנוהל ומצוין: אתה מעלה קובץ גולמי, והוא דואג לכל השאר — transcoding למספר איכויות, יצירת HLS ladder אוטומטית, player מובנה, signed URLs להגנה, ו-analytics לכל צופה. זה pipeline וידאו שלם בלי שתכתוב שורה.
אבל הנוחות הזו עולה כסף, ובמודל חיוב שתופס vibe coders לא מוכנים. החיוב הוא לפי דקות, בשני צירים. Storage: $5 לכל 1,000 דקות וידאו מאוחסנות, prepaid. Delivery: $1 לכל 1,000 דקות שמוגשות, post-paid. ה-encoding וה-ingress (ההעלאה עצמה) חינמיים — אבל ברגע שהקובץ יושב אצלם, השעון רץ.
הנה הנקודה שהכי כואבת: אתה משלם על דקות מאוחסנות גם אם אף אחד לא צופה. סרטון של 10 דקות שהעלית ושכחת ממנו ממשיך לעלות לך כל חודש, לנצח, רק על זה שהוא קיים. במונחים מעשיים: כל 1,000 דקות מאוחסנות = $5 לחודש על storage לבד. זה לא הרבה לסרטון אחד, אבל אם אתה בונה פלטפורמה עם תוכן משתמשים, ה-storage מצטבר בלי קשר לצפיות, ומכפיל את עצמו עם כל העלאה. חיוב Media Transformations, אגב, התחיל ב-1 בנובמבר 2025 — עוד תזכורת שהמספרים האלה חיים ומשתנים.
למה זה מפתה: כל שאר Cloudflare כל כך נדיב ב-free tier, שמניחים אוטומטית ש"בטח יש כמה דקות/תמונות חינם להתחיל". והשם "Cloudflare Images, 5,000 חינם" מחזק את האשליה.
למה זה טעות: ל-Stream אין free tier אחסון/delivery בכלל — $5/1,000 דקות מאוחסנות גם אם אף אחד לא צפה. ו-Cloudflare Images storage הוא paid-only ($5/100k תמונות). רק ה-Transforms חינמיים — לא ה-storage.
מה לעשות במקום: אחסן מקורות ב-R2 (10GB + $0 egress) והשתמש ב-Image/Media Transform URLs לעיבוד. לוידאו — HLS self-host על R2 (הסקשן הבא). שלם על Stream/Images רק כשאתה באמת צריך את ה-pipeline המנוהל ויודע למה.
אז מה בכל זאת מצדיק את Stream? כשהדברים שהוא נותן באמת חשובים לך ואין לך זמן לבנות אותם: transcoding אוטומטי (אתה מעלה קובץ אחד, הוא מייצר את כל האיכויות), adaptive HLS ladder (הנגן עובר בין איכויות לפי מהירות הרשת של הצופה), player מובנה שעובד בכל דפדפן, signed URLs להגבלת גישה, ו-per-viewer analytics. אם אתה בונה פלטפורמת וידאו רצינית עם הרבה תוכן ומשתמשים, וזמן הפיתוח שלך יקר — Stream שווה את הכסף. אם הווידאו שלך קצר, ידוע מראש, ואתה מוכן ל-ffmpeg — תמשיך לסקשן הבא, שם נבנה את אותו דבר ב-$0.
חשב כמה יעלה לאחסן 10 שעות וידאו ב-Stream לחודש: 10 שעות = 600 דקות, ו-$5 לכל 1,000 דקות. רשום את הסכום (רמז: $3 לחודש על storage לבד, לנצח, גם בלי צפייה אחת). זה בדיוק מה ש-R2+HLS חוסך — וכשמכפילים בעשרות סרטונים, ההבדל מתחיל לבעוט.
וידאו ב-$0: HLS self-hosting על R2 (דפוס DIY)
הנה החלופה החינמית ל-Stream — ובמילה אחת היא HLS. HLS (HTTP Live Streaming) הוא הפרוטוקול שעליו רץ כמעט כל הווידאו באינטרנט. הרעיון פשוט: במקום קובץ וידאו ענק אחד, מפצלים את הסרטון לקטעים קצרים (segments) של כמה שניות כל אחד, ויוצרים קובץ playlist (.m3u8) שמפרט את סדר הקטעים. הנגן מוריד את ה-playlist, ואז מושך קטע-קטע — מה שמאפשר התחלה מהירה ומעבר חלק בין איכויות.
הבהרה חשובה לפני שממשיכים: "HLS על R2" הוא דפוס DIY/קהילתי, לא מוצר turnkey של Cloudflare. אין דף תיעוד רשמי של "HLS on R2". אתה מרכיב אותו בעצמך משלושה חלקים סטנדרטיים: ffmpeg לפיצול, R2 לאחסון, ונגן HLS (כמו hls.js או Safari native) בצד הלקוח. זה עובד מצוין — אבל אתה האחראי לכל חלק, וזה בדיוק החיסרון מול Stream המנוהל שראינו זה עתה.
נתחיל בפיצול. ffmpeg הופך קובץ MP4 ל-package של HLS בפקודה אחת. ה--codec: copy אומר "אל תקודד מחדש, רק חתוך" — מהיר וללא איבוד איכות (בהנחה שהמקור כבר ב-codec תקין שהדפדפן מבין):
ffmpeg -i input.mp4 -codec: copy -start_number 0 -hls_time 6 -hls_list_size 0 -f hls playlist.m3u8
זה ייצר playlist.m3u8 ושורה של קבצי .ts (הקטעים). ה--hls_time 6 קובע קטעים של ~6 שניות, ו--hls_list_size 0 אומר "שמור את כל הקטעים ב-playlist" (חשוב ל-VOD, וידאו לפי דרישה, להבדיל מ-live שבו רק החלון האחרון נשמר).
עכשיו מעלים ל-R2 — וכאן המלכודת מספר אחת של כל הפרק. כל קובץ צריך content-type נכון. ה-.m3u8 חייב להיות application/vnd.apple.mpegurl (או application/x-mpegURL), וקובצי ה-.ts חייבים להיות video/mp2t. אם R2 מגיש את ה-playlist כ-application/octet-stream (ברירת המחדל לקובץ לא מוכר), הנגן נכשל בשקט — בלי שגיאה ברורה, פשוט מסך שחור. זו הסיבה השכיחה ביותר ל"למה הווידאו לא מתנגן", ואנשים מבזבזים עליה שעות כי הם בודקים את הנגן ולא את ה-headers.
# העלה את ה-playlist עם content-type מפורש:
wrangler r2 object put bkt/playlist.m3u8 --file ./playlist.m3u8 --content-type application/vnd.apple.mpegurl
# העלה כל קטע .ts עם content-type של video/mp2t:
wrangler r2 object put bkt/playlist0.ts --file ./playlist0.ts --content-type video/mp2t
.m3u8 מ-R2 עם content-type ברירת-מחדל
למה זה מפתה: מעלים את כל הקבצים ל-R2 ב-batch בלי לחשוב על content-type — "זה סתם קבצים, R2 יבין". והעלאה רגילה אכן עוברת בלי שגיאה, אז נדמה שהכול תקין.
למה זה טעות: אם ה-playlist לא חוזר כ-application/vnd.apple.mpegurl וה-segments לא כ-video/mp2t, הנגן נכשל בשקט — מסך שחור, בלי הודעת שגיאה שמכוונת אותך לסיבה. תבזבז שעה על debugging של נגן תקין לחלוטין.
מה לעשות במקום: קבע --content-type מפורש בכל put ל-R2. אם אתה מגיש דרך Worker, השתמש ב-object.writeHttpMetadata כדי לשמר את ה-content-type שנקבע ב-upload. אמת ב-DevTools שה-.m3u8 חוזר עם הסוג הנכון לפני שאתה מאשים את הנגן.
יש גם אופציית ביניים נחמדה: Media Transformations (ה-prefix /cdn-cgi/media/). אם המקור שלך כבר ב-R2 ואתה רק צריך clip קצר, thumbnail או resize — לא צריך ffmpeg בכלל. URL אחד עושה את העבודה, עם 5,000 פעולות ייחודיות בחודש חינם (הקצאה נפרדת מ-Image Transforms, כזכור):
https://your-zone.com/cdn-cgi/media/mode=video,time=5s,duration=5s,width=500,height=500,fit=crop,audio=false/https://pub-xxxx.r2.dev/clip.mp4
# mode=frame ל-thumbnail סטטי בזמן time=Ns. 5,000 פעולות ייחודיות/חודש חינם.
מה אתה מוותר עליו כשבוחרים HLS self-host על פני Stream? שלושה דברים מרכזיים: transcoding אוטומטי (ב-DIY אתה מקודד בעצמך עם ffmpeg, ואם אתה רוצה כמה איכויות אתה מייצר master playlist ידנית), signed URLs מובנים (אתה תצטרך לבנות הגנה משלך, למשל דרך Worker עם Turnstile מפרק 5), ו-per-viewer analytics. בתמורה — $0 egress, שליטה מלאה, ואפס נעילה לספק. עבור וידאו קצר, מדריכים, או תוכן ידוע מראש, ה-trade הזה כמעט תמיד משתלם.
שאלה 1: צריך transcoding אוטומטי + adaptive ladder + signed URLs + per-viewer analytics, ואין לך זמן/רצון ל-ffmpeg?
- כן → Stream/Images המנוהל. שלם $5/1,000 דקות (או storage) וקנה לעצמך זמן.
שאלה 2: הווידאו/תמונות קצרים או ידועים מראש, אתה מוכן ל-ffmpeg/Transform-URLs, ו-$0 egress חשוב לך יותר מ-pipeline מנוהל?
- כן → R2 self-host. אתה עושה את העבודה, אבל החשבון נשאר $0.
שאלה 3: התנועה לא צפויה וגבוהה, וזמן הפיתוח שלך יקר?
- כן → managed. הסיכון של לבנות pipeline שובר תחת עומס לא שווה את החיסכון.
- תקציב = 0 והנפח שולט → self-host. השקעת הזמן משתלמת ככל שהנפח גדל.
- פצל וידאו לוקלית:
ffmpeg -i input.mp4 -codec: copy -hls_time 6 -hls_list_size 0 -f hls playlist.m3u8 - העלה את כל הקבצים ל-R2:
.m3u8עםapplication/vnd.apple.mpegurl, כל.tsעםvideo/mp2t. - הגש דרך bucket public או Worker שמשתמש ב-
writeHttpMetadataלשמירת content-type. - בנה דף HTML עם
hls.js(או Safari native) שמצביע עלplaylist.m3u8. - פתח בדפדפן ואמת שהווידאו מתנגן; פתח Network ואמת ש-
.m3u8חוזר עם content-type הנכון. - השווה: $0 egress מ-R2 מול $1/1,000 דקות delivery ב-Stream.
פלט נראה לעין: וידאו HLS מתנגן בדפדפן מ-R2 ב-$0 egress, עם content-type תקין לכל קובץ — והבנה ברורה מה ויתרת עליו מול Stream.
פצל וידאו קצר ל-HLS לוקלית: ffmpeg -i input.mp4 -codec: copy -start_number 0 -hls_time 6 -hls_list_size 0 -f hls playlist.m3u8. בדוק שנוצרו קובץ .m3u8 וקובצי .ts — זה ה-package שתעלה ל-R2.
Real-time וידאו: Cloudflare Realtime (לשעבר Calls) — WebRTC SFU
החלק האחרון של מחסנית ה-media הוא הכי מרשים: וידאו בזמן אמת. Cloudflare Realtime — שם חדש למה שהיה Cloudflare Calls — נותן לך תשתית WebRTC מלאה לבניית video chat, שידורים חיים ו-multiplayer. חשוב לדעת את שני השמות: רוב המדריכים הקיימים עדיין מדברים על "Calls", אז כשתחפש דוגמאות תמצא אותן תחת השם הישן. התמחור זהה לחלוטין בין השמות.
הלב של המוצר הוא SFU — Selective Forwarding Unit. כדי להבין למה זה קריטי: ב-WebRTC נאיבי (peer-to-peer), כל משתתף שולח את הווידאו שלו ישירות לכל שאר המשתתפים. בחדר של 5 אנשים, כל דפדפן שולח 4 זרמים ומקבל 4 — זה מתפוצץ מהר, גם ברוחב פס וגם ב-CPU של המכשיר. SFU פותר את זה: כל משתתף שולח זרם אחד לשרת, והשרת מעביר אותו לכל השאר. הדפדפן שולח פעם אחת — והחדר יכול לגדול בלי שכל מכשיר יקרוס.
וכאן מודל החיוב הופך את זה לחינמי-בפועל: inbound תמיד חינם (כל מה שנכנס ל-Cloudflare לא עולה), ו-1,000 GB egress בחודש חינם לפני שמתחיל חיוב כלשהו. במונחי וידאו אמיתי: ב-500kbps, 1,000 GB מספיקים ל-כ-4,400 שעות של וידאו יוצא בחודש. מעבר לזה — $0.05 לכל GB. בשביל רוב האפליקציות שתבנה, אתה לעולם לא תיגע בקיר הזה. ה-SFU וה-TURN מחויבים יחד תחת חיוב Realtime המאוחד.
הארכיטקטורה הבטוחה בנויה משלושה חלקים. ל-App שלך יש App ID (ציבורי-למחצה) ו-App Secret (חייב להישאר בצד שרת). ה-Worker משמש כ-signaling/auth proxy דק: הוא קורא ל-Connection API עם ה-App Secret בכותרת Authorization, יוצר sessions ומנהל tracks. ה-דפדפן עושה את חילופי המדיה האמיתיים ישירות מול ה-SFU — הוא מקבל מה-Worker רק session/track IDs ו-SDP, לעולם לא את ה-Secret.
הנה דפוס מייצג של ה-Worker. שים לב היטב: ה-host וה-header המדויקים הם high-confidence אך לא אומתו verbatim מול ה-doc העדכני ביותר. ה-host ההיסטורי (תקופת Calls) הוא https://rtc.live.cloudflare.com/v1, וה-header הסטנדרטי הוא Authorization: Bearer <App Secret> — אבל אמת מול ה-OpenAPI/quickstart הרשמי לפני production. זו דוגמה מייצגת, לא copy-paste מובטח:
// דוגמה מייצגת — אמת host + header מול ה-OpenAPI הרשמי לפני production!
// ה-App Secret נשמר עם: wrangler secret put REALTIME_APP_SECRET
const BASE = "https://rtc.live.cloudflare.com/v1"; // אמת מול ה-OpenAPI!
export default {
async fetch(request, env) {
const appId = env.REALTIME_APP_ID;
const auth = { "Authorization": `Bearer ${env.REALTIME_APP_SECRET}` };
// יצירת session חדש מול ה-SFU (ה-Secret נשאר כאן, בצד שרת):
const res = await fetch(`${BASE}/apps/${appId}/sessions/new`, {
method: "POST",
headers: { ...auth, "content-type": "application/json" },
});
const session = await res.json();
// הדפדפן יקבל רק את ה-sessionId ו-SDP — לעולם לא את ה-Secret:
return Response.json({ sessionId: session.sessionId });
},
};
נקודות המפתח של ה-API (כדפוס, אמת את הנתיבים): POST /apps/{appId}/sessions/new יוצר session, POST /apps/{appId}/sessions/{sessionId}/tracks/new מפרסם או מנוי ל-tracks, ו-PUT .../renegotiate מבצע renegotiation של ה-SDP כשמשהו משתנה (למשל מישהו נכנס או יצא מהחדר). ה-Worker עוטף את אלה, והדפדפן רק קורא ל-Worker — כך ה-Secret נשאר מוסתר.
למה זה מפתה: "הדפדפן צריך לדבר עם ה-SFU, אז בוא נשים את ה-Secret ב-frontend וזהו". זה עובד בדמו ונראה פשוט יותר מ-proxy.
למה זה טעות: כל מי שפותח DevTools רואה את ה-Secret, ויכול ליצור sessions על חשבונך ולשרוף את ה-egress שלך עד שתגיע לחיוב. Secret ב-frontend הוא Secret חשוף, נקודה.
מה לעשות במקום: שמור את ה-Secret עם wrangler secret put REALTIME_APP_SECRET (כמו שלמדת בפרק 2). ה-Worker קורא ל-API עם ה-Secret; הדפדפן מקבל רק session/track IDs ו-SDP. ה-Secret לעולם לא עוזב את צד השרת.
פתח את הדאשבורד → Realtime, צור App וקבל App ID + App Secret. שמור את ה-Secret עם wrangler secret put REALTIME_APP_SECRET — אל תשים אותו בקוד. זה ה-setup שכל פרויקט real-time יושב עליו, וההרגל הנכון מהרגע הראשון.
פרויקט: חדר וידאו real-time ב-$0 (Realtime SFU + Worker signaling)
זה הפרויקט שסוגר את הפרק: חדר וידאו אמיתי שבו שני (או יותר) משתתפים רואים זה את זה — הכול ב-$0, על תשתית Cloudflare בלבד. אנחנו מרכיבים שלושה חלקים שכבר הכרנו: Realtime SFU כשרת המדיה, Worker כ-signaling proxy שמחזיק את ה-Secret, ו-Static Assets (frontend WebRTC) שרץ בדפדפן.
זרימת ה-signaling היא הלב. הדפדפן קורא ל-getUserMedia כדי לקבל גישה למצלמה ולמיקרופון, יוצר offer של SDP, ושולח אותו ל-Worker. ה-Worker — עם ה-App Secret בצד שלו — יוצר session מול ה-SFU, מפרסם את ה-track של המשתמש, ומחזיר את ה-answer ואת ה-track IDs לדפדפן. כדי לראות משתתפים אחרים, הדפדפן מבקש מה-Worker להירשם (subscribe) ל-tracks שלהם. כל חילופי המדיה עצמם — הזרמים בפועל — קורים ישירות בין הדפדפן ל-SFU; ה-Worker רק מתווך את ההסכמות (ה-SDP).
החלוקה הזו היא מה שהופך את זה גם בטוח וגם זול. בטוח — כי ה-Secret נשאר ב-Worker, והדפדפן מקבל רק IDs ו-SDP (חזרה למלכודת מהסקשן הקודם: אם תשים Secret ב-frontend, פתחת חור שמישהו ינצל). זול — כי ה-inbound חינם וה-egress רחב מאוד, אז חדרים רצים אלפי שעות לפני שתשלם משהו.
בוא נעשה את חשבון ה-egress, כי הוא מה שמוכיח את ה-$0. נניח חדר של 3 משתתפים ב-500kbps. כל משתתף מקבל את הזרמים של שני האחרים = 1Mbps יוצא אליו, ולשלושתם ~3Mbps egress מה-SFU. 1,000 GB = 8,000,000 megabits. ב-3Mbps, זה כ-740 שעות חדר בחודש (8,000,000 ÷ 3 ÷ 3600). כלומר חדר של 3 אנשים יכול לרוץ עשרות שעות ביום, כל חודש, ב-$0. אפליקציה קטנה לעולם לא תתקרב לקיר — וזה לפני שספרנו שה-inbound כולו חינם.
ל-frontend אתה משתמש ב-WebRTC API הסטנדרטי של הדפדפן (RTCPeerConnection), שמדבר עם ה-Worker שלך לצורך ה-signaling. את ה-frontend אתה מגיש כ-Static Assets מאותו Worker — אין צורך בשרת נפרד או ב-hosting אחר. הקוד המדויק של ה-SDP exchange תלוי בפרטי ה-API של Realtime, ולכן (כמו שהדגשנו) אמת את ה-host וה-endpoints מול ה-quickstart הרשמי לפני שאתה מסתמך עליהם ב-production.
- צור App ב-Realtime dashboard, קבל App ID + App Secret.
wrangler secret put REALTIME_APP_SECRET— ה-Secret נשאר בצד שרת בלבד.- כתוב Worker signaling: יוצר session ומפרסם/מנוי tracks מול ה-Connection API עם
Authorization: Bearer. ה-host הבסיסי של ה-SFU הואhttps://rtc.live.cloudflare.com/v1— נכון למאי 2026; אמת מול התיעוד הרשמי לפני prod — אל תקשיח אותו בקוד ייצור ללא אימות. - Static Assets frontend:
getUserMedia→ SDP exchange דרך ה-Worker → render של ה-tracks.
שלד להמחשה — אמת מול ה-quickstart הרשמי לפני שימוש בproduction:
// frontend WebRTC skeleton (illustrative — verify against official quickstart)
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
const pc = new RTCPeerConnection();
stream.getTracks().forEach(t => pc.addTrack(t, stream));
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// POST ה-SDP offer ל-Worker (לעולם לא ישירות ל-SFU — הוא מחזיק את ה-Secret)
const { answer } = await fetch('/signaling/session', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ sdp: offer.sdp, type: offer.type }),
}).then(r => r.json());
await pc.setRemoteDescription({ type: 'answer', sdp: answer });
- פתח את החדר בשתי כרטיסיות ואמת שני video feeds.
- חשב egress: 3 משתתפים @500kbps — כמה שעות נכנסות ל-1,000 GB/חודש?
פלט נראה לעין: חדר וידאו חי שבו שני peers רואים זה את זה דרך ה-SFU, כשה-App Secret לעולם לא מגיע לדפדפן, וחישוב כמה שעות/חודש זה $0.
פתח את החדר בשתי כרטיסיות דפדפן באותו מחשב ואמת שאתה רואה את ה-video של עצמך פעמיים — זה מאשר שה-SFU מעביר tracks בין sessions שונים, וזה ה-"hello world" של real-time. אם אתה רואה שני feeds, ה-pipeline עובד.
תקציב media משולב: מה אפשר לדחוס ב-$0 ועל מה מתחיל החיוב
סגרנו ארבעה רכיבי media — תמונות, screenshots, וידאו ו-real-time — וכולם רצים על אותה תשתית. עכשיו השאלה המעשית: כשאתה מרכיב אותם באפליקציה אחת, איזה limit נופל ראשון? זו השאלה החשובה ביותר בתכנון תקציב, כי הקיר הראשון הוא זה שיעצור אותך, ולא משנה כמה רחבים האחרים.
הנה המפה המלאה. שים לב במיוחד לשתי ההקצאות הנפרדות של ה-transforms — זו הטעות החוזרת ביותר:
| מוצר | מה חינם | הקיר / החיוב |
|---|---|---|
| R2 (אחסון) | 10GB, 1M Class A, 10M Class B, $0 egress | חיוב על storage מעבר ל-10GB |
| Image Transforms | 5,000 המרות ייחודיות/חודש | הקצאה נפרדת מ-Media |
| Media Transformations | 5,000 פעולות ייחודיות/חודש | $0.50/1,000 מעבר — הקצאה נפרדת |
| Browser Run | 10 דקות/יום (~60–120 צילומים) + 3 concurrent | נגמר מהר — paid: 10 שעות/חודש |
| Realtime | 1,000 GB egress/חודש (~4,400h), inbound חינם | $0.05/GB מעבר |
| Cloudflare Stream | אין free tier | $5/1,000 דק' אחסון + $1/1,000 delivery |
| Cloudflare Images storage | אין free tier (רק Transforms חינם) | $5/100k אחסון + $1/100k delivery |
איזה limit נופל ראשון? כמעט תמיד Browser Run. 10 דקות ביום זה התקציב הצר ביותר במחסנית — ~85 צילומים, מתאפס יומית. תמונות (5,000 וריאנטים) ו-Realtime (4,400 שעות) הם רחבים מאוד, ו-R2 (10GB) מספיק להמון מקורות. אז אם האפליקציה שלך נשענת על screenshots, שם תרגיש את הלחץ הראשון — ולכן ה-cache ב-KV הוא לא אופציונלי אלא הכרחי. אם, לעומת זאת, אתה מעלה הרבה תמונות בוריאנטים דינמיים, ה-5,000 של Image Transforms עלול ליפול לפני הכול — תלוי איך תכננת את הוריאנטים.
ההחלטה הגדולה שמתגבשת מכל הפרק היא managed מול self-host. R2 + Transforms + ffmpeg + Realtime SFU נותנים לך מחסנית media שלמה ב-$0 — בתמורה לכך שאתה מרכיב ומתחזק את החלקים. Stream + Cloudflare Images נותנים pipeline מנוהל — בתמורה לתשלום. הבחירה אינה אידיאולוגית: היא תלויה בנפח, בזמן הפיתוח שלך, ובכמה הפיצ'רים המנוהלים (transcoding, signed URLs, analytics) באמת קריטיים לך. שתי המסגרות בפרק נותנות לך את הכלים להחליט בכל מקרה לגופו — לא לפי אופנה אלא לפי המספרים שלך.
עצה אחרונה לתכנון: אל תנסה להחליט הכול מראש. הגישה הנכונה ל-vibe coder היא להתחיל self-host ב-$0, ולעבור ל-managed רק כשמשהו קונקרטי שובר. בנה את ה-CDN תמונות על R2+Transforms; אם תגלה שאתה צריך וריאנטים אינסופיים שחורגים מ-5,000, אז תשקול Cloudflare Images. בנה וידאו על HLS+R2; אם תמצא שאתה מבזבז שעות על transcoding ידני ואתה צריך adaptive ladder אוטומטי, אז Stream שווה. הקיר הראשון שתפגוש יגיד לך בדיוק איפה כדאי לשלם — וזה הרבה יותר חכם מלשלם מראש על pipeline מנוהל שאולי לא תצטרך. ה-$0 הוא לא רק חיסכון, הוא גם הדרך הזולה ביותר ללמוד מה הפרויקט שלך באמת צריך.
מלא בעצמך: עבור אפליקציה עם 200 תמונות, 50 screenshots/יום וחדר וידאו של 3 משתתפים — איזה מוצר media ייגע ראשון בקיר החינמי? נמק. (רמז: ספור וריאנטים מול 5,000, צילומים מול ~85/יום, ושעות חדר מול 4,400. אחד מהם הרבה יותר צר מהאחרים.)
| תדירות | משימה | זמן |
|---|---|---|
| בכל העלאת media ל-R2 | קבע --content-type מפורש (תמונה/וידאו/m3u8) — אל תסמוך על ברירת מחדל | 10 שניות |
| לפני בדיקת Browser Run | הרץ wrangler dev --remote (לא dev רגיל), וזכור שזה שורף מהמכסה | 1 דקה |
| בכל screenshot חדש | בדוק שה-cache ב-KV עובד — קריאה שנייה לא צריכה לפתוח דפדפן | 1 דקה |
| שבועי | בדוק את צריכת ה-transforms (Image + Media בנפרד) מול 5,000 כל אחד | 3 דקות |
| חודשי | אמת מול דף ה-pricing הרשמי שהמספרים והשמות לא השתנו | 5 דקות |
קח את ה-screenshot API מהתרגיל והוסף לו את שכבת ה-cache ב-KV — גם אם בנית אותו בלי. זה הרכיב היחיד בפרק שהקיר שלו (10 דקות/יום) באמת קרוב, וה-cache הוא ההבדל בין כלי שעובד יום אחד לבין כלי שעובד תמיד. ברגע שה-cache עומד, יש לך תבנית — "פעולה יקרה + cache לפי hash" — שתשרת אותך בכל פרויקט הבא: screenshots, neurons, קריאות API חיצוניות. זו ההשקעה הכי משתלמת ב-15 דקות שתעשה השבוע.
- למה ה-free tier של Image Transforms סופר "המרות ייחודיות" ולא בקשות, ואיך זה משנה את האופן שבו תתכנן וריאנטים של תמונות? (רמז: cache + סט קבוע של רוחבים)
- באפליקציה עם 200 תמונות, 50 screenshots/יום וחדר וידאו — איזה limit נופל ראשון, ולמה דווקא הוא? (רמז: השווה ~85/יום מול 5,000/חודש מול 4,400 שעות)
- למה אסור לשלוח את ה-App Secret של Realtime לדפדפן, ואיך הארכיטקטורה הנכונה מונעת את זה? (רמז: מי מחזיק את ה-Secret ומה הדפדפן מקבל)
- למה וידאו HLS על R2 יכול להיכשל בשקט גם כשכל הקבצים עלו, ואיך תאתר את הסיבה? (רמז: content-type של ה-.m3u8 ב-Network)
- מתי דווקא תשלם על Stream במקום ללכת על HLS self-host על R2, ואיך תנמק את ההחלטה? (רמז: transcoding, signed URLs, analytics, וזמן פיתוח מול נפח)
בנית שכבת media שלמה ב-$0 על תשתית Cloudflare אחת. תמונות: מקור יחיד ב-R2 + Image Transforms (5,000 ייחודיים/חודש) = CDN מלא עם WebP/AVIF ו-edge cache. Screenshots: Browser Run עם Puppeteer, ממוסגר ב-cache ב-KV כדי לא לשרוף את 10 הדקות היומיות. וידאו: HLS self-host על R2 (דפוס DIY) עם content-type נכון לכל קובץ, כחלופה ל-Stream שאין לו free tier. real-time: Cloudflare Realtime (לשעבר Calls) SFU + Worker signaling, עם 1,000 GB egress חינם וה-App Secret נעול בצד שרת.
שתי תובנות שיישארו איתך מעבר לפרק: ראשית, ההבחנה בין עיבוד (חינם בנדיבות) ל-אחסון/הגשה מנוהלים (בתשלום) — היא שמסבירה איפה מתחיל החיוב בכל מוצר media. שנית, התבנית "פעולה יקרה + cache לפי hash" — שראינו ב-screenshots — חוזרת בכל מקום שיש משאב מוגבל.
בפרק הבא (פרק 5 — Auth, Email, Tunnel ו-Analytics) ניקח את ה-Workers שבנינו כאן ונעטוף אותם בשכבת ה-glue: Turnstile שיגן על ה-screenshot API מ-abuse שמרוקן את 10 הדקות, Zero Trust Access שיגדר את חדר הווידאו לפרטי, ו-Analytics Engine שיספור צפיות ו-screenshots בלי לשבור את ה-$0. ה-media שבנית מקבל שם זהות, הגנה ומדידה.
צ'קליסט — סיכום פרק 4
- הפעלתי Transformations על ה-zone (Images → Transformations) לפני שבניתי URL של
/cdn-cgi/image/. - בניתי URL של Image Transform עם
width,qualityו-format=auto, ואימתתי resize. - אימתתי ב-DevTools ש-
format=autoמגישimage/webpאוimage/avifבדפדפן עדכני. - העליתי מקור ל-R2 עם
--content-typeמפורש, ובניתי CDN תמונות מ-R2 + Transform URL. - אימתתי
cf-cache-status: HITבקריאה שנייה — ההמרה לא נספרה שוב. - הבנתי שה-free tier סופר המרות ייחודיות, ותכננתי סט קבוע של וריאנטים מתחת ל-5,000.
- התקנתי
@cloudflare/puppeteerוהגדרתי טבלת[browser]עליונה ב-wrangler.toml. - הרצתי Browser Run עם
wrangler dev --remote(לא dev רגיל) וקיבלתי screenshot. - חישבתי את התקציב היומי האמיתי (~60–120 צילומים) והוספתי cache ב-KV לפי hash של URL.
- אימתתי שקריאה שנייה ל-screenshot API חוזרת מ-cache בלי לפתוח דפדפן.
- פיצלתי וידאו ל-HLS עם ffmpeg והעליתי עם content-type נכון (
.m3u8=application/vnd.apple.mpegurl,.ts=video/mp2t). - אימתתי שהווידאו HLS מתנגן בדפדפן מ-R2 ב-$0 egress.
- יצרתי App ב-Realtime ושמרתי את ה-App Secret עם
wrangler secret put— לא בקוד. - בניתי חדר וידאו עם Worker signaling ואימתתי שני feeds בשתי כרטיסיות.
- מילאתי טבלת תקציב media משולב וזיהיתי איזה limit נופל ראשון באפליקציה שלי.