sqlite_loom_suite

sqlite_loom_suite is an optional companion to sqlite_loom for related applications from one publisher that need to coordinate durable local state on the same device.

It provides suite and app identities, namespaced JSON values, a durable at-least-once event journal, voluntary app registration, compatibility negotiation, expiring leases, and redacted diagnostics. Initialization is explicit and dependency-injectable; there is no global suite singleton.

This is local-device infrastructure. It is not network synchronization, distributed consensus, installed-app discovery, secure secret storage, encryption at rest, or guaranteed background messaging.

Architecture

Platform Transport Storage owner Status
iOS / macOS Direct App Group file Every entitled app opens the same file Supported
Android Signature-permission ContentProvider broker One configured host app Supported
Windows packaged Direct PublisherCacheFolder Same-publisher packaged apps Supported
Windows unpackaged Explicit local path Application/operator Supported with caller-managed ACLs
Linux Explicit local path Application/operator Supported with caller-managed owner/group/mode
Web None Explicitly unsupported

Direct transports keep the SQLite database, -wal, and -shm in one local directory, enable foreign keys and WAL, and use a bounded busy timeout. Android clients never receive a database path or arbitrary SQL access. Native notifications are wake hints only; SQLite plus the durable journal are authoritative and polling is always available.

The suite database is normally additional to each app's existing private database. Keep app-specific records private and copy only intentionally shared state into the suite store. Moving an existing domain database into an App Group is an explicit application migration: every app then gains access to the whole file and must share compatible schema/migration ownership. This package does not relocate an application's database automatically.

Never place the database on iCloud Drive, OneDrive-synchronized folders, Dropbox, NFS, SMB, or another network filesystem. SQLite WAL requires all processes to be on one host and does not work over network filesystems; see SQLite WAL documentation.

Install

dependencies:
  sqlite_loom_suite: ^0.1.0

The package targets sqlite_loom: ^0.4.0.

Minimal direct setup

final client = await SuiteClient.open(
  SuiteConfiguration(
    suiteId: SuiteId('example.publisher.shared'),
    appId: SuiteAppId('example.publisher.app_a'),
    appName: 'Example App A',
    appVersion: '1.0.0',
    role: SuiteRole.host,
    appleAppGroup: 'group.example.publisher.shared',
  ),
);

// Always close with the application lifecycle.
await client.close();

Linux and unpackaged Windows callers pass an absolute databasePath or inject DirectSuiteTransport(pathResolver: ...). Packaged Windows callers pass windowsPublisherCacheFolder. Android clients use AndroidBrokerTransport; see the host/client guide.

Deployment guardrails

Prefer the platform-specific factories (SuiteConfiguration.appleAppGroup, .androidHost, .androidClient, .windowsPublisherCache, and .desktop) so incompatible role and transport combinations are not assembled by hand. For production suites, declare the same deployment contract in every app:

final deployment = SuiteDeploymentManifest(
  schemaOwnerAppId: SuiteAppId('example.publisher.host'),
  expectedApps: [
    SuiteExpectedApp(
      appId: SuiteAppId('example.publisher.host'),
      protocol: const SuiteProtocolRange(minimum: 1, maximum: 1),
    ),
    SuiteExpectedApp(
      appId: SuiteAppId('example.publisher.client'),
      protocol: const SuiteProtocolRange(minimum: 1, maximum: 1),
    ),
  ],
);

Direct transports automatically run SuitePreflight before opening. Android clients inspect the installed provider and reject missing, non-exported, URI-granting, or non-signature-protected brokers before the handshake. Suite metadata permanently records a random installation identity and the first declared schema owner; conflicting owners fail closed.

A file containing non-suite tables is rejected by default. Only a deliberate domain-database migration may set allowExistingDatabase: true; doing so gives every participating direct app access to the entire file. Known cloud/network paths and world-writable Unix directories are also rejected. The allowUnsafeSharedPath escape hatch is intended only for environments whose security is established outside the package.

Validate application manifests, entitlements, and desktop paths before a release:

dart run sqlite_loom_suite:sqlite_loom_suite_doctor \
  --project . \
  --apple-group group.example.publisher.shared \
  --android-authority example.publisher.shared.sqlite_loom_suite

See the deployment checklist.

Shared values

final write = await client.values.write(
  'preferences',
  'accent',
  {'name': 'indigo'},
);

final wonRace = await client.values.write(
  'preferences',
  'accent',
  {'name': 'teal'},
  ifRevision: write.value!.revision,
);

await for (final value in client.values.watch('preferences', 'accent')) {
  print('revision ${value?.revision}'); // Do not print the stored value in production.
}

Values are JSON-compatible, size-limited, optionally expiring, and revisioned monotonically across the suite. Compare-and-set returns applied: false on a revision conflict. No raw SQL is exposed.

Durable events

final published = await client.events.publish(
  type: 'document.changed',
  payload: {'documentId': '42'},
  target: SuiteEventTarget.apps({
    SuiteAppId('example.publisher.app_a'),
    SuiteAppId('example.publisher.app_b'),
  }),
  deduplicationKey: 'document-42-revision-7',
  timeToLive: const Duration(days: 1),
);

final subscription = client.events.subscribe(
  appId: client.configuration.appId,
  after: 0,
);
await for (final event in subscription.events) {
  await handle(event);
  await subscription.acknowledge(event);
}

Delivery is at least once. Persist the last processed sequence in application state when stronger restart behavior is needed, make handlers idempotent, and acknowledge only after successful processing. Suspended or terminated apps are subject to OS background limits and may not run until reopened.

App registration

SuiteClient.open registers the current app. Registration only proves that an app ran and opted in; stale rows can survive uninstall.

final recentlySeen = await client.apps.list(
  seenWithin: const Duration(days: 30),
);
await client.apps.removeStale(const Duration(days: 90));

Leases

final lease = await client.leases.acquire(
  'event-cleanup',
  'example.publisher.app_a/process-1',
  const Duration(minutes: 1),
);
if (lease != null) {
  try {
    await client.events.cleanup();
  } finally {
    await client.leases.release(lease);
  }
}

Leases recover after process death through expiration. They assume a reasonably consistent local wall clock and are not consensus or a fencing-token service.

Security

Do not store credentials, refresh tokens, private keys, or other secrets in suite values/events. SQLite Loom does not encrypt the database, and this package deliberately does not market shared storage as a secret store. Apple Keychain Access Groups are a separate consumer concern. Android hosts must use a signature permission and the included provider performs a second same-signer check.

Payloads and values are excluded from database observations by default. Treat app IDs, declared capabilities, and non-sensitive metadata as claims within the platform publisher/signing boundary.

Guides

Relationship to SQLite Loom

This package uses the public SqliteLoomProject, migrations, SqliteLoom, connection configuration, invalidation, external-change monitoring, observers, and health helpers. It does not duplicate Loom's typed query layer or modify Loom's compact core. Advanced consumers may use client.loom only on direct transports; Android broker clients receive a typed failure because there is no safe local database instance.

License

MIT © 2026 Omar Yacop.

Libraries

sqlite_loom_suite
Cross-application local coordination built on SQLite Loom.
sqlite_loom_suite_method_channel
sqlite_loom_suite_platform_interface