Making the Most of Vercel’s AI SDK with Cloudflare Durable Objects Developer Adam Rackis published a walkthrough of running Vercel's AI SDK inside Cloudflare Durable Objects, using each object's built-in SQLite database and WebSocket support to persist and push long-running AI workout-generation requests that previously took about 30 seconds and were lost on page refresh. The WorkoutTemplateAIGenerationDO class, roughly 250 lines and using Drizzle ORM for typed SQLite access, saves prompts and results so a refreshed browser can query current and past generations; the code is on the feature/ai-workout-template-generation branch of the fitness-tracker repository. I’ve written about Vercel’s AI SDK and AI Gateway before https://blog.master.dev/having-fun-with-vercels-ai-sdk-and-ai-gateway/ . That post covered the basics of setting up an account in the AI Gateway or directly in a provider of your choice , and making requests against an AI model while constraining the structure of the data it sent back, to ensure you could use it in your application: if your database is expecting a field called weight , things won’t work well if the LLM sends back that data in a field called bodyweight . That post used an existing fitness tracker app I’ve been toying with. It set up a rudimentary UI for the user to provide the LLM with some reference workouts, and a prompt to produce new workouts. The AI SDK received the prompt, the reference workouts, and a list of all exercises; it sent back commentary along with the generated workouts. To keep things simple, I set up a basic modal that simply showed a spinner while the request was being processed which usually takes about 30 seconds, or even more . When the request finished, the workouts displayed, along with a save button if the user wanted to save them into their account. The limitations of this UX should be obvious. If the user refreshed the page while the request was in flight, everything would be lost. If the user even refreshed the page after those results were in the modal, they’d also be lost. Granted, the latter is easy to fix: we could save those results in our own database for later recall. But this post wraps everything into one cohesive UI with one of my favorite infrastructure primitives: Cloudflare Durable Objects. I have also written about Durable Objects https://blog.master.dev/durable-objects-on-cloudflare/ DOs before. The elevator pitch for DOs is that they’re like a regular Cloudflare Worker, except instead of being ephemeral, and spun up quickly to serve a request before dying off, they come with persistent storage SQLite , and even have built-in WebSocket support. Oh, and as the name implies, they’re durable. They’re expected to be long-lived and hibernate at no cost when not in use. You define a DO with a class, and then instantiate it with whatever unique IDs you want one per user, or whatever you can imagine . Each one you spin up has its own dedicated SQLite database and collection of WebSocket connections. This provides us all the missing primitives we need. When the user hits the “Generate” button to run their prompt, we run it on the Durable Object and save it to SQLite. When the request is finished, we again save it in SQLite and then use a WebSocket to push the result to the user’s browser. And if the user refreshes the page, we can hit up that same DO and ask it to query its SQLite db for current prompts, past prompts, etc. I obviously won’t show every line of code, but the repo is here https://github.com/arackaf/fitness-tracker . This is currently a work in progress in the feature/ai-workout-template-generation branch, but by the time you read this, it might be in main . Let’s get started. Here’s an initial, incomplete segment of our Durable Object; the whole thing is about 250 lines, so we’ll just show the important concepts. js import { DurableObject, env } from "cloudflare:workers"; import { drizzle, type DrizzleSqliteDODatabase } from "drizzle-orm/durable-sqlite"; export class WorkoutTemplateAIGenerationDO extends DurableObject { db: DrizzleSqliteDODatabase; constructor ctx: DurableObjectState, env: Env { super ctx, env ; ctx.blockConcurrencyWhile async = { ctx.storage.sql.exec initialWorkoutTemplateDDL ; } ; this.db = drizzle ctx.storage ; } } I like using Drizzle https://orm.drizzle.team/ for data access. It’s basically a TypeScript API that very closely mirrors actual SQL, but with auto-complete and static typings to help prevent invalid queries. That’s what this declares db: DrizzleSqliteDODatabase; In the constructor, I use the ctx.blockConcurrencyWhile helper to essentially lock this DO until the code in the callback is finished. This ensures the current DO runs my SQL migration script, if needed, and prevents other requests from running while the DB is in an inconsistent state. I put it in the constructor, so it runs every time a Durable Object instance is created, or re-created from hibernation. The DDL is therefore structured with things like CREATE TABLE IF NOT EXISTS to only create schema objects if they’re not there already. Then I instantiate the drizzle object. I covered this in detail in my prior Durable Objects post https://blog.master.dev/durable-objects-on-cloudflare/ , but to accept and set up WebSocket connections you need a fetch method which takes the raw request, inside of which we call some built-in Cloudflare utilities to establish and save the connection. fetch request: Request : Response { if request.headers.get "Upgrade" == "websocket" { return new Response "Expected WebSocket", { status: 426, } ; } // ... const pair = new WebSocketPair ; const client = pair 0 ; const server = pair 1 ; this.ctx.acceptWebSocket server ; return new Response null, { status: 101, webSocket: client, } ; } To get all open sockets for this durable object, we call this.ctx.getWebSockets and use the send method accordingly. js sendMessage payload: Object { for const socket of this.ctx.getWebSockets { try { socket.send JSON.stringify payload ; } catch { // The socket may have disconnected before Cloudflare observed it. socket.close 1011, "Unable to send message" ; } } } Simple and humble. If you’re curious how to get a raw connection into the DO, so we can establish a WebSocket connection, the trick is to use what most meta-frameworks call an API route and which TanStack calls a server route . You establish your connection to that , and that API route simply forwards proxies the request to the Durable Object. js import { getWorkoutTemplateAIGenerationDurableObject } from "@/durable-objects/WorkoutTemplateAIGeneration/do"; import { createFileRoute } from "@tanstack/react-router"; export const Route = createFileRoute "/app/admin/workout-templates/ai/$id/subscribe" { server: { handlers: { GET: async { request, context } = { const cart = await getWorkoutTemplateAIGenerationDurableObject context ; return cart.fetch request ; }, }, }, } ; along with a bit of helper code to send the request to the right place, no matter whether you’re in production or development mode. export function openWorkoutTemplateWebSocket sessionId: string, lastPromptId?: number { return new Promise