# `.foldervas` File Format — v3

Open specification of the document format used by Foldervas — an infinite-canvas
organizer. Any application may produce or consume these files.

- **Version:** 3 (`schema_version = '3'`) · **Status:** stable · **License:** CC BY 4.0
- HTML version: <https://foldervas.com/spec/>
- Related: [`.fvtheme` themes](https://foldervas.com/spec/theme/fvtheme.md) ·
  [`.fvlang` language packs](https://foldervas.com/spec/language/fvlang.md)

## 1. Container

A `.foldervas` file is a single **SQLite 3** database. No wrapper, no
compression. Any SQLite library in any language can create or read one.

- All strings are UTF-8.
- Timestamps are ISO 8601 strings (e.g. `2026-07-10T12:34:56.789Z`).
- Booleans are `INTEGER` `0`/`1`.
- Identifiers are UUID v4 strings (lowercase recommended).

A sibling directory `<filename>.assets/` may exist next to the file. It is an
*optional* performance cache maintained by the reading application (see §10) —
generators do not need to create it.

## 2. Database schema

```sql
CREATE TABLE canvas_meta (
  key   TEXT PRIMARY KEY,
  value TEXT
);

CREATE TABLE nodes (
  id                  TEXT PRIMARY KEY,  -- UUID
  type                INTEGER NOT NULL,  -- see §3
  x                   REAL NOT NULL,
  y                   REAL NOT NULL,
  width               REAL NOT NULL,
  height              REAL NOT NULL,
  content             TEXT,
  background_color    TEXT,              -- '#rrggbb', 'none', or NULL
  top_color           TEXT,              -- '#rrggbb' accent strip, or NULL
  z_index             INTEGER DEFAULT 0,
  parent_id           TEXT,              -- NULL = root board
  sort_order          INTEGER DEFAULT 0,
  asset_id            TEXT,              -- FK into assets.id
  is_locked           INTEGER DEFAULT 0,
  lock_aspect_ratio   INTEGER DEFAULT 1,
  gallery_columns     INTEGER DEFAULT 2,
  rich_content        TEXT,              -- reserved, leave NULL
  created_at          TEXT,
  updated_at          TEXT,
  -- columns below may be ABSENT in older files; readers must tolerate that
  source_url          TEXT,              -- link cards: the URL
  gallery_column_hint INTEGER DEFAULT -1,
  file_path           TEXT,              -- reserved, leave NULL
  font_size           INTEGER,           -- notes: base font px; connectors: stroke weight
  icon                TEXT,              -- folders: emoji icon
  connect_start       TEXT,              -- connectors: source node id
  connect_end         TEXT,              -- connectors: target node id or NULL
  caption             TEXT,              -- file cards: caption text
  collapsed           INTEGER DEFAULT 0, -- galleries: header-only
  alias_target        TEXT,              -- aliases: target folder id
  theme               TEXT,              -- folders: board theme (see §7)
  rotation            REAL DEFAULT 0,    -- node rotation in degrees (clockwise)
  curvature           REAL DEFAULT 0,    -- connectors: signed apex offset (px); 0 = straight
  text_color          TEXT,              -- text color (hex); NULL = auto by contrast
  text_align          TEXT,              -- text alignment: left|center|right (NULL: notes/canvas text = left, shapes = center)
  line_spacing        REAL,              -- notes: line-height factor (NULL = 1.45)
  arrow_heads         TEXT,              -- connectors: none|start|end|both (NULL = end)
  line_dash           INTEGER DEFAULT 0, -- connectors: 1 = dashed
  connector_weight    REAL,              -- connectors: stroke px (NULL = font_size, else 2.5)
  column_titles       TEXT,              -- galleries: JSON array of per-column titles
  heading_level       INTEGER,           -- free canvas text: 0 body, 1 H1, 2 H2
  -- v3 (flowcharts)
  shape_kind          TEXT,              -- shapes: process|decision|terminator|data|database
  stroke_color        TEXT,              -- shapes: border color (hex); NULL = automatic
  stroke_width        REAL,              -- shapes: border width px (NULL = 1.5, 0 = no border)
  routing             TEXT,              -- connectors: straight|curved|orthogonal
  start_port          TEXT,              -- connectors: top|right|bottom|left (NULL = auto)
  end_port            TEXT,              -- connectors: top|right|bottom|left (NULL = auto)
  waypoints           TEXT,              -- connectors: JSON [{"x":n,"y":n}, …] in canvas coords
  label_position      REAL,              -- connectors: label position along the path, 0..1
  label_offset        REAL               -- connectors: label offset perpendicular to the path, px
);

CREATE TABLE assets (
  id         TEXT PRIMARY KEY,  -- UUID, referenced by nodes.asset_id
  filename   TEXT NOT NULL,
  mimetype   TEXT NOT NULL,
  data       BLOB NOT NULL,     -- the binary, or zeroblob(0) if externalized
  width      INTEGER,           -- images: natural px
  height     INTEGER,
  size       INTEGER,           -- byte length of the binary
  created_at TEXT
);

CREATE INDEX idx_nodes_parent ON nodes(parent_id);
CREATE INDEX idx_nodes_type   ON nodes(type);
```

## 3. Node types

| `type` | Name | What it is | Key columns |
|---|---|---|---|
| 0 | Text | Note card, or free canvas text when `background_color = 'none'` | `content`, `font_size` |
| 1 | Image | Image card; `content` doubles as the visible caption (empty/NULL = bare image) | `asset_id`, `content` |
| 2 | Attachment | Any file (PDF, video, audio, …); `content` = display filename | `asset_id`, `content`, `caption` |
| 3 | Container | **Folder / board**: a navigable sub-canvas; its children are the nodes whose `parent_id` points at it | `content` (title), `icon`, `theme` |
| 5 | Gallery | Masonry auto-layout with `gallery_columns` columns (1 column = a vertical list) | `gallery_columns`, `collapsed` |
| 7 | Line | **Connector** (arrow) between nodes — see §6 | `connect_start`, `connect_end` |
| 8 | Alias | Shortcut tile that navigates to another folder | `alias_target` |
| 9 | Shape | **Flowchart shape** (rectangle, diamond, …) — see §7 | `shape_kind`, `content` |

Values `4` (Column) and `6` (HStack) are **legacy**: readers convert them to
Gallery on load (a legacy Column becomes a 1-column gallery). Writers must not
emit them — use a Gallery with `gallery_columns = 1` for a vertical list.
Other unknown `type` values degrade to `0` (Text).

## 4. Hierarchy & coordinates

- `parent_id = NULL` → the node lives on the **root board**.
- `parent_id = <folder id>` → the node lives inside that folder (type 3).
  Folders nest arbitrarily deep. Cycles are invalid.
- `parent_id = <gallery id>` → the node is a *cell* of that gallery (§5).
- Each board is an infinite 2-D plane; `x`/`y` is the node's top-left corner in
  canvas units (1 unit = 1 px at zoom 1). Coordinates may be negative.
- `z_index` orders overlapping free nodes within a board (higher = on top).

## 5. Auto-layout containers (Gallery)

- Children are laid out automatically; their stored `x`/`y` are **ignored**
  while parented. Order comes from `sort_order` (ascending).
- Galleries distribute children round-robin over `gallery_columns` (≥ 1)
  masonry columns; `gallery_column_hint` (≥ 0) pins a child to a specific
  column, `-1` = automatic.
- The container's rendered height is derived from its content — the stored
  `height` is a minimum only. `collapsed = 1` renders header-only.
- Child `width`/`height` keep the node's natural size; images are scaled to the
  cell width preserving aspect ratio.

## 6. Connectors (type 7)

- `connect_start` — id of the source node. **Required.** Must be a node on the
  same board (`parent_id` of connector and endpoints must match).
- `connect_end` — id of the target node, or `NULL` for a free endpoint at the
  connector's own `x`/`y`.
- Endpoints are recomputed from the linked nodes' rectangles at render time;
  the connector's `width`/`height` are unused (write `0`).
- Styling: `background_color` = stroke color (NULL = theme default),
  `connector_weight` = stroke px (NULL = fall back to `font_size`, else 2.5),
  `arrow_heads` = `none|start|end|both` (NULL = `end`), `line_dash` = 1 for a
  dashed stroke.
- `routing` — `straight`, `curved` or `orthogonal` (elbowed). When absent,
  readers infer it from `curvature`: non-zero ⇒ `curved`, zero ⇒ `straight`.
- `curvature` — signed distance (px) from the curve's apex to the straight
  segment between endpoints; `0` = straight line. Only used by `curved`
  connectors that carry no `waypoints`; kept so that older readers degrade
  gracefully.
- `start_port` / `end_port` — side of the node the connector attaches to
  (`top|right|bottom|left`). `NULL` = the writer let the app choose; readers
  are free to pick the best side. A free endpoint has `end_port = NULL`.
- `waypoints` — JSON array of `{ "x": number, "y": number }` in canvas
  coordinates. These are **user intent only**: the path between them (elbows
  included) is recomputed on load. Readers should ignore malformed entries and
  may cap the list (Foldervas caps it at 64).
- `content` — the connector's **label** (e.g. `Yes` / `No` on a decision
  branch). `label_position` (0..1 along the drawn path, default 0.5) and
  `label_offset` (px perpendicular to it, default 0) place it.
- Anchors are computed against the node's real **outline**, so a connector to a
  diamond meets its edge, not its bounding box.
- A connector whose endpoints no longer exist is simply not drawn.

## 7. Shapes, captions, aliases, folders

- **Flowchart shapes** (type 9): `shape_kind` picks the figure —
  `process` (rectangle), `decision` (diamond), `terminator` (stadium),
  `data` (parallelogram) or `database` (cylinder). NULL or an unknown value
  degrades to `process`. `content` is the text drawn centred inside the figure
  (plain text: no markdown, no checkboxes), `text_color`/`font_size`/`text_align`
  its type. **Fill**: `background_color`, where both NULL and `'none'` mean *no
  fill* — the default for a shape is an outline. **Border**: `stroke_color`
  (NULL = pick one that contrasts with the fill, or with the canvas when there
  is none) and `stroke_width` px (NULL = 1.5, `0` = no border). The figure is inscribed in
  the node's `width`/`height` rectangle, which stays the node's bounding box for
  selection, layout and rotation.
- **Image captions** live in `content`: non-empty = card with a caption bar,
  empty/NULL = bare image.
- **Attachment captions** live in `caption` (because `content` holds the
  filename). Same rule: non-empty = caption bar. The bar's background is
  `background_color`.
- **Folders** (type 3): `content` = title, `icon` = optional emoji replacing
  the folder glyph, `top_color`/`background_color` = tile tint.
- **Folder themes**: a folder's `theme` holds a theme id (`light`, `sepia`,
  `dark`, `midnight`, `slate`, or `custom:<name>` for an imported
  [`.fvtheme`](https://foldervas.com/spec/theme/)). Entering the folder
  renders the app with that theme; subfolders inherit it unless they set their
  own. Unknown values are ignored.
- **Aliases** (type 8): `alias_target` must reference a type-3 node. If the
  target disappears, readers drop the alias.

## 8. The `canvas_meta` table

Key/value strings. All keys are optional for writers — readers fall back to
the defaults below and clamp out-of-range values.

| Key | Value | Default | Range |
|---|---|---|---|
| `schema_version` | format version | `1` (absent = v1 file) | write `3` |
| `nodeCount` | number of rows in `nodes` (integrity check) | — | must match if present |
| `lastModified` | ISO 8601 timestamp | — | — |
| `zoom`, `offsetX`, `offsetY` | root board viewport | `1`, `0`, `0` | — |
| `folderViewports` | JSON: `{"<folderId>": {"zoom":n,"offsetX":n,"offsetY":n}}` | `{}` | — |
| `drawings` | JSON — see §9 | `{}` | — |
| `trash` | JSON — deleted subtrees, see below | `[]` | — |
| `noteWidth` | default width of new notes (px) | `280` | 160–560 |
| `kanbanWidth` | default width of new galleries | `280` | 220–640 |
| `imageWidth` | target longest side of dropped images | `280` | 120–560 |
| `textHeight` | default height of free canvas text | `72` | 28–120 |
| `defaultFontSize` | default note font size (px) | `14` | 11–28 |
| `defaultNoteColor`, `defaultFolderColor` | `#rrggbb` or empty (theme default) | empty | — |
| `documentTheme` | document-wide theme id (same values as folder `theme`); folders with their own theme win inside their board | empty | — |
| `showGrid` | `1`/`0` | `1` | — |
| `snapSize` | snap grid size (px) | `28` | app-clamped |
| `styleGuide` | free text: house rules the document's AI assistant must follow | empty | — |

**Trash** — an array of deleted subtrees, restorable by the user:

```json
[{ "id": "<root node id>", "name": "…", "type": 0,
   "deletedAt": "<ISO>", "nodes": [ /* full node objects, root included */ ] }]
```

## 9. Drawings (strokes)

The `drawings` meta key holds freehand ink, grouped by surface:

```json
{
  "__root__":        [ "Stroke", "…" ],
  "<folderId>":      [ "Stroke", "…" ],
  "image:<nodeId>":  [ "Stroke", "…" ]
}
```

```
Stroke = {
  "id":      "<uuid>",
  "points":  [x0, y0, x1, y1, …],   // flat pairs, length even, ≥ 2
  "color":   "#rrggbb" | "ink",     // "ink" = adapts to light/dark theme
  "width":   number,                 // px: 2 | 4 | 8 typical
  "opacity": number?                 // 0–1, default 1
}
```

- Board strokes use canvas coordinates (same space as node `x`/`y`).
- **Image strokes** (`image:<nodeId>` keys) use a normalized space:
  coordinates are **per-mille of the image node's width** (0–1000 across the
  width; y in the same scale), so ink survives image resizing.

## 10. Assets (binaries)

Two valid ways to store a binary; readers support both:

1. **Inline (recommended for generators):** put the bytes in `assets.data`.
   Single self-contained file.
2. **Externalized:** write `zeroblob(0)` in `assets.data` and place the raw
   bytes in the sibling directory `<filename>.assets/<asset id>` (no
   extension, filename = the asset UUID). Foldervas itself writes this layout
   for speed and migrates inline blobs to it on save.

- `mimetype` is required and drives previews (`image/*`, `video/*`,
  `application/pdf`, …).
- Asset ids must be plain base names (UUIDs). Readers reject path-like ids
  (`/`, `\`, `..`) for security.
- Unreferenced assets may be garbage-collected by readers.

## 11. Requirements for writers

A generator **MUST**:

- emit valid SQLite with the tables of §2 (extra columns/tables are ignored by
  readers, but don't rely on that);
- write `schema_version = '3'` (see §12) and unique UUID ids;
- keep `parent_id` references acyclic and pointing at existing nodes;
- store booleans as `0`/`1` and timestamps as ISO 8601.

A generator **SHOULD**:

- write `nodeCount` (readers use it to detect truncated files);
- set `assets.width/height` for images and a real `mimetype` always;
- keep numeric settings inside the ranges of §8.

Readers are defensive by design: unknown node types degrade to Text, malformed
colors fall back to theme defaults, out-of-range settings are clamped,
dangling `connect_*`/`alias_target` references are dropped. A partially
conforming file opens; a perfectly conforming file round-trips.

## 12. Versioning

This document describes **version 3**. Future revisions will bump
`schema_version` and be published with a changelog; older files remain
readable. Applications encountering a version newer than they support **must
refuse to write** to the file (never silently downgrade it): a writer that only
knows the older column list would blank out everything it does not understand.

Changelog:

- **v1** — initial public format.
- **v2** — never published as a specification; the number was consumed by an
  earlier desktop build. Readers should treat a v2 file as v1 plus whichever
  optional columns it happens to carry.
- **v3** — flowcharts: node type `9` (Shape) with `shape_kind`, `stroke_color`
  and `stroke_width`, and connector
  geometry `routing`, `start_port`, `end_port`, `waypoints`, `label_position`,
  `label_offset`. All additions are new columns and a new type value, so a v3
  file still opens in a v1 reader — read-only, per the rule above.

---

Published under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
The `.foldervas` format may be implemented freely — producing or consuming
these files requires no permission or fee.
