There is something slightly ridiculous about the amount of technology sitting behind a crossword.
That is probably part of why we built one.
At White Rabbit Foundry, most of our work sits around digital products, data, automation and the infrastructure behind them. A crossword does not solve an urgent operational problem. It will not transform somebody’s productivity, automate a finance process or finally persuade two old enterprise systems to speak to one another.
It is simply something fun to play.
But it also became a useful little experiment in how we think products should be built, how generative AI can fit into a real application without being allowed to control everything, and what happens when you move beyond a prototype and decide an idea is actually worth maintaining.
The idea originally started much smaller. We built an internal version with Python, FastAPI and vanilla JavaScript. It had one fixed puzzle, a timer and a leaderboard. The point was not to create a finished product, but to see whether the concept worked at all. It did.
What we did not do was launch it.
Once the prototype existed, its limitations became fairly obvious. One crossword gives somebody one reason to visit. The standalone application sat apart from the rest of White Rabbit Foundry. The leaderboard added competition, but not necessarily much value. And while the technical stack had been perfect for proving the idea quickly, it was not how we would choose to build something we expected to keep developing.
So the prototype remained a prototype.
We took what was useful from it, left quite a lot behind, and built the version that now lives at www.whiterabbitfoundry.com/crossword.
It is still free. There is no account required, no advertising and nothing to install. The main difference is that there is no longer just one puzzle. There is always another one.
One answer never changes
Every crossword begins in exactly the same place. Running through the centre is WHITERABBITFOUNDRY, all eighteen letters of it. Everything around that answer changes.
Everybody begins with the current shared puzzle. When somebody finishes one, another is generated in the background and added to the pool. Over time, the collection grows with the people playing rather than relying on us to sit down and manually publish another crossword every few days.
The themes stay loosely connected to the world we work in. There are puzzles about building products people actually use, data that leadership teams can trust, automation and the boring work it removes, cloud platforms and the bills that arrive afterwards, the early weeks of consulting work and launch day, usually including the things that choose that exact moment to stop behaving.
Other themes cover analytics, metrics, systems integration, small teams building big pieces of software, curiosity and the slightly uncomfortable process of discovering that the problem you started with was not actually the problem at all.
There are some rabbits too.
We wanted the crossword to have enough personality that it felt like something made by White Rabbit Foundry, without requiring somebody to know anything about White Rabbit Foundry before they could enjoy it.
That matters because the idea underneath the game is partly a marketing one. We think marketing should earn attention before asking for it. There is no email gate before the first clue. No account creation screen. No form promising that the useful part will arrive afterwards.
You get the crossword first.
If you enjoy it and want to hear from us later, there are ways to do that. If not, you still got a crossword. We are quite comfortable with that arrangement.
Letting Gemini write the clues, within limits
An endless crossword creates an obvious practical problem. Somebody has to keep writing the thing.
We could have produced a large fixed library ourselves, but that would have turned a small product experiment into an ongoing editorial job. It also seemed like a good opportunity to use generative AI for something slightly more interesting than adding another chat box to a website.
Gemini now produces the word bank through Vertex AI inside our Google Cloud environment.
The brief is deliberately restrictive. Each request asks for twenty two possible entries around one theme. Answers need to be between four and eleven letters long, use real words rather than abbreviations or brand names, and share at least one character with WHITERABBITFOUNDRY so the layout engine has something useful to work with.
Recently used answers are excluded too. Otherwise, a model repeatedly asked to think about products, data and consulting starts returning to some familiar favourites.
The clues have their own set of rules. British English, dry rather than theatrical, a little knowing and preferably written as though the person setting the puzzle has actually encountered the thing being described. They should not simply be dictionary definitions and the answer cannot appear inside the clue.
Every entry also receives a simpler hint, because there is a point at which an amusingly indirect clue stops being amusing and just becomes annoying. The model is required to return a fixed structure rather than a paragraph of prose.
A simplified version of the production approach looks roughly like this:
const responseSchema = {
type: "object",
properties: {
entries: {
type: "array",
items: {
type: "object",
properties: {
answer: { type: "string" },
clue: { type: "string" },
hint: { type: "string" }
},
required: ["answer", "clue", "hint"]
}
}
},
required: ["entries"]
};
const prompt = `
Create 22 crossword entries about "${theme}".
Use British English.
Answers must:
- be between 4 and 11 letters
- be real words
- avoid abbreviations and brand names
- share at least one letter with WHITERABBITFOUNDRY
- avoid these recently used answers: ${recentAnswers.join(", ")}
Clues should be dry, knowing and slightly playful.
Never include the answer itself.
Include one simpler hint for every answer.
`;That is intentionally simplified. The production version contains validation, retries and the usual defensive code that tends to surround anything involving an external service.
The useful bit is the separation of responsibilities.
Gemini gets freedom over the language.
It does not get freedom over the contract.
That distinction has become increasingly important in the way we think about AI products. The point of a generative model is that its output is not completely predetermined. The surrounding application should be much less imaginative about what it is prepared to accept.
And even once Gemini has supplied twenty two perfectly valid entries, it still has not made a crossword. It has supplied the ingredients. Our code does the rest.
The part we do not let the model improvise
The crossword layout is generated by our own TypeScript engine. This is one of the ideas that survived from the original internal prototype, which used a Python version of the same basic approach.
The eighteen letter central phrase is placed first. Candidate words are then tried around it and scored according to factors such as the number of letters they cross and their length, with a small amount of randomness included so that every grid does not settle into exactly the same shape.
That randomness is seeded.
This means the layout can vary while remaining reproducible. If somebody shares a crossword with a colleague, both people get exactly the same grid rather than two different puzzles generated from the same theme.
There are other rules that do not sound particularly interesting until you remove them. Words cannot sit flush beside one another where they should not, for example. Without that restriction, it is surprisingly easy to create something that technically contains intersecting words but looks more like a word search assembled by accident.
Gemini can also produce a perfectly good list of words that simply refuses to become a good crossword.
We assume that will happen.
The engine tries different seeded layouts. If those attempts still fail, the application falls back to a bank of forty clues and answers we wrote ourselves.
Conceptually, the important part looks something like this:
let entries = await generateWithGemini(theme, recentWords);
let puzzle = tryBuildGrid(entries);
if (!puzzle) {
puzzle = retryWithNewSeeds(entries);
}
if (!puzzle) {
puzzle = buildFromHouseBank();
}
return puzzle;The real implementation is naturally more involved, but the principle is not.
Try the generative route.
Try a deterministic recovery.
Keep a route that does not depend on the model at all.
Then give the player a crossword.
Someone using the site should never need to know which of those routes produced it. We do not need the model to be reliable every single time. We need the product to be reliable every single time. Those are very different requirements.
Seven seconds, moved somewhere nobody notices
A fresh puzzle currently takes around seven seconds to generate in production. Seven seconds is not especially long if somebody tells you a complicated process is happening in the background. It feels much longer when you have clicked a button and are staring at an empty page waiting for something to appear.
So the player does not wait for it.
When somebody completes the current crossword, the next one begins generating in the background. The completed puzzle can show its result immediately while the application quietly prepares what comes next. By the time another grid is needed, it is normally already there. There is a small product lesson in that which we find ourselves applying elsewhere.
Performance is not only about reducing the technical duration of an operation. Sometimes the better solution is to move that operation to a point where its duration is irrelevant. A seven second process can feel instant when nobody is looking at it.
Making it feel like a crossword
The internal prototype established that we could generate a grid and allow somebody to enter letters. That is a long way from making something feel like a crossword. A surprising amount of the second build went into behaviours that most players will probably never consciously notice.
Tap a square and it becomes active. Tap it again and the direction changes. Arrow keys move around the grid. Space switches direction. Tab advances to another clue. Typing skips cells that have already been filled and carries on towards the next unfinished square.
On a phone, the current clue remains visible at the top of the screen rather than disappearing as soon as the keyboard takes over the lower half. Once the crossword is full, it checks itself automatically.
There is also a Check button if you want to know what is wrong sooner. Hint adds one letter and reveals the plainer clue. Reveal Word exists for clues that have stopped being enjoyable, and Reveal All is there for moments when dignity has become less important than closure.
Sharing copies a link to that exact puzzle. Send it to somebody else and they can play the same grid rather than arriving at whichever crossword happens to be current when they open it.
The status bar keeps track of time, hints used, progress through the grid, puzzles solved and the theme. The timer pauses when the browser tab is hidden, because measuring how long somebody spent answering the door tells us very little about their crossword ability.
When a puzzle is completed, the result shows the finishing time, hints used and how many people have solved that particular grid.
One thing we did not carry over from the prototype was the leaderboard.
It was useful when there was only one crossword. Once the product became an ongoing sequence, it started to feel slightly unnecessary. Playing another puzzle, or sending the same one to someone else, felt more interesting than maintaining a permanent ranking.
Prototypes are often useful precisely because they show you which parts of the original idea do not deserve to survive.
Keeping the answers out of the browser
There is one technical decision underneath the game that we particularly like.
The answers never reach the browser.
Checking a letter, requesting a hint and revealing an answer all make calls back to the server. There is no convenient JavaScript object sitting inside the page containing the solution to the whole puzzle.
We are not pretending this is protecting highly sensitive information. It is a crossword. But the principle travels well. If a browser does not need a piece of information to perform its job, there is rarely much benefit in sending that information there and relying on nobody to inspect it. Sometimes the simplest form of data protection is not exposing the data in the first place.
Remembering progress without creating an account
We wanted the crossword to remember people between visits. We did not want that requirement to result in a registration screen. So it does not.
When somebody first plays, the application assigns a random identifier. That identifier is associated with their progress on the server and stored through the browser session mechanism.
The cookie itself does not need to contain a name, an email address or the contents of the crossword. It just needs to tell the server which anonymous player record belongs to this browser.
At a simplified level, the logic is not much more exciting than this:
import { randomUUID } from "crypto";
function getPlayerId(session: SessionData) {
if (session.playerId) {
return session.playerId;
}
const playerId = randomUUID();
session.playerId = playerId;
return playerId;
}Letters are saved locally as the player types, so a refresh does not suddenly wipe the puzzle. A second copy of the progress is stored against that anonymous identifier on the server.
Conceptually, the record contains the things the crossword actually needs rather than an unnecessary personal profile:
type PlayerProgress = {
playerId: string;
puzzleId: string;
letters: Record<string, string>;
hintsUsed: number;
puzzlesSolved: number;
lastSeenAt: Date;
expiresAt: Date;
};The production data structure is a little different because of the way the grid is represented in Firestore, but the underlying idea is the same.
For somebody who only plays on one device, this can remain the entire relationship.
No account.
No password.
No identity layer pretending a crossword requires one.
Inactive player records are removed twelve months after the most recent visit.
Letting progress follow you when you want it to
An anonymous browser identifier works nicely until somebody opens the game on a second device. Their phone does not magically know that it belongs to the same person as their laptop.
We could have solved that by creating user accounts. Instead, email is used as a temporary bridge between two anonymous records.
Enter an address and the system creates a private linking token, then sends a one off email containing the corresponding URL. Open that link on another device and the two sets of progress can be merged.
The basic pattern looks something like this:
const token = crypto.randomUUID();
await saveProgressLink({
tokenHash: hash(token),
email,
playerId,
expiresAt: addDays(new Date(), 90)
});
await sendProgressEmail({
to: email,
link: `${baseUrl}/crossword/link?token=${token}`
});The raw token does not need to sit in the database. A hash is enough for the application to verify it later. When the link is opened, the application finds the corresponding progress and combines it with whatever exists on the new browser.
Again, simplified:
const link = await findProgressLink(hash(token));
if (!link || link.expiresAt < new Date()) {
throw new Error("Link expired");
}
const localProgress = await getCurrentPlayer();
const savedProgress = await getPlayer(link.playerId);
const merged = mergeProgress(savedProgress, localProgress);
await savePlayer(merged);There are more checks around the real implementation, but the important behaviour is fairly straightforward.
Typing an email address alone is not enough to claim another player’s progress. The link has to be opened from the inbox.
This would admittedly be one of the least consequential account takeover attacks imaginable, but we still did not see much reason to design one.
Those emails are not newsletter subscriptions either. Marketing consent remains separate, and nothing about somebody’s email address, entered letters or crossword progress is sent to Gemini.
It is probably more privacy thinking than most people expect to sit behind a free crossword. We rather like that. Small products are good places to practise decisions you eventually need in much more important ones.
Moving the experiment onto our real stack
The internal prototype used Python, FastAPI and vanilla JavaScript. The released crossword does not. It now shares the same broad technology stack as the rest of the White Rabbit Foundry website: Next.js 16 with the App Router, React 19, TypeScript and Tailwind 4.
That was not because we suddenly developed an objection to the original tools. They had done exactly what we needed.
The prototype needed to prove an idea quickly. Once we decided the idea deserved a permanent home, maintaining an entirely separate application simply to preserve the prototype stopped making much sense.
The crossword is now a normal part of whiterabbitfoundry.com rather than a satellite orbiting it.
The application runs on Cloud Run in Google’s London region, with Firebase Hosting providing the domain, TLS and delivery layer in front of it. Cloud Build produces the container from changes pushed to GitHub. Firestore stores puzzles, anonymous player progress and temporary email linking records.
Gemini runs through Vertex AI, with London preferred and a global fallback available because regional capacity can occasionally become restrictive.
Mailgun handles the one off progress emails.
Google Analytics 4 is consent gated, and the game sends a deliberately small group of events that actually help us understand behaviour: starts, checks, hints, reveals, skips, completions, shares and progress linking.
Everything sits in the same Google Cloud project as the wider website.
One bill. One set of logs. One place to look when something odd happens.
There is nothing particularly adventurous about that architecture.
That is one of the reasons we chose it.
The cookie that disappeared
Of course, putting everything onto the real stack also created problems the prototype had never encountered. One of the first was strangely specific.
For the first few hours of production testing, the crossword appeared to remember progress perfectly.
The server, unfortunately, had no idea which player anybody was.
The browser’s local copy of the grid was masking the problem.
Firebase Hosting was the culprit. In this setup, the specially named __session cookie is the one forwarded through to Cloud Run. Our sensibly named progress cookie never arrived at the backend.
Nothing crashed.
There was no giant error telling us what we had misunderstood.
It simply did not arrive.
The application now places the server side session information inside __session, and the problem disappears.
This is exactly the kind of technical discovery that becomes extremely obvious about five minutes after somebody has finally explained it.
Until then it can consume rather more time.
Firestore had an opinion about crossword grids as well
Crosswords are naturally represented as rows of cells. In code, the obvious structure is therefore an array containing other arrays. Firestore does not store arrays inside arrays. So we do not store the grid that way.
Each row is packed into a string when it goes into the database and unpacked again when it comes back out.
There is no sophisticated architectural insight hidden in that decision.
The database would not accept the obvious representation, so we used another one.
A surprising proportion of building software consists of moments like this.
The mobile problem was not really a mobile problem
Dynamically generated grids created another issue. Some were wider than a phone.
We considered making the grid scroll sideways. We looked at more aggressive scaling and responsive transformations. There were several clever ways to make an oversized crossword behave on a small display. None felt especially pleasant.
Eventually we stopped trying to make large grids fit. Every crossword is now capped at eighteen columns, the length of WHITERABBITFOUNDRY itself.
The whole grid fits on a phone without horizontal scrolling, while the letters remain large enough to read and tap comfortably. We thought we had an interface problem. What we actually needed was a better constraint.
That is often the nicer answer.
What “built with AI” actually meant
The released crossword took roughly a day to build, across two main rounds, with a small team directing an AI coding agent through collaborative tools.
That description needs some context because “built with AI” has become almost meaningless on its own.
The human made the product decisions.
Should everybody begin on the same shared crossword or receive their own? What should happen when somebody completes one? Do we need accounts? How should progress move between devices? Is the leaderboard actually improving anything? What happens when Gemini provides words that cannot be laid out? Does the interface make sense when you stop resizing a desktop browser and actually hold it in one hand?
Those were the decisions that shaped the product.
The coding agent handled a substantial amount of implementation. It wrote much of the TypeScript, generated tests, helped with the transactional email and policy wording, worked through errors and ran deployments.
Then we used the product on real devices.
Things were wrong.
Back it went.
That cycle happened more than once.
Our experience sits somewhere between the two caricatures of AI assisted development.
It would be inaccurate to say the AI simply provided autocomplete.
It would be equally inaccurate to say it independently designed and built the product.
What it provided was leverage.
A person could make a decision and have much more implementation happen around that decision than would previously have been possible in the same amount of time.
But somebody still had to decide what should exist, use the result, notice what felt wrong and work out whether the technically correct solution was actually the right product.
The cost of producing code is falling quickly.
The cost of judgement is not.
The model is never the only route through
The crossword has also reinforced something we have been learning across other AI work.
If a generative model sits inside a product, there needs to be an answer for the moment it behaves like a generative model.
Sometimes Gemini produces good words that do not make a good crossword.
Sometimes a regional endpoint rate limits.
Sometimes a clue technically follows every instruction and still sounds a bit strange when somebody actually reads it.
None of those things should stop someone playing.
That is why the grid retries.
It is why the handwritten fallback exists.
It is why generation can fall back from London when required.
We are not trying to make a probabilistic system stop being probabilistic. We are trying to contain the uncertainty so the surrounding product still behaves predictably. That difference has become one of our favourite tests for whether something is really an AI product or still an AI demonstration.
A demonstration shows what the model can do when the model does what you hoped.
A product has to cope with the afternoon when it does not.
Why are we building a crossword at all?
This is probably the reasonable question. White Rabbit Foundry works on digital products, analytics, automation, data and systems for businesses. A crossword sits a little sideways to all of that. Part of the answer is simply that we wanted to make one.
We do not think every interaction between a company and the world around it has to become a lead generation mechanism wearing a slightly different coat. Sometimes it is perfectly acceptable to make something useful, interesting or entertaining. But the crossword has also become a surprisingly useful miniature product environment.
There is persistent state. Transactional email. AI generated content. Deterministic application logic. Analytics. Privacy decisions. Automated deployment. Responsive design. Fallback behaviour. Data retention.
That is real product infrastructure.
It just happens to be using all of it to ask somebody for a seven letter word about cloud bills.
It gives us somewhere relatively harmless to experiment. We can test infrastructure patterns, learn how generated content behaves with real users and discover odd platform behaviours before encountering the same idea in something considerably more important.
Likewise, improvements from the serious work can flow back into the crossword. The subject is playful. The engineering does not need to be.
Marketing should probably give something first
The other reason for building it has very little to do with software. A lot of digital marketing asks for something before it has earned much attention.
Enter your email to download this.
Complete this form to access that.
Tell us who you are and we will eventually show you the interesting bit.
We wanted to reverse that sequence.
The crossword now sits on the White Rabbit Foundry site alongside Emily and Clean Rabbit. It has its own social preview, search presence and an FAQ for people looking for a free online crossword or something more focused on the world of work.
There is still no gate in front of it.
Play first.
Finish the puzzle and you might see an invitation to subscribe to our newsletter. Link your progress by email and we can introduce some of the other things we build.
Both happen after the product has already done something for you. The experience gets the first chance to explain who we are. That feels much more natural to us than demanding the email address and promising that the value will arrive afterwards.
What the prototype was really for
The original internal crossword took only a few hours to make. The version we actually launched took another day. Looking at those numbers, it would be easy to argue that we should simply have skipped the prototype and built the current version immediately.
Perhaps we could have.
We probably would have built a different product.
The prototype gave us something much more useful than reusable code.
It gave us something to react to.
Once we could actually play it, it became obvious that one crossword was not enough. The leaderboard was less interesting than we thought. A standalone application felt unnecessary. Progress needed to survive. Mobile behaviour mattered considerably more once somebody actually tried to play with a thumb.
None of those insights required months of research.
They required something real enough to use.
This is what we think prototypes are particularly good at.
Their job is not always to become production.
Sometimes their job is simply to make the next decision less theoretical.
A final thought
We nearly launched the first crossword. We nearly wrote about it too. In the end, we did neither. That turned out to be quite useful.
Speed in product development is often described as the time between having an idea and putting it in front of the public. That is certainly part of it, but there is another advantage to moving quickly.
You get to discover sooner that the first thing you built is not quite the thing you want to release.
Our internal prototype proved that the basic idea worked.
The useful work came afterwards. We removed the parts that had not earned their place, changed the architecture, added persistence, solved the mobile experience and introduced generative AI where it addressed a real limitation rather than simply making the project sound more fashionable.
The result is still only a crossword.
It is also an AI generated content system, a deterministic layout engine, a cross device memory problem, a small privacy exercise, a deployment pipeline and a marketing experiment wrapped around eighteen letters running through the middle of a grid.
Most people playing it will never know any of that.
That is probably a good sign.
They have clues to solve.
Play the free White Rabbit Foundry crossword at www.whiterabbitfoundry.com/crossword. No account, no adverts and nothing to install.

