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.
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
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 devcurl -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→ classNotesDO - Migration
v1withnew_sqlite_classes: ["NotesDO"]
Cloudflare Features Used
- Workers - Edge compute runtime
- Durable Objects - Per-user stub with SQLite storage
- SQLite in Durable Objects -
storage.sql.execfor the notes table