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:
- User in Tokyo visits your SaaS app
- Instead of traveling to your server in New York (slow!), the request hits Cloudflare’s Tokyo datacenter
- Cloudflare either serves cached content instantly or routes to your nearest server
- Response travels back to Tokyo in milliseconds
Three Core Pieces:
- CDN (Content Delivery Network): Caches your static files globally
- Security: Protects against DDoS attacks, bots, and threats
- 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):
- Go to Cloudflare Dashboard → Zaraz
- Click “Add Tool”
- Select tool (Google Analytics, Facebook Pixel, etc.)
- Configure settings
- 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
- Use KV for hot data: Cache frequently accessed data in KV instead of querying D1
- Batch D1 queries: Use transactions to reduce round trips
- Set appropriate cache TTLs: Don’t cache too long or too short
- Use R2 for large files: Much cheaper than serving from Workers
- 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
- Cloudflare Workers: https://developers.cloudflare.com/workers/
- Wrangler CLI: https://developers.cloudflare.com/workers/wrangler/
- D1 Database: https://developers.cloudflare.com/d1/
- R2 Storage: https://developers.cloudflare.com/r2/
- Workers KV: https://developers.cloudflare.com/kv/
- Durable Objects: https://developers.cloudflare.com/durable-objects/
- Pages: https://developers.cloudflare.com/pages/
- Stream: https://developers.cloudflare.com/stream/
Tutorials & Guides
- Workers Examples: https://developers.cloudflare.com/workers/examples/
- Full-Stack Guide: https://developers.cloudflare.com/workers/tutorials/build-a-full-stack-application/
- API Development: https://developers.cloudflare.com/workers/tutorials/build-an-api/
Community Resources
- Cloudflare Discord: https://discord.gg/cloudflaredev
- Community Forum: https://community.cloudflare.com/
- GitHub Examples: https://github.com/cloudflare/workers-sdk/tree/main/templates
Learning Paths
Beginner (Week 1-2):
- Complete the “Hello World” tutorial
- Build a simple API with Workers
- Deploy a static site with Pages
- Learn basic D1 queries
Intermediate (Week 3-4):
- Build a full authentication system
- Implement rate limiting
- Add file uploads with R2
- Create a real-time feature with Durable Objects
Advanced (Week 5-6):
- Build a complete SaaS (like our URL shortener)
- Implement payment processing (Stripe)
- Add email notifications
- Set up monitoring and alerts
Pro (Week 7+):
- Optimize for scale (caching strategies)
- Multi-region deployment
- Complex WebSocket applications
- 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
- Check documentation first
- Search community forums
- Ask in Discord (very active!)
- 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:
- Pick a SaaS idea you want to build
- Start with a simple MVP using Workers + D1
- Add features incrementally (auth, payments, etc.)
- Deploy early and iterate based on feedback
- 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! 🚀