# Synthesized apps

> Workspace creation via .build/, markdown/plaintext formats, backing tables in tigerfs schema, views, and column/frontmatter mapping.

- 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/synth/build.go`
- `internal/tigerfs/fs/synth/format.go`
- `test/integration/synthesized_test.go`

---

---
title: "Synthesized apps"
description: "Workspace creation via .build/, markdown/plaintext formats, backing tables in tigerfs schema, views, and column/frontmatter mapping."
---

TigerFS **synthesized apps** expose PostgreSQL views as directories of real files (`.md` with YAML frontmatter, or plain `.txt` bodies). Writes to `/.build/<name>` or `/<table>/.format/<format>` create the backing table, view, triggers, and `COMMENT ON VIEW` markers; `fs.Operations` in `internal/tigerfs/fs/synth_ops.go` maps each file path to `INSERT`/`UPDATE`/`DELETE` on the view while `internal/tigerfs/fs/synth/` handles synthesis and parsing.

## Creation paths

Two filesystem entry points provision synthesized apps. They differ in whether TigerFS also creates the backing table and in how the mount path is named.

| Path | Write target | Creates | Mount path after success |
|------|--------------|---------|--------------------------|
| **`.build/`** | `/.build/<app>` | `tigerfs.<app>` table, `<app>` view in the connection schema, triggers, optional history | `/<app>/` (same name as app) |
| **`.format/`** | `/<table>/.format/<format>` | Synthesized view on an **existing** table only | `/<table>_md/` or `/<table>_txt/` |

<Steps>
<Step title="Create a new workspace (.build/)">

Write a comma-separated feature string to a virtual file under `/.build/`:

```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 an existing .build/ app
```

<ParamField body="content" type="string" required>
Feature string parsed by `synth.ParseFeatureString`: `markdown` / `md`, `txt` / `text` / `plaintext` / `plain`, optional `,history`.
</ParamField>

`.build/` is **write-only** at the mount root (listing returns no entries). `.build/` under `/.schemas/<schema>/` is rejected — use the default schema mount.

</Step>
<Step title="Add file-first view to an existing table (.format/)">

```bash
echo ok > /mnt/db/posts/.format/markdown
echo ok > /mnt/db/logs/.format/txt
```

The format name is taken from the **filename** (`markdown`, `txt`); file body content is ignored. `synth.AvailableFormats()` currently exposes `markdown` and `txt` only.

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

```bash
ls /mnt/db/                    # expect <app> or <table>_md
ls /mnt/db/.tables/            # expect backing tables in tigerfs schema
cat /mnt/db/notes/hello.md     # synthesized read
```

</Step>
</Steps>

<Warning>
`history` in a `.build/` string requires the **TimescaleDB** extension. Without it, TigerFS returns an error with a hint to install TimescaleDB.
</Warning>

## Storage layout

`.build/` apps split storage across two schemas:

```text
  User schema (e.g. public)          tigerfs schema
  ---------------------------          ----------------
  VIEW notes  ──SELECT *──►  TABLE notes
  COMMENT 'tigerfs:md,history'         TABLE notes_history  (if history)
                                       TABLE notes_log
                                       TABLE notes_savepoint
                                       TABLE notes_metadata
                                       FUNCTION resolve_path(...)
```

- **Backing table:** `tigerfs.<app>` — UUIDv7 `id`, `parent_id`, `filename`, `filetype` (`file` | `directory`), format-specific columns, `encoding` (`utf8` | `base64`), timestamps.
- **Synthesized view:** `<app>` in the user schema — `CREATE VIEW … AS SELECT * FROM tigerfs.<app>` plus `COMMENT ON VIEW` (`tigerfs:md`, `tigerfs:txt`, or with `,history`).
- **`.format/` views:** `<table>_md` or `<table>_txt` in the user schema; the original `<table>/` path stays data-first.

Access the backing table without synthesis:

```bash
ls /mnt/db/.tables/notes/           # row/column layout on tigerfs.notes
ls /mnt/db/notes/                   # file-first synthesized view
```

Capability directories (`.filter`, `.export`, etc.) work under `/.tables/<app>/` but are blocked on the synthesized workspace path — use `.tables/` for pipeline operations on backing data.

## Supported formats

| Format | `.build/` token | View comment | File shape | Status |
|--------|-----------------|--------------|------------|--------|
| Markdown | `markdown`, `md` | `tigerfs:md` | YAML frontmatter + body | Fully implemented |
| Plain text | `txt`, `plaintext`, `text`, `plain` | `tigerfs:txt` | Body only | Fully implemented |
| Tasks | — (not in `.build/`) | `tigerfs:tasks` | Numbered task filenames | Detected in `synth.DetectFormat`; **skipped** in synth cache (`FormatTasks` not loaded) |

TigerFS does **not** auto-append extensions. Filenames come from the `filename` column exactly as stored (e.g. `hello-world.md` vs `hello-world`).

### Markdown backing schema (`.build/`)

`GenerateMarkdownTableSQL` creates:

| Column | Role |
|--------|------|
| `id` | UUID primary key (uuidv7) |
| `parent_id` | Directory hierarchy (self-FK) |
| `filename` | Display path / leaf name |
| `filetype` | `file` or `directory` |
| `title`, `author` | Example dedicated frontmatter columns |
| `headers` | JSONB for keys without dedicated columns |
| `body` | Markdown body |
| `encoding` | `utf8` or `base64` |
| `created_at`, `modified_at` | File times (not rendered in frontmatter) |

### Plain text backing schema (`.build/`)

Same hierarchy columns; `body` only (no `title`/`author`/`headers`).

## Format detection

At mount, `Operations.loadSynthCache` builds a per-schema map of `synth.ViewInfo`:

1. **View comment** — `tigerfs:md`, `tigerfs:txt`, optional `,history` (set by `.build/` / `.format/`).
2. **View name suffix** — `_md`, `_txt`, `_tasks`, `_todo`, `_items`.
3. **Column patterns** — `filename`+`body`+others → markdown; `filename`+`body` only → plain text; `number`+`name`+`status`+`body` → tasks.

Suffix and comment take precedence over column inference.

## Column and frontmatter mapping

`DetectColumnRoles` assigns columns by **naming convention** (first match wins). Explicit per-column mapping is documented as planned in `docs/file-first.md` but not implemented in code.

| Role | Convention names (priority order) |
|------|-----------------------------------|
| Filename | `filename`, `name`, `title`, `slug` |
| Body | `body`, `content`, `description`, `text` |
| Modified / created time | `modified_at`, `updated_at` / `created_at` |
| Extra headers (JSONB) | `headers` |
| Hierarchy | `filetype`, `parent_id` |
| Encoding | `encoding` |
| Frontmatter (markdown) | All other columns except PK and roles above |

**Read path:** `SynthesizeMarkdown` emits `---` YAML from frontmatter columns (schema order), then merges `headers` JSONB keys alphabetically, then the body. `SynthesizePlainText` returns body only.

**Write path:** `ParseMarkdown` → `MapToColumns` → partial `UpdateRow` (only parsed columns are SET).

| Key type | Write behavior |
|----------|----------------|
| Known frontmatter column | Omitted in YAML → column not in UPDATE → **previous value kept** |
| `headers` JSONB | **Full replace** from keys present in YAML; omitted keys are removed |
| Body | Always replaced with file content |
| Timestamps | Driven by DB defaults/triggers; not set from frontmatter |

Clear a known column explicitly: `title: ""`. Unknown frontmatter keys without a `headers` column return a parse error listing valid keys.

Binary content (NUL or invalid UTF-8) is stored as base64 in `body` with `encoding = base64`; text writes set `encoding = utf8`.

## Directory hierarchy

`.build/` tables include `filetype` and `parent_id`, enabling POSIX-style trees:

```bash
mkdir /mnt/db/notes/tutorials
echo "..." > /mnt/db/notes/tutorials/getting-started.md
mv /mnt/db/notes/tutorials /mnt/db/notes/guides   # atomic directory rename
```

- Parent directories are auto-created on write when missing.
- `tigerfs.resolve_path` resolves path segments for lookups.
- Parent directory `modified_at` is bumped on child create/delete/rename (not on content-only edits).
- Reserved names (`.history`, `.log`, `.savepoint`, `.undo`, capability dirs) cannot be used as user filenames; virtual dirs take precedence.

Views created only via `.format/` on tables **without** `filetype` cannot use `mkdir` on the synthesized path — recreate with `.build/` or add hierarchy columns manually.

## History-enabled workspaces

When `,history` is included (or `echo history > /.build/<app>` on an existing app), TigerFS adds Timescale hypertables and triggers in `tigerfs`:

| Artifact | Table |
|----------|-------|
| Version archive | `<app>_history` |
| Operation log | `<app>_log` |
| Savepoints | `<app>_savepoint` |
| Format boundaries | `<app>_metadata` |

The workspace root exposes `.history/`, `.log/`, `.savepoint/`, and `.undo/` (read-only history; undo via `.apply`). Subdirectories do not duplicate these control dirs.

<Info>
History format migrations record boundaries in `<app>_metadata`; undo across those boundaries is refused. See the history page for savepoint and undo paths.
</Info>

## Implementation map

```mermaid
flowchart TB
  subgraph mount["Mount path"]
    BUILD["/.build/name"]
    FORMAT["/table/.format/markdown"]
    FILES["/app/path/file.md"]
    TABLES["/.tables/app/..."]
  end

  subgraph fsops["internal/tigerfs/fs"]
    WBF["writeBuildFile"]
    WFF["writeFormatFile"]
    WSF["writeSynthFile"]
    CACHE["loadSynthCache / ViewInfo"]
  end

  subgraph synthpkg["internal/tigerfs/fs/synth"]
    GSQL["GenerateBuildSQLWithFeatures"]
    SYN["SynthesizeMarkdown / PlainText"]
    PAR["ParseMarkdown / MapToColumns"]
    COL["DetectColumnRoles"]
  end

  subgraph pg["PostgreSQL"]
    TBL["tigerfs.app"]
    VIEW["schema.app or app_md"]
  end

  BUILD --> WBF --> GSQL --> TBL
  GSQL --> VIEW
  FORMAT --> WFF --> VIEW
  FILES --> WSF --> CACHE
  CACHE --> COL
  WSF --> PAR --> VIEW
  VIEW --> TBL
  FILES --> SYN
  SYN --> COL
  TABLES --> TBL
```

After `.build/` or `.format/` writes, `invalidateSynthCache()` and metadata cache invalidation ensure new views are detected on the next access.

## Errors and constraints

| Situation | Result |
|-----------|--------|
| Unsupported `.build/` format (e.g. `tasks`) | `ErrInvalidPath` — supported: `markdown`, `txt`, optional `,history` |
| `history` without TimescaleDB | `ErrInvalidPath` with install hint |
| `.build/` without app name | Hint: `echo markdown > .build/posts` |
| Unknown frontmatter key (no `headers` column) | Parse error listing valid keys |
| `mkdir` on view without `filetype` | Error — use `.build/` or add columns |
| Filename collides with `.undo`, etc. | Reserved-name error |

Structured hints are logged to stderr (zap) before errno is returned to the caller.

## Related pages

<CardGroup>
<Card title="File-first workspaces" href="/file-first-workspaces">
Day-to-day read/write, directories, and frontmatter examples on mounted workspaces.
</Card>
<Card title="File-first and data-first" href="/file-first-and-data-first">
When a path is a workspace vs a table, and how `.tables/` relates to `.build/`.
</Card>
<Card title="History, savepoints, and undo" href="/history-savepoints-undo">
`.history/`, `.log/`, `.savepoint/`, `.undo/` for history-enabled synthesized apps.
</Card>
<Card title="Filesystem as API" href="/filesystem-as-api">
Mount hierarchy, dot-directories, and how `path.go` resolves paths to SQL.
</Card>
<Card title="Capability directories" href="/capability-directories">
Pipeline paths on `/.tables/<app>/` for filters, export, and pagination.
</Card>
</CardGroup>
