Vibes DIY
Vibes DIY / Blog
From the build log

Apply now, verify in the background

Click a recommended vibe and the new app should just be there. For a while it wasn't — you clicked, and then you watched a little "Verifying access…" spinner before the app you asked for showed up. The frustrating part: the access check itself is fast. The wait was almost entirely a brand-new WebSocket, opening to the target app's shard for the very first time, so it could ask a question the page you just left already knew how to answer.

Fixing that feeling turned into a small tour of one idea, applied four times. None of the four made anything compute faster. Each one asked its question earlier, on a connection it already had, and remembered the answer honestly. The through-line is a contract: apply the answer now, revalidate it silently, and hold onto the working UI until a background check actually succeeds.

Apply now, verify in the background — a duotone title card over an open landscape.
Perceived speed isn't a faster engine. It's asking earlier and never being caught looking at the old thing.

Prefetch the verdict, not the page

Here's the shape of the original stall. When you're already on a /vibe route, your live socket is that vibe's shard connection. So the moment you navigate to a different vibe, the destination's first question — may this viewer see this app? — has to wait on a token mint and a WebSocket handshake to a cold shard before it can even be asked.

The lever is that the grant lookup (internally getAppByFsId) is served on every shard. Which socket you ask on is a free variable. So the source page — the one showing you the Recommended strip — resolves the destination's verdict over its own already-warm socket the moment those outbound links render, and stashes the answer in a tiny one-shot cache. When you click, the destination reads the cached verdict instead of opening a cold connection to re-ask.

One-shot is the honest part: taking the entry deletes it. A retry, or an invite-token link, always re-resolves from scratch, so a cached "yes" can never outlive its single intended use.

Making a speculative auth call free

Prefetching a verdict for a vibe you might never open sounds like it should have a cost — you're speculatively running an access check against apps across the strip. The reason it's safe to do eagerly is worth stating plainly: the access handler's only grant-writing path is redeeming an invite, and that path is gated on an invite token. A prefetch never carries one. So speculatively verifying a vibe changes nothing on the server — it reads, it never writes.

That check is what turned "risky speculative auth call" into "free." The performance win didn't come from a clever cache; it came from reading the handler closely enough to know the eager version couldn't have a side effect.

Get posts like this in your inbox

One email field. Real updates. No algorithm required.

The verdict survives the reload

The first version kept the verdict in an in-memory map — which the browser threw away the instant you hit reload, dropping you right back into the spinner. So the last applied verdict now also persists in sessionStorage, and on mount the page reuses the same apply-now-revalidate-silently machinery: paint from the saved answer immediately, confirm it in the background.

Persisting a security-adjacent decision means stricter rules than the in-memory hop had:

  • Keyed to the viewer. Each entry is tagged with the viewer's identity, so one person's verdict can't be read for another.
  • Only renderable grants are stored. A stale negative or gated state would flash the wrong overlay, so those are never persisted — only a verdict that resolves to "show the app."
  • An anonymous entry may only ever be public access. sessionStorage survives a sign-out reload, so a signed-in verdict persisted during some token gap could otherwise be replayed to the signed-out viewer that tab has become. Pinning anonymous entries to public-only closes that door.
  • Trimmed to what the overlay reads — the grant plus a little display metadata, nothing about the filesystem or environment.

Three caches now — in-memory prefetch, one-shot hop, persisted reload entry — and they all funnel into the same single background revalidate. That's the payoff of turning "apply now, verify in the background" into a primitive: each new place you want speed becomes just another place to read the cached answer from, not a new mechanism to design.

The same street has a pothole

The flip side of a route that remembers is a route that remembers too much. The same navigation flow had a bug that rhymes with all of this: clicking through to a new vibe left the app-switch panel hanging open over it. Client-side navigation keeps the route component mounted, so a piece of "is the panel open" state simply survived the hop as true. Closing the inner view didn't close the outer panel; the two states disagreed; and a stale bit of the previous destination kept painting over the new one until it was explicitly blanked on the identity change.

It's the mirror image of the caching work. A long-lived route is exactly what lets you carry a verdict across a navigation for free — and exactly what lets prior view-state leak forward if you forget to reset it. Same property, cutting both ways.

Never actually looking at an old app

The last piece is about a different kind of staleness. We serve a vibe's unversioned root HTML through a stale-while-revalidate edge cache on purpose: fast first paint, friendly to crawlers and no-JS clients, and "you might see the version from a few minutes ago" was a deliberate call. The catch is that the vibe a browser actually runs is baked into that cached HTML. So right after a republish, a real user could be left running the old app until a background refresh or a manual reload swapped it — the classic "I see the old one, then one refresh fixes it."

The fix leaves the SSR cache exactly as it is and closes the gap in JavaScript. The runtime reports the release it actually booted; the wrapper independently knows the true live release off the uncached lookup socket; and a content-addressed diff — a cheap per-file identity check, no bytes fetched — picks the cheapest correct action:

  • Hot-swap the new source through the existing set-source channel when only the app's entry file changed. That's the common single-file edit, and it swaps with no flash because the swap reuses the original mount and resolves imports against the running release's immutable directory — which is exactly the set of edits a source-only push can express correctly.
  • Reload the same unversioned route with a cache-buster query param, when a helper module, a dependency, the import map, or the environment changed and a source-only swap would be wrong. The extra param is what makes the request skip the stale-while-revalidate layer and re-render fresh — while still riding the unversioned URL, so the live published overlays (env, theme, seed) come along. A version-pinned URL would come back current but drop those overlays, which is exactly why the reload keeps the unversioned route and busts the cache instead.

Stale-but-fast for the first paint, always-latest for what you run — without having to choose between them.

The contract

Put the four together and none of them is really a caching trick. They're one freshness contract, stated four ways: show the best answer you already have right now, kick off a silent check, and let a successful check be the only thing that moves you off it. A failed background check is deliberately not a downgrade — it keeps the working UI you already applied rather than flashing an error or an old-app placeholder over it — so the worst case is staying briefly on a good-enough answer until the next check lands. Speed you can feel, and freshness you can trust, turn out to be the same discipline — apply now, verify in the background, and let staleness end only when a revalidation succeeds.

Build something that feels instant

Every vibe rides the same warm sockets and freshness machinery — you just prompt the app.

Start building →

Enjoyed this? Get the next post by email

One email field. Real updates. No algorithm required.

Prefer a feed? RSS · Atom