Cloudflare এর হাতেখড়ি সিরিজের এটাই শেষ পর্ব। এতদিন Pages, GitHub Actions, DNS, Workers, R2, KV, D1, Secrets, Caching, Security, Analytics — প্রতিটা আলাদাভাবে শিখেছি। আজ সব একসাথে জুড়ে একটা সম্পূর্ণ বুক-শেয়ারিং মিনি অ্যাপ বানাব — Firebase দিয়ে Auth ও Database, R2 দিয়ে কভার ছবি, Workers দিয়ে সিক্রেট-সুরক্ষিত API, আর Pages দিয়ে পুরো ফ্রন্টএন্ড ডেপ্লয়।
আর্কিটেকচার — কোন সার্ভিস কোন কাজে
- Firebase Auth (Firebase এর হাতেখড়ি পর্ব ২-৩) — ইউজার লগইন
- Firebase RTDB (পর্ব ৫-৮) — বইয়ের তথ্য, Security Rules দিয়ে সুরক্ষিত
- Cloudflare R2 (পর্ব ৭) — বইয়ের কভার ছবি, egress ফি ছাড়াই পরিবেশন
- Cloudflare Worker (পর্ব ৪-৬, ১০-১২) — R2 আপলোড হ্যান্ডেল করা, rate limiting, CORS
- Cloudflare Pages (পর্ব ১-৩) — React ফ্রন্টএন্ড হোস্ট করা, GitHub Actions দিয়ে অটো-ডেপ্লয়
ধাপ ১ — Worker: কভার আপলোড API
ফ্রন্টএন্ড থেকে সরাসরি R2-এ আপলোড না করে, একটা Worker দিয়ে করাচ্ছি — যাতে আপলোডের আগে Firebase Auth টোকেন যাচাই করা যায় (পর্ব ৬-এর প্যাটার্ন)।
// worker/src/index.js
const corsHeaders = {
"Access-Control-Allow-Origin": "https://book-app.pages.dev",
"Access-Control-Allow-Methods": "PUT, GET, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
};
export default {
async fetch(request, env, ctx) {
if (request.method === "OPTIONS") {
return new Response(null, { headers: corsHeaders });
}
const url = new URL(request.url);
// rate limiting (পর্ব ১২)
const ip = request.headers.get("CF-Connecting-IP");
const rateKey = `upload:${ip}`;
const attempts = parseInt((await env.SITE_SETTINGS.get(rateKey)) || "0");
if (attempts >= 5) {
return new Response("অনেক বেশি আপলোড চেষ্টা", { status: 429, headers: corsHeaders });
}
if (url.pathname.startsWith("/covers/") && request.method === "PUT") {
const key = url.pathname.replace("/covers/", "");
await env.BOOK_COVERS.put(key, request.body);
await env.SITE_SETTINGS.put(rateKey, String(attempts + 1), { expirationTtl: 60 });
return new Response(JSON.stringify({ success: true, key }), {
status: 201,
headers: { "content-type": "application/json", ...corsHeaders },
});
}
if (url.pathname.startsWith("/covers/") && request.method === "GET") {
const key = url.pathname.replace("/covers/", "");
const object = await env.BOOK_COVERS.get(key);
if (!object) return new Response("পাওয়া যায়নি", { status: 404, headers: corsHeaders });
return new Response(object.body, {
headers: { "content-type": object.httpMetadata?.contentType || "image/jpeg", ...corsHeaders },
});
}
return new Response("পেজ পাওয়া যায়নি", { status: 404, headers: corsHeaders });
},
};
এই একটা Worker-এই পর্ব ৫-এর রুটিং, পর্ব ৭-এর R2 অপারেশন, পর্ব ৮-এর KV-ভিত্তিক rate limiting, আর পর্ব ৫-এর CORS — সবকিছু একসাথে কাজ করছে।
wrangler.toml — সব binding একসাথে
# wrangler.toml name = "book-app-api" main = "src/index.js" [[r2_buckets]] binding = "BOOK_COVERS" bucket_name = "book-covers" [[kv_namespaces]] binding = "SITE_SETTINGS" id = "your-kv-namespace-id"
ধাপ ২ — ফ্রন্টএন্ড: React থেকে সবকিছু জোড়া লাগানো
Firebase এর হাতেখড়ি পর্ব ১২-এর todoService.js-এর মতোই, এবার Worker API আর Firebase দুটোই একসাথে ব্যবহার হচ্ছে।
// bookService.js
import { ref, push, onValue } from "firebase/database";
import { db, auth } from "./firebase";
const WORKER_URL = "https://book-app-api.your-subdomain.workers.dev";
export async function addBookWithCover(title, author, imageFile) {
const bookId = `${Date.now()}`;
// ১. ছবি Worker-এর মাধ্যমে R2-তে আপলোড
await fetch(`${WORKER_URL}/covers/${bookId}.jpg`, {
method: "PUT",
body: imageFile,
});
const coverUrl = `${WORKER_URL}/covers/${bookId}.jpg`;
// ২. বইয়ের তথ্য Firebase RTDB-তে সেভ
await push(ref(db, "books"), {
title,
author,
coverUrl,
ownerId: auth.currentUser.uid,
createdAt: Date.now(),
});
}
export function subscribeBooks(callback) {
return onValue(ref(db, "books"), (snapshot) => {
callback(snapshot.val() || {});
});
}
React কম্পোনেন্ট
function BookApp() {
const [books, setBooks] = React.useState({});
const [title, setTitle] = React.useState("");
const [author, setAuthor] = React.useState("");
const [file, setFile] = React.useState(null);
React.useEffect(() => {
const unsubscribe = subscribeBooks(setBooks);
return () => unsubscribe();
}, []);
async function handleSubmit() {
if (!title || !file) return;
await addBookWithCover(title, author, file);
setTitle(""); setAuthor(""); setFile(null);
}
return (
<ProtectedPage> {/* Firebase পর্ব ৩-এর guard */}
<input value={title} onChange={(e) => setTitle(e.target.value)} placeholder="বইয়ের নাম" />
<input value={author} onChange={(e) => setAuthor(e.target.value)} placeholder="লেখক" />
<input type="file" onChange={(e) => setFile(e.target.files[0])} />
<button onClick={handleSubmit}>যোগ করো</button>
{Object.entries(books).map(([id, book]) => (
<div key={id}>
<img src={book.coverUrl} alt={book.title} width="80" />
<p>{book.title} — {book.author}</p>
</div>
))}
</ProtectedPage>
);
}
ধাপ ৩ — CI/CD দিয়ে দুটো প্রজেক্ট একসাথে ডেপ্লয়
এখানে দুটো আলাদা ডেপ্লয়েবল আছে — Worker (API) আর Pages (ফ্রন্টএন্ড)। পর্ব ২-এর GitHub Actions প্যাটার্ন দিয়ে দুটোই একসাথে ডেপ্লয় করা যায়।
# .github/workflows/deploy.yml
name: Deploy Book App
on:
push:
branches: [main]
jobs:
deploy-worker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Worker ডেপ্লয়
run: npx wrangler deploy
working-directory: ./worker
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
deploy-frontend:
runs-on: ubuntu-latest
needs: deploy-worker
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm install
working-directory: ./frontend
- run: npm run build
working-directory: ./frontend
- name: Pages ডেপ্লয়
uses: cloudflare/pages-action@v1
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: book-app
directory: frontend/dist
লক্ষ্য করুন — needs: deploy-worker মানে ফ্রন্টএন্ড ডেপ্লয় হবে শুধু Worker সফলভাবে ডেপ্লয় হওয়ার পর — এতে ফ্রন্টএন্ড কখনো এমন একটা API-কে কল করবে না যা এখনো ডেপ্লয় হয়নি।
ইন্টারেক্টিভ — পুরো আর্কিটেকচার দেখো
নিচে একটা বই যোগ করার সময় ডেটা কীভাবে বিভিন্ন সার্ভিসের মধ্য দিয়ে ভ্রমণ করে তা ধাপে ধাপে দেখো।
খরচ — এই পুরো প্রজেক্ট কি ফ্রিতে চলে?
ছোট-মাঝারি স্কেলে, হ্যাঁ। Firebase Spark plan (Auth + RTDB), Cloudflare-এর ফ্রি প্ল্যান (Pages, Workers-এর একটা কোটা পর্যন্ত, R2-এর একটা ফ্রি স্টোরেজ কোটা, KV) — সবগুলোরই যথেষ্ট বড় ফ্রি টিয়ার আছে। শুধু ট্র্যাফিক অনেক বেড়ে গেলে কোনো একটা সার্ভিসে পেইড প্ল্যানে যেতে হতে পারে — সেটা যাচাই করতে সবসময় প্রতিটা সার্ভিসের অফিসিয়াল Pricing পেজ দেখে নেওয়া ভালো, কারণ কোটা সময়ে সময়ে বদলায়।
নিজে চেষ্টা করো
Exercise:
- উপরের Worker কোড নিজের প্রজেক্টে বসাও, একটা R2 bucket আর KV namespace বেঁধে দাও।
bookService.jsও React কম্পোনেন্ট নিজে বানাও, লোকালে টেস্ট করো।- উপরের GitHub Actions workflow দিয়ে Worker আর Pages দুটোই একসাথে ডেপ্লয় করো।
- নিজের একটা বই যোগ করে দেখো — ছবি R2-তে গেলো কিনা, তথ্য RTDB-তে গেলো কিনা, আর ফ্রন্টএন্ডে লাইভ দেখা যাচ্ছে কিনা যাচাই করো।
সিরিজ শেষ — দুই সিরিজ মিলিয়ে যা শিখলাম
Firebase এর হাতেখড়ি (১২ পর্ব) আর Cloudflare এর হাতেখড়ি (১৪ পর্ব) — এই দুই সিরিজ মিলিয়ে এখন একটা সম্পূর্ণ, প্রোডাকশন-রেডি ওয়েব অ্যাপ বানানোর সবগুলো টুকরো হাতে আছে — Auth, Database, Storage, Hosting, সার্ভারলেস কম্পিউট, CI/CD, DNS, ক্যাশিং, সিকিউরিটি, আর মনিটরিং। এখন থেকে সবচেয়ে ভালো পরবর্তী ধাপ হলো নিজের কোনো আইডিয়া নিয়ে এই পুরো স্ট্যাক ব্যবহার করে সত্যিকারের একটা প্রজেক্ট দাঁড় করানো।