Dartvel, home
Docs
Features
Studio
Cloud
Compared

Search the site

GitHub
pub.dev

GETTING STARTED

Getting started
Existing Flutter apps
Existing Native apps
Run on your phone

APP

UI and styling
Routing
State
Accessibility
Keyboard shortcuts
Localization
Devices and desktop
Native device access
Media, 3D and XR

DATA

Data models
Forms
Search
Sync and offline
Import and export
Change capture
Database
Cache
File storage
Images
Privacy and erasure

BACKEND

Backend functions
Auth and sessions
Authorization
Queues and jobs
Workers and memory
Notifications and mail
Outbound HTTP
AI
Webhooks
GraphQL and OpenAPI
API keys and OAuth
Multi-tenancy
Billing and commerce
Modules

OPERATIONS

Edge security
Secrets and environments
Monitoring
Releases

SHIPPING

Build targets
Telegram Mini Apps
Static web hosting
Servers and deploying

REFERENCE

Testing
CLI reference
Coding agents

DATA

Sync and offline

React to every change to a model, see who else is on a page, and keep taking writes when the network drops.

All of it runs on your models, signals and queues. There is no separate realtime API to learn.

ON THIS PAGE

Watch model changes

Show who is here

Reaching another server or another device

Keep writing while offline

What the server does with a change made offline

Watch model changes

// Every save, destroy and restore of an Article in this process.
Article.changes.listen((DVModelChange<Article> change) {
  DV.log('${change.kind.name} ${change.model.slug} for ${change.tenant}');
});

// The whole list now, and again after each change.
final DVModelWatch watch = await Article.watch((List<Article> articles) {
  DV.log('${articles.length} articles');
});

// Saving is what publishes it. There is no sync() to call: a model that
// has to be told to sync is not realtime.
await article.save();

// Only because this watch was started outside a widget. One started in a
// page goes when the page does.
await watch.cancel();

Copy code to clipboard

Generated models publish created, updated, deleted, restored and synced changes.

A watcher only gets changes for the current tenant. Article.allChanges is every tenant's, for a process that serves all of them.

The generated backend also serves GraphQL subscriptions over server-sent events.

// A change is delivered only to watchers allowed to see the model.
Article.syncPolicy((Article article) => article.published);

Copy code to clipboard

Show who is here

DVPresence.startSweeping(); // drops members whose heartbeats stopped

DVPresence.channel('article:hello-world').listen((DVPresenceEvent event) {
  DV.log('${event.member.id} ${event.kind.name}');
});

await DVPresence.join(
  'article:hello-world',
  DVPresenceMember(id: 'user-1', state: <String, Object?>{'name': 'Ada'}),
);
await DVPresence.heartbeat('article:hello-world', 'user-1');

final List<DVPresenceMember> here = DVPresence.members('article:hello-world');
await DVPresence.leave('article:hello-world', 'user-1');

Copy code to clipboard

A member is an identity, so one person on two devices counts once.

Members are scoped to the current tenant.

A member who stops sending heartbeats for 45 seconds is gone. A crashed app never says goodbye.

Reaching another server or another device

A model you have opted into syncing is read and written the way any other model is. There is nothing to wire up and no object to implement: saving a record is what publishes the change, and watching one is what receives it.

Today that delivery happens inside one process. Carrying it between servers and devices is the framework's job and is not built yet, so a change on one instance does not reach another.

Partial

Spec section: Model Sync and Presence

Planned work and implementation limits

Delivery is in-process only. Nothing carries a change to another server or to a phone yet.

No reconnect policy, backpressure or collaborative editing.

Keep writing while offline

A data model that has to work with no network says so, and says how a write made offline is resolved when it reaches the server.

// A data model that has to work with no network says so, and says how a
// write made offline is resolved when it reaches the server. Nothing else
// is declared: the device keeps its own copy and a queue, and the server
// applies what the device sends, both from this declaration.
@DVModel(offline: DVConflict.lastWriteWins)
class const _Dispatch({
  required final String id,
  required final String reference,
  required final int quantity,
});

Copy code to clipboard

That is all you declare. The data model is then saved, deleted and read like any other, with a network or without one.

// Saved on this device at once, with a network or without one. It
// reaches the server when the server can be reached.
const Dispatch dispatch = Dispatch(id: 'd1', reference: 'R-1', quantity: 2);
await dispatch.save();

// Read back from the device, in a tunnel as on Wi-Fi.
final Dispatch? again = await Dispatch.find('d1');

// Where this record stands: pending, syncing, synced, conflicted or
// rejected.
dispatch.syncState.listen((DVSyncState state) {
  DV.log('${dispatch.reference} is ${state.name}');
});

// Deleting is the same: gone from the device now, from the server later.
await dispatch.destroy();

Copy code to clipboard

A save returns as soon as the record is on the device. The device keeps its own copy and a queue of every change, in the order they were made: SQLite on phones, desktops and TVs, IndexedDB in a browser. Reads come from that copy, so they answer in a tunnel as they do on Wi-Fi.

You never send the queue. It goes when the app starts, after each save while the server can be reached, and again as soon as the device can reach the server after losing it. A send that fails is tried again later, on its own.

The queue goes in order, and stops at a dropped connection, so a later change never reaches the server before an earlier one.

A change the server keeps differently comes back to the device, and watchers of the data model see it.

syncState on a record says where it stands: pending, syncing, synced, conflicted or rejected.

A change the server refuses for good is not sent again, and its record reads rejected, so nothing is lost silently.

A full queue refuses the next save. It never drops an old one.

Signing out sends what it can, then removes the device's copy and queue, so the next person to sign in on that device sees none of it.

Show what the device can reach

Application code does not branch on connectivity: a write is made the same way in a tunnel as on Wi-Fi. It does show it, though, and that reading is a signal.

Functional

Class

// What the device can reach, as a signal: a banner is a widget that
// rebuilds, not a listener to remember to dispose.
@DVFunctionalWidget()
@pragma('vm:entry-point')
Widget _connectionBanner(BuildContext context) {
  final DVNetworkStatus status = DV.Platform.network.watch(context);
  if (status != DVNetworkStatus.offline) return const DVBox.list(<Widget>[]);
  return DVBox.list(<Widget>[
    const DVText('Offline. Your changes are saved here and will be sent.'),
    DVText('Since ${DV.Platform.network.since}'),
  ]);
}

Copy code to clipboard

status is online, metered, offline, or unknown where no binding on this target has reported.

canReachTheServer is what most call sites are asking. It is true on a metered connection, and true when nothing has reported: a write's own failure is what proves the server is gone.

since is when it last changed, so an offline banner can say how long. A platform that re-reports the same status does not move it.

The browser binding is built. Other targets report unknown until theirs is.

What the server does with a change made offline

The generated backend takes each change a device sends and applies it itself. There is no server code to write for it: what decides is the data model's own declaration and its policy.

Only a signed-in person's changes are taken, and only for data models that declared offline:.

Every change is put to the data model's policy first, as create, update or delete, the same question an online change asks. The policy is asked about the record, as the class your policy is written for. A policy that refuses, or cannot answer, refuses the change.

A change sent twice, because an answer was lost on the way back, is applied once.

A refusal is remembered, so a device sending a refused change again gets the same answer, even if the policy has changed since.

With lastWriteWins, the later change wins by the time it was made on its device, corrected for that device's clock drift. The order changes arrive in does not decide.

DVConflict.ask is refused offline, because nobody is there to answer. A data model that declares it stops the build, so it cannot fail on somebody's phone instead.

Partial

Spec section: Offline-First Models

Planned work and implementation limits

encrypt: true is not applied, and sensitive fields are not encrypted in the device's copy.

The device copy's shape does not come from the migration schema, and a changed data model does not rebuild it.

Replay is not carried by the job queue, and the queue bounds and clock tolerance are not read from pubspec.yaml.

Only the browser reports connectivity. Elsewhere a failed send is what says the server is gone, and the retry is what finds it again.

PREVIOUS Search Full text, hosted engines and semantic search
NEXT Import and export CSV, NDJSON and Excel in and out of a model
GitHub
pub.dev
npm
Acknowledgements
Privacy
Terms

FSL-1.1-MIT licensed. Built with Dartvel.

Dartvel is made by

SigmaDev Digital

To the bottom