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
- Architecture
- Platform setup
- Android host/client setup
- Migration and versioning
- Deployment checklist
- Troubleshooting
- Threat model
- Contributing
- Security policy
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