Dev.to WebDev πŸ›  Dev πŸ‘ 1 πŸ“– 5 min read

How we share 50 browser tabs with a single URL and zero backend servers

Using the URL hash fragment to compress and share entire browser sessions, windows, and tab groups without a database, backend, or user accounts. When building Latr, our browser extension for tab management and tab remi

Using the URL hash fragment to compress and share entire browser sessions, windows, and tab groups without a database, backend, or user accounts.

When building Latr, our browser extension for tab management and tab reminders, we wanted a simple way for people to share a collection of tabs.

Think about common scenarios:

  • Sending a colleague five reference pull requests and design docs for a sprint task.
  • Sharing a curated pack of research papers or tutorials with a student.
  • Moving a window of twelve open tabs from a work laptop to a personal machine.

The industry-standard way to build this is predictable:

  1. Spin up an API endpoint (POST /api/sessions).
  2. Store the URLs in PostgreSQL or Redis.
  3. Generate a short ID (https://service.com/s/xyz123).
  4. Require user authentication to manage or delete the links.

We did not do any of that. Latr runs with zero backend servers, zero databases, and zero user accounts.

Instead, we pack the entire collection of tabs directly into the URL fragment. Here is how that architecture works, how we keep URLs compact, and the security checks needed when decoding arbitrary data on the client.

The Foundation: RFC 3986 and the Hash Fragment

The whole trick relies on a fundamental rule of browser networking:

https://latr.page/add#eyJ0IjoiRG9jcyIsInUiOiJodHRwczovL2V4YW1wbGUuY29tIn0
└─────── static host β”€β”˜β””β”€β”€β”€β”€β”€β”€β”€β”€ client-only payload β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Everything after the # (the fragment identifier) is strictly client-side. Web browsers never transmit the fragment in HTTP request headers.

That single property gives us three major advantages:

  1. Zero server infrastructure: The landing page is just a static HTML file hosted on Cloudflare Pages. It costs nothing to serve.
  2. True privacy: Neither our static host, nor Cloudflare edge logs, nor any middleman can see the URLs or page titles inside the link. The data only exists on the sender's device and the recipient's device.
  3. Infinite lifetime: Because there is no database, records never expire, get purged, or fail during server migrations.

Packing 50 Tabs into a Link

URL lengths cannot grow indefinitely. While browsers can technically handle tens of thousands of characters, chat apps like Slack, Discord, and WhatsApp often truncate links that exceed 8KB.

To fit dozens of tabs into a sharable link, we use a compact JSON schema before base64url encoding.

1. Single tab schema

For a single shared page or reminder, we use single-character keys:

{
  "t": "Page Title",
  "u": "https://developer.mozilla.org/...",
  "w": 1728640000000
}
  • t: Title (clamped)
  • u: URL
  • w: Optional timestamp (when the tab reminder is due)

2. Multi-tab session schema

For an entire window, tab group, or library selection:

{
  "n": "Design System PRs",
  "r": [
    { "t": "Tokens Spec", "u": "https://..." },
    { "t": "Button Component", "u": "https://..." }
  ]
}
  • n: Session or window name
  • r: Array of tab records

We deliberately strip out internal state. Local tags, notes, scroll anchors, and personal IDs are omitted. The recipient gets the clean pages, nothing personal.

Here is the encoder in TypeScript / JavaScript:

export function toSessionLink(name, tabs) {
  // Cap at 50 tabs to stay comfortably under URL limits
  const members = tabs.slice(0, 50).map((tab) => ({
    t: (tab.title || '').slice(0, 150),
    u: tab.url,
  }));

  const payload = { n: name.slice(0, 80), r: members };
  const json = JSON.stringify(payload);
  const encoded = toBase64Url(json);

  return `https://latr.page/add#${encoded}`;
}

function toBase64Url(str) {
  const bytes = new TextEncoder().encode(str);
  let binary = '';
  for (let i = 0; i < bytes.byteLength; i++) {
    binary += String.fromCharCode(bytes[i]);
  }
  return btoa(binary)
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=+$/, '');
}

At typical title lengths, 10 tabs compress to roughly 1.2KB. A full pack of 50 tabs lands around 6KB to 7.5KB, well within chat link handling limits.

The Security Boundary: Decoding Untrusted Fragments

Because any stranger can construct a link and send it to someone, treating location.hash as trusted data would be dangerous.

When the recipient lands on the static /add page, the decoder runs a strict validation pipeline before rendering:

export function fromShareLink(hashString) {
  try {
    const raw = hashString.replace(/^#/, '');
    if (!raw) return null;

    const json = fromBase64Url(raw);
    const data = JSON.parse(json);

    // Verify session payload shape
    if (data && Array.isArray(data.r)) {
      const validTabs = [];
      for (const item of data.r) {
        if (isValidTab(item)) {
          validTabs.push({
            title: String(item.t).slice(0, 150),
            url: item.u,
          });
        }
      }
      if (validTabs.length === 0) return null;
      return { name: String(data.n || 'Shared Tabs').slice(0, 80), tabs: validTabs };
    }

    // Single tab fallback
    if (isValidTab(data)) {
      return {
        name: String(data.t || 'Shared Tab').slice(0, 80),
        tabs: [{ title: String(data.t).slice(0, 150), url: data.u }],
      };
    }

    return null;
  } catch {
    return null;
  }
}

function isValidTab(item) {
  if (!item || typeof item.u !== 'string') return false;

  // CRITICAL: Protocol whitelist
  // Reject javascript:, file:, data:, or chrome: links
  try {
    const parsed = new URL(item.u);
    return parsed.protocol === 'http:' || parsed.protocol === 'https:';
  } catch {
    return false;
  }
}

Key security practices here:

  • Protocol enforcement: Only http: and https: URLs are accepted. This stops dangerous payload injection like javascript: or local file:/// URIs from ever reaching browser navigation calls.
  • Per-item failure tolerance: If one tab in a list of twenty is malformed, we drop that single tab rather than crashing the whole import. The recipient still gets the other nineteen valid pages.
  • Input clamping: String lengths for titles and names are truncated immediately to prevent DOM rendering issues.

What the Recipient Sees

When someone opens the link:

  1. If they have the extension installed, a background handshake offers to open all tabs immediately, save them as a clean tab group, or store them in their library.
  2. If they do not have the extension installed, the static web page displays an interactive, clickable checklist of all shared links. They can open individual links or copy them in one click.

Tradeoffs of the Serverless Approach

This pattern is not suitable for every application. Here are the honest tradeoffs:

  • No revocation: You cannot "delete" a link once you send it. If you paste it into a public Slack channel, anyone with the link can view the payload indefinitely.
  • No analytics on clicks: Because the fragment never touches a server, the sender cannot track who clicked or how many times it was opened.
  • Payload ceilings: You cannot pack 500 tabs into a single URL without breaking browser URI length limits.

For sharing everyday collections of 5 to 50 tabs, however, these tradeoffs are actually features. Users get instant sharing with no logins, zero tracking, and absolute data privacy.

If you want to test how the decoding feels or explore local-first tab organization, check out Latr. The entire extension is designed around zero install-time permission warnings and local storage.

πŸ“° Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.