چرا 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" — و ناگهان:
نه فلگی دارد، نه با 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 قابل حمل است.
برای دیدن اینکه چطور لایهی کش Next.js را کنار همین runtimeها تنظیم میکنم، نوشتهی Next.js 15 Cache و PPR را بخوان. نمونهی عملی دیپلوی روی Vercel را هم در پروژهی DashView میتوانی ببینی؛ اگر سوال فنی داری هم دربارهی من را ببین.