Features

Live Editing

Overview

Vextro's editor is built for real-time collaboration. Every document editor and global editor maintains a live Convex WebSocket subscription. When another user saves a change, non-dirty fields update automatically. When two users edit the same field simultaneously, a conflict modal guides resolution. All of this works transparently -- no extra configuration is required to enable the core sync behavior.

The same architecture applies to both collection document editors and global editors.

Real-time sync matters most when multiple editors work on the same content simultaneously: without it, one editor's save silently overwrites another's, creating data loss that's hard to detect and harder to recover from. By propagating changes over a persistent WebSocket connection, Vextro keeps every open editor in sync and surfaces conflicts explicitly rather than letting them happen invisibly.


Live Sync

The useLiveSync composable drives field-level synchronization. It subscribes to the server document via client.onUpdate and calls formState.applySyncUpdate on every push.

What gets updated

When a server push arrives:

  1. Fields the current user has not modified are updated immediately.
  2. Fields the current user has modified (dirty fields) are left untouched.
  3. If the incoming updatedAt timestamp is not newer than the one already stored locally, the update is skipped entirely.
  4. Updates are skipped while a save is in flight (saveStatus === 'saving').

The syncVersion counter on FormState increments when field values change due to sync. Components can use this as a {#key} value to remount inputs that cannot handle external value changes (e.g. rich text editors).

Only fields present in the server document are applied. Absent fields are never synthesized with empty defaults -- this prevents overwriting in-progress values such as block arrays that have not yet been persisted.

Conflict tracking during sync

If the server updates a field that the current user has dirty, applySyncUpdate records that a conflict exists but does not advance the local serverUpdatedAt timestamp. This means the next save attempt will detect the conflict via optimistic locking and surface the conflict modal.


Auto-Save

Auto-save fires after a debounce period whenever the form has dirty fields. It is disabled by default and must be opted into.

Collection-level config

// convex/collections/posts.ts
import { defineVextroCollection } from 'vextro';

export const posts = defineVextroCollection({
  slug: 'posts',
  // ...
  autoSave: {
    enabled: true,
    debounceMs: 2000, // default: 3000
  },
});

You can also pass autoSave: true to use the project-level defaults:

autoSave: true,

Or disable it explicitly even if a project-level default is set:

autoSave: false,

Project-level config

Set project-wide defaults in your VextroConfig. Per-collection config overrides these values.

// convex/vextro.config.ts
import { defineVextroConfig } from 'vextro';

export default defineVextroConfig({
  // ...
  autoSave: {
    enabled: true,
    debounceMs: 3000,
  },
});

Auto-save options

OptionDescription
enabledTurn auto-save on or off. Inherits from project config when not set.
debounceMsMilliseconds to wait after the last field change before saving. Default: 3000.

When to enable auto-save: Collaborative workflows and long-form content benefit most — browser crashes or accidental tab closes can wipe minutes of work without it.

When to keep it disabled: Collections where saves trigger expensive side effects (webhooks, image processing pipelines, third-party syncs) may incur significant cost or latency from frequent auto-saves. Disable it for those collections and let editors save manually.

Tuning debounceMs: Lower values (1000–2000 ms) reduce data loss risk in fast-paced collaborative sessions at the cost of more Convex mutations. Higher values (5000 ms or more) cut server load for single-editor, long-form writing where saving every keystroke adds no value. The default of 3000 ms is a reasonable middle ground for most cases.

Auto-save does not fire for new (unsaved) documents. The user must perform the initial save manually so the document gets an ID.


Save States

The FormState tracks one of six possible save statuses at all times:

StatusMeaning
cleanNo changes since last save (or initial load).
dirtyAt least one field has been modified.
savingA save mutation is in flight.
savedSave succeeded. Reverts to clean after ~2 seconds.
errorSave failed. The saveError field contains a message, and per-field errors may be set.
conflictOptimistic locking detected a concurrent modification. The conflict modal is shown.

The editor header and sidebar reflect these states visually. The save button is disabled while saving is active.


Conflict Detection & Resolution

Vextro uses optimistic locking to detect concurrent edits. Every update mutation sends the expectedUpdatedAt timestamp captured when the document was last loaded or saved. If the server's current updatedAt does not match, the mutation returns { status: 'conflict', currentDocument: { ... } } instead of applying the change.

Optimistic locking was chosen over pessimistic locking deliberately: no editor is ever blocked from opening or editing a document while someone else has it open. In practice, two editors modifying the exact same field within the same save window is rare — optimistic locking handles that rare case explicitly without degrading the common case where concurrent edits touch different fields.

What happens on conflict

  1. formState.saveStatus is set to 'conflict'.
  2. A warning toast appears: "Another user has modified this document."
  3. The ConflictModal opens with a field-level diff showing:
    • Server version -- what the server currently has
    • Your version -- what you were about to save
    • Base version -- what both users started from

Resolution options

Reload server version -- Discards all local changes and replaces the form with the server document. Safe choice when you do not need to keep your edits.

Force save -- Overwrites the server document with your local dirty fields, bypassing expectedUpdatedAt. Use this when you are confident your changes should win.

Force-saving permanently overwrites whatever the other user saved. Coordinate with collaborators before using this option.


Live Document List

VextroLiveDocumentList replaces the static SSR list on collection index pages. It mounts with the server-rendered HTML (no blank flash) and then subscribes to the same Convex query via WebSocket. This means editors see new content from teammates the moment it's published, and deleted documents disappear immediately — eliminating the confusion of stale list data that leads to duplicate work or editing already-deleted records.

Behavior

  • New documents slide in with a non-intrusive banner so editors notice without disrupting their workflow.
  • Deleted documents fade out rather than disappearing abruptly.
  • Updated documents briefly highlight to draw attention to the change.

This behavior is automatic. No configuration is needed beyond having the collection defined.


Live Review Queue

VextroLiveReviewQueue follows the same pattern as the document list. It mounts with SSR data and subscribes to live updates.

  • New items slide in with a highlight.
  • Stage and approval changes update in-place without a page reload.

This component activates automatically when a collection has workflow configured. See Workflow & Review Queues for setup details.


The useNavigationGuard composable prevents accidental data loss when navigating away with unsaved changes. It is active in all document and global editors.

How it works

  • Astro view transitions -- Listens to the astro:before-preparation event and calls window.confirm before the transition proceeds.
  • Standard navigation / tab close -- Listens to beforeunload and sets e.returnValue to trigger the browser's built-in confirmation dialog.

If the form is clean, neither handler fires.

Custom confirmation message

The composable accepts an optional confirmMessage string. The default is:

"You have unsaved changes. Are you sure you want to leave?"


Field Presence

When the Vextro Convex component is configured with a presence component, field-level presence indicators appear in the editor. Each active field shows avatar badges for other editors currently focused on that field.

Setup requires the presence schema, mutations, and config in the Convex component. Refer to the Convex Component documentation for the full setup steps.

Field presence requires the vextro_field_presence table and the fieldPresence configuration block in the Convex component. It is opt-in and has no effect if not configured.

Previous
Saved Views