Cloudflare এর হাতেখড়ি (পর্ব ১৪) – পূর্ণাঙ্গ প্রজেক্ট: Pages + Workers + R2 + Firebase

Cloudflare পূর্ণাঙ্গ প্রজেক্ট Pages Workers R2 Firebase

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:

  1. উপরের Worker কোড নিজের প্রজেক্টে বসাও, একটা R2 bucket আর KV namespace বেঁধে দাও।
  2. bookService.js ও React কম্পোনেন্ট নিজে বানাও, লোকালে টেস্ট করো।
  3. উপরের GitHub Actions workflow দিয়ে Worker আর Pages দুটোই একসাথে ডেপ্লয় করো।
  4. নিজের একটা বই যোগ করে দেখো — ছবি R2-তে গেলো কিনা, তথ্য RTDB-তে গেলো কিনা, আর ফ্রন্টএন্ডে লাইভ দেখা যাচ্ছে কিনা যাচাই করো।

সিরিজ শেষ — দুই সিরিজ মিলিয়ে যা শিখলাম

Firebase এর হাতেখড়ি (১২ পর্ব) আর Cloudflare এর হাতেখড়ি (১৪ পর্ব) — এই দুই সিরিজ মিলিয়ে এখন একটা সম্পূর্ণ, প্রোডাকশন-রেডি ওয়েব অ্যাপ বানানোর সবগুলো টুকরো হাতে আছে — Auth, Database, Storage, Hosting, সার্ভারলেস কম্পিউট, CI/CD, DNS, ক্যাশিং, সিকিউরিটি, আর মনিটরিং। এখন থেকে সবচেয়ে ভালো পরবর্তী ধাপ হলো নিজের কোনো আইডিয়া নিয়ে এই পুরো স্ট্যাক ব্যবহার করে সত্যিকারের একটা প্রজেক্ট দাঁড় করানো।

একটি মন্তব্য পোস্ট করুন

নবীনতর পূর্বতন

যোগাযোগ ফর্ম