# File-first and data-first

> When a mount path is a workspace vs a table, .build/.format vs raw tables, .tables/ backing schema, and mode detection rules.

- Repository: timescale/tigerfs
- GitHub: https://github.com/timescale/tigerfs
- Human docs: https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3
- Complete Markdown: https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/llms-full.txt

## Source Files

- `docs/file-first.md`
- `docs/data-first.md`
- `skills/tigerfs/SKILL.md`
- `internal/tigerfs/fs/synth/build.go`
- `internal/tigerfs/fs/build.go`

---

---
title: "File-first and data-first"
description: "When a mount path is a workspace vs a table, .build/.format vs raw tables, .tables/ backing schema, and mode detection rules."
---

TigerFS exposes every mounted PostgreSQL database as a directory tree where each top-level name is either a **file-first workspace** (synthesized view presenting rows as `.md` or `.txt` files) or a **data-first table** (primary-key rows as `.json`/`.tsv`/column files plus pipeline capability directories). Runtime routing in `fs.Operations` calls `getSynthViewInfo` on the resolved schema and table/view name; a non-nil result switches listing, read, and write to the synth layer, while `rejectDataFirstCapOnSynthWorkspace` blocks pipeline capabilities on workspace paths unless you use `/.tables/<name>/`, which always parses with `schema=tigerfs`.

## Two surfaces on one mount

| Surface | Typical path | What you see | Primary write path |
|---------|--------------|--------------|-------------------|
| File-first workspace | `/notes/`, `/posts_md/` | `.md` or `.txt` files; with history: `.history/`, `.log/`, `.savepoint/`, `.undo/` at workspace root | `cat`, editors, `mv`, `rm` on files |
| Data-first table | `/users/`, `/orders/` | Row PKs, `.info/`, `.by/`, `.filter/`, `.export/`, etc. | Row/column files, pipeline paths, DDL staging |
| Backing table (data-first on synth storage) | `/.tables/notes/` | Same as data-first; schema fixed to `tigerfs` | Direct row/column writes (bypasses history triggers) |

<Note>
Quick heuristic from the bundled agent skill: if a directory lists `.md`/`.txt` (and optionally history control dirs), treat it as file-first; if it exposes `.info/` and row PKs without a synth view, treat it as data-first.
</Note>

```text
mount/                          (default schema, e.g. public)
├── .build/                     write-only: create workspaces
├── .tables/                    data-first → tigerfs schema tables
│   └── notes/                  backing table for workspace "notes"
├── notes/                      file-first workspace (view in public)
│   ├── hello.md
│   ├── .history/ .log/ ...     (when history enabled)
├── users/                      data-first table
│   ├── .info/ .by/ ...
│   └── 1.json  2/
└── posts/                      data-first (original table)
    └── .format/markdown        write → creates posts_md view
posts_md/                       file-first view on existing posts table
```

## How mode is detected

Detection is **per directory name**, not per mount. `loadSynthCache` scans views in the connection’s current schema and builds a `synth.ViewInfo` map used by `getSynthViewInfo`.

```mermaid
flowchart TD
  subgraph parse [Path parsing]
    P[ParsePath + resolveSchema]
    P --> Q{Context.Schema == tigerfs?}
    Q -->|yes via /.tables/| DF[Data-first table ops]
    Q -->|no| R{getSynthViewInfo non-nil?}
  end
  subgraph detect [Synth detection per view name]
    C[PostgreSQL COMMENT ON VIEW]
    C -->|tigerfs:md / tigerfs:txt / ,history| S[Synth view]
    C -->|missing| V[View suffix _md _txt _tasks ...]
    V -->|no match| Col[Column pattern filename+body ...]
    Col -->|FormatNative| N[Not a workspace]
  end
  R -->|yes| FF[File-first synth ops]
  R -->|no| DF
  detect --> R
```

### Detection precedence

| Priority | Signal | Result |
|----------|--------|--------|
| 1 | View comment from `.build/` or `.format/` | `tigerfs:md`, `tigerfs:txt`, optional `,history` |
| 2 | View name suffix | `_md` → markdown, `_txt` → plaintext, `_tasks` / `_todo` / `_items` → tasks (tasks format not served in listings yet) |
| 3 | Column patterns | `filename`+`body`+extra → markdown; `filename`+`body` only → plaintext |
| 4 | Default | `FormatNative` → data-first table/view |

History is flagged from the comment or by presence of `tigerfs.<view>_history`. `FormatTasks` views are skipped in the synth cache until implemented.

### Workspace vs backing table

| Object | PostgreSQL location | Mount path |
|--------|---------------------|------------|
| Backing table | `tigerfs.<app_name>` (e.g. `tigerfs.notes`) | `/.tables/notes/` |
| Workspace view | `<default_schema>.<app_name>` (same name as app) | `/notes/` |
| Synthesized view on existing table | `<schema>.<table>_md` or `_txt` | `/posts_md/` (table `posts` unchanged at `/posts/`) |

`.build/` creates **both**: backing table in `tigerfs`, view in the user’s default schema, triggers, optional history hypertables (`_history`, `_log`, `_savepoint`, metadata). `.format/` only adds a view + comment over an **existing** table in the user schema; the original table path stays data-first.

## Creating file-first workspaces (`.build/`)

`/.build/` is mount-root only; `/.schemas/<schema>/.build/` is rejected (ADR-015). Listing returns no entries (write-only).

<Steps>
<Step title="Choose format and optional history">
Write a feature string to a virtual file `/.build/<app_name>`:

```bash
echo "markdown,history" > /mnt/db/.build/notes
echo "markdown"           > /mnt/db/.build/notes
echo "plaintext"          > /mnt/db/.build/snippets
echo "history"            > /mnt/db/.build/notes   # add history to existing app only
```
</Step>
<Step title="Verify workspace appears">
`ls /mnt/db/` should show directory `notes/` (writable `0755` for synth views). With history and TimescaleDB: `.history/`, `.log/`, `.savepoint/`, `.undo/` appear at workspace root only.
</Step>
<Step title="Optional: inspect backing table">
`ls /mnt/db/.tables/notes/` exposes the `tigerfs.notes` table with full data-first capabilities.
</Step>
</Steps>

<ParamField body="feature string" type="string" required>
Parsed by `synth.ParseFeatureString`. Supported formats: `markdown` / `md`, `plaintext` / `txt` / `text`. Append `,history` for versioned undo (requires TimescaleDB extension). Standalone `history` adds history to an existing workspace without recreating format.
</ParamField>

<Warning>
`history` on `.build/` fails with a clear error if the `timescaledb` extension is not installed. Native/tasks-only strings are rejected for new app creation.
</Warning>

Markdown workspaces created by `.build/` include `id`, `parent_id`, `filename`, `filetype`, `title`, `author`, `headers` (JSONB), `body`, `encoding`, and timestamp columns—see `GenerateMarkdownTableSQL` in the synth build package.

## File-first on an existing table (`.format/`)

For a data-first table already at `/posts/`, add a synthesized view without moving data:

```bash
echo ok > /mnt/db/posts/.format/markdown
# Creates view posts_md in the same schema (SELECT * FROM posts + COMMENT tigerfs:md)
ls /mnt/db/posts_md/
```

| Input | View name | Extension |
|-------|-----------|-----------|
| `.format/markdown` | `<table>_md` | `.md` |
| `.format/txt` | `<table>_txt` | `.txt` |

`ls /mnt/db/posts/.format/` lists available format names (`markdown`, `txt`). Write content to `.format/<name>` is ignored; the format comes from the filename. Unsupported formats return `ErrInvalidPath` with a hint.

## `/.tables/` and the `tigerfs` schema

`parseTablesPath` maps `/.tables/<name>/...` to `FSContext{Schema: "tigerfs", Table: name}` and then normal segment processing (rows, `.by/`, `.export/`, etc.).

| Path | Behavior |
|------|----------|
| `/.tables/` | `PathTablesList` — lists tables in `tigerfs` schema |
| `/.tables/notes/` | Data-first access to backing storage |
| `/.tables/notes/.filter/...` | Full pipeline (not gated) |

<Warning>
Writes under `/.tables/<workspace>/` hit the backing table directly and **do not** run workspace history triggers. Use `/workspace/` for audited edits; reserve `/.tables/` for diagnostics or repairs.
</Warning>

Auxiliary tables (`notes_history`, `notes_log`, `notes_savepoint`, `notes_metadata`) live in `tigerfs` and are used internally; workspace-level `.log/` and `.savepoint/` paths rewrite context to these tables while preserving `OrigTableName` for the UI.

## Capabilities on file-first workspaces

`rejectDataFirstCapOnSynthWorkspace` blocks data-first pipeline capabilities when `getSynthViewInfo` matches and schema is not `tigerfs`:

**Blocked on `/notes/...`:** `.by/`, `.filter/`, `.order/`, `.columns/`, `.first/`, `.last/`, `.sample/`, `.export/`, `.import/`, table DDL (`.modify/`, `.delete/`, index DDL), and filtered `PathTable` pipeline states.

**Allowed on workspaces:** `.info/`, `.history/`, `.log/`, `.savepoint/`, `.undo/`, `.format/` (workspace control surfaces).

Blocked paths return `ErrInvalidPath` with hint: use `mount/.tables/<view>/...` for data-first access.

## Column roles (file-first rendering)

Roles are auto-detected by naming convention (first match wins)—no explicit mapping file yet:

| Role | Candidate column names |
|------|------------------------|
| Filename | `filename`, `name`, `title`, `slug` |
| Body | `body`, `content`, `description`, `text` |
| Timestamps | `modified_at` / `updated_at`; `created_at` |
| Extra frontmatter (JSONB) | `headers` |
| Directories | `filetype` (`file` / `directory`) + `parent_id` |

Markdown frontmatter merges known columns plus `headers` keys (full replace on write for keys stored only in JSONB). Timestamp columns drive `ls -l` times but are not emitted in frontmatter.

## `.build/` vs `.format/`

| | `.build/<name>` | `<table>/.format/<fmt>` |
|--|----------------|-------------------------|
| Target | New app name | Existing table |
| Storage | New table in `tigerfs` + view in default schema | View only (`<table>_md` / `_txt`) |
| Schema design | Opinionated markdown/plaintext schema | Must already fit column conventions |
| History | Optional at create; add later with `echo history > .build/<name>` | Not added by `.format/` alone |
| Default schema only | Yes (`/.build/` at mount root) | Yes (under table in resolved schema) |
| Listing | Write-only (empty `ls`) | Lists `markdown`, `txt` |

## Root mount layout

`readDirRoot` always includes: `.build`, `.create`, `.info`, `.schemas`, `.tables`, `.views`, then tables and views from `current_schema()`. Synth views are listed with mode `0755`; other views use `0555`. Legacy `_<view>` backing tables in the user schema (pre-migration) trigger a one-time warning to run `tigerfs migrate`.

## Related pages

<CardGroup>
<Card title="Filesystem as API" href="/filesystem-as-api">
Path model, dot-directories, and how `fs.Operations` maps paths to SQL.
</Card>
<Card title="File-first workspaces" href="/file-first-workspaces">
Markdown/plaintext I/O, directories, frontmatter, and workspace control files.
</Card>
<Card title="Data-first exploration" href="/data-first-exploration">
Row/column access, indexes, pagination, PATCH writes, and pipelines.
</Card>
<Card title="Synthesized apps" href="/synthesized-apps">
Backing tables, views, comments, and synth implementation details.
</Card>
<Card title="Capability directories" href="/capability-directories">
Pipeline grammar (`.by`, `.filter`, `.export`, …) used on data-first paths and `/.tables/`.
</Card>
<Card title="History, savepoints, and undo" href="/history-savepoints-undo">
Versioned workspaces, `.log/`, savepoints, and undo apply paths.
</Card>
</CardGroup>
