← Back to blog
Admin 7 min read

Build a Full-Stack App with SvelteKit + Cloudflare D1 for Free

Build a Full-Stack App with SvelteKit + Cloudflare D1 for Free

You have an idea for a web app. You want a modern frontend, a real database, file storage, and global deployment. Sounds expensive? It's not. With SvelteKit, Cloudflare D1, Cloudflare R2, and a $14 domain, you can ship a production-ready full-stack app — for free.

Full-stack apps cost $25-75/month on AWS — Cloudflare gives you the same stack for $0, and this tutorial shows you how in 10 steps.

TL;DR — What you'll build

Stack SvelteKit + Cloudflare Workers + D1 (SQLite) + R2 (file storage)
App A bookmarks manager with full CRUD, file uploads, and global deployment
Cost $0/month — only a ~$14/year domain. Cloudflare free tier covers compute, database, storage, CDN, and SSL
Steps 10 steps from npx sv create to production deployment
Browse data Use MyD1 to visually browse, query, and manage your D1 database

In this guide, we'll build a small bookmarks manager from scratch. By the end, you'll have:

  • A SvelteKit app running on Cloudflare Workers (free tier)
  • A D1 SQLite database storing your data at the edge
  • R2 object storage for file uploads
  • Everything deployed globally — and you can browse your database visually with MyD1

What you'll need

  • Node.js 18+node -v to check
  • A Cloudflare account — free at cloudflare.com
  • A domain name — about $14/year on Cloudflare Registrar or Namecheap

That's it. No credit card required for any Cloudflare service we'll use.

Wait — how is this actually free?

Let's do the math. Here's what you'd pay for the same stack on AWS or Vercel:

What you needAWS / VercelCloudflare
Compute (SSR)Lambda + API Gateway: ~$5–30/moFree 100K requests/day
DatabaseRDS or PlanetScale: ~$15–30/moFree 5M reads, 100K writes/day
File storageS3: ~$2–5/moFree 10GB, 10M reads/mo
CDN + SSLCloudFront: ~$1–10/moFree automatic, global
Domain~$14/year~$14/year
Monthly total$25–75/mo$0/mo
Year 1 total$300–900+$14
$14
Total cost for Year 1
vs $300–900+ on AWS for the same stack

What's included in Cloudflare's free tier

This isn't a crippled "try it for 14 days" free tier. These are permanent, no-credit-card-required limits (per Cloudflare's pricing page):

ServiceFree tier limitsEnough for
Workers compute100K requests/day, 10ms CPU/req~3M pageviews/mo
D1 database5M reads/day, 100K writes/day, 5GBSaaS apps < 10K users
R2 storage10GB, 10M reads/mo, 1M writes/moThousands of files
KV cache100K reads/day, 1K writes/daySessions, feature flags
Workers AI10,000 neurons/dayAI features in your app
Queues10,000 operations/dayBackground jobs

To put this in perspective: 100,000 requests per day means ~70 requests per minute, every minute, 24/7. Most indie projects and early-stage startups never come close.

And when you outgrow the free tier?

The paid plan is $5/month. Five dollars. That gives you:

  • 30s CPU time per request (instead of 10ms)
  • Unlimited D1 reads/writes (pay-as-you-go beyond included)
  • 6 concurrent build slots
  • 7-day log retention
  • Containers, Logpush, and more

Compare that to AWS where scaling from free tier to production often means a 10x–50x price jump. On Cloudflare, it's $0 → $5. That's it.

The real cost of this tutorial

ItemCost
SvelteKitFree (open source)
Cloudflare WorkersFree
Cloudflare D1Free
Cloudflare R2Free
SSL certificateFree (automatic)
Global CDNFree (automatic)
DDoS protectionFree (automatic)
Domain name~$14/year
Total to launch~$14

There has never been a cheaper time to ship a real product. Let's build it.

Step 1 — Create the SvelteKit project

Open your terminal and scaffold a new project:

npx sv create bookmarks-app
cd bookmarks-app
npm install

When prompted, pick the defaults — SvelteKit minimal, TypeScript optional (we'll use plain JS here for simplicity).

Add the Cloudflare adapter:

npm install -D @sveltejs/adapter-cloudflare

Update svelte.config.js to use it:

import adapter from '@sveltejs/adapter-cloudflare';

export default {
  kit: {
    adapter: adapter()
  }
};

Step 2 — Create the D1 database

Install Wrangler (Cloudflare's CLI) and log in:

npm install -D wrangler
npx wrangler login

Create a D1 database:

npx wrangler d1 create bookmarks-db

This outputs a database ID — copy it. Create a wrangler.toml at the project root:

name = "bookmarks-app"
compatibility_date = "2025-01-01"

[[d1_databases]]
binding = "DB"
database_name = "bookmarks-db"
database_id = "your-database-id-here"

Now create your schema. Make a file schema.sql:

CREATE TABLE IF NOT EXISTS bookmarks (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  url TEXT NOT NULL,
  title TEXT NOT NULL,
  description TEXT DEFAULT '',
  tags TEXT DEFAULT '',
  created_at TEXT DEFAULT (datetime('now')),
  updated_at TEXT DEFAULT (datetime('now'))
);

CREATE INDEX idx_bookmarks_created ON bookmarks(created_at DESC);

Apply it locally first:

npx wrangler d1 execute bookmarks-db --local --file=schema.sql

Step 3 — Set up R2 storage

Add R2 to your wrangler.toml:

[[r2_buckets]]
binding = "R2"
bucket_name = "bookmarks-uploads"

Create the bucket:

npx wrangler r2 bucket create bookmarks-uploads

That's it — R2 is ready. We'll use it to store favicon screenshots of bookmarked URLs.

Step 4 — Access D1 and R2 in SvelteKit

In SvelteKit, Cloudflare bindings are available through platform.env. Create src/app.d.ts (or update it if it already exists) for type hints:

declare global {
  namespace App {
    interface Platform {
      env: {
        DB: D1Database;
        R2: R2Bucket;
      };
    }
  }
}
export {};

Now you can use platform.env.DB and platform.env.R2 in any +page.server.js or +server.js file.

Step 5 — Build the bookmarks API

Create src/routes/+page.server.js — this loads all bookmarks and handles adding new ones:

export async function load({ platform }) {
  const db = platform.env.DB;

  const { results } = await db
    .prepare('SELECT * FROM bookmarks ORDER BY created_at DESC')
    .all();

  return { bookmarks: results };
}

export const actions = {
  add: async ({ request, platform }) => {
    const db = platform.env.DB;
    const data = await request.formData();

    const url = data.get('url');
    const title = data.get('title');
    const description = data.get('description') || '';
    const tags = data.get('tags') || '';

    await db
      .prepare(
        'INSERT INTO bookmarks (url, title, description, tags) VALUES (?, ?, ?, ?)'
      )
      .bind(url, title, description, tags)
      .run();

    return { success: true };
  },

  delete: async ({ request, platform }) => {
    const db = platform.env.DB;
    const data = await request.formData();
    const id = data.get('id');

    await db
      .prepare('DELETE FROM bookmarks WHERE id = ?')
      .bind(id)
      .run();

    return { success: true };
  }
};

Step 6 — Build the UI

Create src/routes/+page.svelte:

<script>
  let { data } = $props();
</script>

<h1>My Bookmarks</h1>

<!-- Add bookmark form -->
<form method="POST" action="?/add">
  <input name="url" placeholder="https://..." required />
  <input name="title" placeholder="Title" required />
  <input name="description" placeholder="Description" />
  <input name="tags" placeholder="Tags (comma separated)" />
  <button type="submit">Add Bookmark</button>
</form>

<!-- Bookmarks list -->
{#each data.bookmarks as bookmark}
  <div class="bookmark">
    <a href={bookmark.url} target="_blank">
      {bookmark.title}
    </a>
    <p>{bookmark.description}</p>
    <small>{bookmark.tags}</small>
    <form method="POST" action="?/delete">
      <input type="hidden" name="id" value={bookmark.id} />
      <button type="submit">Delete</button>
    </form>
  </div>
{/each}

This is a fully functional CRUD app using SvelteKit form actions — no client-side JavaScript needed for the core functionality. Progressive enhancement at its best.

Step 7 — Upload files to R2

Let's add a file upload endpoint. Create src/routes/api/upload/+server.js:

export async function POST({ request, platform }) {
  const r2 = platform.env.R2;
  const formData = await request.formData();
  const file = formData.get('file');

  if (!file) {
    return new Response('No file', { status: 400 });
  }

  const key = Date.now() + '-' + file.name;
  await r2.put(key, file.stream(), {
    httpMetadata: { contentType: file.type }
  });

  return Response.json({ key, url: '/api/file/' + key });
}

And a route to serve files from R2. Create src/routes/api/file/[key]/+server.js:

export async function GET({ params, platform }) {
  const object = await platform.env.R2.get(params.key);

  if (!object) {
    return new Response('Not found', { status: 404 });
  }

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

Step 8 — Test locally

Here's the magic — Wrangler can emulate D1 and R2 locally:

npx wrangler dev

This starts your SvelteKit app with real D1 and R2 bindings — all running locally on your machine. Add some bookmarks, upload a file, see it all working. The local D1 database is stored in .wrangler/state/.

Step 9 — Deploy to Cloudflare

First, apply the schema to your production D1 database:

npx wrangler d1 execute bookmarks-db --remote --file=schema.sql

Then deploy:

npx wrangler deploy

That's it. Your app is now live on Cloudflare's global network. Point your domain to it in the Cloudflare dashboard under Workers & Pages → Custom Domains.

Step 10 — Browse your database with MyD1

Your app is deployed, data is flowing into D1. But how do you actually see what's in there? Running wrangler d1 execute queries in the terminal gets old fast.

This is where MyD1 comes in. It's a native macOS app that connects directly to your Cloudflare D1 databases.

Connect in 30 seconds:

  1. Open MyD1
  2. Paste your Cloudflare API token (create one at dash.cloudflare.com with D1 read/write permissions)
  3. Your databases appear instantly — click bookmarks-db

Now you can:

  • Browse tables — see all your bookmarks in a spreadsheet-like grid
  • Run SQL queries — with syntax highlighting and autocomplete
  • Ask the AI Agent — type a question in plain English like "show bookmarks added this week" and MyD1's AI Agent writes the SQL, optimizes queries, and surfaces insights you'd miss manually
  • Edit data inline — click a cell, change the value, done
  • Export to CSV/JSON — one click
  • Inspect schema — columns, types, indexes, all visible

No more terminal gymnastics. No more copy-pasting JSON from wrangler output. Just open MyD1 and see your data.

The full stack — recap

LayerTechnologyCost
Frontend + SSRSvelteKit on Cloudflare WorkersFree
DatabaseCloudflare D1 (SQLite at the edge)Free
File storageCloudflare R2Free
DomainAny registrar~$14/year
Database GUIMyD1Free / Pro
Hosting, CDN, SSLCloudflare (automatic)Free

No AWS bills. No Vercel bandwidth limits. No Docker containers. No database servers to maintain. Just code, deploy, and it's live — globally.

Going further

From here, you can:

  • Add authentication with Cloudflare Access or a simple cookie-based auth
  • Use Drizzle ORM for type-safe queries instead of raw SQL
  • Add full-text searchD1 supports SQLite FTS5
  • Set up a cron trigger with Workers Cron to run scheduled tasks
  • Add KV for caching frequently accessed data

The Cloudflare ecosystem gives you everything a backend needs — for free at small scale, and very cheap as you grow. And with MyD1, you always have a clear window into your data.

Download MyD1 and connect to your first D1 database in under a minute.

Related: Getting Started with D1 · D1 vs MySQL vs PostgreSQL · Cloudflare vs Vercel