Building the browser extension was harder than the back-end (a URL shortener story)

I expected the URL shortener to be the difficult part. It wasn’t. The PHP back-end was finished relatively quickly: database schema, shortening algorithm, QR generation, statistics, rate limiting, and a simple API. The real challenge turned out to be building and publishing browser extensions for Firefox and Chrome.

TL;DR

I built a PHP URL shortener I always wanted to build – mostly to explore the algorithm used for such a tool, along with browser extensions for Mozilla Firefox and Google Chrome, with AI assistance. Technologies used: PHP, JavaScript, TailwindCSS v4.

Some of the technical challenges included:

  • Of course, the shortening algorithm: generating a 6-character alphanumeric code, yielding ~57 billion combinations (62^6)
  • Building browser extensions for Firefox and Chrome using Manifest V3 (MV3)
  • i18n support for the web application (the extensions remained English-only)
  • element.innerHTML and content security restrictions in extension contexts
  • Extension permissions and execution contexts within the browser

Non-technical challenges included:

  • Chrome/Firefox Store compliance – like writing a privacy policy (required for the Chrome Web Store)
  • Extension submission workflows (Firefox was simpler using web-ext cli; Chrome was more verbose via the dashboard)

And of course the usual infrastructure work: Apache configuration, caching, and rate limiting.

Overview

While AI might be cumbersome at times, I used Google Antigravity during development, mainly for boilerplate generation and debugging edge cases in browser extension behavior – especially around background scripts, MV3 constraints, and differences between Firefox and Chrome APIs (browser vs chrome namespaces).

Requirements

  • No additional expenses (hosted on existing PHP infrastructure)
  • Explore simple shortening strategies
  • Keep usage friction minimal (easy to use)

The back-end (PHP):

The back-end was straightforward. It included:

  • Simple relational schema (PDO-based)
  • URL shortening logic
  • QR code generation
  • Basic analytics (i.e. shortened urls and hits)
  • Caching layer for performance
  • Rate limiting for abuse prevention
  • Lightweight API for the browser extension integration

Nothing particularly unusual here, except for the shortening algorithm

The shortening algorithm:

I considered a few approaches:

  • Sequential auto-increment IDs (too predictable)
  • Hashing (less readable, collision considerations)
  • Random generation (best balance of simplicity and scalability)

I ultimately chose random 6-character alphanumeric strings.

That gives ~57 billion possible combinations, which is more than enough for the scale I expect, while keeping URLs short and easy to share or even type manually.

function generate_code(PDO $db): string {
    $chars    = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
    $reserved = array_map('strtolower', RESERVED_CODES);

    do {
        $code = '';
        for ($i = 0; $i < 6; $i++) {
            $code .= $chars[random_int(0, 61)];
        }

        $stmt = $db->prepare('SELECT * FROM urls WHERE code = ?');
        $stmt->execute([$code]);

    } while ($stmt->fetchColumn() || in_array(strtolower($code), $reserved, true));

    return $code;
}

Why six characters? Because five characters would significantly reduce the space (~916 million combinations), while seven would expand it to trillions, which was unnecessary for this use case. Six felt just right.

The extensions (JavaScript):

To meet the “ease of use” requirement, I decided to build browser extensions – for both Firefox and Chrome. I started with Firefox, since it’s my favorite, most flexible and privacy-first browser. The back-end API was already in place, so integration was simple: add an authenticated endpoint where the (long) URL goes and return a shortened URL. That part was easy. The rest was not.

To run an extension, of course you need to know your HTML, CSS and JS, however there are a few additional things to consider – of course the technical part, but also non-functional requirements.

A brief history on manifesting (or manifest.json)

Unfortunately manifesting won’t help here a lot. Here are the facts:

  • Manifest v1 (MV1) – the original Chrome extension format, long retired.
  • Manifest v2 (MV2) (~2012) – the format most of us grew up on. Persistent background pages, broad host permissions, blocking webRequest. Deprecated on Chrome as of 2025.
  • Manifest v3 (MV3) (introduced 2019) – introduces service-worker backgrounds, declarativeNetRequest instead of blocking webRequest, tighter permission model – definitely go with this one if you need cross-browser compatibility – i.e. one extension codebase for multiple browsers

The technical challenges

  1. The codebase – Because of the browser-specific differences, I keep feature work in one primary branch and branch off of it to handle the per-browser differences
  2. manifest.json – small differences like if let’s say you want an item in the browser’s context menu, you will find that this permission for firefox is “menus”, but for chrome is “contextMenus”
  3. Browser-specific differences – Firefox uses browser to access the browser API, while chrome does the same through the chrome namespace. There is a polyfill for that.
  4. CORS limitations – there are such! In my case it was easier to reconfigure the server, but keep in mind there is no getting around them
  5. innerHTML code injections are not welcome. You will need to find a way around that. In my case for a few elements, I was able to programmatically get around it, but something important to keep in mind.

Non-technical complexity

The technical work was only part of the process. Shipping the extension also required:

  • Writing an honest privacy policy (Chrome Web Store requirement)
  • Developer verification (Chrome Web Store requirement)
  • Preparing store assets (screenshots, descriptions, metadata)
  • Navigating Chrome’s more verbose submission process – you need to call out why you need each permission you request.

The publication process itself ended up being a non-trivial part of the project.

Conclusion

Going into the project, I expected the URL shortener to be the interesting engineering challenge. Instead, the back-end became the most predictable part of the system. The real complexity came from the browsers themselves: Manifest V3, permissions, execution contexts, store requirements, and the subtle differences between Firefox and Chrome. If I were to build this again, I would still start with the back-end – but I would no longer assume that is where the hard part is.

Useful resources

  1. What are Extensions? on MDN
  2. Extension Workshop (for Firefox)
  3. Example Extensions – handy starting point
  4. web-ext – CLI tool for verifying your extension before submission in the Mozilla ecosystem
  5. Welcome to Extensions (for Chrome)

Leave a Reply

Your email address will not be published. Required fields are marked *