DEV Community

Cover image for Share Your Web App State via URL — No Backend Required
MaxxMini
MaxxMini

Posted on • Edited on

Share Your Web App State via URL — No Backend Required

Ever wanted users to share their exact setup in your web tool — without a database, accounts, or server?

Here's a pattern I use in my tools: encode the entire app state into the URL. One click, one link, fully restored.

The Problem

You build a client-side tool. Users configure settings, enter data, tweak options. Then they want to share their exact configuration with a teammate.

Traditional solution: database + user accounts + share endpoints. That's a whole backend for one feature.

The Solution: URL State Encoding

function shareURL() {
  // Collect current state
  const state = {
    groups: getKeywordGroups(),
    match: currentMatchType,
    separator: document.getElementById("separator").value
  };

  // Encode as base64 URL parameter
  const encoded = btoa(JSON.stringify(state));
  const url = `${location.origin}${location.pathname}?q=${encoded}`;

  navigator.clipboard.writeText(url).then(() => {
    showToast("Shareable link copied!");
  });
}
Enter fullscreen mode Exit fullscreen mode

On page load, check for the parameter and restore:

const params = new URLSearchParams(location.search);
const shared = params.get("q");

if (shared) {
  try {
    const state = JSON.parse(atob(shared));
    restoreState(state);
  } catch (e) {
    loadDefaults();
  }
}
Enter fullscreen mode Exit fullscreen mode

That's it. ~20 lines of code. No server, no database, no auth.

Why This Works Well

  1. Zero infrastructure — The URL is the storage
  2. Instant sharing — Copy link → paste → done
  3. Bookmarkable — Users can save specific configurations
  4. Debuggable — Support tickets include the exact state
  5. Offline-friendly — Works without any network

Gotchas

  • URL length limits — Most browsers handle 2,000+ characters, but keep state compact
  • base64 bloat — JSON + base64 adds ~33% overhead. For large states, consider pako (gzip)
  • Breaking changes — Version your state format or add fallback parsing
  • Sensitive data — base64 is encoding, not encryption. Don't put secrets in URLs

Adding Persistent Presets (Bonus)

Combine URL sharing with localStorage presets for power users:

function savePreset(name) {
  const presets = JSON.parse(
    localStorage.getItem("presets") || "{}"
  );
  presets[name] = getCurrentState();
  localStorage.setItem("presets", JSON.stringify(presets));
}

function loadPreset(name) {
  const presets = JSON.parse(
    localStorage.getItem("presets") || "{}"
  );
  if (presets[name]) restoreState(presets[name]);
}
Enter fullscreen mode Exit fullscreen mode

Now users can:

  • Save configurations locally (survives page refresh)
  • Share configurations via URL (works across devices)
  • Load saved configurations instantly

All without a single API call.

Real-World Examples

1. Keyword Mixer — Full State in URL

I used this pattern in Keyword Mixer — a PPC keyword combination tool. Users set up keyword groups, match types, and negative keywords, then share the exact setup with their team via a single URL.

The ?q= parameter carries everything. Click a shared link → tool opens with the exact configuration → generate combinations immediately.

2. DonFlow — URL as a Trigger, Not Storage

For DonFlow (a budget tracker I built), I used a simpler variation: URL parameters as triggers rather than state containers.

Try it: donflow/?demo — one parameter loads a full demo experience with income, expenses, categories, and drift detection.

Why this variation matters: When your state is too large for URLs (DonFlow tracks months of transactions), use the URL as a trigger that tells the app what to do, not what to show. The actual data lives in IndexedDB.

The spectrum:
| State Size | Strategy | Example |
|---|---|---|
| < 1KB | URL parameter (base64) | Keyword Mixer configs |
| 1-10KB | URL trigger + localStorage | Presets, themes |
| 10KB+ | URL trigger + IndexedDB | DonFlow financial data |

When to Use This

✅ Configuration-heavy tools (settings, filters, presets)
✅ Small-to-medium state (< 1KB JSON)
✅ Tools where sharing = collaboration
✅ Demo/onboarding flows (URL triggers)

❌ Large datasets without triggers (use IndexedDB + export instead)
❌ Sensitive data (use encryption or server-side)
❌ Frequently changing state (use WebSocket or SSE)

Going Deeper

If you want to see the full zero-backend architecture in action — IndexedDB, CSV parsing, budget drift detection, all in the browser — I wrote about building DonFlow with zero backend.

Also check out 27+ free browser tools — all using variations of this URL state pattern.


What patterns do you use for sharing state in client-side apps? Have you tried the "URL as trigger" approach? I'd love to hear other techniques.


📚 More Build-in-Public

Top comments (3)

Collapse
 
bhavin-allinonetools profile image
Bhavin Sheth

This is a really underrated pattern. I use something similar in my own browser-based tools, and it makes sharing and debugging much easier. One thing I learned the hard way: always add a small version field in the state, otherwise old shared URLs can break when you update the tool.

Collapse
 
maxxmini profile image
MaxxMini

The versioning tip is a great catch — I learned that one the hard way too. Even a small v=1 prefix in the URL hash saves you from breaking shared links when the state schema changes. It's such a simple pattern but almost nobody talks about it. Thanks for sharing your experience with browser-based tools!

Collapse
 
trinhcuong-ast profile image
Kai Alder

Nice writeup. I've used this exact pattern for a config playground tool at work and it's been a lifesaver for bug reports - instead of "it's broken" we get a URL with the exact state that reproduces the issue.

One gotcha I ran into: btoa/atob doesn't handle unicode well. If your state could contain non-ASCII characters (user input, etc), you'll want to do something like btoa(unescape(encodeURIComponent(json))) on encode and the reverse on decode. Learned that one the hard way when a user had emoji in their config 😅

The spectrum table (state size → strategy) is really useful. I'd maybe add one more tier: for states between like 2-8KB, you could use the URL fragment (#) instead of query params - fragments don't get sent to the server and have slightly more relaxed length limits in some browsers.