Skip to main content

Building a Full-Stack SaaS Application with Cloudflare Technology Stack

· 8 min read
Founder, Shan Studio

Cloudflare has evolved from a CDN provider into a comprehensive edge computing platform. In this guide, we'll explore how to build a complete full-stack SaaS application using Cloudflare's ecosystem, providing global performance, scalability, and cost-effectiveness.

Introduction

Cloudflare has evolved from a CDN provider into a comprehensive edge computing platform. In this guide, we'll explore how to build a complete full-stack SaaS application using Cloudflare's ecosystem, providing global performance, scalability, and cost-effectiveness.

Technology Stack Selection

Frontend Layer

  • Cloudflare Pages: Static site hosting with automatic builds and deployments
  • React/Next.js: Modern frontend framework with SSR/SSG support
  • Tailwind CSS: Utility-first CSS framework for rapid UI development

Backend Layer

  • Cloudflare Workers: Serverless compute at the edge (V8 isolates)
  • Hono: Lightweight web framework optimized for Workers
  • Cloudflare Workers AI: Built-in AI/ML capabilities

Database & Storage

  • Cloudflare D1: Serverless SQLite database at the edge
  • Cloudflare R2: S3-compatible object storage (zero egress fees)
  • Cloudflare KV: Global key-value storage for caching

Authentication & Security

  • Cloudflare Access: Zero Trust authentication
  • Cloudflare Turnstile: Privacy-first CAPTCHA alternative
  • Workers JWT: Custom authentication implementation

Additional Services

  • Cloudflare Queues: Message queuing for async processing
  • Cloudflare Durable Objects: Stateful coordination primitives
  • Cloudflare Analytics: Built-in analytics and monitoring
  • Cloudflare Email Routing: Email handling and routing

Architecture Overview

┌─────────────────────────────────────────────────────┐
│ Cloudflare Edge │
├─────────────────────────────────────────────────────┤
│ Cloudflare Pages (Frontend) │
│ ↓ │
│ Cloudflare Workers (API/Backend Logic) │
│ ↓ │
│ ┌──────────┬──────────┬──────────┬──────────┐ │
│ │ D1 (DB) │ R2 (S3) │ KV Cache │ Queues │ │
│ └──────────┴──────────┴──────────┴──────────┘ │
└─────────────────────────────────────────────────────┘

Implementation Steps

Step 1: Project Setup and Environment Configuration

# Install Wrangler CLI (Cloudflare's developer tool)
npm install -g wrangler

# Login to Cloudflare
wrangler login

# Create a new Workers project
npm create cloudflare@latest my-saas-app
cd my-saas-app

# Initialize TypeScript configuration
npm install -D typescript @cloudflare/workers-types

Step 2: Configure Cloudflare Workers Backend

Create wrangler.toml:

name = "my-saas-api"
main = "src/index.ts"
compatibility_date = "2026-03-11"

# D1 Database binding
[[d1_databases]]
binding = "DB"
database_name = "my-saas-db"
database_id = "your-database-id"

# R2 Storage binding
[[r2_buckets]]
binding = "STORAGE"
bucket_name = "my-saas-uploads"

# KV namespace binding
[[kv_namespaces]]
binding = "CACHE"
id = "your-kv-id"

# Environment variables
[vars]
ENVIRONMENT = "production"

Step 3: Build RESTful API with Hono Framework

// src/index.ts
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { jwt } from 'hono/jwt'
import { logger } from 'hono/logger'

type Bindings = {
DB: D1Database
STORAGE: R2Bucket
CACHE: KVNamespace
JWT_SECRET: string
}

const app = new Hono<{ Bindings: Bindings }>()

// Middleware
app.use('*', cors())
app.use('*', logger())

// Health check
app.get('/health', (c) => {
return c.json({ status: 'healthy', timestamp: Date.now() })
})

// Protected routes
app.use('/api/*', jwt({ secret: 'your-secret-key' }))

// User management
app.get('/api/users', async (c) => {
const { results } = await c.env.DB.prepare(
'SELECT id, email, created_at FROM users'
).all()
return c.json(results)
})

app.post('/api/users', async (c) => {
const { email, password } = await c.req.json()

const result = await c.env.DB.prepare(
'INSERT INTO users (email, password_hash) VALUES (?, ?)'
).bind(email, await hashPassword(password)).run()

return c.json({ id: result.meta.last_row_id }, 201)
})

export default app

Step 4: Initialize D1 Database Schema

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

CREATE TABLE subscriptions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
plan_type TEXT NOT NULL,
status TEXT DEFAULT 'active',
expires_at DATETIME,
FOREIGN KEY (user_id) REFERENCES users(id)
);

CREATE TABLE uploads (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
file_key TEXT NOT NULL,
file_size INTEGER,
uploaded_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);

CREATE INDEX idx_user_subscriptions ON subscriptions(user_id);
CREATE INDEX idx_user_uploads ON uploads(user_id);
# Create and initialize database
wrangler d1 create my-saas-db
wrangler d1 execute my-saas-db --file=./schema.sql

Step 5: Implement File Upload with R2

// src/routes/upload.ts
import { Hono } from 'hono'

const upload = new Hono<{ Bindings: Bindings }>()

upload.post('/upload', async (c) => {
const formData = await c.req.formData()
const file = formData.get('file') as File

if (!file) {
return c.json({ error: 'No file provided' }, 400)
}

// Generate unique key
const fileKey = `${crypto.randomUUID()}-${file.name}`

// Upload to R2
await c.env.STORAGE.put(fileKey, file.stream(), {
httpMetadata: {
contentType: file.type,
},
})

// Save metadata to D1
await c.env.DB.prepare(
'INSERT INTO uploads (user_id, file_key, file_size) VALUES (?, ?, ?)'
).bind(userId, fileKey, file.size).run()

return c.json({
fileKey,
url: `https://your-domain.com/files/${fileKey}`
})
})

upload.get('/files/:key', async (c) => {
const key = c.req.param('key')
const object = await c.env.STORAGE.get(key)

if (!object) {
return c.notFound()
}

return new Response(object.body, {
headers: {
'Content-Type': object.httpMetadata?.contentType || 'application/octet-stream',
'Cache-Control': 'public, max-age=31536000',
},
})
})

export default upload

Step 6: Implement Caching with KV

// src/middleware/cache.ts
export async function cacheMiddleware(c: Context, next: Next) {
const cacheKey = `cache:${c.req.url}`

// Check cache
const cached = await c.env.CACHE.get(cacheKey)
if (cached) {
return c.json(JSON.parse(cached))
}

await next()

// Cache successful responses
if (c.res.status === 200) {
const body = await c.res.clone().text()
await c.env.CACHE.put(cacheKey, body, {
expirationTtl: 3600, // 1 hour
})
}
}

Step 7: Setup Frontend with Next.js and Cloudflare Pages

# Create Next.js app
npx create-next-app@latest my-saas-frontend
cd my-saas-frontend

# Install Cloudflare Pages adapter
npm install @cloudflare/next-on-pages
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export', // Static export for Pages
images: {
domains: ['your-r2-domain.com'],
},
}

module.exports = nextConfig
// app/api/client.ts
export class APIClient {
private baseURL = 'https://api.your-domain.com'
private token: string | null = null

setToken(token: string) {
this.token = token
}

async fetch(endpoint: string, options: RequestInit = {}) {
const headers = {
'Content-Type': 'application/json',
...(this.token && { Authorization: `Bearer ${this.token}` }),
...options.headers,
}

const response = await fetch(`${this.baseURL}${endpoint}`, {
...options,
headers,
})

if (!response.ok) {
throw new Error(`API Error: ${response.statusText}`)
}

return response.json()
}
}

Step 8: Implement Authentication

// src/auth/jwt.ts
import { sign, verify } from 'hono/jwt'

export async function generateToken(userId: number, secret: string) {
const payload = {
sub: userId,
exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 7, // 7 days
}
return await sign(payload, secret)
}

export async function verifyToken(token: string, secret: string) {
try {
return await verify(token, secret)
} catch {
return null
}
}

// Login endpoint
app.post('/auth/login', async (c) => {
const { email, password } = await c.req.json()

const user = await c.env.DB.prepare(
'SELECT id, password_hash FROM users WHERE email = ?'
).bind(email).first()

if (!user || !await verifyPassword(password, user.password_hash)) {
return c.json({ error: 'Invalid credentials' }, 401)
}

const token = await generateToken(user.id, c.env.JWT_SECRET)
return c.json({ token })
})

Step 9: Implement Background Jobs with Queues

// wrangler.toml - add queue configuration
[[queues.producers]]
binding = "EMAIL_QUEUE"
queue = "email-notifications"

[[queues.consumers]]
queue = "email-notifications"
max_batch_size = 10
max_batch_timeout = 30
// src/queue-consumer.ts
export default {
async queue(batch: MessageBatch, env: Bindings): Promise<void> {
for (const message of batch.messages) {
const { to, subject, body } = message.body

// Send email via Cloudflare Email Routing or third-party service
await sendEmail({ to, subject, body })

message.ack()
}
}
}

// Enqueue message
app.post('/api/send-email', async (c) => {
await c.env.EMAIL_QUEUE.send({
to: 'user@example.com',
subject: 'Welcome!',
body: 'Thanks for joining!',
})
return c.json({ queued: true })
})

Step 10: Deploy Everything

# Deploy Workers API
wrangler deploy

# Deploy Pages Frontend
cd my-saas-frontend
npm run build
npx wrangler pages deploy out

# Setup custom domain in Cloudflare dashboard
# Configure DNS records to point to your Workers and Pages

Step 11: Add Monitoring and Analytics

// src/middleware/analytics.ts
app.use('*', async (c, next) => {
const start = Date.now()
await next()
const duration = Date.now() - start

// Log to Cloudflare Analytics
c.executionCtx.waitUntil(
fetch('https://cloudflare-analytics-api.com/log', {
method: 'POST',
body: JSON.stringify({
path: c.req.path,
method: c.req.method,
status: c.res.status,
duration,
timestamp: new Date().toISOString(),
}),
})
)
})

Performance Optimization Tips

  1. Edge Caching: Use KV for frequently accessed data
  2. Smart Placement: Leverage Smart Placement for optimal D1 performance
  3. R2 CDN: Use Cloudflare CDN for R2 assets
  4. Worker Size: Keep Workers under 1MB for best cold start performance
  5. Database Indexes: Properly index D1 tables for query performance

Security Best Practices

  1. Rate Limiting: Implement rate limiting with KV
  2. Input Validation: Validate all user inputs
  3. SQL Injection Prevention: Use prepared statements
  4. CORS Configuration: Properly configure CORS policies
  5. Secrets Management: Use Wrangler secrets for sensitive data
wrangler secret put JWT_SECRET
wrangler secret put API_KEY

Cost Estimation

Cloudflare's pricing is extremely competitive for SaaS applications:

  • Workers: 100,000 requests/day free, then $0.50/million requests
  • Pages: Unlimited requests, 500 builds/month free
  • D1: 25GB storage + 5M reads/day free
  • R2: 10GB storage free, zero egress fees
  • KV: 100,000 reads/day free

For a typical SaaS with 10,000 users, monthly costs can be under $50!

Conclusion

Building a SaaS application on Cloudflare's technology stack provides:

Global Performance: Edge computing in 300+ cities
Scalability: Auto-scaling without configuration
Cost-Effective: Generous free tiers and low pricing
Developer Experience: Modern APIs and excellent tooling
Security: Built-in DDoS protection and WAF

The Cloudflare ecosystem offers a complete solution for modern SaaS applications, eliminating the need for complex infrastructure management while providing enterprise-grade performance and security.

Resources


Ready to build your SaaS on Cloudflare? Start with the free tier and scale as you grow! 🚀