Menu

Backups, sources and the project library

On this page

Versions kept in a content-addressed store, the author’s sources, the project library, .elabora archives and machine-local data. Part of the architecture, the contract every change follows.

Backups

The user never sees git. Elabora keeps versions of the project in <data>/backups/, as a content-addressed store:

  • <data>/blobs/sha256/ab/<hash>: each file content once (shared with the journal). macOS uses copy-on-write clones, so masters cost no extra space there.
  • <data>/backups/versions/<time>-<id>.json: a version is the list of files with their hashes, plus why it was taken (auto, before-ai, after-ai, close, publish, migration, import, restore, manual), an optional name and the batch.
  • When: before and after each AI batch, every 5 minutes of activity, when the app closes, before migrations, imports and restores, and as a named version at each release build (“Published 1.2.0”). The CLI takes an automatic version before a write when the last one is older than 5 minutes, so backups exist without the app.
  • Retention: everything for 24 hours, one per hour for 7 days, one per day for 30 days, then one per week, trimmed of binary files that are neither in the current project nor in a named version. Named versions are kept until the user deletes them. Garbage collection then deletes unreferenced blobs: a deleted video leaves the backups after 30 days.
  • elabora backup list | create [--name] | restore <version> [--files <glob>] | status.

Backups do not protect against losing the disk. A copy to a second location, then the cloud, come later.

The author’s sources (D49)

The files a module is made from (a client’s brief, a deck, recorded interviews) live in sources/ at the top of the course folder, with a readable version of each that any agent can read. @elabora/node sources/ implements them; elabora sources …, the MCP tools elabora_sources, elabora_source_read and elabora_source_add, and the app call it.

  • Not the course. sources/ is outside every snapshot (project-files.ts, isSourcePath in @elabora/core project/files.ts): it is never validated, journaled, watched by the engine, backed up, restored, built or published. The validator refuses a reference into it (E_SOURCE_REFERENCED: an asset, a theme’s stylesheet, font or url(), a free object’s file), and the build checks every entry it writes and every media file it reads, and refuses a sharing image taken there. A component’s preview and a translation’s folder leave it out. The library carries it: a module’s copy (duplicateProject), its import from a folder (importFolder), its archive with sources: true; a replacement by an archive without sources keeps the module’s; the trash keeps it with the module. LibraryProject.sourcesSize gives its bytes.
  • Adding (addSources(dir, files)): files and folders copied (cloned where the file system allows it) into sources/, flat, at once. A content already there is not copied again (already-there); a name taken is numbered (« brief (2).docx »); names keep the author’s words, made portable (sources/names.ts). Hidden files are never listed (Elabora’s own work in progress among them: .adding-*, .elabora-readable-*).
  • Readable versions (sources/index.ts): sources/<name>.md and sources/<name>.files/, written on demand (makeSourcesReadable, readSource), one at a time per source in a process, from a staging folder moved into place. The first line is a marker (<!-- elabora:readable {…} -->): the source’s hash, size and time, the reader and its version, whether it is complete; listSources gives each source’s state (ready, partial, stale, missing, failed, native, unsupported), hashing a source only when its size or time changed. The readers: docx.ts (Word), pptx.ts (PowerPoint: texts, notes and the pictures each slide places, with the file’s thumbnail; image-size.ts reads a picture’s size from its header to leave out the icons, bullets and rules), recording.ts (video and audio: the transcriber of D46, frames by ffmpeg), on xml.ts (a small XML reader) and ooxml.ts (an Office package’s parts and relationships, read from the streaming ZIP reader). No program of the author’s is used: a PDF is read as it is by agents (native), and a file in an older office format (.doc, .rtf, .odt, .ppt, .pps, .odp, .key) is unsupported, its notes saying to save it as .docx or .pptx (or as a PDF). Nothing leaves the computer.

Project library and archives

The app keeps its projects in a library it manages, as Resolve does, and people exchange projects as archives. Projects stay plain folders of files: agents (Claude Code, Cowork, Codex) and the elabora command work on them by path. @elabora/node library/ implements both; elabora projects …, elabora export and elabora import call it, and the MCP server reads the library (elabora_projects_list, elabora_projects_path) but never changes it.

  • Library: a local folder that no sync service touches: ELABORA_LIBRARY when set (tests, servers), else the folder the user chose (libraryPath in <local data>/settings.json), else <local data>/Library (~/Library/Application Support/Elabora/Library on macOS, %LOCALAPPDATA%\Elabora\Library on Windows; Documents is often synced by OneDrive or iCloud). The user may move it (an external disk): on one volume the folder is renamed; across volumes everything is copied and checked by hash under every project’s lock, and the originals are removed once the setting points to the copy; .elabora-library.json marks a library, so that a move interrupted by a crash resolves to the complete copy.
  • Projects: one folder per project, named by its project id (.elabora/project-id): ids, not titles, keep paths short and valid on Windows. A new project’s id is elabora-module- and six lowercase letters and digits that cannot be misread (no 0/o, 1/l/i), drawn again while the library, its trash or ELABORA_DATA_DIR has it: the folder an AI app asks the author to trust reads as Elabora’s in any language. Projects made before keep their UUID folder; both forms are project ids, and elabora projects shows a UUID’s first eight characters or the six letters, either of which finds the project again. The index is read from the folders (course.json, file times and sizes, <data>/thumbnail.png drawn by the app), with a validation summary cached in the project’s data while its files keep their sizes and times; a project that cannot be read is listed with its error. Creating, importing and duplicating build the project in <library>/.staging/ and move it into place with one rename, so a crash never leaves half a project visible; every write to an existing project holds its lock. Renaming changes the title only, as a journaled edit of course.json. The app opens only library projects: opening a folder from elsewhere imports a copy (new id; the folder and its history stay where they are). A copy of an example the app ships records it in its data (<data>/origin.json: the example’s id and version, when the copy was made), so that the app opens that copy again rather than making another; it is machine-local, so no archive carries it.
  • Trash: deleting a project moves its folder, history included, to <library>/.trash/<time>-<id>; listTrash gives each entry its title, when it was trashed and when it will be deleted (expiresAt: 30 days after, TRASH_RETENTION_DAYS; the app deletes expired entries once per launch). An entry can be restored (restoreFromTrash: under a new id, with its history, if its id is taken again, so that it sits beside the module that holds the old one). Only deleteFromTrash (one entry, whatever its age) and emptyTrash (all, or those older than some days) delete for good; the command line has elabora projects trash delete <entry> and trash empty.
  • Archives (<course-id>.elabora, see below): the project’s files, without machine-local data; optionally with its history. Import checks the whole archive, then creates a new library project (a new id), migrates an older format and validates it; importing a course that is already in the library asks: replace it or keep both. A replacement keeps the project’s id and folder (agents working on its path keep working), takes a backup version of the project first (reason import), moves the replaced files to the trash, and keeps the project’s history. Archives are also the open-format sources delivered to clients.
  • Storyline and Rise modules are imported into the library too, as new projects (Importing Storyline and Rise modules below).
  • The elabora command still works on any folder (developers, tests, CI).
  • Later, cloud projects use the library as their local copy.

Archive format (elabora-archive/1)

A ZIP file (methods stored and deflate, no encryption, no ZIP64, so at most 4 GB), written as a stream: text files deflated, everything else stored as it is. Its entries:

  • The project’s files at the root, as listProjectFiles lists them (no .elabora/, dist/, node_modules/, OS files or sync-service conflict copies): unzipping an archive gives the course folder. With sources: true (elabora export --sources), the author’s sources and their readable versions too, under sources/, listed in files like the others (an older Elabora imports them as course files); ArchiveSummary.sources and ExportResult.sources count them apart.

  • elabora-archive.json, the manifest:

    interface ArchiveManifest {
      format: 'elabora-archive/1';
      elabora: string;                       // version of Elabora that wrote it
      exportedAt: string;                  // ISO 8601
      course: { id: string | null; title: string | null; language: string | null; format: string | null };  // from course.json
      history: boolean;
      files: { path: string; size: number; hash: `sha256:${string}` }[];         // the project files
      historyFiles: { path: string; size: number; hash: `sha256:${string}` }[];  // the entries under history/
      historyBlobsInFiles: `sha256:${string}`[];  // history contents that are project files: stored once, there
    }
  • history/, with the history only: history/journal/<yyyy-mm>.jsonl (the journal as it is), history/versions/<file>.json (the backup version manifests, under their own names) and history/blobs/<sha256 hex> (the contents the versions and the journal refer to, except those listed in historyBlobsInFiles). Contents no longer in the store are left out and reported; restoring a version that needs them reports them missing.

Import refuses, before writing anything: a ZIP without manifest (a zipped folder: unzip it and import the folder), a manifest of an unknown or newer format, an entry name that is absolute, has .. or . segments, backslashes or a drive letter, a project file in a tool folder (.elabora/ and the others), a history/ entry of another kind than these three, names that differ only by case or put a file where a folder is, an entry the manifest does not list or a listed one that is missing or has another size. It then extracts into the library’s staging folder, checks each file’s SHA-256 (and each ZIP CRC), refuses a journal with an operation outside the project’s files and a version manifest the backups cannot read, and only then moves the project into place. The hashes catch damage and hand edits; they are not a signature. Readers ignore manifest fields they do not know; a new format version (elabora-archive/2) comes with a reader of the older ones, which brings them to the current form before anything else (migrateManifest in library/archive.ts).

Machine-local data

A project’s journal, lock, blob store, backups, media jobs, renditions, caches and previews are machine-local data. @elabora/node projectDataDir(projectDir) (data-dir.ts) says where they live, and every module goes through it; nothing joins .elabora/ by hand. By default it is the project’s own .elabora/ folder; ELABORA_DATA_DIR moves every project’s data under a folder (tests, servers), keyed by the id kept in .elabora/project-id. The agents’ activity (activity.jsonl, kept a day) and the transcripts of Elabora’s chat (chats/<chat>.jsonl, written by the app, read by @elabora/node/chats) are there too, as is what this computer published of a course to Elabora’s hosting (cloud/publications.json, D43); the hosting’s credentials are not project data: they are in the user’s local data (credentials/), with the ElevenLabs key.

Blobs and backups copy files as clones where the file system shares blocks (APFS through cp -c, because Node 24 does not clone on macOS; Btrfs and XFS through COPYFILE_FICLONE_FORCE); NTFS has no clones, so on Windows a master costs its size again in the store.

A folder used with the CLI may still sit in a synced folder: conflict copies left by sync services are reported (W_SYNC_CONFLICT_COPY) instead of being loaded as content, and writes retry on Windows sharing violations (EBUSY, EPERM).