Vibes DIY
Vibes DIY / Blog
From the build log

Where your data lives

Ask most developers where a web app's data lives and they'll point at a server. Open a Vibes app and the honest answer is: on the device you're holding. Each named database is a local replica — an IndexedDB store on the page — and that replica is the database. Reads and writes hit it directly, with no round trip and no spinner. Sync to the server happens in the background, after the fact.

That one inversion changes how everything downstream behaves — offline, conflicts, permissions, what "revoke" means. So we drew the whole path a write takes, start to finish, in a single picture:

NETWORK Your device first-party · vibes.diy origin vibes.diy server authoritative · background Local store + sync engine THE DATABASE An IndexedDB replica on the top page — reads & writes hit it directly, no round-trip. this is the database Vibe app (iframe) RENDERER useFireproof() crosses a postMessage bridge to the local store — not the network. access.js SERVER-ONLY Runs only here, on ingest. Decides who may read & write — the client can’t bypass it. Other devices’ replicas Accepted changes stream to everyone whose access still matches. 1 put() instant · optimistic 2 sync 3 accept reject Accept fans the change out to other replicas. Reject overwrites your optimistic copy, server-wins — silently — your code never catches it. If the rule gave a reason, the platform shows it.

Revocation stops the feed — it doesn’t reach back

When access is removed, the server stops routing new data to that device. Changes already synced there stay on it — “stops receiving,” not remote-erase.

local · instant server · authoritative accept · fan out reject · server-wins
Where your app’s data lives, and how a write travels: applied locally and instantly, synced in the background, decided by access.js on the server, then fanned out or converged server-wins.

Follow the numbers. A put() lands in the local replica immediately (①) — the person sees their change at once, optimistically. Reads are served from that same replica first. Only then, in the background, does the change sync across the network (②) to the server, which is the authoritative copy.

Get posts like this in your inbox

One email field. Real updates. No algorithm required.

The server is where access.js runs — and the only place it runs

On ingest, the server evaluates your app's access.js (③). This is the load-bearing fact about permissions in Vibes: access functions run only on the server. The client can't evaluate them, can't grant itself channels, and can't bypass the decision. A write that looks successful locally hasn't been accepted yet — acceptance happens here.

From the decision, two things can happen:

  • Accept → the change fans out to every other replica whose access still matches. That's how a teammate's edit appears on your screen in real time, no configuration required.
  • Reject → the server's authorized version quietly overwrites your local optimistic one. Server-wins, and — this is the part that surprises people — silently.

You don't catch a rejected write

Because the rejection is silent and server-authoritative, the right thing for app code to do is nothing. You don't wrap put() in a try/catch to surface a permission error, and you don't hand-roll a retry. If the rule spelled out a reason, the platform shows it to the person in a toast — but a reason-less refusal (like a database-level ACL) just converges quietly, with no popup at all. Either way your code doesn't catch it, so keep the UI driven by the live data and let the converged state speak for itself. Your job is to gate the surface up front (offer the button only to people who can use it), not to handle denials after the fact. Transport hiccups and offline are handled for you too: those writes queue and retry automatically; only sync being behind is what "offline" means.

Revocation stops the feed — it doesn't reach back

The same model makes one more thing precise. When you remove someone's access, the server stops routing new data to their device from then on. It does not reach back and erase what already synced there. The honest mental model is a downloaded file: revoking stops future delivery, but a copy someone already received is theirs to keep. So the promise we make in the product is always "stops receiving," never "loses every copy." Design your sharing flows for that reality — only share data a recipient may retain.

Why we built it this way

Local-first is what makes a Vibes app feel instant and keep working when the network doesn't. Putting the database on the device means the UI never waits on a server to paint. Keeping access.js on the server means that speed never costs you a security boundary — the authoritative decision is always made somewhere the client can't touch. Silent server-wins convergence keeps the contract simple: the server is the source of truth, and the app doesn't carry error-handling for a decision it doesn't own.

The full reference — offline, conflicts, blocking, and the exact rules for each — lives in the Local-first data & sync guide.

Build something that works offline by default

Every Vibes app is local-first from the first line. Describe it and start.

Start building →

Enjoyed this? Get the next post by email

One email field. Real updates. No algorithm required.

Prefer a feed? RSS · Atom