Home · Docs · Saving + Versions · Autosave

Autosave

Every change saves automatically. There is no Save button. The status indicator in the toolbar shows what state the save is in, every state has a precise meaning.

There is no Save button in sweetdocs. Every change you make goes through a per-document save queue that handles debouncing, retries, offline buffering, and conflict detection. The status indicator in the toolbar shows you exactly which state the queue is in.

kyoto-trip Saved

Kyoto, days 1-3

We land in Kyoto on the 12th and have three full days before Osaka.

How saves work

A single rule: when you stop typing for 1 second, the queue writes your latest content to the document row. Specifically:

  1. Every keystroke updates the in-memory content for the doc
  2. The same keystroke writes the content to localStorage under sd:draft:{docId} so a crash can’t lose it
  3. A 1-second debounce timer runs
  4. When it fires, the queue sends the content to the server
  5. The server’s response confirms with a new updated_at timestamp
  6. The localStorage draft is cleared

The whole thing is per-document, so editing two docs in two tabs doesn’t slow either of them down.

The status indicator

The small label next to the title shows what the queue is doing:

IndicatorMeaning
SavedEverything is durable in the cloud.
Saving…A write is in flight (debounce fired or a request is pending).
Offline, will save when you reconnectYour network is down. Edits are queued locally.
Save failed, retryingA request failed. Exponential backoff in progress, up to 30 seconds between attempts.
Conflict, needs resolutionAnother tab or device wrote to this doc since your last sync. See Conflict handling below.

The label updates in real time as the state changes. Saved is the only durable state — when you see it, the change is safely in the cloud.

Offline editing

The queue checks navigator.onLine before every save attempt. When offline:

On reconnect, the queue automatically drains. Nothing for you to do.

Tab close / crash recovery

When you close a tab or navigate away, the queue runs a final flush using fetch keepalive — the browser delivers the last save even after the page is gone.

If something interrupts that (process kill, OS shutdown, browser crash), the localStorage draft holds your latest content. Next time you open the doc, the queue compares the draft against the server’s content; whichever is newer wins, and a save runs immediately if your draft was ahead.

Retries and backoff

When a save fails (network blip, server 5xx, transient error):

You don’t need to do anything during retries — the queue keeps pushing until it lands.

Conflict handling

The queue uses optimistic concurrency control keyed on the row’s updated_at timestamp. Every save sends “I think the row was last modified at X; only accept my write if that’s still true.”

If another tab or device wrote to the same doc since you loaded it, the server rejects with no row matched. The queue then:

  1. Re-fetches the current row from the server
  2. CRDT-backed docs: merges the server’s Yjs state into the local Y.Doc (Yjs handles divergence deterministically) and retries the save. No user-visible conflict — see Real-time collaboration for the merge model.
  3. Non-CRDT docs: flips status to Conflict and captures both versions. The user resolves which copy wins.

In practice, the CRDT path covers almost every case where multiple writers are live; the conflict path is reserved for the unusual case where the doc isn’t in a collab session (e.g., an offline write on a doc that nobody else has open).

Snapshot versions

Saves are every-keystroke (debounced). Versions are every-checkpoint. They serve different jobs:

A new entry is added to document_versions when:

The DB has a dedup trigger — identical content (the version you’d be creating matches the most recent one) gets dropped automatically, so you don’t end up with hundreds of identical snapshots. See Version history.

What this means in practice

There’s no scenario where you should reach for a Save button. There isn’t one.