TanStack Start + Cloudflare Durable Objects: Building a Real-Time Pixel Grid
Première diffusion : 26 août, de 9:30 à 10:00 UTC−4
English
Transcription (Bêta)
One of the most requested videos is around dense stack and durable objects, not some generic chat app, but something a little more unique.
Well, today we're going to build a real-time 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 OpenCode.
But of course, the same steps would work with those tools as well.
Let's get started. 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.
I'll walk through this code base as we go. Down here, we're going to follow the instructions to scaffold the project.
BMPM create, and I'll use this template. We will call this 10-stack pixel grid.
I'm not going to use an agents.md because there's already one in this repo.
We'll use git, and I won't deploy yet. See the 10-stack pixel grid.
Let's open up cursor. First things first, I'm going to open the terminal and run PMPM run dev to see what we're starting off with.
Immediately, cursor will open up a browser tab here, which is pretty nice.
Let me just close some of these.
This is good. We start with a 10-stack starter application, but it also has a counter here.
This is nothing groundbreaking, a counter will work. But if I open up my browser, to localhost 3000, and let me resize this a bit here.
We'll open up the same counter.
You can see the durable object in action. Again, not groundbreaking, but you immediately start with a durable object that works and that's persistent.
That's great. Now, we want to start building. I said we're going to use a pixel grid.
We'll just write a prompt. Inside of here, there's a couple of nice models.
For today, I'm going to use Sonnet 4.6, because it's a nice middle ground between Opus knowledge and codec speed.
What we'll build is, let's do true agent decoding and I'll dictate.
In this application, we want to build a 64 by 64 pixel grid that is live editable on the canvas.
The pixel grid stage should be stored in a durable object. There already is a counter durable object that you can replace with the state of our pixel grid.
We want to store the grid in memory in a U unsigned eight integer array, where each entry in the array points to an X term 256 color.
That's pretty good, I think. Nice cursor.
That's pretty good. Actually, let's do plan those for this. We'll build a 64 by 64 pixel grid, durable objects, store the state in the durable objects, stored in memory when a U unsigned integer, eight array, where each entry points to an X term, 265 color, persist to SQL light using drizzle.
Let's start plan mode.
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 10 stack start.
It starts with a custom entry point here, server.ts.
We need this so that we can export a durable object.
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 web socket match, then actually forward it to the durable object.
So this is how we do the web socket connection.
If it doesn't match this web socket, then just do the normal 10 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 our 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 plus, plus, plus, 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 broadcasts 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 me go over the migrations. So here what happens is when I run the scripts in package.json, there's a script called db.generateDurableObject, which will generate the migrations for the durable object.
So if I go to the DO migrations, this one, right? This migration here is gonna have SQL files that will actually be ran inside of that block concurrency wall so that there's always an up-to-date SQLite table.
Let's look at the plan.
Nice diagram, pixel grid. The browser, the web socket, it broadcasts the pixel on connect.
We have a pixel grid table. It's just an ID and the blob, that's correct, because we just store the array.
Then we have some migrations, ID and data, very simple.
Then the durable object will have a in-memory state.
Perfect. We'll have the get grid, set the pixel, fetch the web socket connection.
That's good. What I showed you earlier, we create a new grid pattern, the new route, renders a grid, can delete the counter, can update the durable test, the Xterm colors, which I will get to in a second.
No, just look at 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.generateDO, which will generate the migrations for us. Other than that, this looks good.
Let me update the plan. Migration step, render 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 uint8 array. That's because in this array, we can store an 8-bit unsigned integer, basically from zero to 256.
Each one of those is going to point to a color.
Now there's two ways of doing this. You can use 8-bit colors where you store the red values, the green, and then cut off the blues to fit in the 8-bit.
But honestly, these colors, super mid, super ugly, right?
This is bleh. So instead, we can use the Xterm colors. These are a little more beautiful, but they're just a palette with an index, right?
So zero to 15 is 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 0, 0, 0, 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 three by three grid, not my best grid. So this is a three by three grid, three by three.
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?
That's the array value. So here, if you wrote 0, 0, 0, this would be a black square, right?
And if we did 15, then it would be a white square. We'll store this in an array.
You int eight array with this one will have nine values, but of course our 64 by 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. I'll drop the table if it exists. Creates the migration.
The pixel grid, 64 by 64. The grid is stored as a U int 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 times 64 plus 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 times 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 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. Let's see why we got a SQLite error. This is nice about live coding.
You might also get this error. Let's open a 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 gonna say pnpm run db my, let me open up this script real quick. Generate DO.
Pixel grid is a new table. Or we can just rename the existing one. It's just to make the, let's do, it's a 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. And let's restart it. This looks good.
Pixel grid connected. Okay, cool. Bunch of colors, right? Selected color nine.
This is that external colors. Let's use this nicer red and let's draw.
Okay, this looks promising. But of course, now let's open up this thing here in 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 uint array. And instead of storing zero to eight in the database for the index, we need to be able to address each pixel by coordinates.
So this one will be X zero, Y zero. And this one will be X two, Y two.
That's how you do this. So this is okay. This is two, two. So how do you go from this to the index?
Well, that's this formula that we kept seeing. In here, we saw this thing.
Y times 64 plus data X. That's the, I want to say, algorithm formula.
So it's Y times the width plus X. So if you have two, two, Y is two times the width.
It's three squares. One, two, three, plus two is eight, right?
So this is eight. Zero, one, two, three, four, five, six, seven, eight. 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 eight. How do we get the X and the Y? Well, the X is the modulo.
So we get eight modulo three. Eight modulo three, which is two. So the X is two.
Here we go. And then how would you do the Y? You guessed it correctly.
Division, right? So you would say eight divided by three. But of course, since we're doing division, this is going to be like two dot six, six, six, six.
So eight divided by three.
That's not what we want. So we have to floor it here. And now it's two.
So these are, these are the algorithms on how to get those pixel, how to get those X and Y.
And so we have eight here because I'm picking this square. And we have three because the width is three.
But in our large grid, it's 64. No, that wasn't too much.
Let's go back to the durable object one more time because it is very interesting, right?
So first, why did it fail? It failed 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 got it working locally, but of course we probably want to test it out in production.
So let's do it.
Open up the 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 pnpm 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. 10-stack 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.
Hmm, something went wrong. What's the error? Drizzle error rollback.
This is what we get for live coding. What is the drizzle error rollback?
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, so 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 the agent created a migration initially, right?
So I 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 doesn't work because it's already created.
Probably, honestly, since it's failed, 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.
DO migrations. Meta. Oh, I have to delete the snapshot too. It's a trash.
Let's do that again. One migration, right? New meta, new pixel grid. Let's see what we get when we run.
BMPM run deploy. Now let's follow this. There we go. Pixel grid life.
Vibe coding 101. So we have this pixel grid here. And then for good measure, Safari.
Basis 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 10 stack application where many people can draw to 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.
