977: We Built a CSS Challenge Platform

## Document Notes
## Highlights
- **Built A CSS Battle Platform Fast**
- Scott and Wes decided to host a major CSS coding battle tournament and built a custom platform for it.
- The project was a massive sprint to ship a real-time CSS Battle app for Mad CSS.
Transcript:
Scott Tolinski
We decided to host a major CSS coding battle tournament here on Syntax with 14 of the greatest CSS engineers out there. And then two other competitors in that competition, that being Wes and myself, competing in CSS battles. And we've been doing a lot of CSS battles here on Fridays and our three devs videos with CJ, Wes and I, but we wanted to take it one step further. And so we built our own CSS coding battles platform. So we're going to be talking all about how that works, the stack, how the stack works itself, what we're using for various components, why it's cool. And we're going to be diving into some of the challenges that we had getting this thing up and running. Because let me tell you, this has been a massive sprint to build our own platform from scratch just to use for this CSS coding tournament. My name is Scott Tolinsky. I'm a developer from Denver. With me as always is Wes Boss. What's up, Wes?
Wes Bos
Hey, excited to talk about this. This has been very fun to work on the last, what, two weeks or so. I joined in about two weeks ago and helped crank out issues. But it's a really cool platform. It's all local first. It's all syncs. It's all fast as heck. ([Time 0:00:00](https://share.snipd.com/snip/374a7e6f-ccd0-4561-8f2a-35c6dbf0f921))
- **SvelteKit + Zero Powers Real Time**
- SynHax uses SvelteKit plus a server because the app needs both client and server logic.
- Zero Sync provides the real-time sync layer that makes the platform feel instant.
Transcript:
Scott Tolinski
Real quick, the stack here, the stuff we built this on, it's using SvelteKit because... Hold on.
Wes Bos
Can you just show what it does visually first before we get into the stack? I think that will frame it a little bit better.
Scott Tolinski
Yes.
Wes Bos
Because people might not know what a CSS battle is.
Scott Tolinski
For people who are watching on video, you'll be able to see the UI. For people who are listening, we'll do our best. You know, I don't want to make this only a video only episode. But a CSS battle in terms of like what this is actually doing is people have a target, which is a thing that they are coding towards, a thing that they are trying to recreate with CSS and HTML. And the way the CSS battles works is that you code your HTML and CSS, and you try to get it as close as possible to the target. And the person who gets there first to dead on 100%, or gets the highest score where things are closest is the winner. So if you're looking at this on YouTube, you see right now my code, which is up here in the top left, you see the target, which is over here, then we have a diff view and an overlay so you can See what's different. And then you even get access to your competitors view. So that way, when you're competing and you're coding, you can see how far along they are to either stress you out or calm your nerves, slow you down. So that's the general crux of this is a target is created with either a video, an image or CSS and HTML. Right now we're just using HTML and CSS targets. And again, we are comparing. We'll talk quite a bit about the diffing part of it because that was a huge component of this. And again, this whole thing works real time. ([Time 0:03:42](https://share.snipd.com/snip/6235a814-09fb-40b5-a314-cfa4a68ace27))
- **Let Users Code Locally, Sync Changes**
- Let competitors use their local editor and sync the file system to the platform for speed and comfort.
- Use the File System Access API to create a local folder and persist the handle in IndexedDB for fast saves.
Transcript:
Wes Bos
So the important part about this SYNHACS that we built is that it uses local file system access and you just use your own editor, use your own tools, you use your own completions, all of That stuff locally on your computer. ([Time 0:06:08](https://share.snipd.com/snip/97fd35e4-f997-4d1b-98f9-2cfa02815817))
- **Why Zero Sync Feels Instant**
- Zero stores client data locally and sends small patch diffs over WebSocket to a sync server.
- The sync server replicates Postgres to SQLite and computes minimal diffs for efficient real-time updates.
Transcript:
Scott Tolinski
I have, uh, like if the file has been changed, it is just updating a database record, but because of how zero sync works, it's so fast and instant. So how does zero sync work really quickly? Kind of like no fluff here. Zero is basically a platform with a sync engine as well as a client side library to do the queries and stuff, a server side library to do the server side stuff, and then a sync server. So the big pieces, right? So you have the client, the client has versions, client versions of the queries and the mutation functions. These things run on the client and take place instantly. So when you click something like I'm going to click new battle or something, it creates that battle instantly and saves it into your client side data using the client side queries and Mutations code. Then in the background connects with a web socket to the zero cache sync server. That happens so that when I save it, the database knows what data has changed, and it sends a patch message to the sync server. The sync server is a long running server, as we mentioned, and that replicates Postgres to a SQLite replica file. It then basically determines what client rows have changed since the last updates and then sends a diff to your server. So basically the sync server is handling like what has changed and what messages I need to send back and forth. And it does so with like I've mentioned diffs about like just what's changed. So it's not sending a massive amount of data on everything, which is one of the reasons why zero is so fast. It's incredibly fast. ([Time 0:13:16](https://share.snipd.com/snip/66f4aaa8-9db9-4c82-a2ec-8f6f135d3765))
- **Keep Auth In Your App Routes**
- Handle auth and permissions in your app routes instead of embedding them in the sync server schema.
- Adopt Zero incrementally by writing server routes that reuse your existing auth and permission logic.
Transcript:
Scott Tolinski
This is where like auth happens. This is where permissions happen and stuff like that. The way that zero...
Wes Bos
Like database, like to be clear, zero is not a database, right? Like all of your writes eventually just happen in like drizzle updates.
Scott Tolinski
Yeah, yeah. You're pretty much just writing drizzle code to do those. Well, actually, that's not true. No, you're writing, sorry, the the those mutations, Wes, are happening in the exact same code that is used in the client version of that is also that same code is used in the server versions Of it just being run server side.
Wes Bos
Saves it to the database is not part of of zero right like it eventually zero will then translate it to what the zero drizzle adapter and then save no to the post press no the drizzle zero
Scott Tolinski
Uh adapter is only getting giving us the schema translation so oh so if you wanted to access the database directly with the same schema yes correct okay I was wrong. Yeah. In fact, I can even show you the database inserts are happening in a transaction. So if this is this is actual zero code, if you're looking at the screen, you can see it, this is the zero code that is doing the insert. So it is very similar to an ORM type of thing you're doing TX, which is the transaction dot mutate dot the table dot insert, and you're just passing your data in there. So very similar to how you would do it using any kind of ORM here. Really nice. So the way that auth works, the way that it used to work was kind of a giant PIA. There was the whole schema file. You had to write all of your permissions and auth code in your schema file in a real hot potato way. And then there was a zero process that you had to then apply that permissions and auth stuff to your sync server itself. And the sync server was the thing that was doing all of this code, it was doing all the mutations and stuff for you. The way that they have since changed it is like, Oh, you know what would be way easier than having the sync server try to manage all that stuff? If you just got to write a route that collected all of those queries or collected all of those mutations in your own application and just apply them in your own code. The reason why that's more simple is because you can handle auth the same way that you would handle auth if there was no sync server involved.
Wes Bos
So like, and probably a lot of people that are adding this to their app, this is great if it's like a Greenfield app, right? But if you're trying to just add sync to an existing database or existing application, you probably already have all of your own authentication permissions code.
Scott Tolinski
So now you would just incrementally adopt that. You're just creating those API routes and you're doing whatever incremental ones you want. It's really kind of great. ([Time 0:15:45](https://share.snipd.com/snip/a106ba48-5023-41ce-a2d1-46c4decd8c39))
- **Zero Trades Offline For Speed**
- Zero moved away from offline-first and complex CRDT conflict resolution toward fast sync.
- Concurrent writes are applied by commit order and then reconciled and broadcast to clients by the sync server.
Transcript:
Scott Tolinski
So it's not local first, because zero has really moved away from the idea of it being a good platform for offline. They've kind of determined that offline for this is less useful, and less where they want to go with it. So because of that, local first is still always going to be the app should work purely offline, you should own your own data, it should be easily encrypted. It, you know, there can be a sync process that syncs that data and stuff like that. But I think the zero folks kind of realized that this system can still be very, very impressive, very fast, load your data locally from that index DB, while not necessarily focusing On offline or conflict resolution. The way that the conflict resolution does work. So let's say Wes and I both the same row at the same time. Both of the updates happen locally on our own machines, then those messages get sent to the sync server, that server basically commits those in the order that they were received. So one of them might override the second one if it comes in afterwards. So basically, what happens is I send a message, Wes sends a message, the first one that arrives gets applied first, the next one then gets applied. But then the sync server basically understands what the actual convergence is there and then sends the data back out to everybody. So then someone's data isn't going to be stale if we both write the same thing. ([Time 0:19:48](https://share.snipd.com/snip/7b8fad27-c4b4-4be6-b3a7-2e0941d5c9a4))
- **Client-Side Rendering With SnapDom**
- Wes used SnapDom to rasterize the HTML target inside the browser for client-side diffing.
- He converted the DOM to a canvas via an SVG foreignObject to produce images for pixel comparison.
Transcript:
Wes Bos
Yeah, yeah. So this is one problem that we have with the CSS Battles platform is that it's like you can get so close and then there's like one weird thing. And I think the way that the CSS battles diffing algorithm works is that it sends your code to the server. They probably have like Chromium running. It takes a screenshot and then diffs it against the actual target. Wanted that to happen in browser, both so that any quirks between your browser or whatever are gone because your target and your actual thing that's running, those are both running In the exact same browser. So whatever browser is actually computing the score is going to be rendering them out exactly the same, which is really helpful. So what we did is I ended up using this package called SnapDom. You may have used HTML to Canvas in the past. So there's like a way better version now called SnapDom. And essentially you just give it an HTML element on the page and it will then convert that to like a Canvas and then export as like a PNG or SVG. The way that it actually works is really interesting to me is that they take your HTML, put it inside an SVG foreign object tag. And then I think since since it's then an SVG, which is an image, you can then write the image to the canvas. Right. So like it doesn't have to do a whole bunch of I think that's how works. There is a lot of like gotchas and stuff. I had a couple bugs that I burned a day on, but we got it all working and it works really well. ([Time 0:25:38](https://share.snipd.com/snip/862c821b-0cab-4100-9eb6-67f839fe047e))
- **Diffing Needs Multiple Heuristics**
- The diffing engine blends multiple algorithms and thresholds to match human expectations, not exact pixel equality.
- They ignore background pixels, weight opacity differently, and tune thresholds to avoid punishing near-perfect matches.
Transcript:
Wes Bos
So like we tried using vectorization you know vectorize the images and see how close they are and most vectorization llms will care about what is in the like like if there's a dog in it Right or if there's a red dress in it and they don't necessarily care about pixels that much right and then there's other things called sssim is structural similarity index measure Which is how how similar are two images to each other um that worked okay ish but it still wasn't um good enough to making it close enough to pixel perfect because we we didn't want it to Be pixel perfect but we wanted to be close enough whereas if you had like a weird like outline on something and that would make you lose the whole thing so i think we i probably went through Like six or seven different iterations of it we finally landed on one that had different thresholds um scott if you yeah you can see the diffing engine in the bottom right here um It has Different thresholds, three different algorithms in there, different weighting of different colors. If something has a bit of opacity in it, it weights it a little bit less. And we finally landed on something where this feels really good. ([Time 0:27:24](https://share.snipd.com/snip/dbcd7edf-b5a6-4b6d-b67c-e7d97494b631))
- **Sandbox And CSP User Submissions**
- Sandbox user HTML in iframes and enforce strict CSP headers to prevent malicious scripts.
- Allow only trusted external scripts (e.g., Tailwind CDN) and disable inline scripts via CSP and iframe sandbox attributes.
Transcript:
Wes Bos
I was like, yeah, people like, like one thing you could do early on was you could just write a script tag in your HTML and then it would execute that on everybody else's computer. And I was like, man, we got to like run this through like a sanitizer or whatever. But like iframes, these are all iframes. Iframes have a sandbox attribute where you can allow and disallow things. So what we did is we disallowed inline script tags or no, we allowed JavaScript because we needed the Tailwind CDN to load and compute the Tailwind code. But then we used a CSP, content security policy, where we did a really good episode with Alex Sexton from Stripe on CSP. But basically via header tags or headers on your HTML element, you can say what resources are allowed to be loaded in. So you're only allowed to load script tags from one specific URL, and that is the Tailwind CDN for their JavaScript library. And then otherwise, you're not allowed to do any inline JavaScript. ([Time 0:31:29](https://share.snipd.com/snip/1a6ee5e1-3f4b-4a7c-a4a3-4d831f30124c))
- **JWT Expiry Broke Real-Time Sync**
- A recurring bug caused the sync server to report users as unauthorized after 15 minutes.
- The quick fix was extending JWT lifetime rather than tearing down and rebuilding the Zero client during battles.
Transcript:
Scott Tolinski
Totally i i uh some of the biggest bugs that had, besides just like dialing in the diffing, we kept getting the sync engine would just start randomly saying that you were unauthorized. And this one was tough because you never knew if it was like where the bug was, like what was the issue? Because it's happening on the sync server. And yeah, we're combing through logs and we're adding, we have Sentry in here, we're checking Sentry. And the issue ended up being that the cookie that stored our auth token was long lasting and the JWT had an expiry of 15 minutes. So if we had been working on it for 15 minutes, it would suddenly in the sync process tell you, oh, by the way, you're not authorized anymore, which was a tough, tough bug to find. And luckily, we all have good enough fundamentals that we kind of were able to break down step by step. Where could this be? And CJ was the one that had the idea to be like, you know what? I got to check this JWT to see like what's going on here. And he discovered the 15 minutes expiry time. There's a tough thing there with that expiry time, because the way that the sync engine works for you to update that JWT with the sync engine in how we are doing it, you would have had to Like tear down and rebuild the zero client instance. ([Time 0:35:23](https://share.snipd.com/snip/8979a861-412a-4369-96c3-df8f714052b6))