Why buoyient?

keep your client up even when the network is down

The problem

Your users don't live in a world of perfect connectivity. But most apps are built as if they do.

Spinners and error screens

Users tap a button and see a loading spinner... then an error. They're in an elevator, on a subway, in a rural area. The app is useless.

Rebuilding sync from scratch

Every new feature needs its own connectivity checks, local caching, retry logic, and queue management. You've written this code three times already.

A B

Silent data conflicts

Two edits happen at once — one offline, one on the server. Without proper conflict resolution, someone's work gets silently overwritten.

Try it: online vs offline

This demo simulates how an app using buoyient handles connectivity changes. Toggle the network and add some todos.

Network: Online
Todo App synced
Buy groceries synced
Review PR #42 synced

Toggle offline, add items, then toggle back online. Watch the pending items sync up automatically.

Less code, more confidence

Here's what it takes to create an offline-capable todo item, with and without buoyient.

Without buoyient

~45 lines
suspend fun createTodo(title: String) {
  val todo = Todo(title = title)

  // Save locally first
  db.todoDao().insert(todo)

  if (isNetworkAvailable()) {
    try {
      val response = api.post("/todos", todo)
      if (response.isSuccessful) {
        val server = response.body()
        db.todoDao().update(
          todo.copy(
            serverId = server.id,
            version = server.version,
            syncStatus = "synced"
          )
        )
      } else {
        enqueueRetry(todo)
      }
    } catch (e: IOException) {
      enqueueRetry(todo)
    }
  } else {
    enqueueRetry(todo)
  }
}

// Plus you still need:
// - RetryQueue with SQLite persistence
// - WorkManager job to drain the queue
// - Conflict detection on sync-down
// - Idempotency key management
// - Placeholder ID resolution
// - ConnectivityManager listener
// - Error handling per retry attempt
// ... for every single entity type

With buoyient

~16 lines
suspend fun createTodo(title: String):
    SyncableObjectServiceResponse<Todo> {
  val todo = Todo(title = title)
  return create(
    data = todo,
    requestTag = TodoRequestTag.CREATE,
    request = CreateRequestBuilder { data, key, _, _ ->
      HttpRequest(
        method = HttpRequest.HttpMethod.POST,
        endpointUrl = "https://api.example.com/todos",
        requestBody = buildJsonObject {
          put("idempotency_key", key)
          put("title", data.title)
          put("reference_id", data.clientId)
        },
      )
    },
    unpackSyncData = unpacker,
  )
}

Offline queueing, retries, conflict detection, sync-down, placeholder resolution, and idempotency are all handled automatically by the engine.

What you get

buoyient handles the hard parts so you can focus on your domain logic.

Offline-First

Operations succeed immediately, regardless of connectivity. Data is persisted locally and synced when the network returns.

Bidirectional Sync

Periodic sync-down pulls server changes. Sync-up pushes local changes. Both directions handled automatically.

3-Way Merge

Field-level conflict detection compares base, local, and server states. Pluggable merge policies let you decide how to resolve.

Persistent Queue

Pending requests are saved to SQLite. They survive app restarts and process in order with automatic retries.

{id}

Placeholder Resolution

Reference objects that haven't synced yet with {serverId} placeholders. Resolved automatically at sync time.

Agent Optimized

Built for AI-assisted integration. Includes detailed instruction files, step-by-step guides, and skills for Claude, Copilot, and Cursor.

Ready to dive in?

See how it works → Get started →