This site is not affiliated with or endorsed by Cloudflare, Inc. It simply showcases experiments built using Cloudflare services.
Cloudflare Experiments
Stateful & Async

DO SQLite Notes

Per-user notes stored in SQLite-backed Durable Objects via sql.exec

Store and retrieve per-user notes in SQLite-backed Durable Objects. Each userId maps to its own Durable Object stub; notes live in a notes table via storage.sql.exec.

API Reference

POST /notes

Create or update a note for a user.

userId string (required)

User identifier (letters, numbers, _, -).

id string (required)

Note identifier (letters, numbers, _, -).

content string (required)

Note body (max 4,000 characters).

Example Request

curl -X POST "https://your-worker.workers.dev/notes" \
  -H "Content-Type: application/json" \
  -d '{"userId":"alice","id":"n1","content":"Hello"}'

Success Response

{
  "userId": "alice",
  "id": "n1",
  "content": "Hello",
  "updatedAt": "2025-06-20T12:00:00.000Z"
}

GET /notes

Fetch one note (?userId=&id=) or list all notes for a user (?userId=).

userId string (required)

id string (optional)

When present, returns a single note; otherwise returns { userId, notes }.

Example Request

curl "https://your-worker.workers.dev/notes?userId=alice"
curl "https://your-worker.workers.dev/notes?userId=alice&id=n1"

DELETE /notes

Delete a note by user and id.

userId string (required)

id string (required)

Example Request

curl -X DELETE "https://your-worker.workers.dev/notes?userId=alice&id=n1"

Error Codes

  • 400 - Invalid userId, id, content, or body (INVALID_USER_ID, INVALID_ID, INVALID_CONTENT, INVALID_BODY)
  • 404 - Note not found (NOT_FOUND)

Use Cases

  • Learn SQLite storage inside Durable Objects (new_sqlite_classes)
  • Per-tenant or per-user isolated state without a shared D1 database
  • Prototype note or config APIs with strong consistency per user
  • Compare DO SQLite vs Workers KV for structured records

Limitations

  • Note IDs and userIds are capped to alphanumeric plus _ / -
  • Content max 4,000 characters
  • No authentication; any client can read or write by userId
  • Requires a Durable Objects binding and SQLite migration

Use in your project

Copy these files into an existing Worker. Prefer Deployment to try the full experiment first. Source: apps/experiments/do-sqlite-notes.

DependenciesNoneBindingsDurable ObjectsPlatformFetch API, Durable Objects
import { ID_PATTERN, MAX_CONTENT_LENGTH, MAX_ID_LENGTH } from "../constants/defaults";import type { Env } from "../types/env";import type { NoteRecord } from "../types/note";export function validateId(input: string | undefined): string | null {  if (!input || typeof input !== "string") return null;  const trimmed = input.trim();  if (!trimmed || trimmed.length > MAX_ID_LENGTH) return null;  if (!ID_PATTERN.test(trimmed)) return null;  return trimmed;}export function validateContent(input: string | undefined): string | null {  if (typeof input !== "string") return null;  if (!input || input.length > MAX_CONTENT_LENGTH) return null;  return input;}export function getNotesStub(env: Env, userId: string): DurableObjectStub {  const id = env.NOTES.idFromName(userId);  return env.NOTES.get(id);}export async function upsertNote(  env: Env,  userId: string,  noteId: string,  content: string): Promise<NoteRecord> {  const stub = getNotesStub(env, userId);  const response = await stub.fetch("https://notes/notes", {    method: "POST",    headers: { "Content-Type": "application/json" },    body: JSON.stringify({ id: noteId, content }),  });  if (!response.ok) {    throw new Error(`Upsert failed with status ${response.status}`);  }  return (await response.json()) as NoteRecord;}export async function getNote(  env: Env,  userId: string,  noteId: string): Promise<NoteRecord | null> {  const stub = getNotesStub(env, userId);  const response = await stub.fetch(`https://notes/notes?id=${encodeURIComponent(noteId)}`);  if (response.status === 404) return null;  if (!response.ok) {    throw new Error(`Get failed with status ${response.status}`);  }  return (await response.json()) as NoteRecord;}export async function listNotes(env: Env, userId: string): Promise<NoteRecord[]> {  const stub = getNotesStub(env, userId);  const response = await stub.fetch("https://notes/notes");  if (!response.ok) {    throw new Error(`List failed with status ${response.status}`);  }  const body = (await response.json()) as { notes: NoteRecord[] };  return body.notes;}export async function deleteNote(env: Env, userId: string, noteId: string): Promise<boolean> {  const stub = getNotesStub(env, userId);  const response = await stub.fetch(`https://notes/notes?id=${encodeURIComponent(noteId)}`, {    method: "DELETE",  });  if (response.status === 404) return false;  if (!response.ok) {    throw new Error(`Delete failed with status ${response.status}`);  }  return true;}

Deployment

Click the deploy button

Deploy to Cloudflare Workers

Deploy

Wrangler creates the NOTES Durable Object binding and applies the v1 SQLite migration for NotesDO.

Test your deployment

curl -X POST "https://your-worker.workers.dev/notes" \
  -H "Content-Type: application/json" \
  -d '{"userId":"alice","id":"n1","content":"Hello"}'

Local Development

cd apps/experiments/do-sqlite-notes
npm install
npm run dev
curl -X POST "http://localhost:8787/notes" \
  -H "Content-Type: application/json" \
  -d '{"userId":"alice","id":"n1","content":"Hello"}'

Configuration

wrangler.json declares:

  • Durable Object binding NOTES → class NotesDO
  • Migration v1 with new_sqlite_classes: ["NotesDO"]

Cloudflare Features Used

On this page