Imagine you’re building a house. You could buy land, lay the foundation, install plumbing, wire electricity, build walls, and so on. Or you could move into a fully-equipped building where infrastructure is already handled, and you just focus on decorating and living.

Cloudflare is that fully-equipped building for the internet. It’s a platform that handles the hard infrastructure problems (speed, security, scaling) so you can focus on building your SaaS product.

What makes Cloudflare special for SaaS?

  • Global by default: Your code runs on 300+ servers worldwide
  • Zero cold starts: Unlike AWS Lambda, your functions are always warm
  • Generous free tier: Build and test without spending a dime
  • Simple pricing: Pay only for what you use, no surprise bills
  • Integrated ecosystem: Database, storage, caching - all in one place

Part 1: Understanding Cloudflare - The Foundation

What is Cloudflare?

Think of Cloudflare as a protective shield and speed booster that sits between your users and your application.

The Simple Story:

  1. User in Tokyo visits your SaaS app
  2. Instead of traveling to your server in New York (slow!), the request hits Cloudflare’s Tokyo datacenter
  3. Cloudflare either serves cached content instantly or routes to your nearest server
  4. Response travels back to Tokyo in milliseconds

Three Core Pieces:

  1. CDN (Content Delivery Network): Caches your static files globally
  2. Security: Protects against DDoS attacks, bots, and threats
  3. Compute Platform: Runs your backend code at the edge

What is “The Edge”?

“The Edge” means running code close to your users, not in a single datacenter.

Traditional Setup:

User in Brazil → 200ms → Your Server in Virginia → 200ms → User in Brazil
Total: 400ms+ latency

Edge Computing:

User in Brazil → 10ms → Cloudflare São Paulo → 10ms → User in Brazil
Total: 20ms latency

Why this matters for SaaS: Users expect instant responses. A 100ms delay can reduce conversions by 7%.


Part 2: Wrangler - Your Command Center

What is Wrangler?

Wrangler is Cloudflare’s command-line tool - think of it as your control panel for deploying and managing everything on Cloudflare.

Analogy: If Cloudflare is a spaceship, Wrangler is your mission control console.

Installing Wrangler

# Install Node.js first (if you haven't)
# Download from nodejs.org

# Install Wrangler globally
npm install -g wrangler

# Verify installation
wrangler --version

# Login to Cloudflare
wrangler login

This opens your browser to authenticate. Once done, Wrangler can deploy your projects.

Your First Wrangler Project

# Create a new Workers project
wrangler init my-saas-api

# You'll be asked:
# - Template? Choose "Hello World"
# - TypeScript? Choose "Yes" (recommended)
# - Git repository? Choose "Yes"

cd my-saas-api

What just happened? Wrangler created a project structure:

my-saas-api/
├── src/
│   └── index.ts          # Your code goes here
├── wrangler.toml         # Configuration file
├── package.json          # Dependencies
└── tsconfig.json         # TypeScript config

Key Wrangler Commands

# Develop locally with hot reload
wrangler dev

# Deploy to production
wrangler deploy

# View logs in real-time
wrangler tail

# Manage secrets (API keys, etc.)
wrangler secret put API_KEY

# List your Workers
wrangler deployments list

Part 3: Cloudflare Services - Your SaaS Toolkit

1. Workers - Your Backend Brain

What it is: JavaScript/TypeScript code that runs on Cloudflare’s edge network.

The Simple Explanation: Workers are like tiny, fast servers that exist everywhere at once. Instead of one server in one location, your code runs on 300+ servers worldwide.

How it helps your SaaS:

  • Handle API requests
  • Process payments
  • Authenticate users
  • Transform data
  • Route requests

Real Example - Authentication API:

// src/index.ts
export default {
  async fetch(request: Request, env: Env): Promise {
    const url = new URL(request.url);
    
    // Route: POST /api/login
    if (url.pathname === '/api/login' && request.method === 'POST') {
      const { email, password } = await request.json();
      
      // Verify credentials (simplified)
      if (email === 'user@example.com' && password === 'secret') {
        // Generate token (you'd use JWT in production)
        const token = btoa(`${email}:${Date.now()}`);
        
        return new Response(JSON.stringify({ 
          success: true, 
          token 
        }), {
          headers: { 'Content-Type': 'application/json' }
        });
      }
      
      return new Response(JSON.stringify({ 
        success: false, 
        error: 'Invalid credentials' 
      }), {
        status: 401,
        headers: { 'Content-Type': 'application/json' }
      });
    }
    
    return new Response('Not Found', { status: 404 });
  }
};

Deploy it:

wrangler deploy

Your API is now live at: https://my-saas-api.your-subdomain.workers.dev

Pricing:

  • Free tier: 100,000 requests/day
  • Paid: $5/month for 10 million requests

2. Workers KV - Your Fast Key-Value Store

What it is: A global, low-latency key-value database. Think of it as a giant dictionary that exists everywhere.

The Simple Explanation: Imagine a magical filing cabinet where you can store notes (values) with labels (keys), and access them instantly from anywhere in the world.

How it helps your SaaS:

  • Store user sessions
  • Cache API responses
  • Save feature flags
  • Store configuration

Perfect for: Data you read often but write rarely.

Real Example - Session Storage:

// wrangler.toml
[[kv_namespaces]]
binding = "SESSIONS"
id = "your-namespace-id"

// src/index.ts
interface Env {
  SESSIONS: KVNamespace;
}

export default {
  async fetch(request: Request, env: Env): Promise {
    const url = new URL(request.url);
    
    // Create session
    if (url.pathname === '/api/session/create') {
      const sessionId = crypto.randomUUID();
      const sessionData = {
        userId: '12345',
        createdAt: Date.now(),
        expiresAt: Date.now() + (24 * 60 * 60 * 1000) // 24 hours
      };
      
      // Store in KV (expires in 24 hours)
      await env.SESSIONS.put(
        sessionId, 
        JSON.stringify(sessionData),
        { expirationTtl: 86400 }
      );
      
      return new Response(JSON.stringify({ sessionId }));
    }
    
    // Retrieve session
    if (url.pathname.startsWith('/api/session/')) {
      const sessionId = url.pathname.split('/')[3];
      const data = await env.SESSIONS.get(sessionId, 'json');
      
      if (!data) {
        return new Response('Session not found', { status: 404 });
      }
      
      return new Response(JSON.stringify(data));
    }
    
    return new Response('Not Found', { status: 404 });
  }
};

Create KV Namespace:

# Create namespace
wrangler kv:namespace create "SESSIONS"

# Copy the ID into wrangler.toml

Pricing:

  • Free tier: 100,000 reads/day, 1,000 writes/day, 1 GB storage
  • Paid: $0.50 per million reads, $5 per million writes

When to use KV:

  • ✅ User sessions
  • ✅ Cached API responses
  • ✅ Feature flags
  • ❌ Frequently changing data (use D1 instead)
  • ❌ Complex queries (use D1 instead)

3. D1 - Your SQL Database

What it is: A full SQLite database that runs at the edge.

The Simple Explanation: This is a traditional database (like MySQL or PostgreSQL) but distributed globally. You write SQL queries, get structured data.

How it helps your SaaS:

  • Store user accounts
  • Save product data
  • Track orders and transactions
  • Store complex relationships

Real Example - User Management System:

# Create database
wrangler d1 create saas-users-db

# Add to wrangler.toml
# wrangler.toml
[[d1_databases]]
binding = "DB"
database_name = "saas-users-db"
database_id = "your-database-id"
-- schema.sql
CREATE TABLE users (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  email TEXT UNIQUE NOT NULL,
  name TEXT NOT NULL,
  plan TEXT DEFAULT 'free',
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE api_keys (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  user_id INTEGER NOT NULL,
  key TEXT UNIQUE NOT NULL,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (user_id) REFERENCES users(id)
);
# Apply schema
wrangler d1 execute saas-users-db --file=./schema.sql
// src/index.ts
interface Env {
  DB: D1Database;
}

export default {
  async fetch(request: Request, env: Env): Promise {
    const url = new URL(request.url);
    
    // Create user
    if (url.pathname === '/api/users' && request.method === 'POST') {
      const { email, name } = await request.json();
      
      try {
        const result = await env.DB.prepare(
          'INSERT INTO users (email, name) VALUES (?, ?)'
        ).bind(email, name).run();
        
        return new Response(JSON.stringify({ 
          success: true, 
          userId: result.meta.last_row_id 
        }));
      } catch (error) {
        return new Response(JSON.stringify({ 
          success: false, 
          error: 'User already exists' 
        }), { status: 400 });
      }
    }
    
    // Get user
    if (url.pathname.startsWith('/api/users/')) {
      const userId = url.pathname.split('/')[3];
      
      const { results } = await env.DB.prepare(
        'SELECT * FROM users WHERE id = ?'
      ).bind(userId).all();
      
      if (results.length === 0) {
        return new Response('User not found', { status: 404 });
      }
      
      return new Response(JSON.stringify(results[0]));
    }
    
    // List all users
    if (url.pathname === '/api/users' && request.method === 'GET') {
      const { results } = await env.DB.prepare(
        'SELECT id, email, name, plan, created_at FROM users ORDER BY created_at DESC'
      ).all();
      
      return new Response(JSON.stringify(results));
    }
    
    return new Response('Not Found', { status: 404 });
  }
};

Pricing:

  • Free tier: 5 GB storage, 5 million reads/day
  • Paid: $5/month per 10 GB

When to use D1:

  • ✅ User accounts and profiles
  • ✅ Product catalogs
  • ✅ Orders and transactions
  • ✅ Any data with relationships
  • ✅ Data you need to query flexibly

4. R2 - Your File Storage

What it is: Object storage like AWS S3, but with zero egress fees.

The Simple Explanation: A massive hard drive in the cloud where you store files (images, videos, PDFs, backups). Unlike competitors, downloading files is free.

How it helps your SaaS:

  • Store user uploads (profile pictures, documents)
  • Host static assets (images, videos)
  • Store backups
  • Serve downloadable files

Real Example - File Upload Service:

# wrangler.toml
[[r2_buckets]]
binding = "BUCKET"
bucket_name = "saas-user-uploads"
# Create bucket
wrangler r2 bucket create saas-user-uploads
// src/index.ts
interface Env {
  BUCKET: R2Bucket;
}

export default {
  async fetch(request: Request, env: Env): Promise {
    const url = new URL(request.url);
    
    // Upload file
    if (url.pathname === '/api/upload' && request.method === 'POST') {
      const formData = await request.formData();
      const file = formData.get('file') as File;
      
      if (!file) {
        return new Response('No file provided', { status: 400 });
      }
      
      // Generate unique filename
      const filename = `${Date.now()}-${file.name}`;
      
      // Upload to R2
      await env.BUCKET.put(filename, file.stream(), {
        httpMetadata: {
          contentType: file.type,
        }
      });
      
      return new Response(JSON.stringify({ 
        success: true,
        filename,
        url: `https://your-domain.com/api/files/${filename}`
      }));
    }
    
    // Download file
    if (url.pathname.startsWith('/api/files/')) {
      const filename = url.pathname.split('/')[3];
      const object = await env.BUCKET.get(filename);
      
      if (!object) {
        return new Response('File not found', { status: 404 });
      }
      
      return new Response(object.body, {
        headers: {
          'Content-Type': object.httpMetadata?.contentType || 'application/octet-stream',
          'Content-Length': object.size.toString(),
        }
      });
    }
    
    // List files
    if (url.pathname === '/api/files' && request.method === 'GET') {
      const listed = await env.BUCKET.list();
      const files = listed.objects.map(obj => ({
        name: obj.key,
        size: obj.size,
        uploaded: obj.uploaded,
      }));
      
      return new Response(JSON.stringify(files));
    }
    
    // Delete file
    if (url.pathname.startsWith('/api/files/') && request.method === 'DELETE') {
      const filename = url.pathname.split('/')[3];
      await env.BUCKET.delete(filename);
      
      return new Response(JSON.stringify({ success: true }));
    }
    
    return new Response('Not Found', { status: 404 });
  }
};

Pricing:

  • Free tier: 10 GB storage, 1 million reads/month
  • Paid: $0.015 per GB/month storage
  • Zero egress fees (this is huge - AWS charges $0.09/GB!)

When to use R2:

  • ✅ User uploads (images, documents, videos)
  • ✅ Static assets for your app
  • ✅ Backups and archives
  • ✅ Any files users download frequently

5. Durable Objects - Your Stateful Coordination

What it is: Long-lived objects that maintain state and coordinate real-time activities.

The Simple Explanation: Imagine each user in a multiplayer game needs a “room” where their game state lives. Durable Objects are those rooms - they exist continuously, remember their state, and multiple users can interact with the same object in real-time.

How it helps your SaaS:

  • Real-time collaboration (like Google Docs)
  • Chat systems
  • Live dashboards
  • Multiplayer features
  • Rate limiting per user

Real Example - Real-time Collaborative Counter:

// src/index.ts
export class Counter {
  state: DurableObjectState;
  value: number;
  
  constructor(state: DurableObjectState) {
    this.state = state;
    this.value = 0;
  }
  
  async initialize() {
    // Load persisted value
    let stored = await this.state.storage.get('value');
    this.value = stored || 0;
  }
  
  async fetch(request: Request) {
    await this.initialize();
    
    const url = new URL(request.url);
    
    // Increment counter
    if (url.pathname === '/increment') {
      this.value++;
      await this.state.storage.put('value', this.value);
      return new Response(JSON.stringify({ value: this.value }));
    }
    
    // Get current value
    if (url.pathname === '/value') {
      return new Response(JSON.stringify({ value: this.value }));
    }
    
    // Reset counter
    if (url.pathname === '/reset') {
      this.value = 0;
      await this.state.storage.put('value', this.value);
      return new Response(JSON.stringify({ value: this.value }));
    }
    
    return new Response('Not Found', { status: 404 });
  }
}

// Main worker
interface Env {
  COUNTER: DurableObjectNamespace;
}

export default {
  async fetch(request: Request, env: Env): Promise {
    // Get or create a Durable Object instance
    const id = env.COUNTER.idFromName('global-counter');
    const stub = env.COUNTER.get(id);
    
    // Forward request to the Durable Object
    return stub.fetch(request);
  }
};
# wrangler.toml
[[durable_objects.bindings]]
name = "COUNTER"
class_name = "Counter"

[[migrations]]
tag = "v1"
new_classes = ["Counter"]

Real-World Use Case - Chat Room:

export class ChatRoom {
  state: DurableObjectState;
  sessions: Set;
  
  constructor(state: DurableObjectState) {
    this.state = state;
    this.sessions = new Set();
  }
  
  async fetch(request: Request) {
    // WebSocket upgrade for real-time chat
    if (request.headers.get('Upgrade') === 'websocket') {
      const pair = new WebSocketPair();
      const [client, server] = Object.values(pair);
      
      // Handle new connection
      server.accept();
      this.sessions.add(server);
      
      server.addEventListener('message', (event) => {
        // Broadcast message to all users in room
        const message = JSON.stringify({
          timestamp: Date.now(),
          text: event.data
        });
        
        this.sessions.forEach(session => {
          session.send(message);
        });
      });
      
      server.addEventListener('close', () => {
        this.sessions.delete(server);
      });
      
      return new Response(null, {
        status: 101,
        webSocket: client
      });
    }
    
    return new Response('Expected WebSocket', { status: 400 });
  }
}

Pricing:

  • Free tier: First 1 million requests free
  • Paid: $0.15 per million requests

When to use Durable Objects:

  • ✅ Real-time collaboration
  • ✅ WebSocket connections
  • ✅ Stateful coordination
  • ✅ Per-user rate limiting
  • ❌ Simple data storage (use KV or D1)

6. Queues - Your Background Job System

What it is: A message queue for processing tasks asynchronously.

The Simple Explanation: Imagine a to-do list for your app. When users do something (upload a video, send an email), instead of making them wait, you add it to the queue and process it in the background.

How it helps your SaaS:

  • Send emails without slowing down requests
  • Process uploaded videos
  • Generate reports
  • Batch operations

Real Example - Email Queue:

# wrangler.toml
[[queues.producers]]
binding = "EMAIL_QUEUE"
queue = "email-queue"

[[queues.consumers]]
queue = "email-queue"
max_batch_size = 10
max_batch_timeout = 30
// src/index.ts
interface Env {
  EMAIL_QUEUE: Queue;
}

// Producer: Add jobs to queue
export default {
  async fetch(request: Request, env: Env): Promise {
    if (request.url.endsWith('/api/send-welcome-email')) {
      const { userId, email, name } = await request.json();
      
      // Add to queue instead of sending immediately
      await env.EMAIL_QUEUE.send({
        type: 'welcome',
        userId,
        email,
        name,
        timestamp: Date.now()
      });
      
      return new Response(JSON.stringify({ 
        success: true,
        message: 'Email queued for sending'
      }));
    }
    
    return new Response('Not Found', { status: 404 });
  },
  
  // Consumer: Process queue messages
  async queue(batch: MessageBatch, env: Env): Promise {
    for (const message of batch.messages) {
      const { type, email, name } = message.body;
      
      if (type === 'welcome') {
        // Send email via API (e.g., SendGrid, Mailgun)
        await fetch('https://api.sendgrid.com/v3/mail/send', {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${env.SENDGRID_KEY}`,
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({
            personalizations: [{ to: [{ email }] }],
            from: { email: 'hello@yoursaas.com' },
            subject: `Welcome to our SaaS, ${name}!`,
            content: [{
              type: 'text/plain',
              value: `Hi ${name}, welcome aboard!`
            }]
          })
        });
        
        console.log(`Welcome email sent to ${email}`);
      }
      
      // Acknowledge message (remove from queue)
      message.ack();
    }
  }
};

Pricing:

  • Free tier: First 1 million operations/month
  • Paid: $0.40 per million operations

When to use Queues:

  • ✅ Sending emails
  • ✅ Processing uploads
  • ✅ Generating reports
  • ✅ Any long-running task
  • ✅ Batch operations

7. Workers AI - Your Built-in AI Models

What it is: Run AI models directly in your Workers without managing infrastructure.

The Simple Explanation: Instead of paying OpenAI or building your own AI infrastructure, run AI models right in your Cloudflare Workers.

How it helps your SaaS:

  • Generate text summaries
  • Classify content
  • Translate languages
  • Generate images
  • Analyze sentiment

Real Example - Content Moderation:

// src/index.ts
interface Env {
  AI: any;
}

export default {
  async fetch(request: Request, env: Env): Promise {
    if (request.url.endsWith('/api/moderate')) {
      const { text } = await request.json();
      
      // Use AI model to classify content
      const response = await env.AI.run(
        '@cf/huggingface/distilbert-sst-2-int8',
        { text }
      );
      
      // Response: { label: 'POSITIVE' or 'NEGATIVE', score: 0.99 }
      const isToxic = response.label === 'NEGATIVE' && response.score > 0.8;
      
      return new Response(JSON.stringify({
        allowed: !isToxic,
        reason: isToxic ? 'Content flagged as potentially toxic' : 'OK'
      }));
    }
    
    if (request.url.endsWith('/api/summarize')) {
      const { text } = await request.json();
      
      const summary = await env.AI.run(
        '@cf/facebook/bart-large-cnn',
        { 
          input_text: text,
          max_length: 100
        }
      );
      
      return new Response(JSON.stringify({ summary }));
    }
    
    return new Response('Not Found', { status: 404 });
  }
};

Available Models:

  • Text generation: Llama 2, Mistral
  • Text classification: BERT, DistilBERT
  • Translation: m2m100, NLLB
  • Image generation: Stable Diffusion
  • Embeddings: BGE, Text2Vec

Pricing:

  • Free tier: 10,000 neurons per day (1 neuron ≈ 1 model inference)
  • Paid: $0.011 per 1,000 neurons

8. Cache API - Your Speed Booster

What it is: Automatically cache responses to make repeat requests instant.

The Simple Explanation: If 1,000 users request the same data, why fetch it 1,000 times? Cache it once, serve it 1,000 times instantly.

How it helps your SaaS:

  • Speed up API responses
  • Reduce database load
  • Lower costs
  • Improve user experience

Real Example - Cached API Responses:

export default {
  async fetch(request: Request, env: Env): Promise {
    const url = new URL(request.url);
    
    // Cache GET requests only
    if (request.method !== 'GET') {
      return this.handleRequest(request, env);
    }
    
    // Try to get from cache first
    const cache = caches.default;
    let response = await cache.match(request);
    
    if (response) {
      console.log('Cache HIT');
      return response;
    }
    
    console.log('Cache MISS');
    
    // Not in cache, fetch fresh data
    response = await this.handleRequest(request, env);
    
    // Cache for 5 minutes
    const cacheResponse = response.clone();
    response = new Response(cacheResponse.body, cacheResponse);
    response.headers.set('Cache-Control', 'public, max-age=300');
    
    // Store in cache
    await cache.put(request, response.clone());
    
    return response;
  },
  
  async handleRequest(request: Request, env: Env): Promise {
    const url = new URL(request.url);
    
    if (url.pathname === '/api/products') {
      // Expensive database query
      const { results } = await env.DB.prepare(
        'SELECT * FROM products WHERE active = 1'
      ).all();
      
      return new Response(JSON.stringify(results), {
        headers: { 'Content-Type': 'application/json' }
      });
    }
    
    return new Response('Not Found', { status: 404 });
  }
};

Cache Strategies:

// Strategy 1: Cache-first (serve stale while revalidating)
const response = await cache.match(request);
if (response) {
  // Return cached version immediately
  // Refresh cache in background
  event.waitUntil(refreshCache(request, env));
  return response;
}

// Strategy 2: Network-first (fresh data priority)
try {
  const response = await fetchFresh(request, env);
  await cache.put(request, response.clone());
  return response;
} catch (error) {
  // Network failed, try cache
  const cached = await cache.match(request);
  if (cached) return cached;
  throw error;
}

// Strategy 3: Cache with TTL
response.headers.set('Cache-Control', 'public, max-age=3600'); // 1 hour

Pricing:

  • Included free with Workers

When to use Cache:

  • ✅ Public API responses
  • ✅ Product catalogs
  • ✅ Static content
  • ❌ User-specific data
  • ❌ Rapidly changing data

9. Stream - Your Video Platform

What it is: Upload, encode, store, and stream videos without managing infrastructure.

The Simple Explanation: YouTube’s infrastructure as a service. Upload videos, they get optimized automatically, users stream them smoothly on any device.

How it helps your SaaS:

  • Course platforms (video lessons)
  • User-generated content
  • Marketing videos
  • Webinar recordings

Real Example - Video Upload & Streaming:

interface Env {
  STREAM_API_TOKEN: string;
  STREAM_ACCOUNT_ID: string;
}

export default {
  async fetch(request: Request, env: Env): Promise {
    const url = new URL(request.url);
    
    // Upload video
    if (url.pathname === '/api/videos/upload' && request.method === 'POST') {
      const formData = await request.formData();
      const video = formData.get('video') as File;
      
      // Upload to Stream
      const uploadResponse = await fetch(
        `https://api.cloudflare.com/client/v4/accounts/${env.STREAM_ACCOUNT_ID}/stream`,
        {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${env.STREAM_API_TOKEN}`,
          },
          body: video
        }
      );
      
      const result = await uploadResponse.json();
      
      return new Response(JSON.stringify({
        success: true,
        videoId: result.result.uid,
        playbackUrl: `https://customer-${env.STREAM_ACCOUNT_ID}.cloudflarestream.com/${result.result.uid}/manifest/video.m3u8`
      }));
    }
    
    // Get video status
    if (url.pathname.startsWith('/api/videos/')) {
      const videoId = url.pathname.split('/')[3];
      
      const response = await fetch(
        `https://api.cloudflare.com/client/v4/accounts/${env.STREAM_ACCOUNT_ID}/stream/${videoId}`,
        {
          headers: {
            'Authorization': `Bearer ${env.STREAM_API_TOKEN}`,
          }
        }
      );
      
      const data = await response.json();
      
      return new Response(JSON.stringify({
        status: data.result.status.state, // 'ready', 'inprogress', 'error'
        duration: data.result.duration,
        thumbnail: data.result.thumbnail,
        playback: data.result.playback
      }));
    }
    
    return new Response('Not Found', { status: 404 });
  }
};

Video Player HTML:

<stream
  src="video-id-here"
  controls
  poster="https://customer-xxx.cloudflarestream.com/video-id/thumbnails/thumbnail.jpg"
></stream>
<script
  data-cfasync="false"
  defer
  type="text/javascript"
  src="https://embed.cloudflarestream.com/embed/sdk.latest.js"
></script>

Features:

  • Automatic encoding (multiple qualities)
  • Adaptive bitrate streaming
  • Thumbnails generated automatically
  • Analytics (views, play time)
  • Watermarking
  • Signed URLs (private videos)

Pricing:

  • Creator plan: $5/month for 100 minutes, $1 per 100 additional minutes
  • Stream plan: $1 per 1,000 minutes viewed

10. Pages - Your Frontend Hosting

What it is: Deploy static sites and full-stack applications with Git integration.

The Simple Explanation: Like Netlify or Vercel, but integrated with Cloudflare’s ecosystem. Push to Git, it auto-deploys globally.

How it helps your SaaS:

  • Host your marketing site
  • Deploy your SaaS frontend
  • Preview deployments for each branch
  • Automatic HTTPS

Real Example - Deploy React App:

# Your React project structure
my-saas-frontend/
├── src/
├── public/
├── package.json
└── vite.config.ts

# Build command (in package.json)
"scripts": {
  "build": "vite build"
}

# Deploy
wrangler pages deploy dist

# Or connect to Git (GitHub/GitLab)
# Go to Cloudflare Dashboard → Pages → Create Project → Connect Git

Full-Stack Pages (Frontend + Backend):

// functions/api/users.ts
// This file creates an API route at /api/users

interface Env {
  DB: D1Database;
}

export async function onRequest(context: { 
  request: Request; 
  env: Env 
}): Promise<Response> {
  const { results } = await context.env.DB
    .prepare('SELECT * FROM users')
    .all();
  
  return new Response(JSON.stringify(results), {
    headers: { 'Content-Type': 'application/json' }
  });
}

Pricing:

  • Free tier: Unlimited sites, 500 builds/month
  • Pro: $20/month for 5,000 builds/month

11. Email Routing & Workers

What it is: Receive emails at your domain and process them with Workers.

The Simple Explanation: Instead of using Gmail or Outlook, receive emails directly in your Workers and do whatever you want with them (save to database, trigger actions, auto-respond).

How it helps your SaaS:

  • Support ticketing system
  • Email-to-task features (like “email this address to create a todo”)
  • Newsletter processing
  • Email verification

Real Example - Support Ticket System:

// src/email.ts
interface Env {
  DB: D1Database;
}

export default {
  async email(message: any, env: Env): Promise<void> {
    const from = message.from;
    const subject = message.headers.get('subject');
    const text = await message.text();
    
    // Save to database as support ticket
    await env.DB.prepare(`
      INSERT INTO tickets (email, subject, message, status, created_at)
      VALUES (?, ?, ?, 'open', datetime('now'))
    `).bind(from, subject, text).run();
    
    // Send auto-reply
    await message.reply({
      from: 'support@yoursaas.com',
      subject: `Re: ${subject}`,
      text: `Thanks for contacting support! We've received your message and will respond within 24 hours.
      
Ticket created at: ${new Date().toISOString()}

Original message:
${text}`
    });
  }
};

Setup:

# Add email routing in Cloudflare Dashboard
# DNS → Email Routing → Enable
# Add route: support@yourdomain.com → Worker (email-handler)

Pricing:

  • Free: Unlimited emails routed to Workers

12. Zaraz - Your Third-Party Script Manager

What it is: Load analytics, ads, and tracking scripts without slowing down your site.

The Simple Explanation: Instead of adding 10 different JavaScript tags (Google Analytics, Facebook Pixel, etc.) that slow your site, Zaraz loads them all efficiently from Cloudflare’s edge.

How it helps your SaaS:

  • Faster page loads (better SEO, conversions)
  • Privacy compliance
  • One place to manage all tools
  • No code changes needed

Setup (No Code Required):

  1. Go to Cloudflare Dashboard → Zaraz
  2. Click “Add Tool”
  3. Select tool (Google Analytics, Facebook Pixel, etc.)
  4. Configure settings
  5. Done! Automatically optimized

Example - Track Custom Events:

// In your frontend
zaraz.track('user_signed_up', {
  plan: 'pro',
  value: 29.99
});

zaraz.track('feature_used', {
  feature: 'export_pdf'
});

Pricing:

  • Free: Included with Cloudflare

Part 4: Building Your First SaaS - A Complete Example

Let’s build a URL Shortener SaaS from scratch using multiple Cloudflare services.

Features:

  • Shorten URLs
  • Custom short codes
  • Click analytics
  • User accounts
  • API access

Step 1: Setup Project

wrangler init url-shortener-saas
cd url-shortener-saas

Step 2: Configure Services

# wrangler.toml
name = "url-shortener-saas"
main = "src/index.ts"
compatibility_date = "2025-11-28"

# Database for users and URLs
[[d1_databases]]
binding = "DB"
database_name = "url-shortener-db"
database_id = "your-db-id"

# KV for fast URL lookups
[[kv_namespaces]]
binding = "URLS"
id = "your-kv-id"

# R2 for analytics data
[[r2_buckets]]
binding = "ANALYTICS"
bucket_name = "url-analytics"

Step 3: Create Database Schema

-- schema.sql
CREATE TABLE users (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  email TEXT UNIQUE NOT NULL,
  api_key TEXT UNIQUE NOT NULL,
  plan TEXT DEFAULT 'free',
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE urls (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  user_id INTEGER NOT NULL,
  short_code TEXT UNIQUE NOT NULL,
  long_url TEXT NOT NULL,
  clicks INTEGER DEFAULT 0,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (user_id) REFERENCES users(id)
);

CREATE INDEX idx_short_code ON urls(short_code);
CREATE INDEX idx_user_id ON urls(user_id);
wrangler d1 create url-shortener-db
wrangler d1 execute url-shortener-db --file=./schema.sql
wrangler kv:namespace create "URLS"
wrangler r2 bucket create url-analytics

Step 4: Build the Worker

// src/index.ts
interface Env {
  DB: D1Database;
  URLS: KVNamespace;
  ANALYTICS: R2Bucket;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);
    
    // CORS headers
    const corsHeaders = {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET, POST, DELETE',
      'Content-Type': 'application/json'
    };
    
    // Handle OPTIONS (CORS preflight)
    if (request.method === 'OPTIONS') {
      return new Response(null, { headers: corsHeaders });
    }
    
    // ============ CREATE SHORT URL ============
    if (url.pathname === '/api/shorten' && request.method === 'POST') {
      const { longUrl, customCode, apiKey } = await request.json();
      
      // Verify API key
      const user = await env.DB.prepare(
        'SELECT id, plan FROM users WHERE api_key = ?'
      ).bind(apiKey).first();
      
      if (!user) {
        return new Response(JSON.stringify({ error: 'Invalid API key' }), {
          status: 401,
          headers: corsHeaders
        });
      }
      
      // Generate or use custom short code
      const shortCode = customCode || generateShortCode();
      
      // Check if custom code already exists
      if (customCode) {
        const existing = await env.URLS.get(shortCode);
        if (existing) {
          return new Response(JSON.stringify({ error: 'Short code already taken' }), {
            status: 400,
            headers: corsHeaders
          });
        }
      }
      
      // Save to database
      await env.DB.prepare(`
        INSERT INTO urls (user_id, short_code, long_url)
        VALUES (?, ?, ?)
      `).bind(user.id, shortCode, longUrl).run();
      
      // Cache in KV for fast lookups
      await env.URLS.put(shortCode, longUrl);
      
      return new Response(JSON.stringify({
        success: true,
        shortUrl: `${url.origin}/${shortCode}`,
        shortCode,
        longUrl
      }), {
        headers: corsHeaders
      });
    }
    
    // ============ REDIRECT SHORT URL ============
    if (url.pathname.length > 1 && request.method === 'GET') {
      const shortCode = url.pathname.slice(1);
      
      // Try KV cache first (fast!)
      let longUrl = await env.URLS.get(shortCode);
      
      // If not in cache, check database
      if (!longUrl) {
        const result = await env.DB.prepare(
          'SELECT long_url FROM urls WHERE short_code = ?'
        ).bind(shortCode).first();
        
        if (!result) {
          return new Response('URL not found', { status: 404 });
        }
        
        longUrl = result.long_url as string;
        
        // Cache for next time
        await env.URLS.put(shortCode, longUrl);
      }
      
      // Track click (async, don't wait)
      trackClick(shortCode, request, env);
      
      // Redirect
      return Response.redirect(longUrl, 301);
    }
    
    // ============ GET USER STATS ============
    if (url.pathname === '/api/stats' && request.method === 'GET') {
      const apiKey = url.searchParams.get('apiKey');
      
      const user = await env.DB.prepare(
        'SELECT id FROM users WHERE api_key = ?'
      ).bind(apiKey).first();
      
      if (!user) {
        return new Response(JSON.stringify({ error: 'Invalid API key' }), {
          status: 401,
          headers: corsHeaders
        });
      }
      
      const { results } = await env.DB.prepare(`
        SELECT short_code, long_url, clicks, created_at
        FROM urls
        WHERE user_id = ?
        ORDER BY created_at DESC
      `).bind(user.id).all();
      
      return new Response(JSON.stringify({ urls: results }), {
        headers: corsHeaders
      });
    }
    
    // ============ CREATE USER ============
    if (url.pathname === '/api/signup' && request.method === 'POST') {
      const { email } = await request.json();
      const apiKey = crypto.randomUUID();
      
      try {
        await env.DB.prepare(`
          INSERT INTO users (email, api_key)
          VALUES (?, ?)
        `).bind(email, apiKey).run();
        
        return new Response(JSON.stringify({
          success: true,
          apiKey,
          message: 'Account created! Save your API key.'
        }), {
          headers: corsHeaders
        });
      } catch (error) {
        return new Response(JSON.stringify({ 
          error: 'Email already registered' 
        }), {
          status: 400,
          headers: corsHeaders
        });
      }
    }
    
    return new Response('Not Found', { status: 404 });
  }
};

// Helper: Generate random short code
function generateShortCode(): string {
  const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
  let code = '';
  for (let i = 0; i < 6; i++) {
    code += chars[Math.floor(Math.random() * chars.length)];
  }
  return code;
}

// Helper: Track clicks (async)
async function trackClick(shortCode: string, request: Request, env: Env): Promise<void> {
  // Increment counter in database
  await env.DB.prepare(
    'UPDATE urls SET clicks = clicks + 1 WHERE short_code = ?'
  ).bind(shortCode).run();
  
  // Store detailed analytics in R2
  const analytics = {
    shortCode,
    timestamp: Date.now(),
    userAgent: request.headers.get('user-agent'),
    referer: request.headers.get('referer'),
    country: request.headers.get('cf-ipcountry'),
  };
  
  const key = `${shortCode}/${Date.now()}.json`;
  await env.ANALYTICS.put(key, JSON.stringify(analytics));
}

Step 5: Deploy

wrangler deploy

Your URL shortener is now live!

Step 6: Test It

# Create account
curl -X POST https://your-worker.workers.dev/api/signup \
  -H "Content-Type: application/json" \
  -d '{"email":"you@example.com"}'

# Response: { "apiKey": "xxx-xxx-xxx" }

# Shorten URL
curl -X POST https://your-worker.workers.dev/api/shorten \
  -H "Content-Type: application/json" \
  -d '{
    "longUrl":"https://example.com/very/long/url",
    "apiKey":"your-api-key"
  }'

# Response: { "shortUrl": "https://your-worker.workers.dev/abc123" }

# Visit short URL
curl https://your-worker.workers.dev/abc123
# Redirects to long URL!

# Get stats
curl "https://your-worker.workers.dev/api/stats?apiKey=your-api-key"

Step 7: Add a Frontend (Optional)

# Create Pages project
mkdir url-shortener-frontend
cd url-shortener-frontend
npm create vite@latest . -- --template react-ts
npm install
// src/App.tsx
import { useState } from 'react';

function App() {
  const [apiKey, setApiKey] = useState('');
  const [longUrl, setLongUrl] = useState('');
  const [shortUrl, setShortUrl] = useState('');
  
  const shortenUrl = async () => {
    const response = await fetch('https://your-worker.workers.dev/api/shorten', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ longUrl, apiKey })
    });
    
    const data = await response.json();
    setShortUrl(data.shortUrl);
  };
  
  return (
    <div style={{ padding: '2rem', maxWidth: '600px', margin: '0 auto' }}>
      <h1>URL Shortener</h1>
      
      <input
        type="text"
        placeholder="Your API Key"
        value={apiKey}
        onChange={(e) => setApiKey(e.target.value)}
        style={{ width: '100%', padding: '0.5rem', marginBottom: '1rem' }}
      />
      
      <input
        type="url"
        placeholder="Enter long URL"
        value={longUrl}
        onChange={(e) => setLongUrl(e.target.value)}
        style={{ width: '100%', padding: '0.5rem', marginBottom: '1rem' }}
      />
      
      <button 
        onClick={shortenUrl}
        style={{ padding: '0.5rem 2rem' }}
      >
        Shorten URL
      </button>
      
      {shortUrl && (
        <div style={{ marginTop: '2rem', padding: '1rem', background: '#f0f0f0' }}>
          <strong>Short URL:</strong><br />
          <a href={shortUrl} target="_blank">{shortUrl}</a>
        </div>
      )}
    </div>
  );
}

export default App;
npm run build
wrangler pages deploy dist

Your complete SaaS is now live with both backend and frontend!


Part 5: Advanced Patterns for Production SaaS

Authentication & Authorization

// src/auth.ts
import { SignJWT, jwtVerify } from 'jose';

const JWT_SECRET = new TextEncoder().encode('your-secret-key');

export async function generateToken(userId: number): Promise<string> {
  return await new SignJWT({ userId })
    .setProtectedHeader({ alg: 'HS256' })
    .setExpirationTime('7d')
    .sign(JWT_SECRET);
}

export async function verifyToken(token: string): Promise<number | null> {
  try {
    const { payload } = await jwtVerify(token, JWT_SECRET);
    return payload.userId as number;
  } catch {
    return null;
  }
}

// Middleware
export async function authenticate(request: Request, env: Env): Promise<number | Response> {
  const authHeader = request.headers.get('Authorization');
  
  if (!authHeader?.startsWith('Bearer ')) {
    return new Response(JSON.stringify({ error: 'Missing token' }), {
      status: 401
    });
  }
  
  const token = authHeader.slice(7);
  const userId = await verifyToken(token);
  
  if (!userId) {
    return new Response(JSON.stringify({ error: 'Invalid token' }), {
      status: 401
    });
  }
  
  return userId;
}

Rate Limiting per User

// Using Durable Objects for per-user rate limiting
export class RateLimiter {
  state: DurableObjectState;
  requests: Map<number, number[]>;
  
  constructor(state: DurableObjectState) {
    this.state = state;
    this.requests = new Map();
  }
  
  async fetch(request: Request): Promise<Response> {
    const { userId, limit, window } = await request.json();
    
    const now = Date.now();
    const windowStart = now - window;
    
    // Get user's requests
    let userRequests = this.requests.get(userId) || [];
    
    // Remove old requests outside window
    userRequests = userRequests.filter(time => time > windowStart);
    
    // Check limit
    if (userRequests.length >= limit) {
      return new Response(JSON.stringify({
        allowed: false,
        retryAfter: Math.ceil((userRequests[0] - windowStart) / 1000)
      }), { status: 429 });
    }
    
    // Add current request
    userRequests.push(now);
    this.requests.set(userId, userRequests);
    
    return new Response(JSON.stringify({ allowed: true }));
  }
}

Monitoring & Error Tracking

// src/monitoring.ts
interface ErrorLog {
  timestamp: number;
  error: string;
  stack?: string;
  userId?: number;
  endpoint: string;
}

export async function logError(
  error: Error,
  request: Request,
  env: Env,
  userId?: number
): Promise<void> {
  const log: ErrorLog = {
    timestamp: Date.now(),
    error: error.message,
    stack: error.stack,
    userId,
    endpoint: new URL(request.url).pathname
  };
  
  // Store in R2 for analysis
  await env.ERROR_LOGS.put(
    `errors/${Date.now()}-${crypto.randomUUID()}.json`,
    JSON.stringify(log)
  );
  
  // Also send to external service (e.g., Sentry)
  await fetch('https://sentry.io/api/...', {
    method: 'POST',
    body: JSON.stringify(log)
  });
}

// Usage in your worker
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    try {
      // Your logic here
      return await handleRequest(request, env);
    } catch (error) {
      await logError(error as Error, request, env);
      return new Response('Internal Server Error', { status: 500 });
    }
  }
};

Database Migrations

# Create migration
wrangler d1 migrations create url-shortener-db add_user_limits

# This creates: migrations/0002_add_user_limits.sql
-- migrations/0002_add_user_limits.sql
ALTER TABLE users ADD COLUMN monthly_limit INTEGER DEFAULT 1000;
ALTER TABLE users ADD COLUMN monthly_used INTEGER DEFAULT 0;

CREATE TABLE usage_logs (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  user_id INTEGER NOT NULL,
  endpoint TEXT NOT NULL,
  timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (user_id) REFERENCES users(id)
);
# Apply migration
wrangler d1 migrations apply url-shortener-db

Testing Your Workers

// test/index.test.ts
import { describe, it, expect } from 'vitest';
import worker from '../src/index';

describe('URL Shortener', () => {
  it('should create short URL', async () => {
    const request = new Request('http://localhost/api/shorten', {
      method: 'POST',
      body: JSON.stringify({
        longUrl: 'https://example.com',
        apiKey: 'test-key'
      })
    });
    
    const env = getMiniflareBindings();
    const response = await worker.fetch(request, env);
    const data = await response.json();
    
    expect(data.success).toBe(true);
    expect(data.shortUrl).toContain('http://localhost/');
  });
  
  it('should redirect short URL', async () => {
    const request = new Request('http://localhost/abc123');
    const env = getMiniflareBindings();
    const response = await worker.fetch(request, env);
    
    expect(response.status).toBe(301);
    expect(response.headers.get('location')).toBe('https://example.com');
  });
});
# Run tests
npm test

Part 6: Deployment & Production Best Practices

Environment Variables & Secrets

# Set secrets (never commit these!)
wrangler secret put DATABASE_URL
wrangler secret put STRIPE_SECRET_KEY
wrangler secret put SENDGRID_API_KEY

# Use in code
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const stripeKey = env.STRIPE_SECRET_KEY;
    // Use it securely
  }
};

Custom Domains

# Add custom domain in Cloudflare Dashboard
# Workers → your-worker → Settings → Domains & Routes
# Add: api.yoursaas.com

# Or via wrangler
wrangler deploy --route "api.yoursaas.com/*"

Staging vs Production

# wrangler.toml
[env.staging]
name = "url-shortener-staging"
vars = { ENVIRONMENT = "staging" }

[env.production]
name = "url-shortener-production"
vars = { ENVIRONMENT = "production" }
# Deploy to staging
wrangler deploy --env staging

# Deploy to production
wrangler deploy --env production

CI/CD with GitHub Actions

# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Deploy to Cloudflare
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CF_API_TOKEN }}
          command: deploy --env production

Monitoring & Observability

// Add custom analytics
export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const start = Date.now();
    
    try {
      const response = await handleRequest(request, env);
      
      // Log metrics
      ctx.waitUntil(logMetrics({
        endpoint: new URL(request.url).pathname,
        duration: Date.now() - start,
        status: response.status,
        timestamp: Date.now()
      }, env));
      
      return response;
    } catch (error) {
      // Log error
      ctx.waitUntil(logError(error, request, env));
      throw error;
    }
  }
};

Cost Optimization Tips

  1. Use KV for hot data: Cache frequently accessed data in KV instead of querying D1
  2. Batch D1 queries: Use transactions to reduce round trips
  3. Set appropriate cache TTLs: Don’t cache too long or too short
  4. Use R2 for large files: Much cheaper than serving from Workers
  5. Implement rate limiting: Prevent abuse and control costs

Part 7: Complete SaaS Architecture Example

Here’s a production-ready architecture for a SaaS Analytics Platform:

┌─────────────────────────────────────────────────────────────────┐
│                         Cloudflare Network                      │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌──────────────┐      ┌──────────────┐      ┌──────────────┐│
│  │Pages Frontend│      │Workers API   │      │Stream Video  ││
│  │(React/Vue)   │◄────►│(Business     │◄────►│(Tutorials)   ││
│  └──────────────┘      │Logic)        │      └──────────────┘│
│                        └──────────────┘                        │
│                               │                                 │
│                               ▼                                 │
│         ┌─────────────────────────────────────────┐            │
│         │                                          │            │
│    ┌────▼─────┐  ┌────────┐  ┌────────┐  ┌───────▼──┐        │
│    │D1 Database│  │Workers │  │Durable │  │R2 Storage│        │
│    │(Users,    │  │KV      │  │Objects │  │(Files,   │        │
│    │Analytics) │  │(Cache) │  │(RT)    │  │Backups)  │        │
│    └──────────┘  └────────┘  └────────┘  └──────────┘        │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
                               │
                               ▼
                   ┌────────────────────┐
                   │External Services   │
                   │- Stripe (Payments) │
                   │- SendGrid (Email)  │
                   │- Twilio (SMS)      │
                   └────────────────────┘

Part 8: Resources & Next Steps

Official Documentation

Tutorials & Guides

Community Resources

Learning Paths

Beginner (Week 1-2):

  1. Complete the “Hello World” tutorial
  2. Build a simple API with Workers
  3. Deploy a static site with Pages
  4. Learn basic D1 queries

Intermediate (Week 3-4):

  1. Build a full authentication system
  2. Implement rate limiting
  3. Add file uploads with R2
  4. Create a real-time feature with Durable Objects

Advanced (Week 5-6):

  1. Build a complete SaaS (like our URL shortener)
  2. Implement payment processing (Stripe)
  3. Add email notifications
  4. Set up monitoring and alerts

Pro (Week 7+):

  1. Optimize for scale (caching strategies)
  2. Multi-region deployment
  3. Complex WebSocket applications
  4. Custom analytics dashboards

Essential Tools

  • Wrangler: Command-line interface
  • Miniflare: Local development environment
  • VS Code Extensions: Cloudflare Workers extension
  • Postman/Insomnia: API testing

Cost Calculator

Estimate your costs: https://developers.cloudflare.com/workers/platform/pricing/

Getting Help

  1. Check documentation first
  2. Search community forums
  3. Ask in Discord (very active!)
  4. Submit support ticket (paid plans)

Conclusion: Your SaaS Journey Starts Now

You now have everything you need to build a production-ready SaaS on Cloudflare:

Understanding: You know what Cloudflare is and why it’s perfect for SaaS
Tools: You can use Wrangler to manage your projects
Services: You understand each service and when to use it
Practice: You’ve seen real, working examples
Architecture: You know how to structure a complete application
Production: You have best practices for deployment and monitoring

Your next steps:

  1. Pick a SaaS idea you want to build
  2. Start with a simple MVP using Workers + D1
  3. Add features incrementally (auth, payments, etc.)
  4. Deploy early and iterate based on feedback
  5. Scale as you grow

Remember: Every successful SaaS started as a simple idea. Cloudflare gives you the infrastructure to focus on what matters - building something people love.

Happy building! 🚀