چرا firebase-admin روی Cloudflare Workers کار نمیکند — و جایگزین ۳۰۰ خطیاش
نویسنده: آرش لطیفی
firebase-admin روی workerd میمیرد: protobufjs کد رانتایم تولید میکند. راهحل: fetch + WebCrypto برای JWT، Firestore REST با تراکنش واقعی.
TL;DR — اگر وقت نداری:
firebase-adminچون داخلشprotobufjsباnew Functionکد میسازد، روی Cloudflare Workers (کهevalرا بسته) همان اول میترکد. بیلد رد میشود، پروداکشن میافتد. راهحل؟ دور انداختن SDK و نوشتن ~۳۰۰ خط کد باfetch+crypto.subtle. سه تا کار را با سه تا endpoint جایگزین میکنی: ۱) تایید توکن باfirebase-auth-cloudflare-workers، ۲) گرفتن access token با WebCrypto، ۳) کار با Firestore از طریق REST + تراکنش واقعی. همین. بقیهی مقاله قصهاش است.
تصور کن یک اپ Next.js 16 داری که با OpenNext روی Cloudflare Workers دیپلوی کردهای. همهچیز مرتب است تا روزی که مینویسی import admin from "firebase-admin" — و ناگهان:
EvalError: Code generation from strings disallowed for this context
at new Function (<anonymous>)
at Codegen.gen (.../protobufjs/src/codegen.js)
نه فلگی دارد، نه با nodejs_compat درست میشود. بیلد هم با لبخند پاس میشود! فقط وقتی کاربر واقعی درخواست میزند، اپ میافتد. بدترین جای ممکن.
این داستان واقعی یک دولوپر ۱۷ سالهی ژاپنی هم هست که با همین مشکل پلتفرم ویدیویی پولیاش روی پروداکشن خوابید و آخرش به جای SDK، خودش با fetch همهچیز را از صفر نوشت — و جالب اینکه کد جدیدش از SDK تمیزتر درآمد.
تشبیه: چرا اصلاً این اتفاق میافتد؟
فکر کن firebase-admin یک سرآشپز حرفهای است که عادت دارد دستور غذا را همان لحظه روی تخته بنویسد (new Function). روی Node.js هیچکس جلویش را نمیگیرد — تخته هست، ماژیک هست.
ولی Cloudflare Workers مثل یک آشپزخانهی فوقبهداشتی (V8 isolate) است که میگوید: «اینجا کسی حق ندارد وسط کار دستور جدید بنویسد. امنیت و سرعت مهمتر است.» به همین خاطر eval و new Function کلاً قفل هستند. سرآشپز وارد آشپزخانه میشود، میخواهد بنویسد، در بسته است. تمام.
داخل firebase-admin کتابخانهی protobufjs همین کار را میکند — موقع لود شدن، برای هر پیام پروتوباف یک تابع میسازد. پس دعوا اصلاً سر Firebase نیست، سر فرض پنهان SDK است: «من همیشه روی Node اجرا میشوم.»
قانون طلایی که بنویس و بچسبان به دیوار: هر SDK که زیر کاپوتش protobufjs، grpc یا new Function دارد، روی edge میمیرد. چطور بفهمی؟ دو تا دستور ده ثانیهای:
اگر چیزی پیدا کردی، بدان روی Workers به مشکل میخوری — قبل از اینکه پروداکشن بهت بگوید.
راهحل خودمانی: اصلاً به SDK نیاز نداری، HTTP کافی است
راستش را بخواهی، ما فقط سه تا کار از firebase-admin میخواستیم. هر سه را میشود مستقیم با API خود گوگل صدا زد:
| ما چی میخواهیم؟ | با SDK چطور بود؟ | با REST چطور میشود؟ |
|---|---|---|
| بفهمیم توکن کاربر واقعی است؟ | admin.auth().verifyIdToken | firebase-auth-cloudflare-workers |
| دیتا بخوانیم/بنویسیم؟ | admin.firestore() | firestore.googleapis.com |
| کاربر را حذف کنیم؟ | admin.auth().deleteUser | identitytoolkit .../accounts:delete |
بریم قدمبهقدم — مثل آشپزی، ولی بدون new Function!
قدم ۱: تایید توکن — بدون SDK، فقط با یک کش کوچک
کتابخانهی firebase-auth-cloudflare-workers دقیقاً برای همین ساخته شده — صفر وابستگی، فقط با Web API استاندارد کار میکند. تنها چیزی که ازت میخواهد یک جا برای نگهداشتن کلیدهای عمومی گوگل است. برای سایت کوچک، حافظهی خود Workers کافی است:
همین! نه protobufjs، نه دردسر. انگار به جای اینکه کل جعبهابزار را بیاوری، فقط پیچگوشتی را برداشتهای.
قدم ۲: گرفتن access token با WebCrypto — امضای دیجیتال، نسخهی Workers
برای اینکه با Firestore حرف بزنی، باید به گوگل ثابت کنی کی هستی. در دنیای Node این کار با crypto.createSign انجام میشد. در Workers ابزارت crypto.subtle است — همان کار، فقط API فرق دارد.
تشبیهاش: انگار میخواهی نامهای را با مهر محرمانهات امضا کنی و به گوگل بفرستی تا بهت کارت ورود بدهد.
نکتهی خودمانی: این تابع را خارج از handler کش کن! هر isolate یکبار توکن میگیرد و یک ساعت استفاده میکند. اگر داخل هر request بسازیش، هر بار یک fetch اضافه به گوگل میزنی و سرعتت نصف میشود.
قدم ۳: تراکنش واقعی Firestore — که دو نفر همزمان یک کد را نخرند
فرض کن داری کد سریال اکتیواسیون میفروشی. دو نفر دقیقاً همزمان روی یک کد کلیک میکنند — نباید هر دو موفق شوند. در SDK این را transaction میگفتی؛ در REST هم همان است، فقط با سه تا fetch:
beginTransaction → خواندن سند → commit با شرط currentDocument.exists
این شرط آخر همان «compare-and-swap» است — یعنی «فقط اگر هنوز موجود است، بنویس». اگر کسی زودتر خریده باشد، commit شکست میخورد.
ساده، شفاف، و مهمتر از همه: واقعاً اتمیک است — نه ادای تراکنش.
تلههایی که خودم افتادم توش (که تو نیفتی!)
- یک فاصلهی جاافتاده بعد از
Bearer. نوشتنAuthorization: ***به جایAuthorization: Bearer ***یک ۴۰۱ ساکت میدهد که تا نیم ساعت نمیفهمی چرا. اول template literal را چک کن. - توکن را در scope ماژول کش کن، نه داخل handler. هر isolate یکبار میسازد و یک ساعت نگه میدارد. داخل handler بسازی، هر request یک رفتوبرگشت اضافه به گوگل.
currentDocumentرا فراموش نکن. بدون آن،commitفقط یک نوشتن کور است. تراکنش با شرط معنا پیدا میکند: «اگر هنوز همینطوری است، بنویس».- اگر
ABORTEDگرفتی، کل تراکنش را از اول برو — نه فقطcommitرا. - حتماً با
wrangler devتست کن. تست روی Node پاس میشود ولی روی isolate میافتد — دقیقاً همانجا که باگ قایم شده.
هزینهاش چقدر است؟ و کِی اصلاً سراغش نرو
عدد واقعی: firebase-admin حدود ۴۰۰KB قبل از gzip به باندل اضافه میکند و روی Workers حتی با nodejs_compat هم بالا نمیآید. نسخهی REST حدود ۳۰۰ خط و چند کیلوبایت است. تأخیر هم بهتر است — هر عملیات یک fetch است و توکن هر ساعت یکبار ساخته میشود.
اما صادق باشیم — این راهحل برای همه نیست:
اگر اپت روی Node یا Vercel Serverless اجرا میشود، همون firebase-admin را نگه دار. SDK مزایای واقعی دارد: batch خودکار، retry، query builder با type-safe، و onSnapshot برای realtime. چیزی که اینجا میسازی فقط CRUD ساده است.
پس کِی این راه را برو؟
- وقتی runtime واقعاً edge باشد (Workers / Deno Deploy / Vercel Edge)
- وقتی فقط چند تا عملیات مشخص میخواهی، نه کل API
- وقتی حجم باندل و cold start برایت مهم است (ترافیک پیک)
هیچکدام نیست؟ نرو سراغ ساختن SDK شخصی!
امنیت: کلید خصوصی را کجا بگذاریم؟
این نکته در هیجان حل باگ گم میشود: PRIVATE_KEY_PEM را به عنوان env معمولی نگذار. در Workers بزن:
تا مقدار از stdin برود و هیچوقت در ریپو ننشیند.
و حواست به \n باشد! رایجترین دلیل خطای invalid_grant این است که کلید با \n متنی (دو کاراکتر) ذخیره شده، نه newline واقعی. اگر invalid_grant گرفتی و همهچیز درست به نظر میرسد، اول این را ببین:
اگر \\n دیدی به جای خط جدید، مشکل همین است.
چرا این فقط قصهی Firebase نیست
اگر Next.js را روی Vercel یا Cloudflare دیپلوی میکنی، همین الگو تکرار میشود: @supabase/supabase-js سبک و edge-safe است، ولی stripe با node:crypto کلنجار دارد و هر چیزی که به fs یا child_process دست بزند، روی edge میمیرد. قانون هم همیشه یکی است: اول REST API را نگاه کن، بعد برو سراغ SDK. کد HTTP خالص نه باندل حجیم دارد، نه cold-start، نه سوپرایز رانتایم — و بین Workers و Vercel و Deno قابل حمل است.
چکلیست نهایی قبل از دیپلوی
- [ ]
pnpm why protobufjsرا زدهام و هیچ مسیر edgeای بهfirebase-adminنمیرسد - [ ] کد حتماً روی
wrangler devتست شده، نه فقط روی Node - [ ] توکن دسترسی در scope ماژول کش میشود، نه داخل هر request
- [ ] هر
commitشرطcurrentDocumentدارد (وگرنه نوشتن کور است) - [ ] کلید خصوصی با
wrangler secret putذخیره شده، نه در env معمولی و نه در ریپو - [ ] خطای
invalid_grantرا با چاپ ۴۰ کاراکتر اول کلید دیباگ کردهام
سوالات پرتکرار
Q: آیا firebase-admin روی Node هم مشکل دارد؟
نه. این محدودیت مخصوص workerd است. روی Node، Vercel Serverless یا هر جای دیگری که eval آزاد است، new Function کار میکند و SDK سالم بالا میآید. یعنی این یک باگ سراسری نیست — یک ناسازگاری مشخص بین «کتابخانهی Nodeمحور» و «runtime ایزوله» است.
Q: چرا برای تایید توکن از کتابخانهی آماده استفاده کردیم ولی برای بقیه نه؟
چون تایید توکن کار یکبارموقته با پیچیدگی کم است: گرفتن کلیدهای عمومی، بررسی exp و aud و امضا. کتابخانهی firebase-auth-cloudflare-workers همین را با صفر وابستگی و فقط Web API انجام میدهد. ولی تراکنش Firestore منطق دامنهای دارد (شرط اتمیک، rollback، retry) و کتابخانهای که دقیقاً همان را روی REST بدهد نداریم — پس خودمان مینویسیم.
Q: تراکنش دستی REST چطور retry و خطا را هندل میکند؟
commit میتواند با کد ABORTED برگردد؛ آن وقت کل تراکنش را از beginTransaction دوباره شروع کن، نه اینکه فقط commit را تکرار کنی — نشست تراکنش قبلی دیگر معتبر نیست. برای محدود کردن تعداد تلاشها یک شمارندهی ساده بگذار و بعد از چند تلاش ناموفق خطا بده به کاربر، چون در کد سریال معمولاً رقابت واقعی است و بینهایت retry یعنی درخواست گران روی edge.
اگر به این مسیر علاقه داری، برای دیدن اینکه چطور لایهی کش Next.js را کنار همین runtimeها تنظیم میکنم، نوشتهی Next.js 15 Cache و PPR را بخوان. نمونهی عملی دیپلوی روی Vercel را هم در پروژهی DashView میتوانی ببینی؛ اگر سوال فنی داری هم دربارهی من را ببین.
منابع
- مستندات Web Standards در Cloudflare Workers — چرا
evalوnew Functionروی workerd قفل است - مستندات REST کلود Firestore — مرجع
beginTransaction،commitوcurrentDocument - مخزن firebase-auth-cloudflare-workers — کتابخانهی صفر-وابستگی تایید توکن با Web API
- ایشوی OpenNext Cloudflare: کرش protobufjs روی Workers — همان خطای
EvalErrorاز سمت کامیونیتی - گزارش میدانی: جایگزینی firebase-admin با fetch و WebCrypto