惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园_首页
J
Java Code Geeks
博客园 - 聂微东
量子位
C
Check Point Blog
T
The Blog of Author Tim Ferriss
T
Tailwind CSS Blog
G
Google Developers Blog
Google DeepMind News
Google DeepMind News
B
Blog
罗磊的独立博客
腾讯CDC
GbyAI
GbyAI
博客园 - 【当耐特】
A
About on SuperTechFans
M
MIT News - Artificial intelligence
U
Unit 42
D
Docker
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Y
Y Combinator Blog
大猫的无限游戏
大猫的无限游戏
小众软件
小众软件
S
SegmentFault 最新的问题
有赞技术团队
有赞技术团队

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Designing Pairing Codes: Tradeoffs, Mistakes, and a Simpl...
Aleksandar S · 2026-05-03 · via DEV Community

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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;
    }
}

Enter fullscreen mode Exit fullscreen mode

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
...

Enter fullscreen mode Exit fullscreen mode

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