Bootstrap & Hydration
Bootstrap & Hydration
Bootstrap and hydration are two critical operations that prepare your app for synchronization. Understanding when and how to use them is essential for a robust sync implementation.
Core Concepts
User Identity
A user represents a person who signs into your app across multiple devices. Users are identified by a stable, unique identifier (typically from your authentication system’s JWT sub
claim).
Key characteristics:
- Stable across devices: Same user ID on phone, tablet, laptop, etc.
- Persistent: User ID doesn’t change across app sessions
- Unique: Each person gets their own user ID
- Authentication-based: Usually derived from your auth system (Firebase, Auth0, custom JWT, etc.)
Device Identity (Source ID)
A device ID (also called sourceId
) represents a specific app installation. Each device gets its own unique identifier that persists across app launches.
Key characteristics:
- Per-installation: Each app install gets a new device ID
- Persistent: Survives app restarts, updates, and device reboots
- Independent of user: Same device can be used by different users
- Prevents echo: Server uses device ID to avoid sending back your own changes
Bootstrap: Preparing the Local Database
Bootstrap initializes the sync system for a specific user on a specific device. It sets up the necessary metadata tables, triggers, and client information.
When to Call Bootstrap
// Call bootstrap AFTER user signs in successfully
suspend fun onUserSignIn(userId: String) {
val client = createSyncClient()
client.bootstrap(userId = userId, sourceId = getDeviceId()).getOrThrow()
// Now ready for sync operations
}
// Call bootstrap when switching users
suspend fun switchUser(newUserId: String) {
// Clear old user data or use fresh database
database.clearUserData()
val client = createSyncClient()
client.bootstrap(userId = newUserId, sourceId = getDeviceId()).getOrThrow()
}
What Bootstrap Does
- Creates metadata tables:
_sync_client_info
,_sync_row_meta
,_sync_pending
- Installs triggers: Automatically tracks changes to sync-enabled tables
- Records client info: Stores user ID, device ID, and sync state
- Prepares for sync: Database is now ready for upload/download operations
Bootstrap Parameters
userId
: Stable user identifier from your auth systemsourceId
: Persistent device identifier (see Device ID Management below)
Hydration: Initial Data Download
Hydration downloads the complete dataset for a user, typically used when setting up a new device or recovering from data loss.
When to Use Hydration
// New device setup (first sign-in on this device)
suspend fun setupNewDevice(userId: String) {
client.bootstrap(userId, deviceId).getOrThrow()
client.hydrate(limit = 1000, windowed = true).getOrThrow()
// Device now has complete user dataset
}
// Data recovery scenarios
suspend fun recoverFromDataLoss() {
client.hydrate(limit = 1000, windowed = true).getOrThrow()
// Local database restored from server
}
When to Use Incremental Sync Instead
// Session restore (app restart with existing data)
suspend fun restoreSession(userId: String) {
client.bootstrap(userId, deviceId).getOrThrow()
// Use incremental sync to catch up on changes
var more = true
while (more) {
val (applied, _) = client.downloadOnce(limit = 500).getOrThrow()
more = applied == 500 // Continue if page was full
if (applied == 0) break
}
}
Hydration Modes
windowed = true
(recommended): Creates consistent snapshot, safe for large datasetswindowed = false
: Simpler paging, suitable for small datasetsincludeSelf = false
(default): Excludes your own changes to avoid duplicateslimit
: Page size for downloads (1000 is a good default)
Device ID Management
Device IDs must be generated once per app installation and persisted across app restarts.
Multiplatform Implementation
Using Multiplatform Settings:
class DeviceIdManager {
private val settings: Settings = Settings()
fun getDeviceId(): String {
return settings.getStringOrNull("device_id") ?: generateAndSaveDeviceId()
}
private fun generateAndSaveDeviceId(): String {
val deviceId = "device-${Uuid.random()}"
settings.putString("device_id", deviceId)
return deviceId
}
}
Application Lifecycle Integration
App Startup Flow
class SyncManager {
suspend fun initializeSync(userId: String): Result<Unit> {
return try {
// 1. Ensure device ID exists
val deviceId = deviceIdManager.getDeviceId()
// 2. Create sync client
val client = database.newOversqliteClient(
schema = "myapp",
httpClient = authenticatedHttpClient,
resolver = ServerWinsResolver
)
// 3. Bootstrap is always required
client.bootstrap(userId, deviceId).getOrThrow()
// 4. Determine if this is first-time setup or session restore
val isFirstTime = !hasExistingUserData()
if (isFirstTime) {
// New device: full hydration
client.hydrate(limit = 1000, windowed = true).getOrThrow()
} else {
// Existing device: incremental sync
performIncrementalSync(client)
}
Result.success(Unit)
} catch (e: Exception) {
Result.failure(e)
}
}
}
User Sign-In Flow
suspend fun onUserSignIn(credentials: UserCredentials) {
try {
// 1. Authenticate with your auth system
val authResult = authService.signIn(credentials)
val userId = authResult.userSub // Stable user identifier
// 2. Initialize sync for this user
syncManager.initializeSync(userId).getOrThrow()
// 3. Start periodic sync
startPeriodicSync()
} catch (e: Exception) {
handleSignInError(e)
}
}
User Sign-Out Flow
suspend fun onUserSignOut() {
// 1. Stop sync operations
stopPeriodicSync()
// 2. Clear user data (optional, depends on your app's requirements)
database.clearUserData()
// 3. Clear auth tokens
authTokenManager.clearTokens()
// Note: Device ID persists across sign-outs
}
Best Practices
Bootstrap Guidelines
- Always call bootstrap after user authentication
- Call once per user session (not on every app start)
- Ensure business tables exist before bootstrap
- Handle bootstrap failures gracefully with retry logic
Hydration Guidelines
- Use windowed mode for production apps
- Choose appropriate page size (1000 is usually good)
- Show progress indicators for large datasets
- Handle network interruptions with resume capability
Device ID Guidelines
- Generate once per installation and persist
- Use platform-appropriate storage (SharedPreferences, UserDefaults, etc.)
- Include meaningful prefix like “device-“ for debugging
- Don’t reuse device IDs across app reinstalls
Next Steps: Learn about Sync Operations to understand upload/download patterns and conflict resolution.