Data storage and migrations
Status: partly implemented. Settings, logs, and the history, bookmarks and list of downloads of the data service are stored as described, and the session in a simpler form; the rest of this page is design. See implementation status.
Astela keeps all user data local, in a small number of well-defined stores. Updates must never destroy data: every store has its own schema version, every migration is preceded by a snapshot, and an older app version never writes data it does not understand. The decision record is ADR 0009 (storage with node:sqlite); see the decision record index.
Stores
Section titled “Stores”| Data | Storage | Details |
|---|---|---|
| Settings (user layer) | settings.json |
Carries $schemaVersion. Written atomically and debounced. Invalid keys are moved to settings.rejected.json; nothing is deleted. |
| Session | session.json with 3 rotating backups |
Windows, workspaces, groups, tabs and navigation entries. Written 1 s after the last change (debounced) and on quit. |
| History, bookmarks, downloads list, favicons, site permissions | profile.db (SQLite with WAL and FTS5) in the data-service process |
Migrations through PRAGMA user_version. Before every migration, a snapshot is taken with VACUUM INTO. |
| Passwords (planned for M3) | vault.db |
AES-256-GCM per entry. The data key is protected through the asynchronous safeStorage API. A master password is required in some cases (see Password vault). |
| Themes | themes/*.theme.json |
Validated against the theme JSON Schema. |
| Logs | logs/*.log |
JSON Lines, rotating at 5 MB × 3 files, local only. |
| Web data | Chromium partitions | persist:default, persist:ws-<id> for isolated workspaces, and in-memory partitions for private windows. |
Notes on the stores:
- Settings. Only the user layer is stored in
settings.json. Defaults come from the module manifests and policies come from the operating system (see settings-policy-theme.md). Settings export and import will use JSON with version and schema checks; exports never contain secrets. - Web data. Cookie encryption is enabled through an Electron fuse from the first release on (see security.md). Private windows use an in-memory partition and are designed to leave no history, download history or cookies behind after they close.
- Logs. Logs never leave the machine on their own. The diagnostics page is designed to export a redacted support bundle on request (see enterprise.md).
The data-service process
Section titled “The data-service process”profile.dbbelongs todata-service, a utility process that also handles full-text search, favicons and import. It starts lazily and talks to the browser process over aMessagePort.- It uses
node:sqlite, which is available without a flag in Node 24.22, the Node version in Electron 45 (as of October 2026). Because it is built in, no native module has to be rebuilt for each architecture or ABI. Its API is synchronous, but it runs in the utility process and therefore never blocks the browser’s main thread. - Domain code accesses the database through the
IDatabaseport (exec,transaction) and through repositories such asHistoryRepositoryandBookmarkRepository. - Full-text search in the history is designed to answer in under 50 ms with 100,000 entries; this is one of the M1 acceptance criteria.
Writing safely
Section titled “Writing safely”- Atomic writes. JSON stores are written to a temporary file, flushed with
fsyncand then renamed over the previous file. A crash leaves either the old or the new version, never a partial file. - Debouncing. Frequent changes are collected and written together instead of on every change.
- Backups.
session.jsonkeeps 3 rotating backups, so a damaged session file is designed to be replaced by the newest intact copy. - Nothing silently deleted. Invalid settings keys are moved to
settings.rejected.jsoninstead of being dropped. - Transactions. Database changes and migrations run inside transactions.
Password vault (planned for M3)
Section titled “Password vault (planned for M3)”The password vault is planned for M3 and is designed to follow these rules:
- Encryption. Each entry in
vault.dbis encrypted with AES-256-GCM. - Data key. The data key is protected by the operating system’s key store through Electron’s asynchronous
safeStorageAPI. The synchronous API is slated for removal in Electron 46 (as of October 2026), so Astela uses only the asynchronous one. - Master password. A master password, derived with Argon2id:
- is required on Linux when
safeStorageoffers only thebasic_textbackend; - is optional on Windows, because DPAPI does not protect against other apps running as the same user (as of October 2026).
- is required on Linux when
Migrations
Section titled “Migrations”Updates never destroy data. Migrations follow these rules:
- Schema version per data type. Each data type has its own monotonic schema version, independent of the app version.
settings.jsoncarries$schemaVersion, andprofile.dbusesPRAGMA user_version. - Migration classes. Migrations are classes of the shape
Migration { from, to, up(), verify() }. TheMigrationRunneris designed to run them in order inside a transaction. Modules keep their migrations in theirmigrations/folder (see modules.md). - Snapshots first. Before any migration, a snapshot of the store is taken; for
profile.dbthis usesVACUUM INTO. Snapshots of the last 2 versions are kept.
flowchart TD OPEN["Open a store"] --> CHECK{"Stored schema version"} CHECK -->|same as the app| USE["Open normally"] CHECK -->|older| SNAP["Take a snapshot"] SNAP --> MIGRATE["Run up() and verify() in a transaction"] MIGRATE --> USE CHECK -->|newer than the app knows| OLDER["Use the matching snapshot, never write the unknown schema"]Older app, newer data
Section titled “Older app, newer data”After a rollback, an older version can find data that a newer version has already migrated. The app is designed to detect this, use the matching snapshot and never open a schema it does not know for writing. When the boot guard rolls back a failed update, it is designed to also restore the snapshots taken before the migration (see updates.md).
Golden-profile tests
Section titled “Golden-profile tests”- Profiles from every published version are kept in the repository as test fixtures.
- CI will migrate each of them to the current version and verify the result.
- The profiles will be created by the release preparation step (
pnpm release:prepare) in the release pull request that a maintainer authors, not by a workflow. - The updater tests will additionally check that data is migrated after a real update (see testing.md).
Day-one validation of node:sqlite
Section titled “Day-one validation of node:sqlite”Because the storage engine is hard to change later, node:sqlite was validated in a spike at the very start of M1, before the first walking-skeleton release. It passed on all six platforms; the validation results list the measurements.
- Platforms: 3 operating systems × 2 architectures.
- Checks: importing
node:sqliteinside the utility process, FTS5, WAL andVACUUM INTO. - Documented fallback:
- if
node:sqlitedoes not pass, a prebuiltbetter-sqlite3takes its place; - if FTS5 is not available, full-text search uses FTS4.
- if
Because all database access goes through the IDatabase port, either fallback is designed to stay a change inside the database adapter.
Implementation status
Section titled “Implementation status”Implemented in app/src/platform/node/:
-
Settings:
settings.jsonwith$schemaVersion1, written atomically (temporary file, flush, rename) and debounced, flushed on quit. Invalid values go tosettings.rejected.json; a file that cannot be parsed is moved aside instead of overwritten. -
Logs: JSON Lines in
logs/, rotating at 5 MB × 3 files. -
Web data: the
persist:defaultpartition; cookies and storage are flushed on quit. -
Session:
session.jsonkeeps the windows with their tabs (address and title), the active tab, and where each window was and whether it was maximized, written atomically at most once a second while tabs change, and on quit; the previous file stays assession.json.bakand is read when the current one is broken. The next start restores it when the settingstartup.pageisrestore, the default: only the active tab of each window loads, the others when they are first shown. After the last window closes, the session keeps that window. Popups that pages opened, private windows and the setup guide are never restored, and navigation entries, groups and workspaces are not kept yet. -
Private windows: keep nothing on disk. Their web pages use a session in memory, which is emptied after the last private window closed; they record no history and no zoom, keep the answers to permission questions in memory, and leave their downloads out of
profile.db(ADR 0041). -
Data service:
profile.dbbelongs to thedata-serviceutility process (app/src/main/data-service.ts), which the browser process starts with the first data access, such as the first web page, and talks to through messages (app/src/platform/electron/data-service-client.ts).node:sqliteruns there in WAL mode with foreign keys on; the browser process never touches SQLite. The schema (app/src/platform/data/profile-db.ts) has the history (placeswith an FTS5 index kept current by triggers), the bookmarks with the bookmarks bar and the other bookmarks as roots, and, from schema version 3, the list of downloads (downloads, the most recent thousand; ADR 0039). The browser process keeps a copy of the bookmark list, for the star in the address bar and for the bookmarks bar and its folder menus (app/src/main/bookmarks.ts); it loads with the first web page that a tab shows, or when the bookmarks bar shows. The history, bookmarks and settings pages and the import step of the setup guide start the service as well, when they first ask for data. The import from other browsers runs in the same service (app/src/platform/import/): it finds the profiles of Chromium- and Firefox-based browsers, reads copies of their databases and writes bookmarks and history through the profile database (ADR 0029). Migrations are numbered steps in one transaction each; before one runs on an existing database,VACUUM INTOwritesprofile.db.v<version>.bak, and a database from a newer version is opened read-only. On 2026-11-01 on Linux, the running service added about 23 MB of PSS to the browser (spike S3). -
History: every web page that a tab shows is recorded with its title; the history page (
app://history) lists it by day, searches it by word prefixes and removes single pages or everything.
Not implemented yet: three rotating backups of the session, keeping only the last two migration snapshots, golden-profile tests, themes on disk and the password vault.