local_store
The encrypted local database of the apps: drift on SQLite3MultipleCiphers, schema version 4
with the mirror tables and the local-only tables, data access objects that hand out the domain
types of packages/domain, removal and wipe, the cache cleanup and offline search
(docs/spec/15-client-architecture.md 6, ADR 0029, R-CL-08). The interface reads only from this
database (15 1 principle 2). Kind: pure Dart package, a member of the Dart workspace at the
repository root (resolution: workspace, publish_to: none).
Dependencies
Allowed members (docs/spec/15-client-architecture.md, 3.1; tools/arch-check/rules.yaml): domain (arrows of the diagram).
Every other member is out of reach; make arch fails on a dependency or an import of it.
External packages: drift, sqlite3; dev: drift_dev, build_runner, test, lints.
Further rules checked by make arch:
- no import of
package:flutter/,dart:ui(rule 1). - no native source (rule 4).
Opening and the key source
openLocalStore(file:, keys:, inBackground: true) opens the file in its own isolate
(NativeDatabase.createInBackground, 15 7.1). The key comes only from the port
DatabaseKeySource.obtainKey(), which returns 32 random bytes; the app creates them at the first
start and keeps them in secure storage (M4-18 for desktop, M5 for mobile). The first statement on
the connection is PRAGMA key = "x'<64 hex>'", the raw key form that SQLite3MultipleCiphers
takes without a key derivation; then the opener checks SELECT count(*) FROM sqlite_master
(a wrong key raises LocalStoreKeyError, whose message never contains the key), that
PRAGMA cipher reports chacha20 and that FTS5 is compiled in (otherwise
LocalStoreSetupError). The hex text exists only inside the setup call. openInMemoryForTests()
opens an unencrypted in-memory database with the same feature checks.
The cipher library is selected in the root pubspec.yaml, because build hooks read user
defines only from the workspace root:
hooks:
user_defines:
sqlite3:
source: sqlite3mc
Tables (schema version 4)
| Group | File | Tables |
|---|---|---|
| Structure | lib/src/tables/structure.dart |
spaces, user_space_states, channels (effective operator, caller's level and capabilities, no prompt), channel_operators, forum_tags |
| Threads and messages | lib/src/tables/threads_messages.dart |
threads (local last_accessed_at), thread_tags, messages (trigger_state, thread_seq, tombstone fields, no client_payload_sha256), reactions_summary, my_mentions, message_components |
| Files | lib/src/tables/files.dart |
files, message_attachments |
| Runs and agents | lib/src/tables/runs_agents.dart |
runs (summary of run.state_changed), agents, principals |
| Personal | lib/src/tables/personal.dart |
read_states, drafts, user_settings, notification_preferences, quiet_hours |
| Instance | lib/src/tables/instance.dart |
instance, commands |
| Local only | lib/src/tables/local_only.dart |
outbox, sync_state, loaded_ranges, file_cache, share_inbox, forum_view_states (version 2, M4-19: the Done section per forum), recent_targets (version 3, M4-23: the recently visited spaces, channels and threads of the quick switcher, per device, never synchronized), host_key_pins (version 4, M4-36: the pinned Noise keys of the hosts, per device, a trust anchor) |
| Search | lib/src/search.drift |
search_index (FTS5) |
host_key_pins of 15 6.1 came with version 4 (M4-36); from there on it is a preserved table of
MigrationRules like outbox, drafts and share_inbox (D-M4-36-2). The
schema has no foreign keys to server-owned rows; the version rule and the removal keep it
consistent. test/schema_test.dart compares the columns of every mirror table with chapter 03.
Data access objects
Each DAO hands out domain types, never drift rows, and offers watch... streams for the
providers. Mirror writes are upsert...IfNewer: one statement
INSERT ... ON CONFLICT DO UPDATE ... WHERE excluded.version > <table>.version that reports
whether it applied (04 9); reactions and pins carry absolute values and leave
messages.version alone.
| DAO | Purpose |
|---|---|
StructureDao, ThreadsDao, MessagesDao, RunsDao, PrincipalsDao, CommandsDao, InstanceDao |
mirror tables; MessagesDao.watchContiguous emits only the contiguous range of loaded_ranges that ends at the newest loaded message plus the first gap |
PersonalDao |
read states through mergeReadState (monotonic), drafts, settings, notification preferences, quiet hours |
SyncStateDao |
commitPage stores a page and the cursor with the sync epoch in one transaction; saveSession, markDisconnected, markSnapshot, resetCursor; clearMirror for a resync |
OutboxDao |
FIFO per thread by created_at, then id; depends_on; failures with the current revision of a 412 |
FileCacheDao, ShareInboxDao, SearchDao |
file cache metadata, share inbox, offline search; SearchDao.watchTranscriptsToIndex lists the done transcripts of attached voice files without an index row (the app fetches the text), pruneTranscripts drops the rows of files no stored message attaches any more |
HostKeyPinsDao |
the pin of a host (read, write replaces, all); nothing here deletes a pin (M4-36) |
ForumViewDao, RecentTargetsDao |
local view state per device: the Done section of a forum; the recently visited targets (visit keeps the newest keep rows, watchRecent newest first) |
Removal, wipe and cleanup
removeTargets(List<TargetRef>)deletes spaces, channels and threads with everything below them (messages, attachments, reactions, mentions, components, runs, drafts, read states, loaded ranges, search rows, file cache rows, recently visited rows) in one transaction and returns the file cache paths, so the caller deletes the bytes (04 8.3). Outbox entries stay; they fail with404. Host targets are ignored and the pins ofhost_key_pinsstay.wipe()closes the database and deletes the file with its-waland-shmfiles (4012, R-SY-14); the app deletes keys and tokens.CacheCleaner(limitsclient.cache_max_ageandclient.cache_max_bytesfromAppConfig) deletes threads and file cache rows without access since the maximum age, then the least recently used until the database plus the file cache fit the byte limit. It never touches outbox, drafts, the share inbox or the pinned host keys, and never the structure rows the sidebar needs (15 6.3). Onlywipe()deletes the pins, with the database.
Migrations
Code generation: dart run build_runner build writes lib/src/database.g.dart;
dart run drift_dev make-migrations (options in build.yaml) stores the schema of each version
under drift_schemas/local_store/ and the schema helpers under test/drift/local_store/generated/.
make codegen-check runs both and fails when a file differs from a fresh generation, so a table
change without a new schema version fails. drift writes the schema file without a final newline;
the end-of-file hook excludes drift_schemas/.
Rules for every later version (15 6.2): onUpgrade uses stepByStep with the steps from
MigrationRules. A mirror table may be discarded (MigrationRules.discardCache drops and
recreates it and resets the cursor, so the next connection loads a snapshot); outbox, drafts
and share_inbox are migrated row by row (MigrationRules.keepRows). test/drift/survival_test.dart
migrates every pair of stored versions with fixture rows in those tables and compares them; a new
version adds its fixture rows there.
Encryption library, license and FTS5
Measured with the pinned SDK: package sqlite3 3.7.1 loads SQLite 3.54.0 as
SQLite3MultipleCiphers 2.6.1 with the default cipher chacha20 (ChaCha20-Poly1305).
The sqlite3 build hook downloads a prebuilt library from the releases of the sqlite3
package and checks it against the sha256 hashes in its Dart sources; no C source is compiled in
the workspace. SQLite3MultipleCiphers is under the MIT license (its repository's LICENSE);
SQLite itself is in the public domain. Neither is a package of the lock file, so make licenses
does not list them; see docs/quality/licenses.md "Bundled native code". FTS5 is compiled in
(sqlite_compileoption_used('ENABLE_FTS5') is 1); the opener fails without it. search_index
uses the tokenizer unicode61 remove_diacritics 2 without a stemmer, so German and English words
match without their diacritics (14 6).
Layout
| Path | Content |
|---|---|
lib/local_store.dart |
the public library; every public symbol has a doc comment (make lint, public_member_api_docs) |
lib/src/ |
database, tables, DAOs, mappers, removal, cleanup, migration rules, opening |
drift_schemas/ |
stored schema files, one per version |
test/ |
tests, run by make test with coverage (quality.coverage.dart_min_percent); test/drift/ the migration tests |
Run from the repository root: make lint test coverage arch COMPONENTS=apps. The API pages
come from make docs (Dart API reference).
Libraries
- local_store
- The encrypted local database of the apps (docs/spec/15-client-architecture.md 6, ADR 0029,
R-CL-08): drift on SQLite3MultipleCiphers, schema version 4 with the mirror tables of 15 6.1
and the local-only tables, data access objects that hand out the domain types of
package:domain, removal of targets, the cache cleanup and offline search. No Flutter (15 3.1 rule 1).