# Deno Support Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Add Deno runtime support to pds.js by abstracting the SQLite driver and creating a Deno HTTP/WebSocket adapter. **Architecture:** Create a minimal SQLite driver interface that both `better-sqlite3` (Node) and `node:sqlite` (Deno 2.2+) implement natively. Add a new `@pdsjs/deno` package for the HTTP server adapter using `Deno.serve()`. Reuse `@pdsjs/storage-sqlite` unchanged except for type annotations. **Tech Stack:** Deno 2.2+, node:sqlite, Deno.serve(), TypeScript/JSDoc --- ## Task 1: Add SQLite Driver Interface Types **Files:** - Create: `packages/storage-sqlite/driver.js` **Step 1: Create the driver interface file** ```javascript // @pdsjs/storage-sqlite/driver - SQLite driver interface types // Both better-sqlite3 and node:sqlite implement this interface natively /** * @typedef {Object} SQLiteStatement * @property {(...args: unknown[]) => unknown} get - Single row query * @property {(...args: unknown[]) => unknown[]} all - Multi-row query * @property {(...args: unknown[]) => {changes: number}} run - Execute INSERT/UPDATE/DELETE */ /** * @typedef {Object} SQLiteDatabase * @property {(sql: string) => void} exec - Execute raw SQL (DDL, multiple statements) * @property {(sql: string) => SQLiteStatement} prepare - Create prepared statement */ export {}; ``` **Step 2: Verify file created** Run: `cat packages/storage-sqlite/driver.js` Expected: File contents shown **Step 3: Commit** ```bash git add packages/storage-sqlite/driver.js git commit -m "feat(storage-sqlite): add SQLite driver interface types" ``` --- ## Task 2: Update Storage Functions to Use Driver Interface **Files:** - Modify: `packages/storage-sqlite/index.js` (lines 24-28 and 314-318) **Step 1: Update createActorStorage type annotation** Change line 28 from: ```javascript * @param {import('better-sqlite3').Database} db ``` to: ```javascript * @param {import('./driver.js').SQLiteDatabase} db ``` **Step 2: Update createSharedStorage type annotation** Change line 318 from: ```javascript * @param {import('better-sqlite3').Database} db ``` to: ```javascript * @param {import('./driver.js').SQLiteDatabase} db ``` **Step 3: Run existing tests to verify no breakage** Run: `pnpm test` Expected: All tests pass **Step 4: Commit** ```bash git add packages/storage-sqlite/index.js git commit -m "feat(storage-sqlite): use driver interface instead of better-sqlite3 types" ``` --- ## Task 3: Export Driver Types from Package **Files:** - Modify: `packages/storage-sqlite/package.json` **Step 1: Add driver.js to exports** Add to the exports field: ```json { "exports": { ".": "./index.js", "./driver": "./driver.js" } } ``` **Step 2: Verify package.json is valid** Run: `node -e "console.log(JSON.parse(require('fs').readFileSync('packages/storage-sqlite/package.json')))"` Expected: No JSON parse errors **Step 3: Commit** ```bash git add packages/storage-sqlite/package.json git commit -m "feat(storage-sqlite): export driver interface types" ``` --- ## Task 4: Create @pdsjs/deno Package Structure **Files:** - Create: `packages/deno/package.json` - Create: `packages/deno/deno.json` **Step 1: Create package.json** ```json { "name": "@pdsjs/deno", "version": "0.7.0", "type": "module", "exports": { ".": "./index.js" }, "dependencies": { "@pdsjs/core": "workspace:*" } } ``` **Step 2: Create deno.json** ```json { "name": "@pdsjs/deno", "version": "0.7.0", "exports": "./index.js", "imports": { "@pdsjs/core": "../core/index.js", "@pdsjs/core/": "../core/" } } ``` **Step 3: Commit** ```bash git add packages/deno/package.json packages/deno/deno.json git commit -m "feat(deno): create package structure" ``` --- ## Task 5: Implement Deno WebSocket Port **Files:** - Create: `packages/deno/index.js` **Step 1: Create the WebSocket port implementation** ```javascript // @pdsjs/deno - Deno HTTP server adapter /** * Create WebSocket port for Deno * @param {Map} upgradeMap * @returns {import('@pdsjs/core/ports.js').WebSocketPort} */ export function createWebSocketPort(upgradeMap) { /** @type {Set} */ const clients = new Set(); return { isUpgrade(request) { return request.headers.get('upgrade')?.toLowerCase() === 'websocket'; }, upgrade(request, onConnect) { const upgrade = Deno.upgradeWebSocket(request); const { socket, response } = upgrade; clients.add(socket); socket.onclose = () => clients.delete(socket); socket.onopen = () => { onConnect({ send(data) { if (socket.readyState === WebSocket.OPEN) { socket.send(data); } }, close() { socket.close(); }, }); }; return response; }, broadcast(data) { for (const client of clients) { if (client.readyState === WebSocket.OPEN) { try { client.send(data); } catch { // Client may have disconnected } } } }, }; } ``` **Step 2: Commit** ```bash git add packages/deno/index.js git commit -m "feat(deno): implement WebSocket port" ``` --- ## Task 6: Implement Deno HTTP Server **Files:** - Modify: `packages/deno/index.js` **Step 1: Add createServer function** Append to `packages/deno/index.js`: ```javascript import { PersonalDataServer } from '@pdsjs/core'; /** * @typedef {Object} DenoServerOptions * @property {import('@pdsjs/core/ports.js').ActorStoragePort} actorStorage * @property {import('@pdsjs/core/ports.js').SharedStoragePort} sharedStorage * @property {import('@pdsjs/core/ports.js').BlobPort} blobs * @property {string} jwtSecret * @property {number} [port=3000] * @property {string} [hostname] * @property {string} [appviewUrl] * @property {string} [appviewDid] * @property {string} [relayUrl] * @property {string} [password] */ /** * @typedef {Object} DenoServer * @property {PersonalDataServer} pds * @property {Deno.HttpServer} server * @property {() => void} close */ /** * Create Deno PDS server * @param {DenoServerOptions} options * @returns {DenoServer} */ export function createServer({ actorStorage, sharedStorage, blobs, jwtSecret, port = 3000, hostname, appviewUrl, appviewDid, relayUrl, password, }) { /** @type {Map} */ const upgradeMap = new Map(); const webSocket = createWebSocketPort(upgradeMap); const pds = new PersonalDataServer({ actorStorage, sharedStorage, blobs, webSocket, jwtSecret, hostname, appviewUrl, appviewDid, relayUrl, password, }); const server = Deno.serve({ port }, async (request) => { // Handle WebSocket upgrade if (webSocket.isUpgrade(request)) { return pds.fetch(request); } // Handle regular HTTP return pds.fetch(request); }); // Cleanup interval (every hour) const cleanupInterval = setInterval( async () => { await pds.runBlobCleanup(); await sharedStorage.cleanupExpiredDpopJtis(); }, 60 * 60 * 1000, ); return { pds, server, port, close() { clearInterval(cleanupInterval); server.shutdown(); }, }; } /** * Create server from environment variables * @param {Object} deps - Storage dependencies * @param {import('@pdsjs/core/ports.js').ActorStoragePort} deps.actorStorage * @param {import('@pdsjs/core/ports.js').SharedStoragePort} deps.sharedStorage * @param {import('@pdsjs/core/ports.js').BlobPort} deps.blobs * @returns {DenoServer} */ export function createServerFromEnv({ actorStorage, sharedStorage, blobs }) { return createServer({ actorStorage, sharedStorage, blobs, jwtSecret: Deno.env.get('JWT_SECRET') || 'development-secret', port: parseInt(Deno.env.get('PORT') || '3000', 10), hostname: Deno.env.get('HOSTNAME'), appviewUrl: Deno.env.get('APPVIEW_URL'), appviewDid: Deno.env.get('APPVIEW_DID'), relayUrl: Deno.env.get('RELAY_URL'), password: Deno.env.get('PDS_PASSWORD'), }); } ``` **Step 2: Run type check** Run: `pnpm exec tsc --noEmit` Expected: No type errors **Step 3: Commit** ```bash git add packages/deno/index.js git commit -m "feat(deno): implement HTTP server with createServer" ``` --- ## Task 7: Create Deno Example **Files:** - Create: `examples/deno/main.ts` - Create: `examples/deno/deno.json` **Step 1: Create deno.json** ```json { "imports": { "@pdsjs/core": "../../packages/core/index.js", "@pdsjs/core/": "../../packages/core/", "@pdsjs/deno": "../../packages/deno/index.js", "@pdsjs/storage-sqlite": "../../packages/storage-sqlite/index.js", "@pdsjs/blobs-s3": "../../packages/blobs-s3/index.js" } } ``` **Step 2: Create main.ts** ```typescript import { DatabaseSync } from 'node:sqlite'; import { createActorStorage, createSharedStorage } from '@pdsjs/storage-sqlite'; import { createServer } from '@pdsjs/deno'; // Initialize SQLite database const dbPath = Deno.env.get('PDS_DB_PATH') || './pds.db'; const db = new DatabaseSync(dbPath); // Create storage adapters const actorStorage = createActorStorage(db); const sharedStorage = createSharedStorage(db); // For blobs, use S3 or implement Deno filesystem adapter // This example uses a minimal in-memory blob store for demo purposes const blobs = { async get(_did: string, _cid: string) { return null; // TODO: implement with Deno.readFile or S3 }, async put(_did: string, _cid: string, _data: Uint8Array, _mimeType: string) { // TODO: implement }, async delete(_did: string, _cid: string) { // TODO: implement }, }; // Create and start server const server = createServer({ actorStorage, sharedStorage, blobs, jwtSecret: Deno.env.get('JWT_SECRET') || 'development-secret', port: parseInt(Deno.env.get('PORT') || '3000', 10), hostname: Deno.env.get('HOSTNAME'), appviewUrl: Deno.env.get('APPVIEW_URL'), appviewDid: Deno.env.get('APPVIEW_DID'), relayUrl: Deno.env.get('RELAY_URL'), password: Deno.env.get('PDS_PASSWORD'), }); console.log(`PDS running on http://localhost:${server.port}`); // Handle shutdown Deno.addSignalListener('SIGINT', () => { console.log('Shutting down...'); server.close(); db.close(); Deno.exit(0); }); ``` **Step 3: Commit** ```bash git add examples/deno/main.ts examples/deno/deno.json git commit -m "feat(examples): add Deno example" ``` --- ## Task 8: Add Deno Filesystem Blobs (Optional) **Files:** - Create: `packages/blobs-deno/index.js` - Create: `packages/blobs-deno/package.json` - Create: `packages/blobs-deno/deno.json` **Step 1: Create package.json** ```json { "name": "@pdsjs/blobs-deno", "version": "0.7.0", "type": "module", "exports": { ".": "./index.js" } } ``` **Step 2: Create deno.json** ```json { "name": "@pdsjs/blobs-deno", "version": "0.7.0", "exports": "./index.js" } ``` **Step 3: Create index.js** ```javascript // @pdsjs/blobs-deno - Deno filesystem blob storage import { join, dirname } from 'node:path'; /** * Create filesystem blob storage for Deno * @param {string} baseDir - Base directory for blob storage * @returns {import('@pdsjs/core/ports.js').BlobPort} */ export function createDenoBlobs(baseDir) { /** * Get blob file path * @param {string} did * @param {string} cid */ function blobPath(did, cid) { // Use first 2 chars of CID for sharding const shard = cid.slice(0, 2); return join(baseDir, did, shard, cid); } return { async get(did, cid) { try { const path = blobPath(did, cid); const data = await Deno.readFile(path); const metaPath = `${path}.meta`; const meta = JSON.parse(await Deno.readTextFile(metaPath)); return { data, mimeType: meta.mimeType }; } catch (e) { if (e instanceof Deno.errors.NotFound) { return null; } throw e; } }, async put(did, cid, data, mimeType) { const path = blobPath(did, cid); await Deno.mkdir(dirname(path), { recursive: true }); await Deno.writeFile(path, data); await Deno.writeTextFile(`${path}.meta`, JSON.stringify({ mimeType })); }, async delete(did, cid) { try { const path = blobPath(did, cid); await Deno.remove(path); await Deno.remove(`${path}.meta`); } catch (e) { if (!(e instanceof Deno.errors.NotFound)) { throw e; } } }, }; } ``` **Step 4: Commit** ```bash git add packages/blobs-deno/ git commit -m "feat(blobs-deno): add Deno filesystem blob storage" ``` --- ## Task 9: Update Deno Example to Use Blobs **Files:** - Modify: `examples/deno/main.ts` - Modify: `examples/deno/deno.json` **Step 1: Update deno.json imports** Add to imports: ```json "@pdsjs/blobs-deno": "../../packages/blobs-deno/index.js" ``` **Step 2: Update main.ts to use blobs-deno** Replace the placeholder blobs with: ```typescript import { createDenoBlobs } from '@pdsjs/blobs-deno'; const blobsDir = Deno.env.get('PDS_BLOBS_DIR') || './blobs'; const blobs = createDenoBlobs(blobsDir); ``` **Step 3: Commit** ```bash git add examples/deno/main.ts examples/deno/deno.json git commit -m "feat(examples): use blobs-deno in Deno example" ``` --- ## Task 10: Add README for Deno Example **Files:** - Create: `examples/deno/README.md` **Step 1: Create README** ```markdown # Deno PDS Example Minimal PDS server running on Deno. ## Requirements - Deno 2.2+ (for `node:sqlite` support) ## Quick Start ```bash cd examples/deno deno run --allow-net --allow-read --allow-write --allow-env main.ts ``` ## Environment Variables | Variable | Description | Default | |----------|-------------|---------| | `PDS_DB_PATH` | SQLite database path | `./pds.db` | | `PDS_BLOBS_DIR` | Blob storage directory | `./blobs` | | `JWT_SECRET` | JWT signing secret | `development-secret` | | `PORT` | Server port | `3000` | | `HOSTNAME` | PDS hostname | - | | `APPVIEW_URL` | AppView URL for proxying | - | | `APPVIEW_DID` | AppView DID for service auth | - | | `RELAY_URL` | Relay URL for firehose | - | | `PDS_PASSWORD` | Password for createSession | - | ## Permissions The server requires these Deno permissions: - `--allow-net` - HTTP server and outbound requests - `--allow-read` - Read database and blob files - `--allow-write` - Write database and blob files - `--allow-env` - Read environment variables ``` **Step 2: Commit** ```bash git add examples/deno/README.md git commit -m "docs(examples): add Deno example README" ``` --- ## Task 11: Test Deno Example Manually **Step 1: Run the Deno example** ```bash cd examples/deno deno run --allow-net --allow-read --allow-write --allow-env main.ts ``` Expected: Server starts, prints "PDS running on http://localhost:3000" **Step 2: Test health endpoint** ```bash curl http://localhost:3000/xrpc/_health ``` Expected: `{"version":"..."}` **Step 3: Test describe server** ```bash curl http://localhost:3000/xrpc/com.atproto.server.describeServer ``` Expected: JSON response with server description --- ## Task 12: Update Root README **Files:** - Modify: `README.md` **Step 1: Add Deno to supported platforms** Add to the platforms section: ```markdown ### Deno ```bash cd examples/deno deno run --allow-net --allow-read --allow-write --allow-env main.ts ``` Requires Deno 2.2+ for `node:sqlite` support. ``` **Step 2: Commit** ```bash git add README.md git commit -m "docs: add Deno to supported platforms" ```