Data, backups & moving your work
heap. stores everything locally — there is no account and no server. This page covers where your data lives, how backups work, and how to move a profile between machines today.
Where your data lives
All state is one JSON file, state.json, under the platform’s application-data
location (QStandardPaths::AppDataLocation):
| OS | Typical path |
|---|---|
| Windows | %APPDATA%\heap\heap\state.json |
| macOS | ~/Library/Application Support/heap/heap/state.json |
| Linux | ~/.local/share/heap/heap/state.json |
(The folder is named twice — organisation, then application.) Settings → About shows the exact folder of the running copy.
state.json holds every profile (tasks, people, statuses, docs, notes), the
global events, and your settings blob. It is human-readable — safe to inspect,
and easy to back up by copying.
An event carries, besides its title, type, hours and dates, a few optional
fields a file from before 0.5.3 simply lacks: location, notes, url,
reminderMinutes (-1 = the notification setting, -2 = none, else minutes
before) and tz. tz is empty for an ordinary event (it is at that hour
wherever you are); a series imported from another time zone keeps the zone’s
IANA name there, its hours are that zone’s, and every occurrence is converted
on its own day so it follows that zone’s daylight-saving changes.
Attachments
Files attached to a task or linked from a note live in attachments/, next to
state.json (so --data-dir moves them too). Each file is stored once, named
by its content: the first 32 hex digits of its SHA-256 plus the original
extension, e.g. attachments/3fa94c…c1.png. The same file attached to three
tasks is one copy; a stored file is read-only and never changes.
- A task lists its files in an optional
attachmentsarray —{id, name, size, mime}per file, the name and size being what the file was when it was attached. A task without files has no key, and astate.jsonfrom before attachments simply has none. The key arrived with schema v11, so 0.5.3 (v10), which does not know it, opens such a file read-only instead of saving every task without its files. - A note, a doc page or a task description links a file as markdown:
for an image (shown inline in the preview),[spec.pdf](attachments/<id>)for anything else. The text is the reference — there is no separate list for notes. - Detaching a file, or deleting the link, only drops the reference. Settings → Data → Unused attachments shows how many files (and how many bytes) nothing refers to any more and deletes them after a second press. A file counts as in use while any profile’s tasks, descriptions, notes or pages link it, or while undo or redo could bring such a link back.
- A file missing from the folder (deleted by hand, or a profile imported without its files) shows as a broken chip; the task keeps its name and size.
- One file may be at most 100 MB. Symbolic links, junctions and shortcuts are never followed: attach the file itself.
- Opening a file uses its default application; a type that would run something (a program, a script, a shortcut) is confirmed first.
Backups do not include attachments. The files in backups/ are copies of
state.json only; restoring one brings back the references, and the files are
still in attachments/ unless the cleanup removed them since. To back up
everything, copy the whole data folder.
Saved views
A profile may carry savedViews, the sidebar’s saved views in their order. The
key is optional: a profile without views — and every file written before saved
views existed — simply has none; the key belongs to schema v11. Each entry:
{ "id": "view-3f9c2a1b", "name": "Urgent",
"query": "priority:P0,P1 is:open", "priorities": ["P0"],
"sort": "manual", "archived": false, "showDone": false, "view": "board" }
query is the search box text exactly as typed (whitespace collapsed) and is
read again against the current columns every time, so a view naming a column
that has since been deleted shows the search box’s “not understood” badge
rather than an empty board. priorities are the filter bar chips that were
on, sort the board order (manual, priority, due, updated, title),
archived the Archived toggle, showDone the timeline’s Show done, view the
view it opens in (board, timeline, week, month, archive). Unknown
values read as the defaults; entries without an id or a name are dropped.
Views travel with a profile export/import and a profile duplicate. The three
starter views a new profile gets are ordinary entries, written once when the
profile is created: delete them and they stay deleted.
The filters the window is showing right now are not per profile: they live in
the settings blob under settings.app.filters (search, priorities, sort,
archived, showDone, and savedView, the id of the view they were last set
from).
reminders.json, next to state.json, remembers which reminders were already
shown in the last three days, so a restart does not show them again. Deleting
it is harmless.
Using a different directory
--data-dir <dir> puts state.json, backups/, logs/ and the keychain-less
secrets.json fallback somewhere else for that run:
heap --data-dir /path/to/throwaway-profile
HEAP_DATA_DIR does the same for a whole shell; the flag wins when both are
set. Use it to try a build against a scratch profile, reproduce a bug, or take
screenshots without touching your real data. --data-dir "" is an error, never
a silent fall-back to the real profile, and an unknown option or --view name
exits with the usage text (code 2). Run heap --help for the full option list.
heap --smoke --data-dir <dir> checks a build against a copy of that
profile’s state.json in a temporary folder: the real file is never migrated
or rewritten, and the temporary folder is removed on exit.
One heap per data folder
A data folder is used by one running heap at a time (heap.lock in the
folder). Starting heap again — from the Start menu, a shortcut, or with
--view <name> — brings the running window forward (switching view if asked)
instead of opening a second copy that would save over the first one’s edits.
A heap with a different --data-dir is a different workspace and runs
alongside.
When state.json can’t be used
heap never replaces a state.json it could not read with anything else:
- Locked (an antivirus scan, a backup or sync tool holding the file): heap waits about two seconds for the lock to lift. If it doesn’t, the window opens read-only with a red banner, showing the newest backup (or an empty workspace) — nothing is saved over the real file. heap reopens it by itself once the lock lifts, or when you press Retry (after that, changes typed into the read-only session are not kept).
- Damaged (not JSON, or JSON without any profile): the file is kept as
state.corrupt-<time>.jsonnext to it (moved, or copied when a lock forbids moving) and the newest usable backup is loaded. A banner that stays up until you dismiss it names both files, with Open data folder: whatever changed after that backup is not in it. With no backup at all, heap opens an empty workspace under the same banner — not the demo and the welcome tour, since this is not a new install. If the damaged file cannot be set aside at all, the session is read-only. - From a newer heap (a higher
schemaVersion): opens read-only with a banner that stays up; a copy is kept once asbackups/state-premigration-v<N>-<time>.json. Update heap to edit it. - A save fails (read-only file, full disk, a lock): a red banner says why, your changes stay in memory, and heap retries on its own (2 s, 5 s, 15 s, …) or when you press Retry. An unwritable data folder is reported at start-up (banner, and on stderr).
Every one of these leaves a line in logs/recovery.log. Keys heap does not
know (from a newer point release or a hand edit) are kept on save at the
document, settings and profile level, and on every task, event, person and
column.
Automatic backups
heap. copies state.json into the backups/ folder next to it, at most once
per interval — hourly, daily (the default) or weekly, set in Settings → Data
— and keeps the newest 20 copies (state-<time>.json; the retention count is
fixed). Pre-migration copies (state-premigration-*) are kept apart from that
count.
To restore, pick a backup from the same panel. The current state is always
snapshotted first, whatever the interval (state-<time>-prerestore.json), then
replaced; undo history does not carry across a restore. If the current file
cannot even be copied (it is locked), the restore does not go ahead.
Move one profile between machines
Each profile can be exported and re-imported independently:
- Export. Top-bar profile menu → Export (or Settings → Data → Export
profile) writes a
<profile>.todocpp.jsonfile. - Import. On the other machine, Settings → Data → Import profile (or the profile menu) reads that file and adds it as a new profile.
- For a quick text snapshot instead,
Ctrl+Shift+Ecopies a Markdown summary of the active profile to the clipboard.
Export/import is content-only — it never carries settings or other profiles, so importing is always non-destructive.
A task id is unique across all profiles (reminders, notification actions and
event links find a task by id alone). An imported task whose id another profile
already holds gets the next free one under its prefix — TASK-1 becomes, say,
TASK-8 — and its dependency links, #TASK-1 mentions in descriptions, notes
and pages, and the imported events follow it. Duplicate profile does the
same for every task of the copy.
The export carries the profile’s attached files too, base64-encoded in a
top-level attachments array next to profile, so the file stands on its own
on another machine. Up to 64 MB of files are included; past that the export
still writes every task and note, leaves the files out (attachmentsOmitted
says how many) and says so — copy the attachments/ folder alongside, or use
the notes-folder export. On import every file is re-hashed: its id is
recomputed from its bytes, never taken from the file, and the references follow
if the two disagree.
Notes as a folder (Notes → Export) copies the files the notes link into an
attachments/ folder next to them, so attachments/<id> links read the same
in any markdown editor. Importing a folder brings those files back — and any
other file a note links by a relative path inside the folder (an Obsidian
assets/shot 1.png), which is stored and its link rewritten to
attachments/<id>. Links to other notes, to the web, or outside the folder are
left as written.
Multi-device sync (roadmap)
Continuous multi-device sync — pointing heap. at your own private git remote
as canonical storage, with one human-readable file per profile and git history
for free — is on the roadmap. The serialization layer that produces those
stable, diff-friendly per-entity files already exists
(src/sync/SyncSerializer); the git backend, scheduler and conflict resolver
land in a later release. Until then, the export/import flow above is the
supported way to move work between machines.
File format versions
state.json carries a schemaVersion. A newer build upgrades an older file in
place on first launch (keeping a pre-migration copy in backups/); an older
build refuses to save over a file written by a newer one, so nothing is lost by
running an old version by mistake.
v10 (0.5.3) changed how a task records its times:
dueAtandscheduledAteach say for themselves whether their clock time is real:dueHasTime/scheduledHasTimereplace the singlehasTimeflag. A date-only deadline on a task scheduled for 14:00 used to read as “due at 00:00” and reminded at midnight; it now stays a date and reminds at the end of that day. On upgrade a field keeps the old flag unless it sat at exactly 00:00 next to a timed partner.- Every task has a distinct
rank(its manual place in the column). Columns where cards shared a rank — the demo seed, synced issues, cards saved from the editor — are renumbered once, in the order the board already showed. - Recurrence may be monthly:
every:month(this day each month) orevery:month:15(the 15th, the last day in a shorter month).
Exported profiles from older builds (hasTime) import with the same rule.
v11 (0.5.4) added a task’s attachments and a profile’s savedViews.
Nothing is rewritten on upgrade — a v10 file has neither — but the version
matters the other way round: 0.5.3 does not know attachments, and a v11 file
opens there read-only rather than losing every task’s files on the first save.