Skip to content

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.

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).
  • profile.db belongs to data-service, a utility process that also handles full-text search, favicons and import. It starts lazily and talks to the browser process over a MessagePort.
  • 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 IDatabase port (exec, transaction) and through repositories such as HistoryRepository and BookmarkRepository.
  • 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.
  • Atomic writes. JSON stores are written to a temporary file, flushed with fsync and 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.json keeps 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.json instead of being dropped.
  • Transactions. Database changes and migrations run inside transactions.

The password vault is planned for M3 and is designed to follow these rules:

  • Encryption. Each entry in vault.db is encrypted with AES-256-GCM.
  • Data key. The data key is protected by the operating system’s key store through Electron’s asynchronous safeStorage API. 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 safeStorage offers only the basic_text backend;
    • is optional on Windows, because DPAPI does not protect against other apps running as the same user (as of October 2026).

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.json carries $schemaVersion, and profile.db uses PRAGMA user_version.
  • Migration classes. Migrations are classes of the shape Migration { from, to, up(), verify() }. The MigrationRunner is designed to run them in order inside a transaction. Modules keep their migrations in their migrations/ folder (see modules.md).
  • Snapshots first. Before any migration, a snapshot of the store is taken; for profile.db this uses VACUUM 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"]

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).

  • 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).

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:sqlite inside the utility process, FTS5, WAL and VACUUM INTO.
  • Documented fallback:
    • if node:sqlite does not pass, a prebuilt better-sqlite3 takes its place;
    • if FTS5 is not available, full-text search uses FTS4.

Because all database access goes through the IDatabase port, either fallback is designed to stay a change inside the database adapter.

Implemented in app/src/platform/node/:

  • Settings: settings.json with $schemaVersion 1, written atomically (temporary file, flush, rename) and debounced, flushed on quit. Invalid values go to settings.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:default partition; cookies and storage are flushed on quit.

  • Session: session.json keeps 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 as session.json.bak and is read when the current one is broken. The next start restores it when the setting startup.page is restore, 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.db belongs to the data-service utility 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:sqlite runs 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 (places with 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 INTO writes profile.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.