Skip to content

Instantly share code, notes, and snippets.

@curtis-arch
Created March 25, 2025 23:31
Show Gist options
  • Save curtis-arch/d560ecd9a0ab13a4b7d66b1f76be9e38 to your computer and use it in GitHub Desktop.
Save curtis-arch/d560ecd9a0ab13a4b7d66b1f76be9e38 to your computer and use it in GitHub Desktop.
Durable Objects Database Per User
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