Created
March 25, 2025 23:31
-
-
Save curtis-arch/d560ecd9a0ab13a4b7d66b1f76be9e38 to your computer and use it in GitHub Desktop.
Durable Objects Database Per User
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file is a merged representation of the entire codebase, combined into a single document by Repomix. The content has been processed where security check has been disabled. | |
# File Summary | |
## Purpose | |
This file contains a packed representation of the entire repository's contents. | |
It is designed to be easily consumable by AI systems for analysis, code review, | |
or other automated processes. | |
## File Format | |
The content is organized as follows: | |
1. This summary section | |
2. Repository information | |
3. Directory structure | |
4. Multiple file entries, each consisting of: | |
a. A header with the file path (## File: path/to/file) | |
b. The full contents of the file in a code block | |
## Usage Guidelines | |
- This file should be treated as read-only. Any changes should be made to the | |
original repository files, not this packed version. | |
- When processing this file, use the file path to distinguish | |
between different files in the repository. | |
- Be aware that this file may contain sensitive information. Handle it with | |
the same level of security as you would the original repository. | |
## Notes | |
- Some files may have been excluded based on .gitignore rules and Repomix's configuration | |
- Binary files are not included in this packed representation. Please refer to the Repository Structure section for a complete list of file paths, including binary files | |
- Files matching patterns in .gitignore are excluded | |
- Files matching default ignore patterns are excluded | |
- Security check has been disabled - content may contain sensitive information | |
## Additional Info | |
# Directory Structure | |
``` | |
durable-objects-database-per-user/ | |
drizzle/ | |
meta/ | |
_journal.json | |
0000_snapshot.json | |
0000_great_bloodscream.sql | |
migrations.js | |
src/ | |
db/ | |
index.ts | |
notes.ts | |
schemas.ts | |
types.ts | |
index.ts | |
bindings.ts | |
drizzle.config.ts | |
package.json | |
README.md | |
tsconfig.json | |
wrangler.json | |
.gitignore | |
LICENSE | |
README.md | |
``` | |
# Files | |
## File: durable-objects-database-per-user/drizzle/meta/_journal.json | |
````json | |
{ | |
"version": "7", | |
"dialect": "sqlite", | |
"entries": [ | |
{ | |
"idx": 0, | |
"version": "6", | |
"when": 1742764490428, | |
"tag": "0000_great_bloodscream", | |
"breakpoints": true | |
} | |
] | |
} | |
```` | |
## File: durable-objects-database-per-user/drizzle/meta/0000_snapshot.json | |
````json | |
{ | |
"version": "6", | |
"dialect": "sqlite", | |
"id": "a2c707e4-eaab-4a26-94db-707ca0c63f63", | |
"prevId": "00000000-0000-0000-0000-000000000000", | |
"tables": { | |
"notes": { | |
"name": "notes", | |
"columns": { | |
"id": { | |
"name": "id", | |
"type": "text", | |
"primaryKey": true, | |
"notNull": true, | |
"autoincrement": false | |
}, | |
"text": { | |
"name": "text", | |
"type": "text", | |
"primaryKey": false, | |
"notNull": true, | |
"autoincrement": false | |
}, | |
"created": { | |
"name": "created", | |
"type": "integer", | |
"primaryKey": false, | |
"notNull": true, | |
"autoincrement": false | |
}, | |
"updated": { | |
"name": "updated", | |
"type": "integer", | |
"primaryKey": false, | |
"notNull": true, | |
"autoincrement": false | |
} | |
}, | |
"indexes": {}, | |
"foreignKeys": {}, | |
"compositePrimaryKeys": {}, | |
"uniqueConstraints": {}, | |
"checkConstraints": {} | |
} | |
}, | |
"views": {}, | |
"enums": {}, | |
"_meta": { | |
"schemas": {}, | |
"tables": {}, | |
"columns": {} | |
}, | |
"internal": { | |
"indexes": {} | |
} | |
} | |
```` | |
## File: durable-objects-database-per-user/drizzle/0000_great_bloodscream.sql | |
````sql | |
CREATE TABLE `notes` ( | |
`id` text PRIMARY KEY NOT NULL, | |
`text` text NOT NULL, | |
`created` integer NOT NULL, | |
`updated` integer NOT NULL | |
); | |
```` | |
## File: durable-objects-database-per-user/drizzle/migrations.js | |
````javascript | |
import journal from './meta/_journal.json'; | |
import m0000 from './0000_great_bloodscream.sql'; | |
export default { | |
journal, | |
migrations: { | |
m0000 | |
} | |
} | |
```` | |
## File: durable-objects-database-per-user/src/db/index.ts | |
````typescript | |
import { eq } from "drizzle-orm"; | |
import { notes } from "./notes"; | |
import { DB, InsertNote, Note } from "./types"; | |
export async function create(db: DB, note: InsertNote): Promise<Note> { | |
const [res] = await db | |
.insert(notes) | |
.values(note) | |
.onConflictDoUpdate({ | |
target: [notes.id], | |
set: note, | |
}) | |
.returning(); | |
return res; | |
} | |
export async function del(db: DB, params: { id: string }): Promise<Note> { | |
const [note] = await db | |
.delete(notes) | |
.where(eq(notes.id, params.id)) | |
.returning(); | |
return note; | |
} | |
export async function get(db: DB, params: { id: string }): Promise<Note | null> { | |
const [result] = await db | |
.select() | |
.from(notes) | |
.where(eq(notes.id, params.id)); | |
if (!result) return null; | |
return result; | |
} | |
export async function list(db: DB): Promise<Note[]> { | |
const ns = await db | |
.select() | |
.from(notes) | |
return ns; | |
} | |
```` | |
## File: durable-objects-database-per-user/src/db/notes.ts | |
````typescript | |
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; | |
function randomString(length = 16): string { | |
const chars = "abcdefghijklmnopqrstuvwxyz"; | |
const resultArray = new Array(length); | |
for (let i = 0; i < length; i++) { | |
const randomIndex = Math.floor(Math.random() * chars.length); | |
resultArray[i] = chars[randomIndex]; | |
} | |
return resultArray.join(""); | |
} | |
export const notes = sqliteTable( | |
"notes", | |
{ | |
id: text("id") | |
.notNull() | |
.primaryKey() | |
.$defaultFn(() => `note_${randomString()}`), | |
text: text("text").notNull(), | |
created: integer("created", { mode: "timestamp_ms" }) | |
.$defaultFn(() => new Date()) | |
.notNull(), | |
updated: integer("updated", { mode: "timestamp_ms" }) | |
.$onUpdate(() => new Date()) | |
.notNull(), | |
}, | |
); | |
```` | |
## File: durable-objects-database-per-user/src/db/schemas.ts | |
````typescript | |
export * from "./notes"; | |
```` | |
## File: durable-objects-database-per-user/src/db/types.ts | |
````typescript | |
import type { DrizzleSqliteDODatabase } from "drizzle-orm/durable-sqlite"; | |
import type * as schema from "./schemas"; | |
import { notes } from "./notes"; | |
export type DB = DrizzleSqliteDODatabase<typeof schema>; | |
export type Note = typeof notes.$inferSelect; | |
export type InsertNote = typeof notes.$inferInsert; | |
```` | |
## File: durable-objects-database-per-user/src/index.ts | |
````typescript | |
import { DurableObject } from 'cloudflare:workers'; | |
import { Hono } from 'hono' | |
import { Bindings } from '../bindings'; | |
import { drizzle } from 'drizzle-orm/durable-sqlite'; | |
import { migrate } from 'drizzle-orm/durable-sqlite/migrator'; | |
import migrations from '../drizzle/migrations'; | |
import * as schema from "./db/schemas"; | |
import * as notes from "./db/index"; | |
import { DB } from './db/types'; | |
const app = new Hono<{ Bindings: Bindings }>(); | |
app.get('/', async (c) => { | |
return c.json({ message: "Hello World!" }) | |
}); | |
export default { | |
async fetch(request: Request, env: Bindings, ctx: ExecutionContext): Promise<Response> { | |
return app.fetch(request, env, ctx); | |
}, | |
}; | |
function getDurableDatabaseStub(env: Bindings, userId: string) { | |
const doId = env.DurableDatabase.idFromName(userId); | |
return env.DurableDatabase.get(doId); | |
} | |
// Create a note for a user | |
app.post('/:userId', async (c) => { | |
const userId = c.req.param("userId"); | |
const { text } = await c.req.json(); | |
const stub = getDurableDatabaseStub(c.env, userId); | |
const note = await stub.notesCreate({ text }); | |
return c.json({ note }) | |
}); | |
// List all notes for a user | |
app.get('/:userId', async (c) => { | |
const userId = c.req.param("userId"); | |
const stub = getDurableDatabaseStub(c.env, userId); | |
const notes = await stub.notesList() | |
return c.json({ notes }) | |
}); | |
// Get a specific note for a user | |
app.get('/:userId/:noteId', async (c) => { | |
const userId = c.req.param("userId"); | |
const noteId = c.req.param("noteId"); | |
const stub = getDurableDatabaseStub(c.env, userId); | |
const note = await stub.notesGet({ id: noteId }); | |
if (!note) { | |
return c.notFound(); | |
} | |
return c.json({ note }) | |
}); | |
// Delete a note for a user | |
app.delete('/:userId/:noteId', async (c) => { | |
const userId = c.req.param("userId"); | |
const noteId = c.req.param("noteId"); | |
const stub = getDurableDatabaseStub(c.env, userId); | |
const note = await stub.notesDel({ id: noteId }); | |
return c.json({ note }) | |
}); | |
export class DurableDatabase extends DurableObject { | |
private db: DB; | |
constructor(ctx: DurableObjectState, env: Bindings) { | |
super(ctx, env); | |
// Initialize Drizzle with the Durable Object's storage | |
this.db = drizzle(ctx.storage, { schema, logger: true }); | |
// Run migrations before accepting any requests | |
ctx.blockConcurrencyWhile(async () => { | |
await this._migrate(); | |
}); | |
} | |
async notesCreate(note: Parameters<typeof notes.create>[1]): ReturnType<typeof notes.create> { | |
return await notes.create(this.db, note); | |
} | |
async notesGet(params: Parameters<typeof notes.get>[1]): ReturnType<typeof notes.get> { | |
return await notes.get(this.db, params); | |
} | |
async notesList(): ReturnType<typeof notes.list> { | |
return await notes.list(this.db); | |
} | |
async notesDel(params: Parameters<typeof notes.get>[1]): ReturnType<typeof notes.del> { | |
return await notes.del(this.db, params); | |
} | |
private async _migrate() { | |
await migrate(this.db, migrations); | |
} | |
} | |
```` | |
## File: durable-objects-database-per-user/bindings.ts | |
````typescript | |
import type { DurableDatabase } from "./src/index.ts"; | |
export type Bindings = { | |
DurableDatabase: DurableObjectNamespace<DurableDatabase>; | |
}; | |
```` | |
## File: durable-objects-database-per-user/drizzle.config.ts | |
````typescript | |
import { defineConfig } from 'drizzle-kit'; | |
export default defineConfig({ | |
out: './drizzle', | |
schema: './src/db/schemas.ts', | |
dialect: 'sqlite', | |
driver: 'durable-sqlite', | |
}); | |
```` | |
## File: durable-objects-database-per-user/package.json | |
````json | |
{ | |
"name": "durable-objects-database-per-user", | |
"main": "src/index.ts", | |
"scripts": { | |
"drizzle:generate": "drizzle-kit generate", | |
"deploy": "wrangler deploy src/index.ts", | |
"dev": "wrangler dev src/index.ts", | |
"start": "wrangler dev src/index.ts" | |
}, | |
"dependencies": { | |
"drizzle-orm": "^0.41.0", | |
"hono": "^4.4.6" | |
}, | |
"devDependencies": { | |
"@cloudflare/vitest-pool-workers": "^0.7.4", | |
"@cloudflare/workers-types": "^4.20250312.0", | |
"@types/node": "^22.13.8", | |
"drizzle-kit": "^0.30.5", | |
"wrangler": "^4.4.0" | |
} | |
} | |
```` | |
## File: durable-objects-database-per-user/README.md | |
````markdown | |
# Durable Objects Database Per User | |
Reference code for the blog post: [Cloudflare Workers 101: One Database Per User with Cloudflare Durable Objects and Drizzle ORM](https://boristane.com/blog/durable-objects-database-per-user) | |
A Cloudflare Workers application that demonstrates how to create isolated databases for each user using Durable Objects and Drizzle ORM. | |
## Table of Contents | |
- [Overview](#overview) | |
- [Features](#features) | |
- [Setup](#setup) | |
- [Usage](#usage) | |
- [Architecture](#architecture) | |
## Overview | |
This project showcases a powerful pattern for multi-tenant applications on Cloudflare Workers: creating one SQLite database per user with Durable Objects. By leveraging Drizzle ORM, we get type-safe database operations while maintaining complete isolation between users' data stores. | |
## Features | |
- **Isolated User Databases**: Each user gets their own SQLite database | |
- **Type-safe ORM**: Uses Drizzle ORM for database operations | |
- **Automatic Migrations**: Database schema is managed through Drizzle migrations | |
- **RESTful API**: Simple API for interacting with user notes | |
- **TypeScript Support**: Full type safety throughout the codebase | |
## Setup | |
1. Install dependencies: | |
```bash | |
npm install | |
``` | |
2. Generate the initial database migration: | |
```bash | |
npm run drizzle:generate | |
``` | |
3. Deploy to Cloudflare: | |
```bash | |
npm run deploy | |
``` | |
### NPM Scripts | |
- **drizzle:generate**: Generates SQL migration files based on schema changes | |
- **deploy**: Deploys the application to Cloudflare Workers | |
- **dev**: Starts the application in development mode | |
- **start**: Alias for development mode | |
## Usage | |
Once deployed, you can interact with the API to create and manage notes for different users. | |
### API Endpoints | |
- **POST /:userId**: Creates a new note for a user | |
- **Request**: `{ "text": "Note content" }` | |
- **Response**: `{ "note": { "id": "note_xyz", "text": "Note content", ... } }` | |
- **Curl Command**: | |
```bash | |
curl -X POST https://durable-objects-database-per-user.<account-name>.workers.dev/john \ | |
-H "Content-Type: application/json" \ | |
-d '{"text":"Buy groceries"}' | |
# Response: | |
# { | |
# "note": { | |
# "id": "note_jxqegmbonzvdstpy", | |
# "text": "Buy groceries", | |
# "created": "2025-03-23T15:42:18.760Z", | |
# "updated": "2025-03-23T15:42:18.760Z" | |
# } | |
# } | |
``` | |
- **GET /:userId**: Retrieves all notes for a user | |
- **Response**: `{ "notes": [ ... ] }` | |
- **Curl Command**: | |
```bash | |
curl https://durable-objects-database-per-user.<account-name>.workers.dev/john | |
# Response: | |
# { | |
# "notes": [ | |
# { | |
# "id": "note_jxqegmbonzvdstpy", | |
# "text": "Buy groceries", | |
# "created": "2025-03-23T15:42:18.760Z", | |
# "updated": "2025-03-23T15:42:18.760Z" | |
# } | |
# ] | |
# } | |
``` | |
- **GET /:userId/:noteId**: Retrieves a specific note for a user | |
- **Response**: `{ "note": { ... } }` | |
- **Curl Command**: | |
```bash | |
curl https://durable-objects-database-per-user.<account-name>.workers.dev/john/note_jxqegmbonzvdstpy | |
``` | |
- **DELETE /:userId/:noteId**: Deletes a specific note for a user | |
- **Response**: `{ "note": { ... } }` | |
- **Curl Command**: | |
```bash | |
curl -X DELETE https://durable-objects-database-per-user.<account-name>.workers.dev/john/note_jxqegmbonzvdstpy | |
``` | |
## Architecture | |
The application uses Cloudflare Workers with Durable Objects to provide isolated SQLite databases for each user. The Hono framework handles routing, while Drizzle ORM manages database operations. | |
### System Diagram | |
```mermaid | |
graph TD; | |
A[Client] -->|API Request| B[Cloudflare Worker] | |
B -->|Route to User's DO| C[Durable Object] | |
C -->|SQLite Operations| D[User Database] | |
D -->|Note Data| C | |
C -->|Response| B | |
B -->|API Response| A | |
``` | |
### Key Components | |
- **Hono Router**: Handles API requests and routes them to the appropriate Durable Object | |
- **Durable Objects**: Maintains state and SQLite database for a specific user | |
- **Drizzle ORM**: Provides type-safe database operations | |
```` | |
## File: durable-objects-database-per-user/tsconfig.json | |
````json | |
{ | |
"compilerOptions": { | |
"target": "es2021", | |
"lib": [ | |
"es2022" | |
], | |
"jsx": "react", | |
"module": "es2022", | |
"moduleResolution": "Bundler", | |
"types": [ | |
"@cloudflare/workers-types/experimental", | |
"@cloudflare/vitest-pool-workers", | |
"@types/node" | |
], | |
"resolveJsonModule": true, | |
"allowJs": true /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */, | |
"checkJs": false /* Enable error reporting in type-checked JavaScript files. */, | |
"noEmit": true /* Disable emitting files from a compilation. */, | |
"isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */, | |
"allowSyntheticDefaultImports": true /* Allow 'import x from y' when a module doesn't have a default export. */, | |
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, | |
"strict": true /* Enable all strict type-checking options. */, | |
"skipLibCheck": true /* Skip type checking all .d.ts files. */, | |
"noImplicitAny": true, | |
"strictNullChecks": true, | |
"strictFunctionTypes": true, | |
"strictPropertyInitialization": true, | |
"noImplicitThis": true, | |
"alwaysStrict": true, | |
}, | |
"include": [ | |
"./**/*.ts" | |
], | |
"exclude": [] | |
} | |
```` | |
## File: durable-objects-database-per-user/wrangler.json | |
````json | |
{ | |
"name": "durable-objects-database-per-user", | |
"compatibility_date": "2024-11-12", | |
"workers_dev": true, | |
"upload_source_maps": true, | |
"observability": { | |
"enabled": true | |
}, | |
"main": "./src/index.ts", | |
"migrations": [ | |
{ | |
"new_sqlite_classes": [ | |
"DurableDatabase" | |
], | |
"tag": "v1" | |
} | |
], | |
"rules": [ | |
{ | |
"type": "Text", | |
"globs": [ | |
"**/*.sql" | |
], | |
"fallthrough": true | |
} | |
], | |
"durable_objects": { | |
"bindings": [ | |
{ | |
"class_name": "DurableDatabase", | |
"name": "DurableDatabase" | |
} | |
] | |
}, | |
"vars": {} | |
} | |
```` | |
## File: .gitignore | |
```` | |
# Logs | |
logs | |
*.log | |
npm-debug.log* | |
yarn-debug.log* | |
yarn-error.log* | |
lerna-debug.log* | |
.pnpm-debug.log* | |
# Diagnostic reports (https://nodejs.org/api/report.html) | |
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json | |
# Runtime data | |
pids | |
*.pid | |
*.seed | |
*.pid.lock | |
# Directory for instrumented libs generated by jscoverage/JSCover | |
lib-cov | |
# Coverage directory used by tools like istanbul | |
coverage | |
*.lcov | |
# nyc test coverage | |
.nyc_output | |
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) | |
.grunt | |
# Bower dependency directory (https://bower.io/) | |
bower_components | |
# node-waf configuration | |
.lock-wscript | |
# Compiled binary addons (https://nodejs.org/api/addons.html) | |
build/Release | |
# Dependency directories | |
node_modules/ | |
jspm_packages/ | |
# Snowpack dependency directory (https://snowpack.dev/) | |
web_modules/ | |
# TypeScript cache | |
*.tsbuildinfo | |
# Optional npm cache directory | |
.npm | |
# Optional eslint cache | |
.eslintcache | |
# Optional stylelint cache | |
.stylelintcache | |
# Microbundle cache | |
.rpt2_cache/ | |
.rts2_cache_cjs/ | |
.rts2_cache_es/ | |
.rts2_cache_umd/ | |
# Optional REPL history | |
.node_repl_history | |
# Output of 'npm pack' | |
*.tgz | |
# Yarn Integrity file | |
.yarn-integrity | |
# dotenv environment variable files | |
.env | |
.env.development.local | |
.env.test.local | |
.env.production.local | |
.env.local | |
# parcel-bundler cache (https://parceljs.org/) | |
.cache | |
.parcel-cache | |
# Next.js build output | |
.next | |
out | |
# Nuxt.js build / generate output | |
.nuxt | |
dist | |
# Gatsby files | |
.cache/ | |
# Comment in the public line in if your project uses Gatsby and not Next.js | |
# https://nextjs.org/blog/next-9-1#public-directory-support | |
# public | |
# vuepress build output | |
.vuepress/dist | |
# vuepress v2.x temp and cache directory | |
.temp | |
.cache | |
# vitepress build output | |
**/.vitepress/dist | |
# vitepress cache directory | |
**/.vitepress/cache | |
# Docusaurus cache and generated files | |
.docusaurus | |
# Serverless directories | |
.serverless/ | |
# FuseBox cache | |
.fusebox/ | |
# DynamoDB Local files | |
.dynamodb/ | |
# TernJS port file | |
.tern-port | |
# Stores VSCode versions used for testing VSCode extensions | |
.vscode-test | |
# yarn v2 | |
.yarn/cache | |
.yarn/unplugged | |
.yarn/build-state.yml | |
.yarn/install-state.gz | |
.pnp.* | |
```` | |
## File: LICENSE | |
```` | |
MIT License | |
Copyright (c) 2025 Boris Tane | |
Permission is hereby granted, free of charge, to any person obtaining a copy | |
of this software and associated documentation files (the "Software"), to deal | |
in the Software without restriction, including without limitation the rights | |
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
copies of the Software, and to permit persons to whom the Software is | |
furnished to do so, subject to the following conditions: | |
The above copyright notice and this permission notice shall be included in all | |
copies or substantial portions of the Software. | |
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
SOFTWARE. | |
```` | |
## File: README.md | |
````markdown | |
# cloudflare-dev-101 | |
A set of demos implementing patterns on Cloudflare Developer Platform | |
```` |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment