Deploying a Full-Stack App on Cloudflare Workers with D1: A Complete Guide
What if you could deploy a full-stack web application — API, database, and frontend — without provisioning a single server? With Cloudflare Workers for compute and D1 for your database, you can build and deploy production-grade applications that run at the edge, scale automatically, and cost almost nothing at low traffic.
In this guide, we'll walk through building a complete full-stack application on Cloudflare's platform — from project setup to production deployment.
Why Cloudflare Workers + D1?
Before diving in, here's why this stack is compelling:
Edge Computing
Your code runs in 300+ data centers worldwide. Users get sub-50ms response times regardless of location.
Zero Cold Starts
Unlike AWS Lambda or Google Cloud Functions, Workers have no cold start penalty. Every request is fast.
SQLite-Based Database
D1 is SQLite at the edge. If you know SQL, you know D1. No ORMs required, no connection pooling headaches.
Generous Free Tier
100,000 requests/day on Workers, 5M reads/day on D1. Most small-to-medium apps run for free.
Project Setup
Step 1: Initialize the Project
# Create a new Workers project
npm create cloudflare@latest my-app -- --type=hello-world --ts
cd my-app
# Initialize D1 database
npx wrangler d1 create my-app-dbStep 2: Configure wrangler.toml
After creating the D1 database, Wrangler gives you a database ID. Add it to your config:
# wrangler.toml
name = "my-app"
main = "src/index.ts"
compatibility_date = "2025-01-01"
[[d1_databases]]
binding = "DB"
database_name = "my-app-db"
database_id = "your-database-id-here"Step 3: Define Your Schema
Create a migration file for your database schema:
-- migrations/001_init.sql
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
password_hash TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
title TEXT NOT NULL,
content TEXT NOT NULL,
slug TEXT UNIQUE NOT NULL,
published INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (user_id) REFERENCES users(id)
);
CREATE INDEX idx_posts_slug ON posts(slug);
CREATE INDEX idx_posts_user ON posts(user_id);# Apply the migration
npx wrangler d1 migrations apply my-app-db --local # local dev
npx wrangler d1 migrations apply my-app-db --remote # productionBuilding the API
Type Definitions
// src/types.ts
export interface Env {
DB: D1Database;
}
export interface User {
id: number;
email: string;
name: string;
created_at: string;
}
export interface Post {
id: number;
user_id: number;
title: string;
content: string;
slug: string;
published: number;
created_at: string;
}The Worker Entry Point
// src/index.ts
import { Env } from "./types";
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const path = url.pathname;
// CORS headers for API
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
};
if (request.method === "OPTIONS") {
return new Response(null, { headers: corsHeaders });
}
try {
// Route matching
if (path === "/api/posts" && request.method === "GET") {
return handleGetPosts(env, corsHeaders);
}
if (path === "/api/posts" && request.method === "POST") {
return handleCreatePost(request, env, corsHeaders);
}
const postMatch = path.match(/^\/api\/posts\/([\w-]+)$/);
if (postMatch) {
const slug = postMatch[1];
if (request.method === "GET") {
return handleGetPost(slug, env, corsHeaders);
}
}
return new Response("Not Found", { status: 404 });
} catch (err) {
return Response.json(
{ error: "Internal Server Error" },
{ status: 500, headers: corsHeaders }
);
}
},
};
async function handleGetPosts(env: Env, headers: Record<string, string>) {
const { results } = await env.DB.prepare(
"SELECT id, title, slug, created_at FROM posts WHERE published = 1 ORDER BY created_at DESC"
).all();
return Response.json(results, { headers });
}
async function handleGetPost(slug: string, env: Env, headers: Record<string, string>) {
const post = await env.DB.prepare(
"SELECT * FROM posts WHERE slug = ? AND published = 1"
).bind(slug).first();
if (!post) {
return Response.json({ error: "Post not found" }, { status: 404, headers });
}
return Response.json(post, { headers });
}
async function handleCreatePost(request: Request, env: Env, headers: Record<string, string>) {
const body = await request.json() as {
title: string;
content: string;
slug: string;
user_id: number;
};
const result = await env.DB.prepare(
"INSERT INTO posts (user_id, title, content, slug, published) VALUES (?, ?, ?, ?, 1)"
).bind(body.user_id, body.title, body.content, body.slug).run();
return Response.json(
{ id: result.meta.last_row_id, slug: body.slug },
{ status: 201, headers }
);
}Serving a Frontend
You have two options for the frontend:
Option A: Server-Rendered HTML
Return HTML directly from your Worker. Great for server-rendered apps where SEO matters. No build step for the frontend, and everything deploys as a single unit.
function renderPage(posts: Post[]) {
return new Response(`
<!DOCTYPE html>
<html>
<head>
<title>My Blog</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="max-w-2xl mx-auto p-8">
<h1 class="text-3xl font-bold mb-8">My Blog</h1>
${posts.map(p => `
<article class="mb-6">
<a href="/posts/${p.slug}" class="text-xl font-semibold text-blue-600 hover:underline">
${p.title}
</a>
<p class="text-gray-500 text-sm">${p.created_at}</p>
</article>
`).join("")}
</body>
</html>
`, { headers: { "Content-Type": "text/html" } });
}Option B: Separate Frontend on Cloudflare Pages
Deploy a React/Vue/Svelte app to Cloudflare Pages and have it call your Worker API. Best for complex, interactive UIs. The Worker handles the API, Pages handles the static assets.
Development Workflow
# Local development with live reload
npx wrangler dev
# Run migrations locally
npx wrangler d1 migrations apply my-app-db --local
# Deploy to production
npx wrangler deploy
# Run migrations in production
npx wrangler d1 migrations apply my-app-db --remote
# View production logs
npx wrangler tailAdding Authentication
For authentication, you can implement a simple session-based auth system using cookies:
// Simple password hashing (use a proper library in production)
async function hashPassword(password: string): Promise<string> {
const encoder = new TextEncoder();
const data = encoder.encode(password);
const hash = await crypto.subtle.digest("SHA-256", data);
return btoa(String.fromCharCode(...new Uint8Array(hash)));
}
// Session token stored as an HttpOnly cookie
function setSession(token: string): string {
return `session=${token}; HttpOnly; Secure; SameSite=Strict; Max-Age=86400; Path=/`;
}
// Auth middleware
async function requireAuth(request: Request, env: Env): Promise<User | null> {
const cookie = request.headers.get("Cookie") || "";
const match = cookie.match(/session=([^;]+)/);
if (!match) return null;
const session = await env.DB.prepare(
"SELECT users.* FROM sessions JOIN users ON sessions.user_id = users.id WHERE sessions.token = ?"
).bind(match[1]).first<User>();
return session || null;
}Production Checklist
Before deploying to production, make sure you've covered:
- ☐Custom domain — Set up a custom domain in the Cloudflare dashboard
- ☐Environment variables — Use
wrangler secret putfor sensitive values - ☐Error handling — Catch all errors and return meaningful error responses
- ☐Rate limiting — Protect your API from abuse
- ☐Input validation — Never trust client-side data
- ☐Database backups — D1 supports Time Travel for point-in-time recovery
What We've Built on This Stack
At AXOX, we've built multiple production applications using Cloudflare Workers + D1:
EdgeSubmit
Form submission API — handles thousands of form submissions daily with sub-50ms response times
PxShot
Screenshot API — captures web pages as images using Browser Rendering + D1 for API key management
AXOX Portal
Internal CRM — client management, deal pipeline, project tracking, all on Workers + D1
Need Help Building on Cloudflare?
We build production applications on Cloudflare's platform — Workers, D1, Pages, R2, and more. Whether you need an API, a full-stack app, or a migration from traditional infrastructure, we can help.
Start a Project →Questions about deploying on Cloudflare Workers? Get in touch — we're happy to answer questions or help you get started.