Designing Pairing Codes: Tradeoffs, Mistakes, and a Simple Approach That Works

Designing Pairing Codes: Tradeoffs, Mistakes, and a Simple Approach That Works

posted Originally published at softwarewitchcraft.com 7 min read

While building Booth Beam, a digital signage tool, I ran into a problem that looks trivial at first glance: how do you connect a TV to a web app in a way that’s fast, reliable, and hard to mess up? You could force users to log in on a TV, but anyone who has ever typed an email and password with a remote knows how painful that is. There’s a reason most modern apps avoid that entirely.

The approach used by apps like Netflix is simple and effective: pairing codes. Instead of logging in directly on the TV, you display a short code on the screen and let the user enter it on a device they actually enjoy using, like a phone or laptop. That small shift removes friction and makes the whole experience feel instant.

In BoothBeam, the flow is straightforward. A user opens the app on a TV and immediately sees a pairing code. Then they open the web app on their laptop or phone, enter that code, and the TV is connected. From that point on, they can send content to the screen without thinking about the pairing process again. It’s a one-time action that should feel effortless.

Constraints you can’t ignore#

It’s tempting to think “just generate a random code” and move on, but a few real-world constraints quickly complicate things. Users are often typing these codes from a distance, sometimes in busy environments like conferences or events. That means readability matters. Mistakes will happen, so the system should minimize the chance of confusion.

At the same time, codes need to be short-lived. In my case, they expire after five minutes. Multiple TVs can generate codes at the same time, and the first person who enters a valid code claims that connection. Behind the scenes, codes are stored in a database, uniqueness is enforced, and used or expired entries are cleaned up periodically.

It’s also important to be clear about what this system is and what it isn’t. This is not authentication. It’s temporary device linking. The security model is the same one used by Netflix and similar apps: short-lived codes, limited active set, and a narrow window of opportunity.

Choosing the right character set#

The first design decision is what your codes should look like. Numbers-only might seem like the simplest option, but they come with downsides. Digits are easy to confuse when viewed from a distance, and they don’t give you as many combinations per character, which forces you to make codes longer.

Letters-only is slightly better, but still limited. You reduce the total number of combinations and increase the chance of accidentally generating real words, which can lead to awkward situations.

A combination of letters and numbers strikes the right balance. It gives you a larger pool of possibilities and allows you to keep codes short without sacrificing uniqueness. That’s the direction I took.

How long should a pairing code be?#

Once you decide on the character set, length becomes the next question. With 26 letters and 10 digits, you get 36 possible characters. A 4-character code gives you:

36^4 = 1,679,616 combinations

At first glance, that seems more than enough. But practical issues start to show up as soon as you think about how people actually read and type these codes.

The 0 vs O problem#

One of those issues is the classic confusion between the number 0 and the letter O. Years ago, I learned to write a slashed zero to distinguish it from the letter, and it stuck with me. The problem is that most people don’t share that habit. When they see something like O0O0 on a screen, they hesitate or make mistakes.

The simplest way to solve this is to remove both characters entirely. By excluding 0 and O, you reduce the character set slightly, but you eliminate a very real source of user error. Now you’re working with 25 letters and 9 digits, for a total of 34 characters:

34^4 = 1,336,336 combinations

You lose some combinations, but you gain clarity, which is far more valuable in this context.

The profanity rabbit hole#

Another issue shows up once you start generating random combinations of letters: sooner or later, you’ll produce something that looks like a real word. Sometimes that word won’t be something you want displayed on a TV at a conference booth.

My first instinct was to filter these cases. Build a list of profanity, check generated codes against it, and discard anything problematic. On paper, that sounds reasonable. In practice, it quickly turns into a mess. No list is ever complete, languages vary, slang evolves, and you end up maintaining something that never truly solves the problem.

After thinking it through and discussing it out loud, it became clear that this approach was solving the wrong problem. Instead of filtering out bad words, you can design the system so that words never appear in the first place.

A simpler approach#

The key insight is simple: if your code always contains at least one digit, it can’t form a real word. That removes the need for any filtering.

For a 4-character code, you can enforce this by placing a digit in the second or third position. This guarantees that every code includes at least one number, breaking any potential word pattern.

With this constraint, the total number of combinations becomes:

9 * 34^3 = 353,304

That’s still more than enough for short-lived codes and a relatively small number of active devices. If you ever need more capacity, you can increase the length to five characters or even six. The important part is that the solution stays simple and predictable.

Handling collisions without overthinking it#

Even with hundreds of thousands of possible combinations, collisions can happen. The important thing is how you handle them. In this case, a simple retry loop is enough: generate a code, check if it already exists, and if it does, generate another one.

Because codes are short-lived and the number of active entries is small, the probability of repeated collisions is low. Adding more complex mechanisms doesn’t provide meaningful benefits and only increases maintenance cost.

Lifecycle of a pairing code#

Each code follows a simple lifecycle. It’s generated and stored in the database, then displayed on the TV. If the user enters it within five minutes, it’s marked as used and the devices are linked. If not, it expires and becomes invalid.

To keep the system clean, a periodic job runs once a day and removes both used and expired codes. This allows the same combinations to be reused over time without the database growing indefinitely.

Small UX details that matter#

The visual presentation of the code has a bigger impact than it might seem. Using uppercase letters improves readability, especially from a distance. A large font ensures the code is visible across the room, and slight spacing between characters helps users distinguish each symbol without breaking the flow of reading.

A code like AB3D is quick to scan and type. Adding spaces between characters (A B 3 D) slows users down and introduces uncertainty about whether spaces should be typed. Keeping it compact and consistent makes the experience feel faster and more intuitive.

Why this is secure enough#

Since this isn’t a login mechanism, the security requirements are different. The system relies on short expiration times, a limited number of active codes, and the fact that the first valid entry claims the connection. This is the same model used by widely adopted apps, and it works well in practice.

The chance of someone guessing a valid code within a five-minute window and using it before the intended user is extremely low, especially with a few hundred thousand possible combinations.

Final approach#

After exploring different options, the final setup in BoothBeam is straightforward:

  • 4-character codes
  • Uppercase letters and digits
  • Exclude 0 and O
  • Force at least one digit in the middle
  • Enforce uniqueness
  • Expire after 5 minutes
  • Clean up used and expired codes daily

It’s simple, easy to reason about, and avoids unnecessary complexity.

Conclusion#

It’s easy to overcomplicate problems like this, especially when the first idea involves filtering, validation, and edge cases. But if your solution requires maintaining profanity dictionaries across multiple languages, you’re already heading in the wrong direction.

A better approach is to step back and rethink the constraints. By slightly reshaping the problem—forcing a digit into the code—you remove the need for an entire class of solutions. The result is simpler, more reliable, and easier to maintain.

Sometimes the best engineering decision isn’t adding more logic, but removing the need for it entirely.


Appendix: Simple PHP Pairing Code generator#

class PairingCodeGenerator
{
    private const LETTERS = 'ABCDEFGHIJKLMNPQRSTUVWXYZ'; // no O
    private const DIGITS  = '123456789';                 // no 0
    private const POOL    = self::LETTERS . self::DIGITS;

    public static function generate(): string
    {
        $code = '';

        // 1st char
        $code .= self::POOL[random_int(0, strlen(self::POOL) - 1)];

        // force digit in 2nd or 3rd position
        if (random_int(0, 1) === 1) {
            // digit in 2nd
            $code .= self::DIGITS[random_int(0, strlen(self::DIGITS) - 1)];
            $code .= self::POOL[random_int(0, strlen(self::POOL) - 1)];
        } else {
            // digit in 3rd
            $code .= self::POOL[random_int(0, strlen(self::POOL) - 1)];
            $code .= self::DIGITS[random_int(0, strlen(self::DIGITS) - 1)];
        }

        // 4th char
        $code .= self::POOL[random_int(0, strlen(self::POOL) - 1)];

        return $code;
    }
}

And here is the example how to use it:

...
do {
    $code = PairingCodeGenerator::generate();
} while (codeExistsInDatabase($code));

// now you have unique valid $code for furth§er use
...

The implementation is simple on purpose. You can adapt it to any language, but the core idea stays the same.

More Posts

I’m a Senior Dev and I’ve Forgotten How to Think Without a Prompt

Karol Modelskiverified - Mar 19

TypeScript Complexity Has Finally Reached the Point of Total Absurdity

Karol Modelskiverified - Apr 23

Your Tech Stack Isn’t Your Ceiling. Your Story Is

Karol Modelskiverified - Apr 9

How I Built a React Portfolio in 7 Days That Landed ₹1.2L in Freelance Work

Dharanidharan - Feb 9

The Hidden Program Behind Every SQL Statement

lovestacoverified - Apr 11
chevron_left

Related Jobs

View all jobs →

Commenters (This Week)

1 comment
1 comment

Contribute meaningful comments to climb the leaderboard and earn badges!