A Profile is one self-contained set of tracking data — its own trackables, entries, catalogs and visualizations — in one SQLite file. Nothing is shared between profiles, and switching changes everything on screen.

Several people, or one person’s separate experiments, never share a table with a profile column: each is its own file. That is the stronger privacy shape, not the weaker one, since one file can be exported, deleted, backed up or synced without touching any other, and no query anywhere has to remember to filter by profile. For the same reason the app never shows two profiles’ data side by side, which would need two open databases.

Where profiles are kept

  • The files sit in the app’s data directory, named by the app’s identifier (%APPDATA%\ca.datler.app\ on Windows): seed-john.db and seed-chad.db for the demos, and user-<slug>.db for each profile made in the app, its slug made from the name and never reused while an old file of that slug waits to be deleted. The SQL plugin opens a relative sqlite:<file>, so the same names work on every platform. In the browser build the files are in the browser’s private file storage, opened by SQLite compiled to WebAssembly in a worker.
  • Which profile is open, and which of yours exist, is kept in localStorage, where every reader looks, and in the desktop app also in profiles.json beside the databases, which is the truth there. The WebView writes its own storage to disk seconds late, so a switch made just before closing the app would otherwise be lost and the next launch would open a different profile. Every change saves the file at once, written whole and then renamed over the old one, so a crash mid-write leaves the old state; main.tsx restores it into localStorage before sync starts or anything renders, and a first launch with no file saves what localStorage already holds. The file also keeps where a demo was entered from and the profile files waiting to be deleted. A browser build and the tests keep plain localStorage, since a browser has no shell to write a file and does not lose its storage the same way.
  • Switching saves that state and then reloads the app. The driver chooses its file when its first connection opens, after the restore, and the SQL plugin keeps one pool per file, so a reload lets the connection, the migrations and every cached query start again from the new pointer rather than each being invalidated by hand. A newly opened file migrates itself (Migration Runner).

Your profiles

No person’s profile is built in. On a first launch, with no profile of your own, the app opens no database and shows one screen with three ways in: make a profile (a name, and where it lives), sign in and download one from your account, or look around first, in a demo profile or on the tour of both (components/onboarding/FirstProfile.tsx). Wherever the app would fall back to “your profile” — leaving a demo, deleting the open profile, a saved pointer that no longer resolves — it goes to your first profile, or back to that screen if you have none.

A profile of yours can be made, renamed and deleted from the profile switcher, and lives either on this device or in your account, kept in step with your other devices; see Synced Databases for moving one to your account, downloading one, and deleting one everywhere. A new profile lives on this device unless you choose your account, and a profile on this device needs no account: an account is needed only to sync. Choosing your account needs a sign-in and syncing included on it, and the form says which is missing. Renaming changes only the label; the file keeps its name. Deleting one asks with a plain confirm rather than having its name typed, since throwaway profiles are made and dropped often, and typing a name each time teaches typing it without reading. It removes its file once nothing has it open (an open file is removed at the next launch), so a profile remade with the same name never reopens old data. The desktop shell deletes only a user-<slug>.db file, with what SQLite keeps beside it. A new profile starts with the setup wizard, which turns plain-language goals into the modules that build its first trackables.

Export is free and needs no account. Settings → Data → Export this profile writes entries.csv (one row per entry: when, the trackable, the entry as the app shows it, its note, and the raw values; text a spreadsheet would read as a formula, starting =, +, -, @, a tab or a carriage return, is written with a ’ in front, OWASP’s guidance, so a bulleted note opens as text), profile.json (every data table, JSON columns as structures, without the sync bookkeeping or the migration ledger, and its schema named by the last migration the build applies, schema_version), profile.db (a consistent copy made with VACUUM INTO while the app has the file open, by core’s copyDatabase) and a README.txt. On the desktop they go in a new folder in Downloads, never over an earlier export, with an Open folder button; the browser build downloads the CSV and the JSON.

The demo profiles

Two personas show what the app does with data in it: John, a beginner a month in (how he feels, his energy, sleep and focus, and three habits), and Chad, a power user’s year (sleep, meals from a food catalog, push, pull and legs workouts, reading, work and therapy on a schedule, the weather, the odd week away). Each is a profile like any other, marked disposable: nothing in it is anyone’s data, and it never syncs. Each has one description, its about in PROFILES, true to its builder, which its banner and its setup screen both show.

  • Built by the app, from code. The builders live in @datler/core and write through the Repository Seam, so the phone and the browser build them the same way the tooling does. Opening a demo that has no data offers to build it, and shows its progress; nothing is built at boot. A correlated generator with a fixed-seed PRNG (mulberry32) produces the same year every time and plants real relationships — sleep shapes the next day’s pleasantness, meditation lowers stress — so the analysis has something true to find and nothing where none was planted. A demo’s last day is built only up to the moment it is built, and schedules and the weather fill it in as the day goes on.
  • Reset to factory, in Settings, rebuilds the demo you are in. Only the open demo can be reset, since the open file is the only one the app can write.
  • Always built by the current code. Each demo’s file carries a stamp in SQLite’s user_version: a fingerprint of @datler/core’s source, worked out when the app is built. A build writes 0 first and the stamp last, so a build that fails halfway is never taken for current. Opening a demo whose stamp differs from the running code’s rebuilds it, saying why, so new seed content reaches every device.
  • Kept apart from your profiles. The switcher lists them below New profile, under “Demos · example data”, so nothing of yours sits one mis-click from them. While you are in one, a banner says it is a demo and gives the way back: to setup if you came from setup, otherwise to the profile you came from. The banner, the tour’s End and an empty demo’s Back to my profile all leave the same way (leaveDemo), which ends any running tour first, since a tour left running switches back to its stop after the reload. Getting started is not shown on a demo.
  • The tour walks through both: eight stops, three on John and five on Chad, each naming a profile, a route and what to read there. The stops are data, so adding one is adding an object; the position is kept in localStorage because crossing personas reloads the app, and the banner hides while the tour runs. The setup wizard offers the tour, and Settings → Data offers it beside a button for each demo.

The demos are reached from the first-launch screen, the switcher, Settings, and the setup wizard’s tour. Leaving one goes where you came from, or with no profile of your own back to the first-launch screen; its button says which (leaveDemoLabel in onboarding/leave-demo.ts: Back to setup, Back to a profile’s name, or Back to the start).

Safety

disposable is the line between example data and real data, and everything reads it: the switcher’s grouping, the banner, the stamp, Reset, sync, and the database tooling, whose destructive commands refuse a profile that is not disposable and, by file name alone, any file that may hold a person’s data (DB Dev Commands lists them).

Code

  • packages/core/src/profiles.ts — the demo profiles, the refusal rules, a made profile’s file name (userProfileFile, userProfileSlug), and the profile state’s keys, file name and reader, which the renderer, the db tooling and the probe all use; packages/core/src/seed.ts (resetDemoProfile, readDemoStamp), seed-john.ts, seed-chad.ts, seed-generator.ts, seed-now.ts.
  • apps/renderer/src/db/profiles.ts, profile-state.ts, profile-files.ts, driver.ts, demo.ts; apps/desktop/src-tauri/src/lib.rs (the state file, deleting a profile’s file, the carry-over).
  • apps/renderer/src/components/onboarding/ — FirstProfile, OnboardingGate, DemoSetup, DemoBanner, DemoReset, DemoLinks, Tutorial; apps/renderer/src/onboarding/tutorial.ts.
  • apps/renderer/src/export/export-profile.ts; scripts/demo-stamp.ts.

Synced Databases · Repository Seam · DB Dev Commands · Getting Started · Migration Runner