TanStack Start + Cloudflare Durable Objects: Building a Real-Time Pixel Grid

Cloudflare Developers| 00:17:05|Mar 26, 2026
Chapters11
The host outlines building a realtime pixel grid drawing app using TanStack and durable objects, aiming for hundreds or thousands of simultaneous contributors and noting the tooling choices used in the project.

A practical, hands-on walk-through building a real-time 64x64 TanStack pixel grid with Cloudflare Durable Objects, using Cursor for agent decoding and Drizzle with D1 for persistence.

Summary

Cloudflare Developers’ video with TanStack and Durable Objects guides you through building a live pixel grid where hundreds can draw on a canvas in real time. The host starts from a starter template that includes Drizzle with D1 and a durable object, then shifts to a 64x64 grid stored as a Uint8Array, where each entry indexes a color from an extended xterm palette. Cursor is used for authoring prompts and agent decoding, demonstrating an accessible workflow alongside traditional cloud code. The presentation dives into exporting a durable object from a custom server entry point, websocket routing to the durable object, and the in-memory state management within the object, plus the corresponding database migrations managed by Drizzle. Expect a few hiccups in live coding—like migrations not running or conflicting migrations during deployment—and see how the author resolves them by regenerating migrations and redeploying. The takeaway is a concrete pattern: store grid state in a durable object, map pixels with y*width + x, and broadcast changes to connected clients via WebSocket. Finally, the host candidly shares debugging tips and deployment gotchas, making this a realistic, pain-and-gain-oriented guide for developers integrating TanStack, Durable Objects, and Drizzle in production.

Key Takeaways

  • The 64x64 pixel grid is stored as a Uint8Array in the durable object, with each entry pointing to an xterm color index.
  • Pixels are addressed by coordinates using the formula index = y * width + x, and x = index % width, y = Math.floor(index / width).
  • Migrations are generated and run via Drizzle; the plan includes a pixel_grid table with an ID and a blob for the grid data.
  • Deployment requires removing unused D1 bindings to avoid conflicts, followed by regenerating migrations and redeploying to fix issues.
  • Live coding reveals common pitfalls (e.g., migrations not running, conflicting migrations) and demonstrates practical debugging steps like using Wrangler logs and adjusting migrations on the fly.

Who Is This For?

Essential viewing for developers who want to implement real-time collaboration with TanStack, Cloudflare Durable Objects, and Drizzle/D1. It’s especially valuable for those curious about turning a canvas into a distributed, persistent grid with WebSocket synchronization.

Notable Quotes

"One of the most requested videos is around Tanstack and durable objects."
Sets up the motivation for the video and the topic.
"The pixel grid state should be stored in a durable object."
Clarifies the core data model choice.
"Index is y * 64 + x, and x is index modulo width; y is floor(index / width)."
Explains the pixel addressing math used for the grid.
"If there are two conflicting migrations, you delete old migrations and re-run to fix deployment."
Gives practical insight into a common deployment hurdle.
"Pixel grid live vibe coding 101."
Encapsulates the moment the grid goes live after fixes.

Questions This Video Answers

  • How do Cloudflare Durable Objects work with TanStack for real-time apps?
  • What are best practices for storing a pixel grid in a durable object using Drizzle and D1?
  • How can I map 2D coordinates to a 1D array for a 64x64 canvas in JavaScript?
  • What deployment pitfalls should I watch for when using Wrangler with D1 and Durable Objects?
  • How do you implement WebSocket routing to a Durable Object in a TanStack project?
Full Transcript
One of the most requested videos is around Tanstack and durable objects. Not some generic chat app, but something a little more unique. Well, today we're going to build a realtime pixel grid drawing application where hundreds or thousands of people can draw on a canvas in real time. In this video, I'll be using cursor for agent decoding because it has a nice built-in browser and is also a little more accessible than cloud code or open code. But of course, the same steps would work with those tools as well. Let's get started. So, I'm going to start from this template that I built. It has Drizzle with D1 and the durable objects that also uses Drizzle. The reason why I want to start with this starter is because agents and AIs can just follow the patterns. So, you don't have to start from scratch every time. And I'll walk through this codebase as we go. So down here, we're going to follow the instructions to scaffold the project ppm create. And I'll use this template. And we will call this 10stack pixel grid. I'm not going to use an agent MD because there's already one in this repo. We will use get and I won't deploy yet. Okay. CD 10 stack pixel grid. and let's open up cursor. So, first things first, I'm going to open the terminal and run pmppm run defaf to see what we're starting off with. Immediately, cursor will open up a browser tab here, which is pretty nice. And let me just close some of these. This is good. And we start with a tens starter application, but it also has a counter here. Okay, this is nothing groundbreaking, right? A counter will work. But if I open up my browser to localhost 3000 and let me resize this a bit here. We open up the same counter, right? You can see the durable object in action. So again, not groundbreaking, but you immediately start with a durable object that works and that's persistent. Right? So that's great. Now we want to start building. And I said we're going to use a pixel grid. So we'll just write a prompt. And inside of here, there's a couple of nice models. For today, I'm going to use Sonnet 4.6 cuz it's a nice middle ground between Opus uh knowledge and codec speed. And what we'll build is let's do true agent decoding. and I'll dictate. [clears throat] In this application, we want to build a 64x 64 pixel grid that is live editable on a canvas. The pixel grid state should be stored in a durable object. There already is a counterdurable object that you can replace with the state of our pixel grid. We want to store the grid in memory in a u unsigned 8 integer array where each entry in the array points to an exterm 256 color. That's pretty good, I think. Oh, [snorts] nice cursor. That's pretty good. And actually, let's do plan mode for this. [sighs and gasps] So, we'll build a 64x 64 pixel grid. Durable objects store the state in the durable objects stored [snorts] in memory when a unsigned integer 8 array reach entry points to an exterm to 65 color persist to SQL light using drizzle. Okay, let's start plan mode. And while it's planning, I'm going to show you a couple of the things that this template has that's different from the normal tens start. So it starts with a custom entry point here server.ts. We need this so that we can export a durable object, right? Each entry point in a worker needs to export a durable object if you're using this. So this is what you don't get out of the box, but you'll get this with this template. So we create a server entry and we do say if there's a websocket match then actually forward it to the durable object. So this is how we do the websocket connection. If it doesn't match this websocket then just do the normal tense stack start fetch. Next we have our durable object. So let me open this one. Right. It's just a pretty straightforward durable object. It initializes its storage then also drizzle and then while it's constructing it runs the migrations and the migrations are stored in the schema that I'll show you in a second and then it's just a counter. So get create counter. This is great. Let's see the actual schema. So inside of DB I have a do schema and currently it's just a counter with an ID and that count right. So you've seen me press++ that also stores this count here. Let me go back to the durable object. Right. So when I do increment, it gets the current count. It increases it by one. It saves the new counter and then it broadcast the value. So we talked about the durable object. We talked about the durable schema. Next is the migrations. The plan is done. But before we go look at the plan, let's let me go over the migrations. So here what happens is when I run the scripts in package.json JSON there's a script called DB generate durable object which will generate the migrations for the durable object. So if I go to the do migrations this one right this migration here is going to have SQL files that will actually be ran inside of that block concurrency well so that there's always an upto-date SQL light table. Let's look at the plan. Nice diagram. Pixel grid. The browser, the websocket, it broadcast the pixel on connect. We have a pixel grid table. It just an ID and the blob. That's correct cuz we just store the array. Then we have some migrations ID and data. Very simple. Then the durable object. We'll have a in-memory state. Perfect. We have the get grid. Set the pixel. Fetch the websocket connection. That's good. what we what I showed you earlier. We create a new grid pattern, the new route, render a grid, can delete the counter, can update the durable test, the extern colors, which I will get to in a second. No, drizzle could regeneration the do migrations. I don't know about that. Let me follow up with this real quick. Add to chat. We do want to run db generate do which will generate the migrations for us. Other than that, this looks good. Let me update the plan migration step. Run the migration. Migration generated and it starts implemented because I switched to agent mode from plan mode. Okay. So now it's building and we'll review the code later. Let's clarify some of these things. I said you int 8 array. That's because in this array we can store an 8 bit unsigned integer basically from 0 to 256. Each one of those is going to point to a color. Now there's two ways of doing this. You can use eight bit colors where you store the red values, the green and then cut off the blues to fit in a bit eight bits. But honestly, these colors super mid, super ugly, right? This is so instead we can use the xterm colors. These are a little more more beautiful, but they're just a a palette with an index, right? So 0 to 15 is these these colors. Then we have the grayscale colors and some more colors here. And actually, we can draw a nice pixel grid that way. If we store in our array 000, then that will be black. So, it's going to make sense later. Let me just draw a grid. It's going to be a 3x3 grid. Not my not my best grid. So, this is a 3x3 grid. 3x3. So, it has nine squares. Each index would be 0 1 2 3 4 5 6 7 8. Right? So, this is eight and this is zero. That works, right? Those that's the array values. So, here if you wrote 0 0, this would be a black square, right? Right? And if we did 15, then it would be a white square. We'll store this in an array. U in 8 array with this one will have nine values. But of course, our 64x 64 grid will have 96 squares. But this is just easier to simplify. Cursor just alerted me. Let's see what it did. We have a pixel grid with an ID and data. Perfect. Keep that. We'll drop the table if it exists. Creates the migration. The pixel grid 64x 64. The grid is stored as a U in array with the size. We load the grid. We persist the grid by just saving it into durable object. Then we get the grid, set a pixel Y * 64 + X. I'll show you that in a second. That's how we set a pixel and then we broadcast it. Then the server, the new grid pattern. Here we have our route has all these colors. It does some fancy math to get that nice color grid that we have. So we can kind of ignore that. Here we get the grid. We set the pixels. Create a new route. The grid page. Not a big fan of use effect, but for the web sockets, it's pretty good. Set the grid to its default. And if we're getting a update, we're getting a pixel update. Then we just update the pixel there. Paint the pixel. All right. Again, y * 64 handle the pointer down when I draw. Okay. Here's the grid. Some tests. And then the header will now have a grid. Okay. Let's see. Let's see if this works. Go to the browser tab. And then, of course, this counter doesn't exist anymore. It loads. Let me go to the pixel grid. SQLite error. Hm. Let's see why we get a SQLite error. This is nice about live coding. You might also get this error. Let's open the terminal. No such table. Pixel grid. Okay. So, it looks like the migrations haven't ran. So, let's do a new terminal. We're going to say pmppm run db my Let me open up the script real quick. Generate do pixel grid is a new table. Or we can just rename the existing one. Let's just to make the let's do let's new table. Okay, now we have this thing. So now we created this migration, right? That actually creates a table and it drops this. Now we should be able to go back to this dev server. Let's restart it. This looks good. Pixel grid connected. Okay, cool. Bunch of colors, right? Selected color 9. This is that extern colors. Let's use this nice red and let's draw. Okay, this looks promising. But of course, now let's open up this thing here in [snorts] Chrome and see if we got a durable object working. I see two I see a live counter. Now let's grab a cool yellow. Okay. Okay. This is a live pixel grid. So we got it working. But let's go back to my explanation real quick. You see that array, right? This will have nine items. The int array. And instead of storing 0 to 8 in the database for the index, we need to be able to address each pixel by coordinates. So this one will be x 0 y 0 and this one will be x2 y2. That's how you uh how you do this. So this is okay. This is 22. So how do you go from this to the index? Well, that's this formula that we kept seeing in here. We saw this this thing y * 64 + theta x. That's the I want to say algorithm formula. So it's y * the width + x. So if you have 2, y is 2 * the width, it's three squares 1 2 3 + 2 is 8, right? So this is eight. 0 1 2 3 4 5 6 7 8. So this is how you get that square. And then the opposite is where you get to use the modulo the let's say we have the we have eight. How do we get the x and the y? Well the x is the modulo. So we get 8 modulo 3. 8 modulo 3 which is two. So the x is two. Here we go. And then how would you do the y? You guessed it correctly. Division. All right. So you would say 8 divided by three. But of course, since we're doing division, this is going to be like 2.6666. So 8 / 3. That's not what we want. So we have to floor it here. And now it's two. So these are um these are the algorithms on how to get those pixel how to get those X and Y. And so we have eight here [snorts] um because I'm picking this square. And we have three because the width is three. But in our large grid, it's 64. Hope that wasn't too much. Let's go back to that durable object one more time because it is very interesting, right? So, first, why did it fail? It fill because it called this migrate and we hadn't generated the migration correctly or the agent hadn't generated the migration correctly. Then it loads the grid, right? It does this that we've done before. Here's that algorithm we just saw. This is cool. We get working locally, but of course, we probably want to test it out in production. Open up as a terminal again. First, I'm going to deploy it right now, but it's not going to work. Increase the text size here. If I run pmppm run deploy, I will get an error because the template has a D1 database in there like separate from the durable object. There's a D1 database for users or whatever you want to store. But we didn't remove that from this template. While this is loading, it's going to tell me, oh, sorry, but the binding of D1 must have a valid ID. Open up Wrangler. And in here, see how we have a durable object that works. There's this D1 database that we're not using, right? Your database name, your database ID, separate of the durable object. Delete this bad boy. Back to the terminal. Rerun the same thing. And it's like, hey, you know, this script didn't work. We can override because the deployment failed earlier. Great. Tanstack pixel grid. Let's open it up. This is on the internet, so I'll share this link later. And you can also draw. Everybody can draw. Hm. Something went wrong. What's the error? Drizzle error roll back. This is what we get for live coding. What is the drizzle error roll back? Okay, at first I'm going to debug it myself, right? Wrangler tail to see what actually happens. Waiting for logs an exception. Roll back. Okay, let's do this. Copy. And I'm going to open up my agent. We deployed our app but are getting this. So I think it has to do with the fact that these there are these the migr that the agent created a migration initially. Right. So have this pixel grid but there's also this one that does this right. Yeah. So exactly the user ran it after I created the pixel grid. So now there's two conflicting migrations. This one drops counters and create pixel grid. And then this creates pixel grid, but it [clears throat] doesn't work because it's already created. Probably honestly, since it's filled, I might just stop the agent here and do it myself. And I'm just going to delete all the initial migrations. Delete. Do create new migrations. The o migrations meta. Oh, I have to delete the snapshot, too. It's the trash. Let's do that again. one migration, right? New meta, new pixel grid. Let's see what we get when we run BMBBM run deploy. Now, let's follow this. There we go. Pixel grid live vibe coding 101. So, we have this pixel grid here and then for good measure, Safari base URL. And this one gets a nice purple pink, whatever this color is. Maybe more of a cloud orange and draw. Hey, cool. Okay, it worked. All right, we had a few hiccups, but at the end of the day, we have a working tent stack application where many people can draw onto a grid. And you saw me stumble a bit, but I feel like that's also more real than just everything going perfect at once. So, I hope that was useful. And I hope that my explanation of the pixel grid wasn't too off topic. Yeah, let me know if this was a fun kind of video or if you would rather have me do everything without issues, cut out all the mistakes, and yeah, please let me know in the comments what you'd like to see next. And thanks for watching.

Get daily recaps from
Cloudflare Developers

AI-powered summaries delivered to your inbox. Save hours every week while staying fully informed.