Sudoku
I enjoy a relaxing game of Sudoku, but I just wanted to play the game without ads or tracking. So, I built a simple Sudoku web app that lets me play the game in my browser.
One of the goals of the project was to keep the app as simple as possible, while still delivering a game that plays well on browser and mobile devices with or without constant internet connection.
Another goal was to learn new technologies and tools, like Swift, SwiftUI, Rust, and Angular.
Multiplayer Sudoku
I added a collaboration mode that allows users in different locations to work on the same puzzle together in real-time. Since I was already using Firebase for this project, I decided to leverage Firebase Realtime Database for the multiplayer functionality.
However, this presented an interesting challenge: Firebase's architecture is primarily client-driven, which meant I couldn't rely on a single server endpoint to ensure data consistency. For example, I couldn't create an atomic endpoint that would read the current puzzle state, apply changes, and write it back—all in one transaction. If the client handled this logic, there would be a race condition where the client's view of the puzzle could become stale between reading and writing, potentially overwriting other users' changes.
To solve this, I implemented an event-driven architecture. Instead of syncing entire puzzle states, each user action (like placing a number or adding a pencil mark) is broadcast as an individual event. All connected clients listen for these events and apply them to their local puzzle state. This approach eliminates the need for atomic state updates while ensuring that every user's actions are preserved.
I also implemented optimistic updates to make the experience feel snappy and responsive. When a user makes a move, it's immediately reflected in their UI before being sent to Firebase. Once the event comes back from the Realtime Database, the code checks for conflicts—for instance, if two users tried to place different numbers in the same cell at nearly the same time. The conflict resolution logic ensures the puzzle state remains consistent across all clients, giving everyone the same view of the game.
This event-driven, optimistic approach provides a smooth collaborative experience while working within Firebase's constraints, proving that you don't always need a traditional backend API to build robust real-time features.
I knew this system was complex, and without proper testing, I could never be fully confident in the consistency guarantees. That's where Playwright came in. I wrote end-to-end tests that simulate multiple clients editing the same cell at the same time, verifying that the conflict resolution logic works correctly and that all clients eventually converge to a consistent state.
Playwright was perfect for this because it can spawn multiple browser contexts in parallel, each acting as a separate user. This allowed me to create realistic race conditions—something that would be nearly impossible to test reliably with unit tests alone. I could watch in real-time as multiple "players" make conflicting moves, then assert that the final puzzle state matches across all clients.
These tests gave me confidence that the multiplayer feature works as intended, even under challenging conditions. They also serve as living documentation of how the system behaves when things get messy, making it easier to maintain and extend the feature in the future.
Angular Frontend
The currently selected row and column are highlighted to make it easier to see where you are on the board. The app also highlights all cells and notes with the same number of the selected cell. This makes it easier to see where a number can go.

You can also toggle between note taking mode and fully entering numbers into the cells.
You can also use the "Quick Pencil" button to automatically fill in pencil marks for all empty cells. This is a great way to quickly get started on a puzzle.

When I started this project I was new to Angular. It was by coding projects like this, taking an idea and making it a reality, building it from the ground up, that took me from a novice to a pro on my team. I was the one who helped others learn Angular and was the go-to person for solving challenging bugs at my full time job.
This project is very modern, using the latest Angular features like standalone components, signals, and zoneless.
The UI is clean and simple, with a focus on usability. Features include pencil marks, and multiple difficulty levels. The app is also responsive, working well on both desktop and mobile devices.
First Try
View source for the Swift appBefore I started building the web app, I tried implementing an application in iOS using Swift and SwiftUI, all without any server-side code.
One issue with my approach was that generating hard puzzles would take a long time (up to 5 minutes), heat up the iPhone, and drain the battery quickly. My initial thought was that I needed to take advantage of multithreading to speed things up, but even the most optimal solution was only bringing the average time down by a couple of minutes.
I set up experiments to test new algorithms and optimizations, but none of them were able to generate hard puzzles in under a minute. I finally discovered that there were 2 fundamental problems with my approach:
- The biggest performance bottleneck was how the algorithm decided when to give up on a puzzle and start to try generating a new one. My initial implementation would try to generate a puzzle until it found one with a unique solution, which could take a long time for hard puzzles. I later realized that I could set a limit on the number of attempts to generate a puzzle before giving up and starting over. This drastically reduced the average time to generate hard puzzles. 
- The second issue was that Sudoku generation is unpredictable as stated in the previous point. I was trying to generate a puzzle when the user clicks "Start new game", when I should have been generating puzzles in the background while the user is playing. 
Given these realizations, I decided there was no reason to continue trying to generate puzzles on the client. And since I wasn't going to generate on the client, I didn't need to use Swift on the client for the performance boost over JavaScript.
Mobile applications are much more complicated to develop and take a significant amount of time and effort to create. And this is just a fun project for me, I don't want to pay Apple over $100 per year to publish to the app store if I don't need to. By moving the puzzle generation to the server, I can leverage the power of Rust and its ecosystem to create a more efficient and performant solution.
Generate Puzzles with Rust
There are many ways to generate Sudoku puzzles with different difficulties, including creating and using a solver that only knows a subset of solving techniques. Conceptis Puzzles has a great list of techniques. I chose to use a backtracking algorithm to generate puzzles, which is a brute-force method that ensures a unique solution. The algorithm works by filling in numbers one by one, and backtracking when it encounters a conflict.
The backtracking difficulty comes from the number of cells that are initially filled in. More filled cells generally lead to an easier puzzle, while fewer filled cells lead to a harder puzzle.
Node Server
Keeping this app as simple as possible was a top priority for me. I knew I could quickly set up a Node + Express server and have the rust code send over the generated puzzles via HTTP requests. It is also very easy to have the Node server send those puzzles to a Firebase DB.
There may be a way to send messages to Firebase directly from Rust, but that would definitely be an overcomplicated. This especially satisfies the YAGNI (You Aren't Gunna Need It) and KISS (Keep It Simple, Stupid) principles.
To keep costs low and to avoid server maintenance, I run the Rust and Node code on my own machine, creating batches of puzzles and posting them to the remote database.
Firebase Database
I chose Firebase for the database because it is easy to set up and manage, and it has a generous free tier. The Firebase Realtime Database is a NoSQL database that stores data in JSON format. It is also easy to integrate with the Angular frontend.
The database structure is simple, with a collection of puzzles categorized by difficulty level. Each difficulty level has a collection of batches, and each batch stores 50 puzzles which can be returned efficiently in a single query. Each puzzle is stored as a 2D array of numbers, with 0 representing an empty cell. The Rust code creates a hash for each puzzle which is also stored to ensure uniqueness.
Client Storage
To minimize the number of requests to the database, the Angular app stores puzzles in the browser's local storage. When the user starts a new game, the app first checks if there are any puzzles stored locally. If there are, it retrieves a puzzle from local storage. If not, it fetches a batch of puzzles from the database and stores them locally for future use. If there are fewer than 50 puzzles left, it stores another batch.
Now if you get on a plane without internet access, you can still play with at most 50 puzzles for each difficulty level if you have already loaded the site into the browser.