- טופס/endpoint מוגן ב-Turnstile עם אימות טוקן בצד Worker — מחזיר OK להגשה אמיתית ו-
403לטוקן חסר/מזויף, ואפס challenge למשתמש אנושי. - Worker או אתר מגודר ב-Zero Trust Access עם login של email-OTP (עד 50 seats) — בלי ספריית auth ובלי DB משתמשים, אף שורת קוד אימות.
- named tunnel חי לדומיין מותאם, שמגיש גם endpoint SSE/streaming שעובד — בניגוד ל-Quick Tunnel שמפיל SSE.
- Worker שמעבד email נכנס דרך Email Routing וכותב event ל-Analytics Engine, שניתן לשאול ב-SQL API — triage תיבה + analytics ב-$0 בלי שירות צד-שלישי.
- מפת ה-glue: לכל רכיב (Turnstile / Access / Tunnel / Email / Analytics Engine) — המגבלה החינמית, היכן היא נשברת, ומה זה עולה אחרי.
wrangler.tomlמורחב: אותו קובץ מהפרקים הקודמים, עכשיו עם[[analytics_engine_datasets]]ו-secret של Turnstile.
- להוסיף Turnstile (CAPTCHA בלתי נראה, חינם ללא מגבלת נפח) לטופס או endpoint, ולאמת את הטוקן בצד שרת ב-Worker מול
siteverify. - לגדר Worker או אתר מאחורי Zero Trust Access עם email-OTP (50 seats חינם) — בלי לכתוב אף שורת קוד auth — ולדעת מתי לא לגדר.
- להקים named tunnel יציב עם
wrangler tunnel create/runלדומיין מותאם, כולל תמיכת SSE שה-Quick Tunnel לא נותן. - לעבד email נכנס ב-Worker עם Email Routing ולכתוב events ל-Analytics Engine (100k/יום) כתחליף ל-Google Analytics — בלי cookies ובלי consent.
- סיימת את פרק 1-2: יש לך Worker חי עם
wrangler.tomlשכבר מכיל bindings של KV / D1 / R2, ואתה יודע להריץnpx wrangler deployו-npx wrangler secret put. - נגעת ב-Quick Tunnel מפרק 1 (
wrangler tunnel quick-start) — פה נבנה את ה-named tunnel היציב שמסיר את המגבלות שלו. - מפרק 3 יש לך
AI.run()זמין — נשתמש בו (אופציונלי) לסיווג email בזרימת ה-glue. - חשבון Cloudflare חינמי — אותו אחד. אזהרה: הפרק הזה רגיש-לזמן. ניווט ה-dashboard של Zero Trust ופקודות
wrangler tunnelהשתנו ב-2026 וייתכן שישתנו שוב — תמיד אמת מול ה-UI והדף הרשמי לפני שמקבעים צעד. - תרגילים 2-4 מניחים: (א) דומיין (zone) פעיל ב-Cloudflare — נדרש ל-named tunnel ו-Email Routing; ללא zone אתה יכול ליצור tunnel אבל לא לקשר אותו לדומיין מותאם. (ב) כתובת Email Routing destination מאומתת — אמת אותה מראש דרך Dashboard → Email → Email Routing → Destination addresses; קישור האימות מגיע למייל ויש ללחוץ עליו לפני שניסיון
forward()יעבוד.
הפרקים הקודמים (1-4): יש לך Worker פרוס + R2 + KV + D1 מפרקים 1-2, שכבת AI/RAG מפרק 3, ושכבת media מפרק 4 — וכבר השתמשת ב-Quick Tunnel מפרק 1 כדי לחשוף localhost.
הפרק הזה: מוסיפים את שכבת ה-glue שהופכת את כל זה לאפליקציה אמיתית: הגנת bot (Turnstile), זהות (Access), חשיפה יציבה (named tunnel), email נכנס (Email Routing) ועין (Analytics Engine). כולם על אותו wrangler.toml, כולם ב-$0 — מלבד שליחת email יזומה, שהיא הדבר היחיד פה שדורש Workers Paid.
הפרק הבא (פרק 6 — Full-Stack חינמי): שכבת ה-glue הזו נכנסת ל-capstone — SaaS חי full-stack מגודר ב-Turnstile + Access עם observability ב-Analytics Engine, ולצדו מטריצת ה-pay-or-not שממפה לכל primitive את המגבלה שנופלת ראשונה.
| מונח | בעברית | הסבר |
|---|---|---|
| Turnstile | טרנסטייל | ה-CAPTCHA הבלתי נראה והחינמי של Cloudflare. מאמת שמשתמש אנושי בלי challenge מציק, ומציג checkbox רק כשצריך. חינם ללא מגבלת נפח; עד 20 widgets לחשבון. |
| server-side token validation | אימות טוקן בצד שרת | אימות הטוקן של Turnstile בצד ה-Worker מול endpoint ה-siteverify עם ה-secret key. חובה — בדיקת הלקוח קוסמטית וניתנת לזיוף. |
| Zero Trust / Access | זירו-טראסט / אקסס | שירות Cloudflare שמגדר כל URL מאחורי אימות זהות (Google / GitHub / email-OTP) לפני שהבקשה מגיעה ל-origin — בלי לכתוב קוד auth. 50 seats חינם (hard-limit). |
| email OTP | קוד חד-פעמי לאימייל | התחברות בקוד חד-פעמי שנשלח לאימייל — שיטת זהות ש-Access מספק בלי סיסמאות ובלי DB משתמשים. כל אימייל ייחודי נספר כ-seat. |
| Cloudflare Tunnel | טאנל | חיבור outbound-only שחושף שירות לוקלי לאינטרנט בלי פורט פתוח או IP ציבורי. שני מצבים: Quick ו-Named. |
| named tunnel | טאנל בעל-שם | Tunnel קבוע עם דומיין מותאם, ניהול מרחוק, ללא תקרת concurrent ועם תמיכת SSE — לעומת Quick Tunnel המוגבל. |
| cloudflared | קלאודפלייר-די | ה-binary שמריץ את ה-Tunnel. מ-2026 Wrangler מוריד ומנהל אותו אוטומטית דרך פקודות wrangler tunnel. |
| Email Routing | ניתוב אימייל | ניתוב email נכנס ל-Worker לעיבוד / forward / reply — חינם ללא הגבלה בכל הטיירים (קבלה בלבד). |
| Email Service | שירות אימייל | ה-productization (beta, אפריל 2026) של שליחת email מ-Worker — Workers Paid בלבד, 3,000/חודש כלול ואז $0.35/1,000 (כמתועד). |
| Analytics Engine | מנוע אנליטיקה | time-series DB בתוך Workers — כותבים events מקוד ושואלים ב-SQL. 100k data points/יום, כרגע unbilled (beta). |
| data points | נקודות-נתון | יחידת הכתיבה של Analytics Engine — רשומה עם blobs (מימדי string), doubles (מדדים מספריים) ו-index יחיד. נכתבת fire-and-forget. |
| Secrets Store | מאגר סודות | מנהל secrets ברמת חשבון לשיתוף בין Workers (beta) — 20 secrets חינם, Workers-only, מחיר טרם פורסם. |
מה זה ה-Glue, ולמה Worker עירום הוא לא אפליקציה
פרסת Worker. הוא חי על URL, מחזיר HTML, מדבר עם D1 ו-R2. אבל אם תיתן את ה-URL הזה ללקוח אמיתי או תפרסם אותו ברשת, תוך יום תגלה שחסרים לו דברים שאף primitive של אחסון או AI לא נותן: בוטים ימלאו את הטופס בזבל, כל אחד יכול להיכנס ל-/admin, אין דרך לקבל email מהמשתמשים, ואין לך מושג כמה אנשים בכלל נכנסו ומה הם עשו.
הדברים האלה הם ה-glue — שכבת הדבק שמחברת את הלוגיקה שכתבת לעולם האמיתי. ב-stack רגיל היית מוסיף לכל אחד מהם שירות צד-שלישי בתשלום: reCAPTCHA, Auth0, ngrok בתשלום, SendGrid, Google Analytics. ב-Cloudflare כל החמישה האלה כבר יושבים ליד ה-Worker שלך, רובם בחינם, ומתחברים אליו ב-binding או בשורת config — בלי שרת נוסף ובלי חשבון חדש.
למה זה משנה ל-vibe coder ספציפית: כשאתה בונה עם כלי AI, הקוד מגיע מהר — אבל שכבת ה-glue היא בדיוק המקום שבו פרויקטים נתקעים. מוסיפים ספריית auth ופתאום צריך להבין JWT ו-refresh tokens; רוצים CAPTCHA ונכנסים ל-API keys של ספק חיצוני; צריך email ופתאום יש חשבון SMTP לתחזק. כל אחד מאלה הוא תלות חדשה, חשבון חדש, וחיוב חדש. הגישה של הפרק הזה הפוכה: במקום להוסיף שירותים, להפעיל יכולות שכבר קיימות. ה-Worker שלך כבר רץ על הרשת של Cloudflare — Turnstile, Access, Tunnel, Email ו-Analytics הם פשוט מתגים שאתה מדליק על אותה תשתית.
חמשת חורי ה-glue, וה-primitive שסותם כל אחד
- הגנת bot — בוטים ממלאים טפסים ושוחקים את ה-API. סותמים עם Turnstile (חינם, ללא מגבלת נפח).
- זהות / login — מי מורשה להיכנס. סותמים עם Zero Trust Access (50 seats חינם, אפס קוד auth).
- חשיפה יציבה — דומיין קבוע + SSE שעובד. סותמים עם named tunnel (חינם).
- email נכנס — לקבל ולעבד mail. סותמים עם Email Routing (חינם, unlimited).
- observability — לראות מה קורה בלי cookies. סותמים עם Analytics Engine (100k/יום, unbilled beta).
הכלל המנחה של הפרק: כל רכיב glue פה הוא $0 בנקודת ההתחלה — חוץ מחור אחד ספציפי, שליחת email יזומה, שדורש Workers Paid. נחזור לזה שלוש פעמים בפרק כי כל מדריך שני מבלבל בין קבלה (חינם) לשליחה (בתשלום).
פתח את ה-Worker שפרסת בפרק 1-2 וכתוב לעצמך ב-3 שורות מה עדיין חסר לו כדי שתיתן אותו ללקוח אמיתי: יש לו הגנת bot? יש login? מישהו יכול לשלוח לו email? אתה יודע כמה אנשים נכנסו? סמן אילו מהחמישה אתה צריך — זה ה-glue שתחבר בפרק הזה.
בוא נתחיל מהחור הראשון שכל אפליקציה ציבורית נתקלת בו ביום הראשון: בוטים.
Turnstile — CAPTCHA בלתי נראה וחינמי לחלוטין
Turnstile הוא ה-CAPTCHA של Cloudflare, והתכונה החשובה ביותר שלו היא שלמשתמש אנושי הוא לרוב בלתי נראה. במקום לבחור אופנועים בתמונות, Turnstile מריץ smart challenge ברקע — בודק אותות של הדפדפן ומחליט אם מולו אדם. רק כשהוא לא בטוח הוא מציג checkbox פשוט. אין פאזלים, אין תמונות.
ולמה זה מתאים בדיוק ל-vibe coder: Turnstile חינם לחלוטין, ללא מגבלת נפח. אין תקרת אימותים ביום, אין כרטיס אשראי. זה אחד הרכיבים הבודדים ב-Cloudflare שאין לו מגבלה חינמית שנשברת ראשונה — הוא פשוט חינם. המגבלות היחידות הן מבניות: עד 20 widgets לחשבון, עד 10 hostnames ל-widget, ו-7 ימי analytics לאחור. (Enterprise פותח unlimited widgets, 200 hostnames ו-30 ימי analytics — לא רלוונטי ל-free tier.)
מה שכדאי להבין מהמגבלות האלה: הן מבניות, לא נפחיות. 20 widgets זה לא "20 אימותים" — זה 20 הגדרות נפרדות של widget, ואתה יכול להשתמש באחד מהם למיליון אימותים. רוב הפרויקטים צריכים widget אחד או שניים (אחד ל-production, אחד ל-staging), אז התקרה הזו כמעט אף פעם לא רלוונטית. מגבלת 7 ימי ה-analytics אומרת רק שהגרף ב-dashboard מראה שבוע אחורה — אם אתה רוצה היסטוריה ארוכה יותר, אתה תרשום את האירועים בעצמך ל-Analytics Engine (שנגיע אליו בהמשך הפרק). כלומר: בניגוד לכל primitive אחר בקורס, פה אין "מגבלה שנופלת ראשונה" שצריך לתכנן סביבה. זה נדיר, ושווה לנצל.
site key מול secret key — שני מפתחות, שני צדדים
כשתיצור widget תקבל שני מפתחות, וההבחנה ביניהם היא הלב של Turnstile:
- site key — מפתח ציבורי. הולך בצד הלקוח (ב-HTML), כולם יכולים לראות אותו. הוא רק אומר ל-widget איזה widget אתה.
- secret key — מפתח סודי. נשאר בצד ה-Worker בלבד, אף פעם לא בקוד הלקוח. בלעדיו אי אפשר לאמת טוקן.
הזרימה כולה: הלקוח פותר את ה-challenge ומקבל טוקן. הטוקן נשלח לשרת. ה-Worker שולח את הטוקן + ה-secret key ל-Cloudflare כדי לוודא שהוא אמיתי. נראה את זה בקוד בסעיף הבא.
implicit render מול explicit render
יש שתי דרכים להזריק את ה-widget לדף:
- implicit (ברירת מחדל, הכי פשוט) — שמים
<div class="cf-turnstile" data-sitekey="...">בתוך ה-form. ה-script מזהה אותו אוטומטית, מצייר את ה-widget, ומזריק<input>נסתר בשםcf-turnstile-responseשמכיל את הטוקן. כשתשלח את ה-form הטוקן נשלח איתו אוטומטית. - explicit — מוסיפים
?render=explicitל-script וקוראים ל-turnstile.render('#container', {sitekey, callback})ידנית. שימושי כש-ה-widget נטען דינמית או כשרוצים שליטה ב-timing.
<!-- ב-<head> -->
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<!-- בתוך ה-form: implicit מזריק input נסתר בשם cf-turnstile-response -->
<form action="/submit" method="POST">
<input type="text" name="email" />
<div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY"></div>
<button type="submit">שלח</button>
</form>
נקודה קריטית שנחזור אליה: הטוקן תקף 300 שניות, הוא single-use (חד-פעמי), וניתן לזיוף. לכן הבדיקה בצד הלקוח היא קוסמטית בלבד — האימות האמיתי חייב לקרות בשרת.
איזה mode לבחור
כשתיצור widget תתבקש לבחור mode, וההבדל ביניהם הוא כמה אגרסיבי ה-challenge:
- Managed (ברירת מחדל מומלצת) — Cloudflare מחליטה דינמית: לרוב המשתמשים בלתי נראה, checkbox רק כשיש חשד. זה מה שתרצה ב-99% מהמקרים.
- Non-interactive — אף פעם לא מציג checkbox, רץ לגמרי ברקע. נוח כשאתה לא רוצה אף רכיב ויזואלי, אבל מאבד את ה-fallback האנושי.
- Invisible — אין widget נראה כלל; האימות קורה לגמרי מאחורי הקלעים.
בפועל, התחל ב-Managed. אם תראה שהמשתמשים מתלוננים על checkbox מיותר, תוכל לכוונן — אבל זה נדיר עם Managed.
למה לא reCAPTCHA
אם בנית באינטרנט בעבר, ה-reflex שלך יהיה reCAPTCHA של Google. שני הבדלים מעשיים: ראשית, Turnstile לא שולח את התנועה של המשתמשים שלך ל-Google — הוא first-party ל-Cloudflare. שנית, ה-UX טוב יותר: אין "בחר את כל הרמזורים". ושלישית, אתה כבר על Cloudflare — אין integration נוסף. בשביל vibe coder שרוצה הגנת bot בלי כאב ראש, Turnstile הוא ברירת המחדל הטבעית.
היכנס ל-dashboard ← Turnstile ← Add widget, בחר מצב Managed, והעתק את ה-site key ל-clipboard. אל תיגע בקוד עדיין — רק תוודא שיש לך widget ושני המפתחות מולך.
שילוב Turnstile ב-Worker — אימות טוקן בצד שרת
עכשיו לחצי השני, שבלעדיו ה-Turnstile חסר ערך: server-side token validation. ה-Worker מקבל את הטוקן מה-form, ושולח אותו יחד עם ה-secret key ל-endpoint ה-siteverify של Cloudflare. רק התשובה משם קובעת אם להמשיך או להחזיר 403.
קודם — לאחסן את ה-secret key נכון
ה-secret key הוא סוד. הוא לא הולך בקוד, לא ב-wrangler.toml, ולא בצד הלקוח. הוא נכנס דרך wrangler secret put — בדיוק כמו שעשית בפרק 2:
npx wrangler secret put TURNSTILE_SECRET_KEY
# הדבק את ה-secret key מה-dashboard כשהוא מבקש
npx wrangler secret list # ודא שהוא מופיע
הזרימה: client → token → Worker → siteverify → allow/deny
// Worker: אימות הטוקן של Turnstile בצד שרת
export default {
async fetch(request, env) {
const body = await request.formData();
const token = body.get('cf-turnstile-response');
const ip = request.headers.get('CF-Connecting-IP');
const form = new FormData();
form.append('secret', env.TURNSTILE_SECRET_KEY); // לעולם לא בצד לקוח
form.append('response', token);
form.append('remoteip', ip);
const res = await fetch(
'https://challenges.cloudflare.com/turnstile/v0/siteverify',
{ method: 'POST', body: form }
);
const outcome = await res.json();
// outcome = { success, challenge_ts, hostname, 'error-codes': [], action, cdata }
if (!outcome.success) {
return new Response('Bot check failed', { status: 403 });
}
return new Response('OK');
},
};
שלוש נקודות שמפילות אנשים פה:
- ה-
secretהוא ה-secret key, לא ה-site key. החלפה ביניהם תחזיר תמידsuccess: false. - גוף הבקשה ל-
siteverifyהואapplication/x-www-form-urlencoded(או JSON) — והתשובה תמיד JSON עם השדהsuccessוהמערךerror-codes. - אם האימות נכשל, הטוקן נשרף (single-use). אל תשלח שוב את אותו טוקן — צריך לקרוא ל-
turnstile.reset()בצד הלקוח כדי לקבל טוקן חדש.
למה זה מפתה: ה-callback של ה-widget מחזיר טוקן ונראה כאילו "עבר אימות" — אז למה לטרוח עם שרת? ובמקביל, Analytics Engine היום לא מחויב כלל, אז קל להניח שהוא חינם לתמיד.
למה זה טעות: הטוקן של Turnstile ניתן לזיוף ובדיקת הלקוח קוסמטית בלבד — בוט יכול לשלוח טוקן מזויף ישר ל-endpoint שלך ולעקוף widget שלא אומת בשרת. ובאשר ל-Analytics Engine: Cloudflare כבר פרסמה מחיר מתוכנן ($0.25 ל-מיליון writes + $1 ל-מיליון reads) — ה-unbilled הוא זמני.
מה לעשות במקום: תמיד הרץ siteverify בצד שרת עם ה-secret key (דרך wrangler secret put); אל תקבע ארכיטקטורה שמניחה ש-Analytics Engine יישאר חינם לנצח.
- צור Turnstile widget ב-dashboard במצב Managed, קבל site key + secret key.
- הוסף ל-Static Assets טופס עם
<script src="...turnstile/v0/api.js">ו-<div class="cf-turnstile" data-sitekey="...">עם ה-site key. - הרץ
npx wrangler secret put TURNSTILE_SECRET_KEYוהדבק את ה-secret key. - ב-Worker: קרא את
cf-turnstile-responseמה-formData, ושלח POST ל-siteverifyעםsecret+response+remoteip. - אם
outcome.success === false— החזר403; אחרת עבד את הטופס והחזרOK. - הרץ
npx wrangler deploy, הגש את הטופס פעם תקינה (תקבל OK בלי challenge), ופעם עם טוקן חסר/מזויף (תקבל 403).
פלט נראה לעין: endpoint חי שמחזיר OK להגשה אמיתית ו-403 כשה-Turnstile token חסר או מזויף — ואפס challenges למשתמש אנושי.
Zero Trust Access — לגדר URL לפי זהות בלי לכתוב קוד auth
Turnstile מסנן בוטים, אבל הוא לא יודע מי אתה. ברגע שאתה צריך login — "רק חברי הצוות יכולים להיכנס ל-/admin" — אתה בדרך כלל מתחיל בכאב ראש: ספריית auth, ניהול סיסמאות, טבלת משתמשים, איפוסים, hashing. Zero Trust Access מבטל את כל זה.
הרעיון: Access מגדר URL מאחורי אימות זהות. כשמשתמש מגיע, Cloudflare מאמת אותו (דרך Google, GitHub, או email-OTP) לפני שהבקשה בכלל מגיעה ל-Worker שלך. אם הוא לא עבר — הוא לא רואה את ה-origin בכלל. אתה לא כותב אף שורת קוד auth, ואין לך DB משתמשים לתחזק.
שווה לעצור על העומק של זה. ב-auth רגיל, הבקשה מגיעה ל-Worker שלך, ה-Worker בודק token/cookie ומחליט אם להחזיר 401. כלומר ה-Worker שלך חשוף — הוא רץ, צורך CPU, ורק אז דוחה. ב-Access ההיגיון הפוך: הגידור קורה ב-edge של Cloudflare, לפני שה-Worker מתעורר בכלל. בקשה לא-מאומתת לא מגיעה לקוד שלך, לא צורכת לך invocation מתוך 100k היומיים, ולא יכולה לנצל באג בקוד ה-auth שלך — כי אין קוד auth.
מה ש-Access נותן בלי שתכתוב שורה: דף login מעוצב, אינטגרציה עם Google/GitHub/SAML, ניהול sessions ו-TTL, ואכיפת policies. מה שאתה מאבד: שליטה דקה בלוגיקת ההרשאות (זו policy ב-dashboard, לא קוד), והתקרה של 50 seats שנדבר עליה מיד. ל-admin panel או beta מוזמן זו עסקה מצוינת; לאפליקציה ציבורית גדולה — לא.
הניווט ב-dashboard של 2026
שים לב — זה הסעיף הכי רגיש-לזמן בפרק. ב-2026 Cloudflare שינתה את שמות התפריטים. הנתיב הנוכחי הוא:
Zero Trust → Access controls → Applications → Create new application → Self-hosted and private
מדריכים ישנים אומרים "Access → Applications → Self-hosted" — אם השמות אצלך שונים מאלה שכאן, אל תיבהל: חפש את האפשרות שמדברת על self-hosted application, היא אותה אחת גם אם המילים זזו. הנחה כללית: כל צעד UI בסעיף הזה ובתרגיל 2 עלול להיראות מעט אחרת אצלך — אמת מול ה-dashboard החי.
policy: default-deny + Allow
אחרי שיצרת את ה-application, אתה מצרף אליו Access policy. הדפוס הבטוח הוא default-deny: כברירת מחדל אף אחד לא נכנס, ואתה מוסיף כלל Allow שמתאים על Emails (email-OTP) עם רשימת המיילים המורשים — ו/או על IdP כמו Google/GitHub. בנוסף קובעים Session Duration — כמה זמן הטוקן תקף לפני שצריך לאמת מחדש.
למה דווקא default-deny ולא ההפך: בכל מערכת הרשאות, הברירת מחדל הבטוחה היא "אסור אלא אם הותר במפורש". אם תתחיל מ"הכל מותר חוץ מ-X", שכחה של מקרה קצה אחד פותחת חור. עם default-deny, גרוע ביותר שיקרה הוא שמישהו לגיטימי לא יכנס ויפנה אליך — לא שמישהו לא מורשה ייכנס. ב-Access זה מתורגם לפרקטיקה פשוטה: אל תוסיף כלל "Allow everyone" כדי "לבדוק שזה עובד", ואל תשכח אותו אחר כך. בדוק עם email מורשה אחד מ-incognito.
על Session Duration: זה ה-TTL של הטוקן ש-Cloudflare נותן למשתמש אחרי אימות מוצלח. קצר מדי (שעה) — המשתמשים יתאמתו שוב ושוב וזה מעצבן; ארוך מדי (שבוע) — אם מכשיר נגנב, יש חלון גישה רחב. ל-admin panel פנימי 24 שעות זה איזון סביר. הנקודה החשובה: Cloudflare מאמתת את הטוקן בכל בקשה ב-edge, אז אתה לא צריך לנהל את זה בקוד שלך כלל.
ב-Zero Trust dashboard לך ל-Access controls → Applications ופשוט סייר במסך Create new application. אתר את האפשרות Self-hosted and private — מבלי ליצור עדיין. שים לב אם השם אצלך שונה מ-tutorials ישנים, ורשום לעצמך את הנתיב המדויק שראית.
למה זה מפתה: Access נותן login יפהפה בלי קוד — מתפתה לגדר הכל מאחוריו, כולל כלי שאתה רוצה לפרסם לעולם.
למה זה טעות: התוכנית החינמית היא hard-limit של 50 seats. ב-email-OTP כל אימייל ייחודי שמתחבר נספר כ-seat. כלי ציבורי שיותר מ-50 אנשים מנסים להיכנס אליו מאלץ אותך ל-Teams ב-$7/user/month — שמתרחב לינארית, ומהר: 200 משתמשים = $1,400 בחודש.
מה לעשות במקום: השתמש ב-Access רק לקהל סגור וקטן (<50: צוות, admin panel, beta מוזמן). לכלי ציבורי — Turnstile להגנת bot + auth קל משלך (למשל magic link על KV), לא Access.
שאלה 1 — כמה אנשים ייכנסו? אם הקהל סגור וקטן (<50 משתמשים: צוות, admin, beta מוזמן) → Zero Trust Access חינם, אפס קוד auth. זה ה-use-case המושלם.
שאלה 2 — האם זה כלי ציבורי/פתוח? אם יותר מ-50 אנשים עשויים להתחבר → אל תגדר ב-Access. תקפוץ ל-$7/user/month שמתרחב מהר.
שאלה 3 — מה כן לציבורי? Turnstile (הגנת bot, חינם) + auth קל משלך (magic link / OTP על KV/D1). זה נשאר $0 בלי תקרת seats.
כלל ברזל: email-OTP סופר כל אימייל ייחודי כ-seat. אם אתה לא יכול לנקוב במספר המשתמשים מראש — אל תסמוך על Access החינמי.
- לך ל-
Zero Trust → Access controls → Applications → Create new application → Self-hosted and private(אמת את השמות מול ה-UI החי). - הוסף public hostname (דומיין שנמצא על Cloudflare) — או חבר את ה-Worker/origin דרך ה-named tunnel מהסעיף הבא.
- צור Access policy: default-deny + כלל Allow על Emails (email-OTP) עם רשימת מיילים מורשים.
- קבע Session Duration (למשל 24 שעות).
- התחבר מ-incognito: הזן email מורשה → הזן את ה-OTP שהגיע → קבל גישה. נסה email לא-מורשה → חסום.
- פתח את קוד ה-Worker ואמת שלא נכתבה בו אף שורת קוד auth.
פלט נראה לעין: URL חי שדורש email-OTP login (עד 50 seats) לפני שהבקשה מגיעה ל-Worker — בלי ספריית auth ובלי DB משתמשים.
Cloudflare Tunnel — מ-Quick ל-Named tunnel יציב
בפרק 1 השתמשת ב-Quick Tunnel: wrangler tunnel quick-start נתן לך URL ציבורי ב-HTTPS ל-localhost תוך שניות, בלי חשבון. מצוין לדמו מהיר. אבל יש לו שתי מגבלות שמתחילות לכאוב ברגע שהאפליקציה אמיתית:
- אין תמיכת SSE — Server-Sent Events (כמו ה-streaming של ה-RAG chat מפרק 3) נופלים בשקט. אין שגיאה ברורה, פשוט לא עובד.
- תקרת 200 concurrent requests — מעבר לזה בקשות נדחות.
- subdomain אקראי ב-
trycloudflare.comשמשתנה בכל הרצה — אי אפשר לתת אותו ללקוח.
Named tunnel מסיר את שלושתם: דומיין מותאם יציב, ניהול מרחוק מה-dashboard, אין תקרת concurrent, ו-SSE עובד. זה ה-tunnel שתחבר ל-Access ותיתן ללקוח.
למה SSE זה לא פרט טכני שולי: אם בנית בפרק 3 את ה-RAG chat שמזרים תשובה מילה-מילה (streaming), או בפרק 4 כל UI שמעדכן בזמן אמת — אתה משתמש ב-Server-Sent Events. זו טכניקה שבה השרת משאיר חיבור פתוח ודוחף עדכונים ברצף. Quick Tunnel לא יודע להחזיק את החיבור הפתוח הזה — הוא סוגר אותו, וה-streaming פשוט מפסיק באמצע בלי שגיאה. תראה את התשובה הראשונה ואז שקט. זה אחד הבאגים הכי מתסכלים לאבחון כי שום דבר לא "נשבר" — פשוט אין יותר נתונים. הפתרון היחיד הוא named tunnel.
למה זה outbound-only ולמה אכפת לך: ה-tunnel יוצר חיבור מהמחשב שלך החוצה ל-Cloudflare, ולא להפך. כלומר אתה לא פותח פורט ב-router, לא צריך IP ציבורי, וה-firewall שלך לא צריך חריגה. זה גם בטוח יותר — אין port פתוח שמישהו יכול לסרוק. בשביל לחשוף localhost או dev server לדמו, זה בדיוק מה שאתה רוצה.
פקודות wrangler tunnel (מרץ 2026)
עד לא מזמן היית מתקין את cloudflared בנפרד. מ-2026-03-19 Wrangler מנהל אותו בשבילך — בהרצה ראשונה הוא יציע להוריד את ה-binary ל-cache המקומי, ואתה רק מאשר. ששת ה-subcommands:
# Quick tunnel — URL ציבורי מיידי, בלי חשבון (חלופת ngrok)
npx wrangler tunnel quick-start
# Named tunnel — יציב, דומיין מותאם, מנוהל מרחוק
npx wrangler tunnel create my-app
npx wrangler tunnel list
npx wrangler tunnel info my-app
npx wrangler tunnel run my-app
npx wrangler tunnel delete my-app
# נוסף 2026-03-19; ה-binary של cloudflared יורד אוטומטית בהרצה ראשונה
אזהרת freshness: פקודות ה-wrangler tunnel מסומנות experimental ונכון למרץ 2026 עלולות להשתנות. הדפוס הבטוח: קבע את גרסת wrangler ב-package.json כדי שהפקודות לא ישברו תחת רגליך, ובדוק את ה---help אם משהו לא תואם.
הרץ npx wrangler tunnel list. אם cloudflared לא מותקן — אשר את ההורדה ל-cache. ראה אילו tunnels כבר קיימים בחשבון שלך (כנראה רשימה ריקה אם זו הפעם הראשונה).
שאלה 1 — האם זה דמו חד-פעמי? אם צריך URL מהיר ללא חשבון, בלי streaming, לכמה דקות → Quick Tunnel (quick-start, trycloudflare).
שאלה 2 — האם יש SSE/streaming? כל UI עם SSE (כמו ה-RAG chat מפרק 3 או streaming מפרק 4) → חובה Named Tunnel. Quick Tunnel יפיל את ה-SSE בשקט.
שאלה 3 — דומיין יציב או >200 concurrent? דמו קבוע ללקוח, חיבור ל-Access, או יותר מ-200 בקשות במקביל → Named Tunnel (create + run).
כלל ברזל: אם יש streaming או שאתה נותן את ה-URL למישהו אחר — named tunnel. Quick Tunnel הוא רק לבדיקה מהירה משלך.
למה זה מפתה: Quick Tunnel עובד מצוין ל-endpoints רגילים, אז קל להניח שהוא יעבוד גם ל-chat המוזרם מפרק 3 — והוא הכי קל להרצה.
למה זה טעות: Quick Tunnels (trycloudflare) מפילים SSE בשקט ומגבילים ל-200 concurrent — ה-streaming מפרק 3/4 פשוט לא יזרום, בלי שגיאה ברורה, ותבזבז שעה על דיבאג של "למה הצ'אט תקוע".
מה לעשות במקום: לכל UI עם SSE/streaming או יותר מ-200 concurrent — wrangler tunnel create + run (named tunnel). זה הצעד שהופך את ה-RAG chat מפרק 3 ל-demo יציב.
npx wrangler tunnel create my-app(אשר הורדתcloudflaredאם נדרש).npx wrangler tunnel listו-info my-app— ודא שה-tunnel נוצר.- הרץ שירות לוקלי (Worker
devאו אפליקציה) על פורט מקומי. npx wrangler tunnel run my-appוחבר אותו לדומיין מותאם על Cloudflare. כדי שה-tunnel ינתב תעבורה לדומיין שלך צריך להגדיר ingress route: דרך הדאשבורד — Zero Trust → Networks → Tunnels → [tunnel שלך] → Public Hostnames → Add a public hostname (בחר את הדומיין/zone וקבע service ל-http://localhost:<port>). חלופה דרך CLI:npx wrangler tunnel route dns my-app subdomain.example.comשמוסיף CNAME ב-Cloudflare DNS אוטומטית. שתי הדרכים מגיעות לאותה תוצאה.- בדוק endpoint רגיל, ואז endpoint SSE/streaming — ודא שה-SSE עובד (בניגוד ל-Quick Tunnel).
npx wrangler tunnel delete my-appבסיום אם זה היה זמני.
פלט נראה לעין: named tunnel חי שמגיש דומיין מותאם יציב כולל endpoint SSE שעובד — לעומת Quick Tunnel שמפיל SSE.
Email Routing — לעבד email נכנס ב-Worker (חינם, unlimited)
עד עכשיו ה-Worker רק קיבל HTTP. Email Routing נותן לו ערוץ קלט חדש לגמרי: email. אתה מגדיר כתובת (למשל support@yourdomain.com) שמנותבת ישר ל-Worker, וכל מייל שמגיע אליה מפעיל handler בקוד שלך. זה חינם ללא הגבלה בכל הטיירים — כל עוד מדובר ב-קבלה (נחזור להבחנה הקריטית הזו בסעיף הבא).
ה-email() handler ב-Module Worker
בדיוק כמו ש-fetch הוא ה-handler ל-HTTP, email הוא ה-handler ל-mail נכנס. הוא חי על אותו export default:
// Email Routing: handler ל-email נכנס (חינם, unlimited)
export default {
async email(message, env, ctx) {
// message: from, to, headers (Headers), raw (ReadableStream), rawSize
const subject = message.headers.get('subject');
// אפשרות A: forward ליעד מאומת
await message.forward('me@example.com');
// אפשרות B: לדחות (bounce)
// message.setReject('Address not accepting mail');
// אפשרות C: auto-reply (דורש EmailMessage לתשובה)
// await message.reply(replyMessage);
},
};
ה-message הוא אובייקט ForwardableEmailMessage עם השדות:
from/to— כתובות ה-envelope.headers— אובייקטHeaders(כך מגיעים ל-subject, from וכו').raw—ReadableStreamשל ה-MIME המלא;rawSize— הגודל ב-bytes.
ושלוש מתודות פעולה: forward(rcptTo) ליעד מאומת, reply(EmailMessage) לתשובה אוטומטית, ו-setReject(reason) ל-bounce עם שגיאת SMTP. חשוב: ה-syntax הישן של Service Worker הוסר — רק Module Worker (export default) נתמך.
שתי מלכודות תפעוליות: forward() עובד רק ליעד שאימתת מראש ב-Email Routing (לא לכל כתובת שתרצה), וכדי שכתובת המקור תעבוד הדומיין שלה חייב Email Routing פעיל.
מה אפשר לבנות עם email נכנס
הערוץ הזה פותח דפוסים שלמים שבדרך כלל דורשים שירות צד-שלישי:
- תיבת support חכמה — מייל ל-
support@נכנס ל-Worker, מסווג אוטומטית (דחוף/רגיל), נשמר ב-D1, ומועבר לאדם הנכון. - parsing של מיילים אוטומטיים — קבלות, התראות, או דוחות שמגיעים במייל — ה-Worker חולץ מהם נתונים ושומר אותם.
- auto-reply מותאם — מענה אוטומטי שמבוסס על תוכן ההודעה, לא תבנית קבועה.
- סינון / quarantine —
setReject()דוחה מיילים שעונים על קריטריון, עם הודעת SMTP מסבירה.
הנקודה: ה-email() handler הוא JavaScript מלא. כל מה שאתה יכול לעשות ב-fetch — לקרוא ל-AI, לכתוב ל-D1, לשמור ב-R2 — אתה יכול לעשות על מייל נכנס. זה לא "ניתוב mail", זה compute על mail.
מתי לא להשתמש בזה
Email Routing הוא לקבלה. אם הצורך שלך הוא לשלוח עדכונים יזומים למשתמשים — newsletter, התראות push-by-email — זה לא הכלי, וזה גם לא חינמי (נגיע לזה מיד). וגם לקבלה: ה-handler רץ בתוך אותו מודל isolate של Workers, אז עיבוד כבד מאוד על raw גדול עדיין כפוף לאותם שיקולי CPU מפרק 2.
ב-dashboard → Email → Email Routing ודא שיש לך לפחות destination address אחד מאומת (תיבת ה-gmail שלך, עם וי ירוק). תצטרך אותו כדי ש-forward() יעבוד — בלי יעד מאומת, ה-forward ייכשל.
מלכודת ה-Email: קבלה חינם מול שליחה בתשלום
זו ההבחנה היחידה בפרק הזה שיכולה לעלות לך כסף בלי שתשים לב, וכמעט כל מדריך מבלבל אותה. תכניס אותה לראש ונקודה:
| Email Routing (קבלה) | Email Service (שליחה) | |
|---|---|---|
| מה זה עושה | מקבל ומעבד mail נכנס | שולח mail יזום החוצה |
| מחיר | חינם, unlimited, כל הטיירים | Workers Paid בלבד ($5/חודש) |
| מכסה | ללא הגבלה | 3,000/חודש כלול, ואז $0.35/1,000* |
| בקוד | forward() / reply() / setReject() | new EmailMessage(...).send() |
* המספרים 3,000/חודש ו-$0.35/1,000 הם כפי שמתועד — דף ה-pricing של Email Service החזיר 404 באימות האחרון. אמת מול הדף הרשמי לפני שתסתמך על נפח שליחה.
למה הבלבול מסוכן
שים לב לעדינות: forward() ו-reply() בתוך ה-email() handler הם inbound — חלק מ-Email Routing, חינם. אבל new EmailMessage(...).send() דרך binding של cloudflare:email הוא outbound — Email Service, בתשלום. שתי השורות נראות כמעט זהות בקוד, אבל אחת חינמית והשנייה דורשת Workers Paid. מדריך שמראה לך "לשלוח email מ-Worker בחינם" כמעט תמיד מתכוון ל-reply() על מייל נכנס — לא לשליחה יזומה.
הדרך הבטוחה לזכור את ההבחנה: שאל את עצמך "האם יש מייל נכנס שאני מגיב עליו?". אם כן — reply()/forward(), חינם, כי אתה בתוך זרימה שכבר התחילה במייל נכנס. אם אתה פותח מייל חדש מאפס (משתמש נרשם ואני שולח לו welcome) — זה send(), paid, כי אתה מקור התקשורת. ההבחנה היא "מי התחיל": אם הלקוח התחיל, התגובה חינם; אם אתה התחלת, השליחה עולה.
ולמה ההבחנה הזו שווה כל כך הרבה תשומת לב: זו המלכודת היחידה בפרק שיכולה להפיל פרויקט שלם. אם בנית את כל ה-onboarding flow שלך מסביב ל"נשלח מייל אימות" והנחת שזה חינם — תגלה ביום ההשקה שאף מייל לא יוצא, או שאתה צריך פתאום $5 שלא תכננת. עדיף לדעת את זה עכשיו ולתכנן את ה-flow בהתאם: או ש-Access מטפל ב-OTP בשבילך (חינם), או שאתה מתקצב Workers Paid מראש.
מה אפשר לבנות ב-$0
בלי Workers Paid אתה יכול: לקבל mail, לעבד אותו (לסווג, לשמור ב-D1, להפעיל AI), להעביר (forward) ליעד מאומת, ולענות אוטומטית (reply) למי ששלח. מה שאתה לא יכול: לפתוח email יזום — welcome mail, התראות, OTP משלך — בלי לשלם.
// Email Service / send_email binding — Workers PAID
import { EmailMessage } from 'cloudflare:email';
import { createMimeMessage } from 'mimetext';
export default {
async fetch(request, env) {
const msg = createMimeMessage();
msg.setSender({ name: 'My App', addr: 'noreply@example.com' }); // הדומיין חייב Email Routing פעיל
msg.setRecipient('user@example.com');
msg.setSubject('Welcome');
msg.addMessage({ contentType: 'text/plain', data: 'תודה שנרשמת!' });
const message = new EmailMessage('noreply@example.com', 'user@example.com', msg.asRaw());
await env.SEB.send(message); // SEB = שם ה-[[send_email]] binding
return new Response('sent');
},
};
כתוב לעצמך בשורה אחת: באפליקציה שאתה מתכנן — האם אתה צריך לשלוח email יזום (paid) או רק לעבד/לענות לנכנס (free)? סמן את התשובה בבירור. אם התשובה היא "רק לקבל" — אתה נשאר ב-$0.
למה זה מפתה: "Email מ-Cloudflare זה חינם" — קראת את זה בעשרה מקומות, ו-forward()/reply() באמת חינמיים, אז למה שגם שליחה לא תהיה?
למה זה טעות: Email Service (שליחה) הוא Workers Paid בלבד — אפס emails/חודש ב-free tier. רק Email Routing (קבלה) חינם. forward()/reply() הם inbound; new EmailMessage().send() הוא paid. מדריכים מערבבים את השניים, ותגלה את זה רק כשה-welcome mail לא יוצא.
מה לעשות במקום: ב-$0 בנה קבלה + auto-reply דרך Email Routing בלבד. אם אתה חייב שליחה יזומה — תכנן ל-$5 Workers Paid (3,000/חודש כלול, $0.35/1,000 אחרי, כמתועד — אמת לפני volume גבוה).
שאלה 1 — מה הזרימה צורכת? אם רק קבלה / עיבוד / forward / auto-reply של mail נכנס → Email Routing, $0 unlimited.
שאלה 2 — צריך לשלוח mail יזום? welcome, התראות, OTP משלך → Email Service, Workers Paid ($5/חודש, 3,000/חודש כלול ואז $0.35/1,000 כמתועד).
שאלה 3 — אפשר להחליף שליחה בקבלה? לעיתים כן: במקום לשלוח OTP יזום, גדר ב-Access (שמטפל ב-OTP בעצמו) או השתמש ב-magic link על דומיין שלך.
כלל ברזל: אל תבנה flow ב-free tier שתלוי בשליחת email. אם הוא תלוי — זה כבר תקציב $5, תכנן אותו ככה מראש.
Analytics Engine — observability ב-$0 כתחליף ל-Google Analytics
החור האחרון ב-glue: אתה לא יודע מה קורה בפנים. כמה אנשים נכנסו? אילו endpoints נקראים? מאיפה הגיע ה-email? במקום להדביק Google Analytics (cookies, consent banner, scripts של צד-שלישי), Cloudflare נותן לך Analytics Engine — time-series DB בתוך Workers. אתה כותב events מהקוד, ושואל אותם ב-SQL. אין cookies, אין consent, הכל first-party ב-edge.
ההבדל המנטלי מ-Google Analytics: GA הוא client-side — script בדפדפן שמדווח מה המשתמש עשה, ולכן תלוי ב-cookies, נחסם על ידי ad-blockers, ודורש consent banner ב-GDPR. Analytics Engine הוא server-side — אתה רושם event מתוך הקוד שלך, על אירוע שאתה כבר יודע שקרה (בקשה הגיעה, מייל התקבל, פעולה הצליחה). אין מה לחסום ואין מה לבקש עליו הסכמה כי לא נשמר מידע אישי בדפדפן. זה גם הופך אותו למתאים לדברים ש-GA בכלל לא רואה: events מ-cron jobs, מ-email handlers, או מ-API שאין לו UI.
מתי זה לא מתאים: Analytics Engine הוא aggregation — מצוין לספור, לקבץ ולממצע ("כמה page views לכל path", "ממוצע גודל מייל נכנס"). הוא לא תחליף ל-D1 לשמירת רשומות שאתה צריך לקרוא בודדות במדויק — הוא משתמש ב-sampling בקנה מידה גדול. אם אתה צריך "תראה לי בדיוק את ה-event הזה" — זה D1. אם אתה צריך "תראה לי מגמה" — זה Analytics Engine.
המגבלות
- 100,000 data points/יום (כתיבות) + 10,000 read queries/יום חינם.
- כרגע unbilled (beta) — אבל מחיר מתוכנן כבר פורסם: $0.25 ל-מיליון writes (מעל 10M/חודש) + $1 ל-מיליון read queries (מעל 1M/חודש). אל תניח "חינם לתמיד".
ה-binding וה-writeDataPoint
מצרפים dataset ב-wrangler.toml, ואז כותבים מהקוד. הדבר הכי חשוב לזכור: writeDataPoint() הוא fire-and-forget — אסור ל-await. הוא חוזר מיד וה-runtime כותב ברקע.
# wrangler.toml
[[analytics_engine_datasets]]
binding = "AE"
dataset = "my_app_events"
// כתיבת event מותאם (fire-and-forget — בלי await!)
export default {
async fetch(request, env) {
env.AE.writeDataPoint({
blobs: ['page_view', '/pricing', 'US'], // מימדי string (filterable)
doubles: [1, 42.5], // מדדים מספריים
indexes: ['user_123'], // בדיוק index אחד מותר
});
return new Response('logged');
},
};
שלושת השדות: blobs הם מימדי string (מה אפשר לסנן/לקבץ לפיהם), doubles הם המדדים המספריים (מה אפשר לסכום/לממצע), ו-indexes הוא מפתח ה-sampling — בדיוק אחד מותר ל-data point.
שאילתה ב-SQL API
לקריאה שולחים POST ל-endpoint ה-SQL. שתי מלכודות שתופסות כמעט כל אחד:
- גוף הבקשה הוא raw text של ה-SQL — לא JSON עטוף (
{"query":...}יחזיר שגיאה). - ה-token צריך scope של Account Analytics Read.
POST https://api.cloudflare.com/client/v4/accounts/{account_id}/analytics_engine/sql
Authorization: Bearer <API_TOKEN> # Account Analytics Read
# גוף הבקשה — raw text, לא JSON:
SELECT blob1 AS path, sum(double1) AS views
FROM my_app_events
GROUP BY path
השם של ה-dataset הוא ה-FROM table; העמודות נקראות blob1, double1, index1 וכו' לפי הסדר שכתבת. יש גם עמודות מובנות של timestamp ו-sampling.
כלל מעשי לתכנון schema של data point: שים ב-blobs את מה שתרצה לסנן ולקבץ לפיו (path, country, event type, sender) ושמור על סדר עקבי — blobs[0] תמיד אותו דבר. שים ב-doubles את מה שתרצה לסכום או לממצע (count, duration, size). וה-index היחיד הוא מפתח ה-sampling — בחר משהו עם cardinality גבוה (כמו user id) כדי שה-sampling יהיה מייצג. שינוי ה-schema אחרי שכבר כתבת נתונים מסבך שאילתות, אז שווה לחשוב על הסדר פעם אחת מראש.
ולמה ה-fire-and-forget הזה (בלי await) הוא דווקא יתרון ולא ויתור: הוא אומר שרישום event לא מאט את הבקשה למשתמש. ה-Worker מחזיר את התשובה מיד, וה-runtime כותב את ה-data point ברקע על חשבונו. בניגוד לכתיבה ל-D1 (שכן צריך await וכן סופר לך CPU/זמן), observability כאן היא כמעט חינמית בביצועים — בדיוק מה שאתה רוצה ממדידה: שלא תשלם עליה בחוויית המשתמש.
הוסף ל-wrangler.toml בלוק [[analytics_engine_datasets]] עם binding = "AE" ו-dataset = "my_app_events". שמור — אל תפרוס עדיין. בסעיף הבא נחבר אותו לזרימת ה-email.
Secrets Store — ניהול secrets ברמת חשבון (beta)
עד עכשיו כל secret נכנס דרך wrangler secret put — והוא נשמר פר-Worker. זה מצוין כשה-secret שייך ל-Worker אחד. אבל מה אם אותו TURNSTILE_SECRET_KEY צריך לשרת שלושה Workers שונים? עם secret put תכפיל אותו שלוש פעמים, ובכל סיבוב צריך לעדכן את כולם.
Secrets Store (beta) פותר את זה: secrets ברמת החשבון, שמשותפים בין Workers בלי כפילות. מגדירים את ה-secret פעם אחת, ומחברים אליו כל Worker שצריך. כשמסובבים את ה-secret (rotation) — מעדכנים במקום אחד וכל ה-Workers מקבלים את הערך החדש, במקום לרדוף אחרי כל אחד מהם.
למי זה לא רלוונטי כרגע: אם אתה בונה Worker בודד — וזה רוב הפרויקטים בקורס הזה — wrangler secret put מספיק לחלוטין, והוא פשוט יותר. Secrets Store מצדיק את עצמו רק כשיש לך כמה Workers שחולקים את אותו סוד (מפתח API משותף, secret של Turnstile שמשרת כמה אפליקציות). הכלל: אל תוסיף אותו "ליתר ביטחון" — זו עוד תלות ב-beta שעלולה לקבל מחיר, ועוד role להגדיר. הוסף אותו רק כשהכאב של כפילות secret אמיתי.
- 20 secrets לחשבון חינם (כמתועד — לא אומת מחדש על דף live בסשן האחרון), מחיר טרם פורסם, Workers-only כרגע.
- הגדרה ב-
wrangler.tomlדרך[[secrets_store_secrets]]עםbinding/store_id/secret_name; ב-runtime:const v = await env.BINDING.get(). - החיבור דורש role של Super Administrator או Secrets Store Deployer.
- מלכודת dev: secrets של production (שנוצרו ב-dashboard/API/
--remote) לא נראים ל-wrangler devמקומי — צור secrets מקומיים בלי--remoteל-dev.
# wrangler.toml
[[secrets_store_secrets]]
binding = "TURNSTILE"
store_id = "<STORE_ID>"
secret_name = "TURNSTILE_SECRET_KEY"
// runtime
const secret = await env.TURNSTILE.get();
החלט לפרויקט שלך: האם אותו secret (למשל TURNSTILE_SECRET_KEY) צריך להיות משותף לכמה Workers? אם כן → Secrets Store. אם לא → wrangler secret put מספיק. רשום את ההחלטה — אל תוסיף מורכבות שאתה לא צריך.
חיבור ה-Glue: email-to-AI + analytics בזרימה אחת
עכשיו נחבר כמה חוטים יחד לזרימה אחת שמדגימה את כל מה שבנינו: email-to-AI triage. מייל נכנס → Worker מסווג אותו עם Workers AI (פרק 3) → שומר ב-D1 (פרק 2) → כותב event ל-Analytics Engine. זו תיבת triage חכמה, ב-$0, בלי שום שירות צד-שלישי.
export default {
async email(message, env, ctx) {
const from = message.from;
const subject = message.headers.get('subject') || '';
// (אופציונלי) סיווג עם Workers AI מפרק 3
// const { response: label } = await env.AI.run('@cf/...', {
// prompt: `סווג את הנושא לקטגוריה אחת: ${subject}` });
// שמירה ב-D1 (פרק 2)
await env.DB.prepare(
'INSERT INTO inbound (sender, subject, size) VALUES (?, ?, ?)'
).bind(from, subject, message.rawSize).run();
// event ל-Analytics Engine — בלי await!
env.AE.writeDataPoint({
blobs: ['inbound_email', from, subject],
doubles: [message.rawSize],
indexes: [from],
});
// forward ליעד מאומת (אופציונלי)
await message.forward('me@example.com');
},
};
שים לב כמה מעט קוד זה דורש, וכמה primitives מהפרקים הקודמים נכנסים פנימה: email() מ-Email Routing, AI.run() מפרק 3, DB (D1) מפרק 2, ו-AE.writeDataPoint() מהסעיף הקודם — וכולם ב-$0. זו בדיוק שכבת ה-glue שתיכנס ל-capstone של פרק 6: full-stack מגודר ב-Turnstile + Access, עם observability ב-Analytics Engine.
למה הזרימה הזו היא דוגמה טובה ל-glue בפעולה: היא לא משתמשת בשום שירות חיצוני, אין בה API key של ספק אחר, ואין בה תשלום. מערכת triage כזו ב-stack מסורתי הייתה דורשת שירות לקבלת mail (Mailgun/SendGrid inbound), endpoint לעבד אותו, קריאה ל-OpenAI, DB, ושירות analytics — חמישה חשבונות, חמישה חיובים פוטנציאליים. כאן זה Worker אחד ו-wrangler.toml אחד. זה לא רק זול — זה פחות נקודות כשל ופחות דברים לתחזק, וזה בדיוק היתרון שאתה רוצה כ-vibe coder.
נקודת זהירות אחת על שילובים: אם תוסיף ל-handler הזה גם שליחת תשובה יזומה (לא reply() על הנכנס, אלא מייל חדש למישהו אחר) — חצית את הקו ל-Email Service, וזה כבר Workers Paid. כל עוד אתה רק מקבל, מעבד, שומר, מודד ו-forward/reply — אתה ב-$0.
צייר על נייר את הזרימה ב-5 חצים בלבד: אימייל נכנס → איזה שדה ב-message תשתמש לסיווג → לאיזה מודל Workers AI → איזו טבלת D1 → איזה data point ל-Analytics Engine. זה ה-blueprint של תרגיל 4.
- לפני הכל — ב-Dashboard → Email → Email Routing → Destination addresses: ודא שיש לך לפחות destination address אחד עם וי ירוק (Verified). אם קיבלת email אימות ולא לחצת על הקישור — כל
forward()ייכשל בשקט; לחץ על הקישור עכשיו. - ב-Email Routing הגדר route של כתובת ל-Worker (לא forward פשוט).
- הוסף
[[analytics_engine_datasets]](binding = "AE",dataset = "my_app_events") ל-wrangler.toml. - כתוב
email(message, env, ctx): קראsubject/from, החלט סיווג (אופציונלי עם Workers AI מפרק 3). env.AE.writeDataPoint({ blobs:['inbound_email', from, subject], doubles:[message.rawSize], indexes:[from] })— בלי await.- אופציונלי:
message.forward()ליעד מאומת, אוsetReject(). npx wrangler deploy, שלח email לכתובת, ואז הרץ שאילתת SQL API לאמת שה-event נכתב.
פלט נראה לעין: Worker חי שמקבל email נכנס, כותב event ל-Analytics Engine, וניתן לשאול אותו ב-SQL API — triage/analytics ב-$0 בלי צד שלישי.
סיכום — checklist ומפת המגבלות של שכבת ה-Glue
חיברת את כל שכבת ה-glue: הגנת bot, זהות, חשיפה יציבה, email נכנס, ו-observability — חמישה רכיבים שהופכים Worker עירום לאפליקציה אמיתית, מאובטחת ונצפית. כל אחד מהם, חוץ משליחת email, נשאר ב-$0 — וזה לא מקרי: זו בדיוק הנקודה של הפרק. אבל "חינם" אצל Cloudflare תמיד מגיע עם מגבלה ספציפית שנופלת ראשונה, וההבדל בין מי שנכווה למי שלא הוא מי שיודע איזו מגבלה ו-מתי. לפני שנעבור ל-capstone, הנה מפת המגבלות של מה שבנית — היכן כל רכיב נשבר:
| רכיב | מגבלה חינמית | נשבר ב... | אחרי |
|---|---|---|---|
| Turnstile | ללא מגבלת נפח | 20 widgets/חשבון (מבני) | Enterprise |
| Zero Trust Access | 50 seats (hard) | המשתמש ה-51 | $7/user/month |
| named tunnel | חינם, ללא תקרת concurrent | — | — |
| Email Routing (קבלה) | unlimited | — | — |
| Email Service (שליחה) | 0 ב-free | כל שליחה | $5 + 3,000/חודש, $0.35/1,000* |
| Analytics Engine | 100k writes/יום, unbilled | החיוב המתוכנן יופעל | $0.25/M writes + $1/M reads |
* מספרי Email Service כמתועד — אמת מול הדף הרשמי לפני volume שליחה.
עבור על ה-checklist בסוף הפרק וסמן אילו רכיבים כבר חיברת בפועל מול אילו רק קראת עליהם. הפער הזה הוא מה שתשלים לפני שתיכנס ל-capstone של פרק 6.
| מתי | פעולה | פקודה / מקום |
|---|---|---|
| יש טופס/API ציבורי | הוסף Turnstile + siteverify בשרת | wrangler secret put TURNSTILE_SECRET_KEY |
| צריך login לקהל סגור | גדר ב-Access (אם <50) | Access controls → Applications |
| יש SSE/streaming | החלף Quick ל-named tunnel | wrangler tunnel create/run |
| צריך לקבל email | הוסף email() handler | Email Routing → route ל-Worker |
| רוצה לראות מה קורה | כתוב events (בלי await) | env.AE.writeDataPoint(...) |
| secret משותף לכמה Workers | העבר ל-Secrets Store | [[secrets_store_secrets]] |
קח את ה-Worker הציבורי הכי "אמיתי" שלך וחבר לו רק דבר אחד מהפרק: Turnstile עם siteverify בצד שרת על הטופס שלו. זה החיבור עם ה-ROI הכי גבוה — חינם לחלוטין, מגן מיידית מפני בוטים, ולוקח 25 דקות. את שאר ה-glue תוסיף כשתצטרך, אבל הגנת bot על endpoint ציבורי היא לא אופציה — היא בסיס.
- למה אימות Turnstile חייב לקרות בצד שרת (
siteverify) ולא מספיק ה-callback בצד הלקוח? - למה Quick Tunnel נכשל ב-SSE, ומה ה-named tunnel עושה אחרת שמאפשר streaming?
- מתי email דורש תשלום ומתי הוא חינם — ואיזו שורת קוד מבדילה בין השניים?
- כיצד מחליטים אם לגדר כלי ב-Zero Trust Access, ומה קורה כשהמשתמש ה-51 מנסה להיכנס?
- למה אסור ל-
awaitאתwriteDataPoint(), וכמהindexesמותר ל-data point אחד?
הוספת את חמשת רכיבי ה-glue שהופכים Worker לאפליקציה: Turnstile (הגנת bot חינמית עם אימות בצד שרת), Zero Trust Access (login בלי קוד auth, 50 seats), named tunnel (חשיפה יציבה עם SSE), Email Routing (קבלת mail בחינם — להבדיל משליחה בתשלום), ו-Analytics Engine (observability ב-$0 בלי cookies). למדת את ההבחנה הקריטית — כל ה-glue חינמי חוץ משליחת email, ש-Workers Paid — ואת ה-gotchas: siteverify בשרת בלבד, writeDataPoint בלי await ו-index יחיד, ו-named tunnel ל-SSE.
הפרק הבא (פרק 6 — Full-Stack חינמי): נחבר את הכל ל-SaaS חי full-stack — Worker + Static Assets + D1 + R2 + KV + Workers AI + Vectorize RAG, מגודר ב-Turnstile ו-Access עם observability ב-Analytics Engine — ונבנה את מטריצת ה-pay-or-not שממפה לכל primitive את המגבלה שנופלת ראשונה ומתי ה-$5 הופך משתלם. שכבת ה-glue שבנית פה נכנסת לשם ישירות.
צ'קליסט — סיכום פרק 5
- יצרתי Turnstile widget (Managed) וקיבלתי site key + secret key.
- שמרתי את ה-secret דרך
wrangler secret put TURNSTILE_SECRET_KEY— לא בקוד ולא בצד לקוח. - אני מאמת את הטוקן בצד שרת מול
siteverify, ומחזיר403כש-success === false. - אני יודע שהטוקן single-use + 300s, ושצריך
turnstile.reset()בכישלון. - הבנתי את כלכלת ה-50 seats של Access ולא גידרתי כלי ציבורי מאחוריו.
- גידרתי Worker/אתר ב-Access עם email-OTP ו-policy של default-deny + Allow — בלי קוד auth.
- אני יודע שהניווט הוא
Access controls → Applications → Self-hosted and private(ושהשמות עלולים להשתנות). - הקמתי named tunnel עם
wrangler tunnel create/runלדומיין מותאם. - אימתתי ש-SSE עובד דרך ה-named tunnel (ולא דרך Quick Tunnel).
- קיבעתי גרסת wrangler כי פקודות ה-tunnel experimental.
- יש לי destination address מאומת ל-Email Routing, ו-
email()handler שמעבד mail נכנס. - אני מבחין בין קבלה (חינם:
forward/reply) לשליחה (paid:send()) ולא בניתי free-flow שתלוי בשליחה. - הוספתי
[[analytics_engine_datasets]]וכותב events עםwriteDataPoint— בליawait, index יחיד. - שאלתי את Analytics Engine ב-SQL API (גוף raw text, token עם Account Analytics Read).
- החלטתי אם secret מסוים צריך Secrets Store (משותף) או
wrangler secret put(פר-Worker).