# File-first workspaces

> Create workspaces, read/write markdown and plaintext, directories as state, frontmatter semantics, and backing-table access via .tables/.

- 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`
- `internal/tigerfs/fs/synth/markdown.go`
- `internal/tigerfs/fs/synth/plaintext.go`
- `internal/tigerfs/fs/build.go`
- `internal/tigerfs/fs/write.go`
- `test/integration/fs_operations_test.go`

---

---
title: "File-first workspaces"
description: "Create workspaces, read/write markdown and plaintext, directories as state, frontmatter semantics, and backing-table access via .tables/."
---

TigerFS file-first workspaces are synthesized PostgreSQL views (`internal/tigerfs/fs/synth/`) that expose each row as a file under `/<workspace>/`. Creating a workspace writes a format string to `/.build/<name>`; TigerFS provisions a backing table in the `tigerfs` schema, a user-schema view, and optional history infrastructure. File content is always read from the database on each `ReadFile`; metadata may be cached with invalidation on writes.

## Workspace layout

A `.build/` workspace creates two mount surfaces for the same data:

| Path | Role |
|------|------|
| `/<workspace>/` | File-first: markdown or plaintext files, optional subdirectories |
| `/.tables/<workspace>/` | Data-first: row directories, column files, pipeline capabilities |

```text
mount/
├── .build/              write-only (listing empty)
├── .tables/             tigerfs-schema backing tables
│   └── notes/           data-first row/column access
└── notes/               file-first workspace (view)
    ├── hello.md
    ├── tutorials/
    │   └── intro.md
    ├── .history/        (history-enabled only)
    ├── .log/
    ├── .savepoint/
    └── .undo/
```

History-enabled workspaces add `.history/`, `.log/`, `.savepoint/`, and `.undo/` at the **workspace root only**; subdirectories list real files and child directories, not those control dirs.

<Note>
Pipeline capabilities (`.filter/`, `.export/`, `.by/`, and similar) are **blocked** on file-first workspace paths and return `ErrInvalidPath` with a hint to use `/.tables/<workspace>/...`. Workspace control surfaces that remain allowed include `.info/`, `.history/`, `.log/`, `.savepoint/`, `.undo/`, and `.format/` (see `rejectDataFirstCapOnSynthWorkspace` in `internal/tigerfs/fs/operations.go`).
</Note>

## Create a workspace

`/.build/` is mount-root only; it is rejected under `/.schemas/<schema>/.build/` (ADR-015).

<Steps>
<Step title="Choose format and name">

Write a format (and optional `history`) to a virtual file `/.build/<app>`:

```bash
echo "markdown,history" > /mnt/db/.build/notes
echo "markdown" > /mnt/db/.build/posts
echo "plaintext" > /mnt/db/.build/snippets
echo "history" > /mnt/db/.build/notes    # add history to existing app
```

</Step>
<Step title="Verify">

```bash
ls /mnt/db/          # expect <app> and .tables
ls /mnt/db/notes/    # workspace directory (empty until files exist)
ls /mnt/db/.tables/  # expect backing table name
```

`writeBuildFile` (`internal/tigerfs/fs/build.go`) parses features via `synth.ParseFeatureString`, requires TimescaleDB when `history` is requested, executes generated DDL in `tigerfs`, and invalidates synth/metadata caches.

</Step>
</Steps>

### Supported `.build/` inputs

| Written content | Effect |
|-----------------|--------|
| `markdown` or `md` | Markdown workspace: `tigerfs.<app>` table + `<app>` view, comment `tigerfs:md` |
| `txt`, `text`, `plaintext`, `plain` | Plaintext workspace, comment `tigerfs:txt` |
| `markdown,history` | Markdown + versioned history tables |
| `history` alone | Add history to an existing app (table must already exist in `tigerfs`) |
| `tasks`, `native` | Rejected — unsupported for `.build/` |

Plaintext and markdown `.build/` tables include `parent_id`, `filetype` (`file` \| `directory`), `filename`, `body`, `encoding`, and timestamps. Markdown tables also include `title`, `author`, and `headers JSONB` for extra frontmatter keys (`internal/tigerfs/fs/synth/build.go`).

<Warning>
`history` requires the TimescaleDB extension. Without it, `.build/` writes fail with a structured error and install hint.
</Warning>

## Add file-first to an existing table

For tables that already exist in the user schema, write to `/<table>/.format/<format>` instead of `.build/`:

```bash
echo ok > /mnt/db/posts/.format/markdown
# Creates view posts_md (table + "_md" suffix)
```

Supported `.format/` names: `markdown`, `txt` (`synth.AvailableFormats()`). The view selects all columns from the existing table; column roles are auto-detected from names (see below). This path does **not** create a new `tigerfs` backing table.

## Markdown and plaintext files

### Markdown

Each file is YAML frontmatter (from columns) plus a body column:

```markdown
---
title: Getting Started
author: alice
tags:
  - tutorial
draft: false
---

# Getting Started

Body text stored in the `body` column.
```

Synthesis: `synth.SynthesizeMarkdown` (`internal/tigerfs/fs/synth/markdown.go`). Parsing: `synth.ParseMarkdown` + `synth.MapToColumns`.

### Plaintext

No frontmatter — the entire file maps to the body column (`synth.SynthesizePlainText`, `synth.ParsePlainText` in `internal/tigerfs/fs/synth/plaintext.go`).

### Binary content

If file bytes contain NUL or invalid UTF-8, `writeSynthFile` stores base64 in `body` and sets `encoding` to `base64` when that column exists.

## Frontmatter semantics

Column roles are detected by naming convention (first match wins) in `synth.DetectColumnRoles` (`internal/tigerfs/fs/synth/columns.go`):

| Role | Convention (priority order) | Required |
|------|----------------------------|----------|
| Filename | `filename`, `name`, `title`, `slug` | Yes |
| Body | `body`, `content`, `description`, `text` | Yes |
| Modified / created | `modified_at`/`updated_at`, `created_at` | No |
| Extra headers | `headers` (JSONB) | No |
| Hierarchy | `filetype`, `parent_id` | On `.build/` tables |

Remaining markdown columns (except PK, timestamps, `filetype`, `parent_id`, `encoding`) become frontmatter fields in schema order.

### Write behavior

| Field class | On edit, if key omitted from frontmatter |
|-------------|------------------------------------------|
| Known frontmatter columns (`title`, `author`, …) | **Unchanged** — `UpdateRow` only sets columns present in the parsed map |
| `headers` JSONB keys | **Removed** — full replace; omitting a key drops it |
| `body` | Always replaced with file body |
| Timestamps | Not in frontmatter; drive `ls -l` mtimes only |

To clear a known column, set it explicitly (e.g. `title: ""`). Unknown frontmatter keys without a `headers` column cause a parse error.

Discover columns on the backing table:

```bash
cat /mnt/db/.tables/notes/.info/columns
```

## Read and write files

Standard tools work on the workspace path:

```bash
ls /mnt/db/notes/
cat /mnt/db/notes/hello-world.md
grep -r "TODO" /mnt/db/notes/

cat > /mnt/db/notes/new-post.md << 'EOF'
---
title: New Post
author: bob
---
Content here.
EOF

mv /mnt/db/notes/old.md /mnt/db/notes/new.md
rm /mnt/db/notes/unwanted.md
```

`ReadFile` always queries the database (no content cache). Writes invalidate stat and path caches for the view schema/table.

### Reserved filenames

Leaf names matching TigerFS capability directories (`.history`, `.log`, `.savepoint`, `.undo`, `.info`, `.by`, `.filter`, `.export`, …) are rejected with `ErrPermission`. `ReadDir` filters rows that collide with reserved names so virtual control dirs take precedence.

## Directories as state

`.build/` markdown and plaintext apps use the relational directory model (ADR-017): each directory is a row with `filetype='directory'`; files reference `parent_id`. Path segments under the workspace map to `parent_id` + leaf `filename`.

```bash
mkdir /mnt/db/notes/tutorials
# Or deep paths via mkdir -p semantics:
```

| Operation | Behavior |
|-----------|----------|
| `mkdir` | Inserts `filetype=directory` row; does not auto-create missing ancestors (explicit per segment, like POSIX) |
| `WriteFile` via FUSE/NFS | Parent must exist before create (kernel enforces `O_CREAT` parent) |
| `WriteFileEnsureDirs` / `MkdirAll` | API helpers that create missing ancestors (tests, batch import) |
| `mv` directory | Atomic rename: updates directory row and descendant paths (`TestSynth_HierarchicalRenameDir`) |
| `rmdir` | Fails with not-empty if children exist |

Integration coverage: `test/integration/synthesized_test.go` (`TestSynth_HierarchicalMkdir`, `TestSynth_HierarchicalWriteFile`, `TestSynth_HierarchicalRenameDir`, and related tests).

```mermaid
flowchart LR
  subgraph mount["Mount path"]
    W["/notes/a/b/doc.md"]
  end
  subgraph fs["fs.Operations"]
    P["parseSynthContent"]
    WSF["writeSynthFile"]
  end
  subgraph db["PostgreSQL tigerfs.notes"]
    D["directory rows parent_id"]
    F["file rows body + frontmatter cols"]
  end
  W --> WSF --> P
  WSF --> D
  WSF --> F
```

## Backing table access via `.tables/`

`/.tables/<name>/` rewrites paths to schema `tigerfs` with the same logical table name as the workspace. Use it for data-first exploration while keeping agents on file paths:

```bash
ls /mnt/db/notes/              # file-first
ls /mnt/db/.tables/notes/      # row IDs + capabilities
cat /mnt/db/.tables/notes/<pk>/title
```

`TestSynth_TablesRoute` in `test/integration/synthesized_test.go` verifies round-trip between `/posts/hello-world.md` and `/.tables/posts/<pk>/title`.

For `.format/` views (e.g. `posts_md`), the original table stays data-first at `/posts/`; the synthesized view is file-first at `/posts_md/`.

## Optional history

Append `,history` at `.build/` time (or write `history` later) to enable `.history/`, `.log/`, `.savepoint/`, and `.undo/` at the workspace root. Requires TimescaleDB.

<CardGroup>
<Card title="History, savepoints, and undo" href="/history-savepoints-undo">
Version browsing, savepoint JSON, undo preview/apply, and per-user filters.
</Card>
</CardGroup>

## Implementation map

| Concern | Package / entry |
|---------|-----------------|
| Build / format CLI surface | `internal/tigerfs/fs/build.go` |
| Synth read/write/rename | `internal/tigerfs/fs/synth_ops.go` |
| Path parsing `/.build/`, `/.tables/` | `internal/tigerfs/fs/path.go` |
| DDL generation | `internal/tigerfs/fs/synth/build.go` |
| Format detection | `internal/tigerfs/fs/synth/format.go` |

Human-oriented examples and recipes also appear in `docs/file-first.md` and `skills/tigerfs/files.md`.

## Related pages

<CardGroup>
<Card title="File-first and data-first" href="/file-first-and-data-first">
Mode detection, `.build/` vs `.format/`, and when a path is a workspace vs a table.
</Card>
<Card title="Synthesized apps" href="/synthesized-apps">
View comments, backing schema, and column/frontmatter mapping details.
</Card>
<Card title="Filesystem as API" href="/filesystem-as-api">
Full mount hierarchy and how `fs.Operations` maps paths to SQL.
</Card>
<Card title="Consistency and caching" href="/consistency-and-caching">
Why `cat` always hits the DB and which metadata caches apply to workspaces.
</Card>
<Card title="Workflow recipes" href="/workflow-recipes">
Kanban, knowledge base, and other file-first patterns.
</Card>
</CardGroup>
