Agent-readable docs

TigerFS Documentation

Reference for mounting PostgreSQL as a versioned filesystem: CLI, dot-directory capabilities, file-first workspaces, data-first pipelines, agent skills, and platform adapters (FUSE/NFS).

Pages

  1. OverviewWhat TigerFS exposes (mount + dot-directories), file-first vs data-first entry paths, runtime assumptions (PostgreSQL, FUSE/NFS), and the first routes to read.
  2. InstallationInstall paths (curl installer, go install, releases), platform prerequisites (Linux FUSE, macOS NFS, Docker), and verification commands.
  3. QuickstartFirst successful mount (postgres:// or tiger:), explore with ls/cat, demo.sh workflows, and expected success signals.
  4. Filesystem as APIMount hierarchy, schema/table/row/column path model, dot-directory control surface, and how fs.Operations maps paths to SQL.
  5. File-first and data-firstWhen a mount path is a workspace vs a table, .build/.format vs raw tables, .tables/ backing schema, and mode detection rules.
  6. Capability directoriesPipeline path grammar (.by, .filter, .order, .first, .last, .export), chaining limits, SQL pushdown, and PathType resolution in path.go.
  7. Synthesized appsWorkspace creation via .build/, markdown/plaintext formats, backing tables in tigerfs schema, views, and column/frontmatter mapping.
  8. Consistency and cachingCross-mount read freshness (no content cache), metadata/stat/path cache TTLs, invalidation on writes, and pipeline cache key isolation.
  9. Platform backendsLinux FUSE vs macOS in-process NFS (go-nfs), write/commit model on NFS, mount registry, and adapter delegation to fs.Operations.
  10. File-first workspacesCreate workspaces, read/write markdown and plaintext, directories as state, frontmatter semantics, and backing-table access via .tables/.
  11. Data-first explorationRow/column reads, index navigation (.by), pagination (.first/.last/.sample), PATCH writes, import/export, and large-table safety limits.
  12. History, savepoints, and undoEnable history on workspaces, .history/.log paths, savepoint JSON, .undo preview/apply, per-user filters, auto-savepoints, and migration boundaries.

Complete Markdown

# TigerFS Documentation

> Reference for mounting PostgreSQL as a versioned filesystem: CLI, dot-directory capabilities, file-first workspaces, data-first pipelines, agent skills, and platform adapters (FUSE/NFS).

## Context Links

- [Agent index](https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/llms.txt)
- [Human interactive docs](https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3)
- [GitHub repository](https://github.com/timescale/tigerfs)

## Repository Metadata

- Repository: timescale/tigerfs

- Generated: 2026-06-03T08:03:54.777Z
- Updated: 2026-06-03T08:04:10.955Z
- Runtime: Grok CLI
- Format: Documentation
- Pages: 25

## Page Index

- 01. [Overview](https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/01-overview.md) - What TigerFS exposes (mount + dot-directories), file-first vs data-first entry paths, runtime assumptions (PostgreSQL, FUSE/NFS), and the first routes to read.
- 02. [Installation](https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/02-installation.md) - Install paths (curl installer, go install, releases), platform prerequisites (Linux FUSE, macOS NFS, Docker), and verification commands.
- 03. [Quickstart](https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/03-quickstart.md) - First successful mount (postgres:// or tiger:), explore with ls/cat, demo.sh workflows, and expected success signals.
- 04. [Filesystem as API](https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/04-filesystem-as-api.md) - Mount hierarchy, schema/table/row/column path model, dot-directory control surface, and how fs.Operations maps paths to SQL.
- 05. [File-first and data-first](https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/05-file-first-and-data-first.md) - When a mount path is a workspace vs a table, .build/.format vs raw tables, .tables/ backing schema, and mode detection rules.
- 06. [Capability directories](https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/06-capability-directories.md) - Pipeline path grammar (.by, .filter, .order, .first, .last, .export), chaining limits, SQL pushdown, and PathType resolution in path.go.
- 07. [Synthesized apps](https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/07-synthesized-apps.md) - Workspace creation via .build/, markdown/plaintext formats, backing tables in tigerfs schema, views, and column/frontmatter mapping.
- 08. [Consistency and caching](https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/08-consistency-and-caching.md) - Cross-mount read freshness (no content cache), metadata/stat/path cache TTLs, invalidation on writes, and pipeline cache key isolation.
- 09. [Platform backends](https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/09-platform-backends.md) - Linux FUSE vs macOS in-process NFS (go-nfs), write/commit model on NFS, mount registry, and adapter delegation to fs.Operations.
- 10. [File-first workspaces](https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/10-file-first-workspaces.md) - Create workspaces, read/write markdown and plaintext, directories as state, frontmatter semantics, and backing-table access via .tables/.
- 11. [Data-first exploration](https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/11-data-first-exploration.md) - Row/column reads, index navigation (.by), pagination (.first/.last/.sample), PATCH writes, import/export, and large-table safety limits.
- 12. [History, savepoints, and undo](https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/12-history-savepoints-and-undo.md) - Enable history on workspaces, .history/.log paths, savepoint JSON, .undo preview/apply, per-user filters, auto-savepoints, and migration boundaries.
- 13. [Agent skills](https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/13-agent-skills.md) - Bundled skills/tigerfs pack, install-time staging to agent config dirs, SKILL.md navigation (files, data, ops, recipes), and mount-time copy behavior.
- 14. [Cloud backends](https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/14-cloud-backends.md) - tiger: and ghost: prefix resolution, create/fork/list flows, default_backend config, and Tiger CLI credential requirements.
- 15. [DDL staging](https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/15-ddl-staging.md) - .create/.modify/.delete staging dirs, sql/.test/.commit control files, index DDL via .indexes/, grace period, and errno on validation failures.
- 16. [CLI reference](https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/16-cli-reference.md) - All cobra commands (mount, unmount, stop, status, info, list, create, fork, migrate, test-connection, config, version), flags, and argument patterns.
- 17. [Configuration reference](https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/17-configuration-reference.md) - Config struct fields, ~/.config/tigerfs/config.yaml, TIGERFS_* env vars, precedence, TLS enforcement, and mount-specific overrides.
- 18. [Data formats reference](https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/18-data-formats-reference.md) - TSV, CSV, JSON, YAML row encoding, PATCH semantics, NULL representation, type extensions (.txt/.json/.bin), and binary_encoding options.
- 19. [Connection reference](https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/19-connection-reference.md) - postgres:// URIs, PG* env vars, password_command, .pgpass, sslmode=require rules, localhost exemptions, and pool settings.
- 20. [Error codes](https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/20-error-codes.md) - fs.ErrorCode to POSIX errno mapping, structured zap hints on failure, constraint violations, and reading stderr when tools report EINVAL/EACCES.
- 21. [Demo environment](https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/21-demo-environment.md) - scripts/demo workflows (Docker vs macOS native), seed data, demo.sh start/shell/stop, and sample exploration commands.
- 22. [Workflow recipes](https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/22-workflow-recipes.md) - Copy-paste file-first patterns: kanban (todo/doing/done + mv), knowledge base, session context, from skills/tigerfs/recipes.md.
- 23. [Migrate workspaces](https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/23-migrate-workspaces.md) - tigerfs migrate --describe/--dry-run, history-format migrations, pre-boundary undo EPERM, and release verification against test/integration migrate tests.
- 24. [Develop and test](https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/24-develop-and-test.md) - Build commands, go test ./... and integration testcontainers, pre-commit checks, stress runner, and release-process checklist.
- 25. [Troubleshooting](https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/25-troubleshooting.md) - Stale mounts, force unmount, NFS monotonicity warnings in stress runs, logging --debug, .refresh metadata clear, and common errno recovery.

## Source File Index

- `.github/workflows/test.yml`
- `.goreleaser.yaml`
- `CHANGELOG.md`
- `CLAUDE.md`
- `cmd/tigerfs/main.go`
- `docs/adr/001-fuse-library.md`
- `docs/adr/003-ddl-staging-pattern.md`
- `docs/adr/013-backend-prefix-scheme.md`
- `docs/adr/014-query-reduction-caching.md`
- `docs/adr/016-undo-and-recovery.md`
- `docs/adr/019-undo-boundary-via-metadata-table.md`
- `docs/data-first.md`
- `docs/file-first.md`
- `docs/history.md`
- `docs/quickstart.md`
- `docs/release-process.md`
- `docs/spec.md`
- `go.mod`
- `internal/tigerfs/backend/ghost.go`
- `internal/tigerfs/backend/resolve.go`
- `internal/tigerfs/backend/tiger.go`
- `internal/tigerfs/cmd/cmd_test.go`
- `internal/tigerfs/cmd/config.go`
- `internal/tigerfs/cmd/create.go`
- `internal/tigerfs/cmd/fork.go`
- `internal/tigerfs/cmd/migrate.go`
- `internal/tigerfs/cmd/mount.go`
- `internal/tigerfs/cmd/root.go`
- `internal/tigerfs/cmd/stop.go`
- `internal/tigerfs/cmd/test_connection.go`
- `internal/tigerfs/cmd/unmount.go`
- `internal/tigerfs/config/config_test.go`
- `internal/tigerfs/config/config.go`
- `internal/tigerfs/db/connection.go`
- `internal/tigerfs/db/password.go`
- `internal/tigerfs/db/pipeline.go`
- `internal/tigerfs/db/query.go`
- `internal/tigerfs/db/tls.go`
- `internal/tigerfs/format/convert.go`
- `internal/tigerfs/format/csv.go`
- `internal/tigerfs/format/json.go`
- `internal/tigerfs/format/yaml.go`
- `internal/tigerfs/fs/auto_savepoint_test.go`
- `internal/tigerfs/fs/build.go`
- `internal/tigerfs/fs/constants.go`
- `internal/tigerfs/fs/context.go`
- `internal/tigerfs/fs/ddl.go`
- `internal/tigerfs/fs/errors_test.go`
- `internal/tigerfs/fs/errors.go`
- `internal/tigerfs/fs/extensions.go`
- `internal/tigerfs/fs/history.go`
- `internal/tigerfs/fs/metadata_cache.go`
- `internal/tigerfs/fs/operations.go`
- `internal/tigerfs/fs/path_cache.go`
- `internal/tigerfs/fs/path.go`
- `internal/tigerfs/fs/staging.go`
- `internal/tigerfs/fs/synth/build.go`
- `internal/tigerfs/fs/synth/format.go`
- `internal/tigerfs/fs/synth/markdown.go`
- `internal/tigerfs/fs/synth/plaintext.go`
- `internal/tigerfs/fs/undo_boundary_test.go`
- `internal/tigerfs/fs/undo.go`
- `internal/tigerfs/fs/write.go`
- `internal/tigerfs/fuse/adapter.go`
- `internal/tigerfs/fuse/control_files.go`
- `internal/tigerfs/logging/logging.go`
- `internal/tigerfs/mount/registry.go`
- `internal/tigerfs/nfs/handler.go`
- `internal/tigerfs/nfs/mount_darwin.go`
- `README.md`
- `scripts/demo/demo.sh`
- `scripts/demo/init.sql`
- `scripts/demo/README.md`
- `scripts/demo/seed.sh`
- `scripts/install.sh`
- `scripts/pre-commit`
- `scripts/test-install.sh`
- `skills/tigerfs/data.md`
- `skills/tigerfs/files.md`
- `skills/tigerfs/ops.md`
- `skills/tigerfs/recipes.md`
- `skills/tigerfs/SKILL.md`
- `test/integration/crud_test.go`
- `test/integration/ddl_test.go`
- `test/integration/demo_data.go`
- `test/integration/format_test.go`
- `test/integration/fs_operations_test.go`
- `test/integration/main_test.go`
- `test/integration/migrate_test.go`
- `test/integration/pipeline_test.go`
- `test/integration/setup.go`
- `test/integration/synthesized_test.go`
- `test/integration/undo_test.go`
- `test/stress/README.md`

---

## 01. Overview

> What TigerFS exposes (mount + dot-directories), file-first vs data-first entry paths, runtime assumptions (PostgreSQL, FUSE/NFS), and the first routes to read.

- Page Markdown: https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/01-overview.md
- Generated: 2026-06-03T07:53:57.161Z

### Source Files

- `README.md`
- `docs/spec.md`
- `cmd/tigerfs/main.go`
- `internal/tigerfs/cmd/root.go`
- `docs/quickstart.md`

---
title: "Overview"
description: "What TigerFS exposes (mount + dot-directories), file-first vs data-first entry paths, runtime assumptions (PostgreSQL, FUSE/NFS), and the first routes to read."
---

TigerFS is a Go CLI (`tigerfs`) that mounts a PostgreSQL database at a local path and serves all filesystem semantics through `internal/tigerfs/fs.Operations`, with Linux using FUSE (`go-fuse`) and macOS using an in-process NFS v3 server (`go-nfs`). Every path under the mount resolves to SQL against PostgreSQL; Unix tools and agent file APIs operate on tables, rows, columns, and dot-directory capabilities without a separate query language.

## Runtime assumptions

| Requirement | Detail |
|-------------|--------|
| Database | PostgreSQL 12+ (driver: `pgx/v5`) |
| Linux | FUSE; user typically in `fuse` group |
| macOS | Native NFS mount to local NFS server (no FUSE kernel extension) |
| Connection | `postgres://` URI, `PG*` env vars, or `tiger:` / `ghost:` cloud prefixes |
| TLS | Non-localhost connections enforce `sslmode=require` unless `--insecure-no-ssl` |
| Go | 1.23+ to build from source |

The `mount` command connects once, registers the mount in `internal/tigerfs/mount`, and blocks until unmount or `SIGINT`/`SIGTERM`. `cmd/tigerfs/main.go` wires signal-aware shutdown into `cmd.Execute`.

<Note>
Windows is listed in `docs/spec.md` as WinFsp-backed; Linux and macOS are the primary supported paths in the mount implementation (`mount_linux.go`, `mount_darwin.go`).
</Note>

## What a mount exposes

At the mount root, tables from the default schema (usually `public`) appear as top-level directories. Non-default schemas use `/mount/<schema>/<table>/` or explicit `/mount/.schemas/<schema>/<table>/`.

```text
/mnt/db/                          # mount root (PathRoot)
├── users/                        # data-first table
│   ├── .info/                    # schema, columns, count
│   ├── .by/ .filter/ .order/     # pipeline capabilities
│   ├── 123/                      # row-as-directory
│   └── 123.json                  # row-as-file (not always in ls)
├── notes/                        # file-first workspace (.md files)
│   ├── hello.md
│   ├── .history/ .log/ .savepoint/ .undo/
│   └── tutorials/
├── .build/                       # create workspaces (PathBuild)
├── .create/ .modify/ .delete/    # DDL staging
├── .tables/                      # tigerfs-schema backing tables
├── .schemas/                     # explicit schema listing
└── .refresh                      # metadata cache invalidation
```

`ls` hides dot-directories by default; `ls -a` reveals `.info/`, `.by/`, and other control paths. Reserved dot names (`.build`, `.filter`, `.history`, etc.) cannot be created as user data — attempts return `EACCES`. User dotfiles such as `.gitignore` are allowed inside file-first workspaces when not on the reserved list.

## Dot-directories as the control surface

Capabilities are navigable paths, not separate APIs. `fs/path.go` classifies each path (`PathType`) and accumulates query state in `FSContext` for pipeline segments.

| Dot path | Role |
|----------|------|
| `.info/` | Table metadata (`schema`, `columns`, `count`, `indexes`) |
| `.by/` | Index-based navigation |
| `.filter/`, `.order/`, `.columns/` | Pipeline query segments |
| `.first/`, `.last/`, `.sample/` | Pagination |
| `.export/`, `.import/` | Bulk read/write |
| `.build/` | Create synthesized workspace (`markdown`, `plaintext`, `history`, …) |
| `.format/` | Add synthesized view to an existing table |
| `.create/`, `.modify/`, `.delete/` | DDL staging with `.commit` / `.abort` |
| `.history/`, `.log/`, `.savepoint/`, `.undo/` | File-first versioning and undo |
| `.tables/` | Data-first access to `tigerfs` schema backing tables |

Example pipeline (one SQL query):

```bash
cat /mnt/db/orders/.by/customer_id/123/.order/created_at/.last/10/.export/json
```

`--max-pipeline-depth` (default 10) limits how deep chained capabilities appear in listings.

## File-first vs data-first entry paths

Both modes share one mount and one PostgreSQL database. Per-directory mode is inferred from layout:

| Signal | Mode | Typical entry |
|--------|------|----------------|
| `.md` / `.txt` workspace files | File-first | `echo "markdown,history" > /mnt/db/.build/notes` |
| `.info/` under a table directory | Data-first | `ls /mnt/db` then `cat /mnt/db/users/.info/schema` |
| New project | File-first | `.build/` with `markdown` or `plaintext` |
| Existing database | Data-first | `tigerfs mount postgres://… /mnt/db` |

**File-first** presents rows as domain files (markdown with YAML frontmatter, or plaintext). Workspaces opt into `history` for `.log/`, `.savepoint/`, and `.undo/`. Subdirectories (`mkdir`, `mv`) map to hierarchical state; kanban-style flows use directory moves. Backing storage lives in the `tigerfs` schema; inspect via `/mnt/db/.tables/<workspace>/`.

**Data-first** presents each row as a file (`123.json`) and/or directory (`123/email.txt`). Updates use PATCH semantics for structured formats. Index navigation, pipeline paths, and DDL staging apply at the table level.

You can add file-first views to an existing table without rebuilding data:

```bash
echo "markdown" > /mnt/db/posts/.format/markdown
```

## Architecture

```mermaid
flowchart LR
  subgraph clients [Clients]
    Unix[Unix tools]
    Agents[Agent file APIs]
  end
  subgraph adapters [Platform adapters]
    FUSE[fuse.MountOps Linux]
    NFS[nfs.Mount macOS]
  end
  subgraph core [Shared core]
    Ops[fs.Operations]
    Path[fs/path.go PathType]
    DB[db + format]
  end
  PG[(PostgreSQL)]
  Unix --> FUSE
  Unix --> NFS
  Agents --> FUSE
  Agents --> NFS
  FUSE --> Ops
  NFS --> Ops
  Ops --> Path
  Ops --> DB
  DB --> PG
```

New filesystem behavior belongs in `internal/tigerfs/fs/`, not in FUSE/NFS adapters. Linux defaults to `fuse.MountOps` (shared `fs.Operations`); `--legacy-fuse` selects the older FUSE node tree. macOS always uses NFS because FUSE requires third-party kernel extensions.

<Warning>
On macOS NFS, `go-nfs` synthesizes Open/Write/Close per WRITE RPC; content commits on Close. `ReadFile` always queries PostgreSQL fresh; stat/path caches hold metadata only with write invalidation.
</Warning>

## CLI surface

Root command (`internal/tigerfs/cmd/root.go`) registers:

`mount`, `unmount`, `stop`, `status`, `info`, `list`, `create`, `fork`, `migrate`, `test-connection`, `config`, `version`

`mount` accepts:

```bash
tigerfs mount postgres://user@host:5432/mydb /mnt/db
tigerfs mount tiger:<service-id>              # optional auto mountpoint
tigerfs mount ghost:<service-id> /mnt/db
```

Configuration loads from `~/.config/tigerfs/config.yaml` with `TIGERFS_*` overrides (see configuration reference). Persistent flags include `--log-level`, `--config-dir`, `--read-only`, `--user-id`, and `--auto-savepoint-interval`.

Bundled agent skills live under `skills/tigerfs/` (`SKILL.md`, `files.md`, `data.md`, `ops.md`, `recipes.md`). The install script (`scripts/install.sh`) copies them into detected agent skill directories or stages them under `~/.config/tigerfs/skills/tigerfs`.

## Consistency model (read this early)

PostgreSQL is the single source of truth across concurrent mounts:

- **Do not cache row content** — reads must reflect latest commits.
- **Cache metadata only** — directory listings, stat sizes, column lists (short TTL, invalidation on writes).
- **Pipeline paths need isolated cache keys** — e.g. `.export/json` vs `.filter/active/true/.export/json` must not share stat entries.

Violations surface as cross-mount staleness; new write paths must invalidate `statCache` and `pathCache` with the user schema key.

## Verification signals

After install or build:

```bash
go build -o bin/tigerfs ./cmd/tigerfs
tigerfs version
tigerfs mount postgres://localhost/mydb /mnt/db &
ls /mnt/db
tigerfs unmount /mnt/db
```

For a zero-setup trial, `scripts/demo/demo.sh start` mounts sample data (Docker or native macOS). Success looks like table directories (`users/`, `orders/`, …) and readable row files (`cat users/1.json`).

## First routes to read

Use this order to go from mount behavior to implementation detail:

<Steps>
<Step title="Product orientation">
Read `README.md` for file-first vs data-first examples, dot-directory tables, and cloud backend prefixes.
</Step>
<Step title="Hands-on paths">
Follow `docs/quickstart.md` for mount, `.build/` workspaces, savepoints, and pipeline `cat` paths.
</Step>
<Step title="Mode guides">
`docs/file-first.md` — workspaces, frontmatter, `.tables/`. `docs/data-first.md` — row formats, PATCH writes, DDL, pipelines.
</Step>
<Step title="Full contract">
`docs/spec.md` — complete hierarchy, formats, configuration, error mapping, NFS/FUSE notes.
</Step>
<Step title="Implementation map">
`CLAUDE.md` — package layout (`fs/`, `fuse/`, `nfs/`, `db/`), pipeline parsing in `fs/path.go`, synth layer in `fs/synth/`.
</Step>
<Step title="Agent usage">
`skills/tigerfs/SKILL.md` — mode detection rules and safe patterns for agents using Read/Write/Glob/Grep.
</Step>
</Steps>

| Topic | Location |
|-------|----------|
| Path parsing & `PathType` | `internal/tigerfs/fs/path.go` |
| Filesystem operations | `internal/tigerfs/fs/operations.go` |
| Mount & backends | `internal/tigerfs/cmd/mount.go`, `cmd/mount_linux.go`, `cmd/mount_darwin.go` |
| Synth formats | `internal/tigerfs/fs/synth/` |
| Integration tests | `test/integration/` |

## Related pages

<CardGroup>
<Card title="Installation" href="/installation">
Install paths, platform prerequisites (FUSE, NFS), and verification.
</Card>
<Card title="Quickstart" href="/quickstart">
First mount, `ls`/`cat` exploration, and demo workflows.
</Card>
<Card title="Filesystem as API" href="/filesystem-as-api">
Mount hierarchy and how paths map to SQL.
</Card>
<Card title="File-first and data-first" href="/file-first-and-data-first">
Mode detection, `.build/` vs raw tables, `.tables/` backing schema.
</Card>
<Card title="Capability directories" href="/capability-directories">
Pipeline grammar and `PathType` resolution.
</Card>
<Card title="Platform backends" href="/platform-backends">
FUSE vs NFS adapters, NFS write/commit model, mount registry.
</Card>
<Card title="Consistency and caching" href="/consistency-and-caching">
Cross-mount freshness, cache TTLs, invalidation rules.
</Card>
<Card title="CLI reference" href="/cli-reference">
All cobra commands, flags, and connection argument patterns.
</Card>
</CardGroup>

---

## 02. Installation

> Install paths (curl installer, go install, releases), platform prerequisites (Linux FUSE, macOS NFS, Docker), and verification commands.

- Page Markdown: https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/02-installation.md
- Generated: 2026-06-03T07:54:10.200Z

### Source Files

- `scripts/install.sh`
- `.goreleaser.yaml`
- `go.mod`
- `docs/spec.md`
- `scripts/test-install.sh`

---
title: "Installation"
description: "Install paths (curl installer, go install, releases), platform prerequisites (Linux FUSE, macOS NFS, Docker), and verification commands."
---

TigerFS ships as a single static `tigerfs` binary built with GoReleaser (`CGO_ENABLED=0`) for Linux and macOS on `x86_64` and `arm64`. Release archives bundle the CLI, `skills/tigerfs/` agent instructions, and SHA256 checksums served from `https://install.tigerfs.io`. Mounting requires PostgreSQL reachability; the filesystem backend is FUSE on Linux and an in-process NFS server on macOS.

## Install paths

<Tabs>
<Tab title="Curl installer (recommended)">

The curl installer downloads a versioned release archive, verifies its checksum, installs the binary, and optionally copies agent skills.

```bash
curl -fsSL https://install.tigerfs.io | sh
```

Pin a version or override install location:

```bash
VERSION=v0.7.0 curl -fsSL https://install.tigerfs.io | sh
INSTALL_DIR=/usr/local/bin curl -fsSL https://install.tigerfs.io | sh
```

| Variable | Default | Effect |
|----------|---------|--------|
| `BASE_URL` | `https://install.tigerfs.io` | CDN root for `latest.txt` and `releases/{version}/` |
| `VERSION` | Latest from `/latest.txt` | Release tag; normalized to `v*` prefix |
| `INSTALL_DIR` | `~/.local/bin` if `~/.local` exists, else `~/bin` | Binary destination |
| `TIGERFS_INTERACTIVE` | unset | Set to `1` to force the agent-skills menu in non-TTY contexts |

**Installer dependencies:** `curl` or `wget`, `tar`, and `sha256sum` or `shasum`.

**Download layout:** `{BASE_URL}/releases/{version}/tigerfs_{OS}_{arch}.tar.gz` plus a sibling `.sha256` file. Supported OS values are `Linux` and `Darwin`; architectures are `x86_64` and `arm64`.

On macOS, the installer clears the Gatekeeper quarantine attribute (`xattr -d com.apple.quarantine`) when present.

</Tab>
<Tab title="Go install">

Build and install the latest module release into your Go binary path:

```bash
go install github.com/timescale/tigerfs/cmd/tigerfs@latest
```

Requires Go **1.25.1** or newer (see `go.mod`). Agent skills are **not** copied by `go install`; clone the repository or use the curl installer to get `skills/tigerfs/`.

</Tab>
<Tab title="GitHub releases">

Tagged releases (`v*.*.*`) trigger `.github/workflows/release.yml`, which runs GoReleaser and publishes:

- **GitHub Releases** — per-platform `.tar.gz` archives and checksums
- **S3** (`tigerfs-releases`) — same artifacts under `releases/{tag}/`, plus root `latest.txt` and `install.sh` on stable (non-prerelease) tags

Archive names follow `tigerfs_{Os}_{Arch}.tar.gz` (for example `tigerfs_Linux_x86_64.tar.gz`). Each archive contains the `tigerfs` binary, `README.md`, `docs/spec.md`, and `skills/tigerfs/*`.

Download manually, verify the `.sha256` sidecar, extract, and place `tigerfs` on your `PATH`.

</Tab>
<Tab title="Build from source">

For development or unreleased commits:

```bash
git clone https://github.com/timescale/tigerfs.git
cd tigerfs
go build -o bin/tigerfs ./cmd/tigerfs
```

Optional snapshot matching release builds:

```bash
goreleaser release --snapshot --clean
./dist/tigerfs_*/*/tigerfs version
```

Skills live at `skills/tigerfs/` in the repository tree.

</Tab>
</Tabs>

### Agent skills (curl / release archives)

Release archives include `skills/tigerfs/` (`SKILL.md`, `files.md`, `data.md`, `ops.md`, `recipes.md`). After binary install, `scripts/install.sh`:

- **Interactive (TTY or `TIGERFS_INTERACTIVE=1`):** prompts to install skills into detected agents (Claude Code, Cursor, Codex CLI, Gemini CLI, Windsurf, Antigravity, Kiro) or skip
- **Non-interactive (`curl | sh`):** stages skills to `~/.config/tigerfs/skills/tigerfs/` with copy hints for detected agents

Upgrades replace the target `tigerfs` skill directory (`rm -rf` then `cp -r`) so stale skill files are removed.

Validate the installer locally:

```bash
./scripts/test-install.sh
```

## Platform prerequisites

TigerFS needs a running **PostgreSQL** database (local, remote, or Tiger/Ghost backends). Filesystem prerequisites depend on the host OS.

```mermaid
flowchart LR
  subgraph clients["Client tools"]
    LS["ls / cat / grep"]
  end
  subgraph tigerfs["tigerfs process"]
    OPS["fs.Operations"]
  end
  subgraph backends["OS filesystem backend"]
    FUSE["Linux: go-fuse"]
    NFS["macOS: go-nfs in-process"]
  end
  PG[(PostgreSQL)]
  LS --> FUSE & NFS
  FUSE & NFS --> OPS --> PG
```

| Platform | Filesystem backend | Host prerequisites |
|----------|-------------------|-------------------|
| **Linux** | FUSE (`hanwen/go-fuse/v2`) | FUSE userspace support; `/dev/fuse` present. CI installs `fuse` / `libfuse-dev`. Unprivileged mounts typically require membership in the `fuse` group. |
| **macOS** | In-process NFS v3 (`go-nfs`) | No FUSE or kernel extension install. Uses built-in `mount_nfs`. |
| **Windows** | WinFsp (planned) | Documented in `docs/spec.md` as requiring WinFsp; **not** in the current curl installer OS matrix (`Linux` / `Darwin` only). |
| **Docker** | Linux FUSE inside container | Privileged container, `/dev/fuse` device, `SYS_ADMIN` capability for mount workflows |

<Warning>
Managed platforms without FUSE device access (for example AWS Fargate, Cloud Run, Kubernetes without privileged pods) cannot run TigerFS mounts as implemented today.
</Warning>

### Linux FUSE setup

On Debian/Ubuntu-family systems:

```bash
sudo apt-get update
sudo apt-get install -y fuse libfuse-dev
```

Fedora/RHEL-style:

```bash
sudo dnf install fuse3
```

Confirm the device node exists before mounting:

```bash
test -e /dev/fuse && echo "FUSE available"
```

Integration and stress Docker stacks use `privileged: true`, map `/dev/fuse`, and add `SYS_ADMIN` (see `test/docker/docker-compose.test.yml`, `test/stress/docker/docker-compose.yml`).

### macOS

No additional filesystem packages. TigerFS selects the NFS backend at compile time (`mount_darwin.go`). The curl installer strips quarantine on the installed binary.

### Docker

**Root `Dockerfile`** — multi-stage Alpine image with `fuse` runtime packages; intended as a container entrypoint for the CLI (`ENTRYPOINT ["/usr/local/bin/tigerfs"]`).

**Demo stack** (`scripts/demo/docker/`) — Ubuntu-based image with `fuse3`, PostgreSQL client tools, and a prebuilt `tigerfs` binary. Compose runs the `tigerfs` service `privileged: true` against a `timescale/timescaledb-ha:pg18` database.

Typical FUSE mount flags in container docs and spec:

```bash
docker run --rm -it \
  --device /dev/fuse \
  --cap-add SYS_ADMIN \
  -v /path/to/mount:/mnt/db \
  tigerfs mount postgres://user:pass@host:5432/db /mnt/db
```

Use `./scripts/demo/demo.sh start` for a guided demo (auto-detects macOS NFS vs Linux Docker FUSE). See the demo environment page for `demo.sh` commands and seed data.

<Note>
Package managers (Homebrew, apt, yum) are listed in `docs/spec.md` as **Phase 2 (deferred)**. The supported distribution paths today are curl install, `go install`, GitHub/S3 releases, and source build.
</Note>

## Verification

Run these after any install path before attempting a mount.

<Steps>
<Step title="Confirm binary and version">

```bash
command -v tigerfs
tigerfs version
```

<RequestExample>

```bash
$ tigerfs version
TigerFS v0.7.0
Build time: 2026-...
Git commit: ...
Go version: go1.25.1
Platform: darwin/arm64
```

</RequestExample>

</Step>
<Step title="Test database connectivity">

Verify credentials without mounting:

```bash
tigerfs test-connection postgres://localhost:5432/mydb
```

Or use `PGHOST`, `PGUSER`, `PGDATABASE`, etc., or Tiger Cloud `TIGER_SERVICE_ID` per your config. The command prints PostgreSQL version, current database/user, and accessible schema/table counts (30s timeout).

</Step>
<Step title="Optional: run install script tests">

From a source checkout:

```bash
./scripts/test-install.sh
```

Exercises download, checksum verification, binary placement, and skills staging/copy paths against a `file://` mock CDN.

</Step>
</Steps>

### PATH

If `tigerfs` is not found, ensure the install directory is on `PATH`:

```bash
export PATH="$HOME/.local/bin:$PATH"   # default curl target when ~/.local exists
# or
export PATH="$HOME/bin:$PATH"
```

The curl installer prints this hint when the install dir is missing from `PATH`.

### Install script integration test (maintainers)

`scripts/test-install.sh` validates non-interactive staging, interactive agent selection, skip behavior, and upgrade cleanup of stale skill files. Requirements match `install.sh`: `sh`, `tar`, `curl` or `wget`, `sha256sum` or `shasum`.

## Release and CDN topology

```text
git tag vX.Y.Z
    │
    ▼
.github/workflows/release.yml  →  GoReleaser (.goreleaser.yaml)
    │                              ├─ linux/darwin × amd64/arm64
    │                              ├─ tar.gz + checksums
    │                              ├─ GitHub Releases
    │                              └─ S3 tigerfs-releases/releases/{tag}/
    ▼
CloudFront install.tigerfs.io
    ├─ /install.sh          (from scripts/install.sh on stable tags)
    ├─ /latest.txt          (current version string)
    └─ /releases/{tag}/tigerfs_{OS}_{arch}.tar.gz[.sha256]
```

GoReleaser embeds version metadata via ldflags (`Version`, `BuildTime`, `GitCommit` in `internal/tigerfs/cmd`). Stable releases also upload `latest.txt` and invalidate CloudFront paths `/latest.txt` and `/install.sh`.

## What to do next

After installation and `test-connection` succeed, mount a database and explore with standard Unix tools. Use `--debug` on the CLI for structured stderr logs when mounts fail with errno-only tool messages.

## Related pages

<CardGroup>
<Card title="Quickstart" href="/quickstart">
First mount with `postgres://` or `tiger:`, explore with `ls`/`cat`, and demo success signals.
</Card>
<Card title="Platform backends" href="/platform-backends">
Linux FUSE vs macOS NFS adapters, write/commit behavior, and mount registry.
</Card>
<Card title="Connection reference" href="/connection-reference">
`postgres://` URIs, TLS/`sslmode=require`, localhost exemptions, and pool settings.
</Card>
<Card title="Agent skills" href="/agent-skills">
Bundled `skills/tigerfs` pack, install-time staging, and agent directory layout.
</Card>
<Card title="Demo environment" href="/demo-environment">
`demo.sh` workflows, Docker vs macOS native, and seeded exploration data.
</Card>
<Card title="Develop and test" href="/develop-and-test">
Source builds, `go test`, Docker FUSE integration tests, and release checklist.
</Card>
</CardGroup>

---

## 03. Quickstart

> First successful mount (postgres:// or tiger:), explore with ls/cat, demo.sh workflows, and expected success signals.

- Page Markdown: https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/03-quickstart.md
- Generated: 2026-06-03T07:53:36.240Z

### Source Files

- `docs/quickstart.md`
- `scripts/demo/demo.sh`
- `scripts/demo/README.md`
- `internal/tigerfs/cmd/mount.go`
- `test/integration/setup.go`

---
title: "Quickstart"
description: "First successful mount (postgres:// or tiger:), explore with ls/cat, demo.sh workflows, and expected success signals."
---

`tigerfs mount` connects to PostgreSQL (direct URI, `PG*` environment variables, or `tiger:` / `ghost:` backend prefixes) and exposes the default schema as a directory tree at a mountpoint. A successful first mount returns table names from `ls`, row payloads from `cat`, and registry entries visible to `tigerfs status`.

## Prerequisites

| Requirement | Notes |
|-------------|-------|
| TigerFS binary | `curl -fsSL https://install.tigerfs.io \| sh`, `go install github.com/timescale/tigerfs/cmd/tigerfs@latest`, or build from source (`go build -o bin/tigerfs ./cmd/tigerfs`) |
| PostgreSQL | Reachable database with at least one user schema and tables |
| Linux | FUSE support (typically pre-installed) |
| macOS | No extra kernel extensions; TigerFS uses an in-process NFS v3 server |
| Demo only | Docker with Compose v2; macOS native demo also needs Go 1.21+ |

<Note>
Remote connections require TLS (`sslmode=require`) unless you pass `--insecure-no-ssl` or set `InsecureNoSSL` in config. Localhost and Unix sockets default to `sslmode=disable` when no `sslmode` is in the URI.
</Note>

## Fastest path: demo environment

From the repository root, `scripts/demo/demo.sh` starts PostgreSQL, mounts TigerFS, seeds file-first apps, and prints exploration commands.

<Tabs>
<Tab title="Auto-detect">
```bash
cd scripts/demo
./demo.sh start          # Darwin → --mac; Linux → --docker
./demo.sh status
./demo.sh shell          # cwd = mount root
ls
cat users/1.json
./demo.sh stop
```
</Tab>
<Tab title="Docker (any platform)">
```bash
cd scripts/demo
./demo.sh start --docker
./demo.sh shell          # inside container at /mnt/db
ls
cat users/1.json
cat products/1.json
ls orders/.first/10/
./demo.sh stop
```
</Tab>
<Tab title="macOS native">
```bash
cd scripts/demo
./demo.sh start --mac
ls /tmp/tigerfs-demo
cat /tmp/tigerfs-demo/users/1.json
cat /tmp/tigerfs-demo/blog/hello-world.md
./demo.sh stop
```
</Tab>
</Tabs>

### Demo lifecycle

```text
demo.sh start
  ├─ docker compose up (postgres; + tigerfs container in --docker mode)
  ├─ wait_for_postgres (init.sql → data-first tables)
  ├─ tigerfs mount postgres://demo:demo@… [--insecure-no-ssl]
  ├─ seed.sh <mount> → .build/blog|docs|snippets + sample files
  └─ print success + example commands

demo.sh stop
  ├─ unmount / kill tigerfs
  └─ docker compose down
```

| Command | Action |
|---------|--------|
| `start` | Build/start containers, mount, run `seed.sh` |
| `stop` | Unmount TigerFS, tear down containers |
| `status` | Report container and mount state |
| `restart` | `stop` then `start` |
| `shell` | Interactive session at mount root (`/mnt/db` in Docker, `/tmp/tigerfs-demo` on macOS) |

<ParamField body="--docker" type="flag">
Both PostgreSQL and TigerFS run in containers. Mount inside the `tigerfs` container at `/mnt/db`. Connection: `postgres://demo:demo@postgres:5432/demo`.
</ParamField>

<ParamField body="--mac" type="flag">
PostgreSQL in Docker on `localhost:5432`; TigerFS runs natively on the host at `/tmp/tigerfs-demo`. Connection: `postgres://demo:demo@localhost:5432/demo`.
</ParamField>

<ParamField body="--debug" type="flag">
Passes `--log-level debug` to the mount command.
</ParamField>

### Demo seed data

**Data-first** (from `init.sql` at PostgreSQL startup): `users` (~1,000 rows), `categories` (10, TEXT PK), `products` (~200), `orders` (~8,000, UUIDv7 PK), views, and indexes for `.by/` navigation.

**File-first** (from `seed.sh` after mount):

| Path | Format | History |
|------|--------|---------|
| `blog/` | Markdown, 5 posts | Yes |
| `docs/` | Markdown, 4 pages | Yes |
| `snippets/` | Plain text, 3 files | Yes |

## Mount your own database

<Steps>
<Step title="Verify connectivity">
```bash
tigerfs test-connection postgres://user:password@localhost:5432/mydb
```
<ResponseExample>
```text
Connected to PostgreSQL 16.x ...
Database:   mydb
User:       user
Schemas:    N
Tables:     M
```
</ResponseExample>
</Step>
<Step title="Create mountpoint and mount">
```bash
mkdir -p /mnt/db
tigerfs mount postgres://user:password@localhost:5432/mydb /mnt/db &
```
Or with standard PostgreSQL environment variables (no URI argument):
```bash
export PGHOST=localhost PGPORT=5432 PGUSER=user PGPASSWORD=password PGDATABASE=mydb
tigerfs mount /mnt/db &
```
</Step>
<Step title="Explore">
```bash
ls /mnt/db
ls -la /mnt/db/users
cat /mnt/db/users/1.json
cat /mnt/db/users/.info/schema
cat /mnt/db/users/.info/count
```
</Step>
<Step title="Unmount">
```bash
tigerfs unmount /mnt/db
```
</Step>
</Steps>

### Connection argument patterns

`mount` accepts one or two positional arguments. Resolution order:

| Args | Interpretation |
|------|----------------|
| `CONN MOUNT` | Explicit connection and mountpoint |
| `tiger:ID` or `ghost:ID` | Backend prefix; mountpoint defaults to `<default_mount_dir>/<ID>` (config default `/tmp`) |
| `MOUNT` only | Mountpoint; connection from `PG*` env or config |

<CodeGroup>
```bash title="postgres:// URI"
tigerfs mount postgres://user:pass@host:5432/db /mnt/db &
```
```bash title="PG* environment"
export PGHOST=localhost PGDATABASE=mydb PGUSER=user PGPASSWORD=secret
tigerfs mount /mnt/db &
```
```bash title="Tiger Cloud (auto mountpoint)"
tiger auth login   # prerequisite
tigerfs mount tiger:abcde12345
# → /tmp/abcde12345 by default
```
```bash title="Ghost"
ghost login
tigerfs mount ghost:fghij67890 /mnt/cloud &
```
</CodeGroup>

<Warning>
Demo and local Docker Postgres use `--insecure-no-ssl` because the sample connection strings omit TLS. Do not use that flag against production hosts.
</Warning>

### Useful mount flags

| Flag | Purpose |
|------|---------|
| `--foreground` | Run in foreground (default daemonizes) |
| `--read-only` | Disallow writes |
| `--schema` | Override default PostgreSQL schema |
| `--insecure-no-ssl` | Allow non-TLS remote connections |
| `--log-level debug` | Verbose structured logging to stderr |

## Explore with ls and cat

Each table in the default schema appears as a directory. Row IDs are files; extensions select serialization.

| Path | Result |
|------|--------|
| `/mnt/db/` | Table directories |
| `/mnt/db/users/123` | Row as TSV (default) |
| `/mnt/db/users/123.json` | Row as JSON object |
| `/mnt/db/users/123/email` | Single column value |
| `/mnt/db/users/.info/schema` | `CREATE TABLE` DDL |
| `/mnt/db/users/.info/columns` | Column names, one per line |
| `/mnt/db/users/.info/count` | `SELECT COUNT(*)` |
| `/mnt/db/users/.first/100/` | First 100 rows (directory listing) |
| `/mnt/db/users/.by/email/alice@example.com/` | Index lookup |

<RequestExample>
```bash
ls /mnt/db
cat /mnt/db/users/1.json
```
</RequestExample>

<ResponseExample>
```text
categories/  orders/  products/  users/

{"id":1,"name":"Alice Smith","first_name":"Alice","last_name":"Smith",...}
```
</ResponseExample>

Dot-directories (`.info`, `.filter`, `.by`, `.export`, …) are hidden from plain `ls` but visible with `ls -a` or `ls -la`.

### Minimal file-first workspace

```bash
echo "markdown,history" > /mnt/db/.build/notes
cat > /mnt/db/notes/hello.md << 'EOF'
---
title: Hello
---
Body text.
EOF
ls /mnt/db/notes/
cat /mnt/db/notes/hello.md
```

Writes go through PostgreSQL transactions; concurrent readers on other mounts see updates immediately (no row-content caching).

## Expected success signals

Use these checks to confirm the mount is healthy before deeper workflows.

### Filesystem probes

| Check | Success signal |
|-------|----------------|
| `ls <mountpoint>` | Lists table (and workspace) directories without errno |
| `cat <mountpoint>/users/1.json` | Valid JSON for an existing row |
| `cat <mountpoint>/users/.info/count` | Non-empty integer string |
| `ls -a <mountpoint>/notes` | Shows `.history/`, `.savepoint/`, `.undo/` when `history` was enabled |

### CLI registry and connectivity

```bash
tigerfs status
tigerfs status /mnt/db
tigerfs test-connection postgres://...
```

<ResponseField name="status table" type="object">
Columns: `MOUNTPOINT`, `DATABASE` (password-redacted), `PID`, `STATUS` (`active` / `stale`), `UPTIME`. Empty table means no registered mounts.
</ResponseField>

<ResponseField name="test-connection" type="object">
Prints PostgreSQL version, `current_database()`, `current_user`, and counts of accessible schemas and tables. Exit code 0 only when the pool connects and queries succeed.
</ResponseField>

### Demo launcher output

After `./demo.sh start`:

```text
==> PostgreSQL is ready
==> Connection string: postgres://demo:demo@...
==> Mounting TigerFS at ...
==> Seeding file-first apps...
==> TigerFS mounted successfully!
```

`./demo.sh status` on a healthy run:

| Mode | Containers | Mount |
|------|------------|-------|
| `--docker` | `RUNNING` | `MOUNTED` at `/mnt/db` (inside container) |
| `--mac` | Postgres running | `MOUNTED` at `/tmp/tigerfs-demo` |

Failure paths exit non-zero with `Failed to mount TigerFS` or `PostgreSQL init did not complete after 60 seconds`.

### Process and OS mount table

```bash
# macOS / Linux
mount | grep tigerfs-demo    # macOS demo path
pgrep -f "tigerfs.*mnt/db"   # tigerfs process

# Docker demo
docker compose -f scripts/demo/docker/docker-compose.yml exec tigerfs mount | grep /mnt/db
```

On successful `tigerfs mount`, the daemon logs `Filesystem mounted successfully` (warn-level in production, debug with `--log-level debug`). `tigerfs unmount` prints `Successfully unmounted <path>`.

## demo.sh exploration cheat sheet

Inside the mount root (via `./demo.sh shell` or `cd /tmp/tigerfs-demo`):

<CodeGroup>
```bash title="Data-first"
cat users/1.json
cat products/.by/category/electronics/.export/json
ls orders/.sample/10/
cat categories/.export/tsv
```
```bash title="File-first"
ls blog/
cat blog/hello-world.md
cat docs/getting-started/installation.md
cat snippets/todo.txt
ls docs/.history/getting-started/installation.md/
```
</CodeGroup>

## Common first-mount failures

| Symptom | Likely cause | Mitigation |
|---------|--------------|------------|
| `connection failed` from `test-connection` | Wrong host, credentials, or `sslmode` | Fix URI; use `--insecure-no-ssl` only for known-local/demo DBs |
| `Failed to mount TigerFS` in demo | Postgres not ready, port conflict, stale mount | `./demo.sh stop`; on macOS check `lsof -i :5432` |
| Empty `ls` at mountpoint | Wrong `--schema`, empty database, or mount not active | `tigerfs status`; confirm tables exist in target schema |
| `tigerfs mount tiger:ID` errors | Tiger CLI not authenticated | `tiger auth login`; verify service ID |
| Mount already exists | Prior demo or manual mount | `./demo.sh stop` or `tigerfs unmount <path>` |

Structured hints for filesystem operations appear on stderr as JSON when TigerFS returns errno (for example invalid synth task numbers). Run mounts with `--log-level debug` when diagnosing.

## Related pages

<CardGroup>
<Card title="Installation" href="/installation">
Install paths, platform prerequisites (FUSE vs NFS), and verification before your first mount.
</Card>
<Card title="Demo environment" href="/demo-environment">
Full demo architecture, `init.sql` / `seed.sh` split, and Docker vs macOS troubleshooting.
</Card>
<Card title="Filesystem as API" href="/filesystem-as-api">
Mount hierarchy, dot-directory control surface, and path-to-SQL mapping.
</Card>
<Card title="Cloud backends" href="/cloud-backends">
`tiger:` and `ghost:` prefixes, `create` / `fork`, and CLI credential requirements.
</Card>
<Card title="Connection reference" href="/connection-reference">
`postgres://` URIs, `PG*` variables, TLS rules, and pool settings.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
Stale mounts, force unmount, and errno recovery.
</Card>
</CardGroup>

---

## 04. Filesystem as API

> Mount hierarchy, schema/table/row/column path model, dot-directory control surface, and how fs.Operations maps paths to SQL.

- Page Markdown: https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/04-filesystem-as-api.md
- Generated: 2026-06-03T07:54:06.838Z

### Source Files

- `docs/spec.md`
- `internal/tigerfs/fs/path.go`
- `internal/tigerfs/fs/operations.go`
- `internal/tigerfs/fs/context.go`
- `README.md`

---
title: "Filesystem as API"
description: "Mount hierarchy, schema/table/row/column path model, dot-directory control surface, and how fs.Operations maps paths to SQL."
---

TigerFS exposes PostgreSQL through a mount point where `fs.Operations` parses every path into a `ParsedPath` and `FSContext`, then issues SQL via `internal/tigerfs/db` — FUSE (Linux) and in-process NFS (macOS) are thin adapters that delegate `ReadDir`, `Stat`, and `ReadFile` to that core without reimplementing query logic.

## Request flow

```mermaid
flowchart LR
  subgraph clients [Clients]
    Unix[Unix tools ls cat grep]
  end
  subgraph adapters [Platform adapters]
    FUSE[fuse.OpsFS + FSAdapter]
    NFS[nfs.OpsFilesystem]
  end
  subgraph core [Shared core internal/tigerfs/fs]
    Ops[Operations]
    Parse[parsePath → ParsePath]
    Ctx[FSContext]
    DB[db.Client pipeline + row queries]
  end
  PG[(PostgreSQL)]
  Unix --> FUSE
  Unix --> NFS
  FUSE --> Ops
  NFS --> Ops
  Ops --> Parse
  Parse --> Ctx
  Ops --> DB
  DB --> PG
```

<Note>
`ReadFile` always queries PostgreSQL for row content. `Stat` and directory listings may use short-TTL metadata caches; see the consistency page for invalidation rules.
</Note>

## Mount hierarchy

After `tigerfs mount <connection> <mountpoint>`, the mount root lists tables and views from the connection’s current schema (typically `public`), plus global control directories.

| Path | Role |
|------|------|
| `/` | Default-schema tables/views plus `.build`, `.create`, `.info`, `.schemas`, `.tables`, `.views` |
| `/<table>/` | One PostgreSQL table (or synth workspace view) as a directory |
| `/.schemas/<schema>/` | Explicit schema; non-default schemas also appear as `/<schema>/<table>/` |
| `/.tables/<name>/` | Backing table in the `tigerfs` schema (data-first escape hatch for synth apps) |

**Schema flattening:** Tables in the default schema appear at the mount root (`/users/`). Other schemas use a prefix (`/analytics/reports/`) or `/.schemas/analytics/reports/`. Root-level `/.build/` creates synthesized apps; `/.build/` under `/.schemas/<schema>/` is rejected to avoid `tigerfs` schema collisions.

**Root listing** (`readDirRoot`) adds control dirs first, then cached table names, then views (synth views are writable `0755`; plain views `0555`).

## Path model: schema → table → row → column

Paths are absolute, start with `/`, and are parsed syntactically by `ParsePath` then enriched by `(*Operations).parsePath` (schema resolution and file-first policy gates).

### PathType dispatch

`ParsedPath.Type` drives all operations. Common values:

| PathType | Example path | Meaning |
|----------|--------------|---------|
| `PathRoot` | `/` | Mount root |
| `PathTable` | `/orders` or `/orders/.filter/status/shipped/` | Table dir; pipeline state in `FSContext` |
| `PathRow` | `/orders/123` or `/orders/123.json` | Row directory or format file |
| `PathColumn` | `/orders/123/email` | Single column |
| `PathCapability` | `/orders/.by/customer_id/` | Mid-pipeline capability listing |
| `PathExport` | `/orders/.export/json` | Terminal bulk export |
| `PathInfo` | `/orders/.info/count` | Table metadata file |
| `PathBuild` | `/.build/blog` | Create synth workspace (write format name) |
| `PathHistory` | `/blog/.history/post.md/` | Version snapshots (file-first) |
| `PathDDL` | `/.create/mytable/sql` | DDL staging |

Hierarchical synth paths (e.g. `/blog/tutorials/intro.md/.history/`) use **scan-ahead**: non-dot segments before a known capability become `PrimaryKey` / `RawSubPath`, then capability parsing continues.

### Dual row representation

Every row is reachable two ways:

1. **Row directory** — `/<table>/<pk>/` appears in `ls`; contains per-column files and hidden format files (`.json`, `.csv`, `.tsv`, `.yaml`).
2. **Row file** — `/<table>/<pk>.json` (etc.) is readable but **not** listed, keeping `ls` output small.

Column files use type extensions (`.txt`, `.json`, `.bin`) per table metadata; full-row writes use PATCH semantics for `.json`/`.yaml`/`.tsv`/`.csv`.

### Format detection

Extensions `.json`, `.csv`, `.tsv`, `.yaml` set `ParsedPath.Format` for serialization in `readRowFile` via `internal/tigerfs/format`.

## Dot-directory control surface

Dot-prefixed names are reserved **capability directories** (hidden from plain `ls`, visible with `ls -a`). User dotfiles (`.gitignore`, `.env`) are allowed in file-first workspaces when not in the reserved set.

Capabilities are centralized in `internal/tigerfs/fs/constants.go`:

| Group | Directories | Purpose |
|-------|-------------|---------|
| Navigation | `.by`, `.filter`, `.order`, `.first`, `.last`, `.sample`, `.all` | Filters, sort, pagination (pipeline) |
| Projection / I/O | `.columns`, `.export`, `.import` | Column select, bulk export/import |
| Metadata | `.info` | `count`, `ddl`, `schema`, `columns`, `indexes` |
| Schema / DDL | `.schemas`, `.create`, `.modify`, `.delete`, `.indexes`, `.views` | DDL staging and schema browsing |
| Synth / workspace | `.build`, `.format`, `.history`, `.tables` | Create/configure workspaces, history, backing tables |
| Undo / audit | `.log`, `.savepoint`, `.undo` | Operation log, bookmarks, preview/apply undo |
| Mount | `/.info/user` | Mount-level identity for log entries |

<Warning>
Creating files or directories whose names match reserved capability names returns `EACCES`. Hard links or renames into those names must enforce the same rule.
</Warning>

### File-first vs data-first surfaces

On **synth workspace** paths (markdown/plaintext views), `rejectDataFirstCapOnSynthWorkspace` blocks data-first pipeline capabilities (`.by`, `.filter`, `.export`, table DDL, etc.) at the workspace root. Allowed workspace controls include `.info`, `.history`, `.log`, `.savepoint`, `.undo`, `.format`.

Data-first access to the backing table uses **`/.tables/<view>/...`**, which parses with `Schema=tigerfs` and bypasses the gate.

## FSContext: path state → SQL parameters

`FSContext` is **immutable**: each capability segment returns a cloned context with updated fields:

- `Filters` — AND-combined from `.by/<col>/<val>/` (indexed) and `.filter/<col>/<val>/`
- `OrderBy` / `OrderDesc` — from `.order/<col>/` (no further filters after order)
- `Limit` / `LimitType` — `.first`, `.last`, `.sample`; nested limits set `PreviousLimit*` and `NeedsSubquery()`
- `Columns` — `.columns/col1,col2/` then only `.export/` allowed
- `IsTerminal` — after `.export/`
- `PipelineDepth` — incremented per stage; compared to `max_pipeline_depth` (default **10**, `0` = unlimited)

`AvailableCapabilities()` drives which capability subdirs appear in `readDirTable`. When depth is exceeded or `HideCapabilities` is set (`.log`/`.savepoint` in workspaces), listings hide further pipeline dirs to avoid recursive scanner blowups (`find`, `rm -rf`, agents).

`ToQueryParams()` copies context into `db.QueryParams` for `QueryRowsPipeline` / `QueryRowsWithDataPipeline`.

### Pipeline → SQL

`internal/tigerfs/db/pipeline.go` builds one SQL statement from `QueryParams`:

- Simple: `WHERE` + `ORDER BY` + `LIMIT`
- Nested pagination: subquery wrapper (e.g. `.first/100/.last/50/`)
- Identifiers quoted via `QuoteIdent` / `QuoteTable`

Example path and effective query shape:

```bash
cat /mnt/db/orders/.by/customer_id/123/.order/created_at/.last/10/.export/json
```

```sql
SELECT * FROM "public"."orders"
WHERE "customer_id" = $1
ORDER BY "created_at" DESC
LIMIT 10
```

(Exact SQL depends on PK columns, projection, and nested limits.)

## How Operations maps operations to SQL

| Operation | Entry | SQL path |
|-----------|-------|----------|
| List table / pipeline dir | `ReadDir` → `readDirTable` | `ListRows` or `QueryRowsPipeline(ToQueryParams())` |
| Read export | `ReadFile` → `readExportFile` | `QueryRowsWithDataPipeline` or `GetAllRows` |
| Read row | `readRowFile` | `GetRow` + format encode; synth → synthesized markdown/plaintext |
| Read column | `readColumnFile` | Column value query |
| Read `.info/*` | `readInfoFile` | Metadata queries (`count`, DDL, etc.) |
| Stat | `statWithParsed` | Metadata cache + size heuristics; export sizes from pipeline |

Default row listing cap: `dir_listing_limit` (default **1000**). Pipeline listings without an explicit limit inherit the same cap. Large tables may refuse bare `ls` on the table root; use `.first/`, `.sample/`, or index paths (see spec large-table section).

`parsePath` also:

1. Fills empty `Context.Schema` from `GetCurrentSchema()` once per mount (`sync.Once`).
2. Applies synth workspace policy before any listing/read.

## Configuration knobs

<ParamField body="dir_listing_limit" type="int">
Max rows returned when listing a table without an explicit pipeline limit (default 1000).
</ParamField>

<ParamField body="max_pipeline_depth" type="int">
Hide further capability directories after this many pipeline stages (default 10; 0 = unlimited).
</ParamField>

<ParamField body="dir_filter_limit" type="int">
Threshold for `.filter/` distinct-value listing behavior on large tables (default 100000).
</ParamField>

CLI: `tigerfs mount --max-ls-rows`, `--max-pipeline-depth`, etc. bind into the same `config.Config` struct passed to `NewOperations`.

## Mental model (compact)

```text
/mount/
├── .build/ .create/ .schemas/ .tables/ .views/ .info/   # mount control
├── <table>/                    # public (or default) table
│   ├── .by/ .filter/ .order/ … .export/   # pipeline (data-first)
│   ├── .info/ .indexes/ .modify/ .delete/ # metadata + DDL
│   └── <pk>/                   # row dir
│       ├── col.txt  age  .json …
│   └── <pk>.json               # row file (not in ls)
└── <schema>/<table>/           # non-default schema
```

Synth workspace `notes/` looks like files (`hello.md`) but rows live in `tigerfs` backing tables; `/.tables/notes/` exposes the relational shape.

## Verification

<Steps>
<Step title="Mount and inspect root">
```bash
tigerfs mount postgres://localhost/mydb /mnt/db
ls /mnt/db
ls -a /mnt/db/notes/    # expect .history, .log, etc. on history-enabled workspaces
```
</Step>
<Step title="Trace a pipeline path">
```bash
ls /mnt/db/orders/.by/customer_id/
ls /mnt/db/orders/.by/customer_id/123/.last/10/
cat /mnt/db/orders/.by/customer_id/123/.last/10/.export/json | head
```
</Step>
<Step title="Confirm backing-table route">
```bash
ls /mnt/db/.tables/notes/    # data-first columns/rows for synth app "notes"
```
</Step>
</Steps>

Run with `--debug` to see structured zap logs when paths return `EINVAL`/`ENOENT` (hints often include the `/.tables/...` alternative for blocked synth paths).

## Related pages

<CardGroup>
<Card title="Overview" href="/overview">
What TigerFS exposes at mount time, runtime assumptions, and where to start reading.
</Card>
<Card title="Capability directories" href="/capability-directories">
Pipeline grammar, chaining rules, and PathType resolution in depth.
</Card>
<Card title="File-first and data-first" href="/file-first-and-data-first">
Mode detection, `.build`/`.format`, and `.tables/` backing schema.
</Card>
<Card title="Data formats reference" href="/data-formats-reference">
TSV, CSV, JSON, YAML encoding and PATCH write semantics.
</Card>
<Card title="Platform backends" href="/platform-backends">
FUSE vs NFS adapters and delegation to `fs.Operations`.
</Card>
<Card title="Consistency and caching" href="/consistency-and-caching">
Fresh reads vs metadata cache TTLs and write invalidation.
</Card>
</CardGroup>

---

## 05. 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.

- Page Markdown: https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/05-file-first-and-data-first.md
- Generated: 2026-06-03T07:54:43.669Z

### 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>

---

## 06. Capability directories

> Pipeline path grammar (.by, .filter, .order, .first, .last, .export), chaining limits, SQL pushdown, and PathType resolution in path.go.

- Page Markdown: https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/06-capability-directories.md
- Generated: 2026-06-03T07:55:04.097Z

### Source Files

- `internal/tigerfs/fs/path.go`
- `internal/tigerfs/fs/constants.go`
- `internal/tigerfs/db/pipeline.go`
- `docs/data-first.md`
- `docs/spec.md`
- `test/integration/pipeline_test.go`

---
title: "Capability directories"
description: "Pipeline path grammar (.by, .filter, .order, .first, .last, .export), chaining limits, SQL pushdown, and PathType resolution in path.go."
---

Capability directories are dot-prefixed path segments under a table (for example `orders/.filter/status/pending/.last/10/`) that accumulate query state in an immutable `FSContext`. `ParsePath` in `internal/tigerfs/fs/path.go` performs syntactic parsing only; `Operations.parsePath` resolves schema, applies file-first policy gates, and hands the resulting `ParsedPath` to `readDirWithParsed` / `readFileWithParsed`. When `FSContext.HasPipelineOperations()` is true, row listings and `.export/` reads call `db.QueryRowsPipeline` or `db.QueryRowsWithDataPipeline`, which compile the full chain into one parameterized SQL statement.

## Pipeline capabilities

Navigation and bulk-query capabilities are defined as constants in `internal/tigerfs/fs/constants.go`:

| Directory | Segments | Effect on `FSContext` |
|-----------|----------|------------------------|
| `.by/` | `.by/`, `.by/<col>/`, `.by/<col>/<val>/` | Equality filter; `Indexed: true` on the filter |
| `.filter/` | `.filter/`, `.filter/<col>/`, `.filter/<col>/<val>/` | Equality filter; `Indexed: false` |
| `.order/` | `.order/`, `.order/<col>/`, `.order/<col>.desc/` | Sets `OrderBy` / `OrderDesc`; blocks further filters |
| `.columns/` | `.columns/`, `.columns/col1,col2/` | Column projection; only `.export/` allowed next |
| `.first/N/` | Positive integer `N` | `LimitFirst` |
| `.last/N/` | Positive integer `N` | `LimitLast` |
| `.sample/N/` | Positive integer `N` | `LimitSample` |
| `.export/` | `.export/<fmt>`, `.export/.with-headers/<fmt>` | Terminal; sets `IsTerminal` → `PathExport` |
| `.all/` | `.all/` | No-op limit (equivalent to table root; hidden from `ls`) |

Supported export format names: `csv`, `tsv`, `json`, `yaml` (with or without a file extension such as `data.csv`).

<Note>
`.import/`, `.info/`, DDL (`.create/`, `.modify/`, `.delete/`), `.indexes/`, and synth/history paths (`.build/`, `.format/`, `.history/`, `.log/`, `.savepoint/`, `.undo/`) are separate capability families. This page focuses on the data-first **pipeline** set: `.by`, `.filter`, `.order`, `.columns`, `.first`, `.last`, `.sample`, `.export`.
</Note>

### Example paths

```bash
# List pending orders for customer 123, newest first, as JSON
cat /mnt/db/orders/.by/customer_id/123/.filter/status/pending/.order/created_at/.last/10/.export/json

# Project columns then export
cat /mnt/db/orders/.filter/status/shipped/.columns/id,total,created_at/.export/csv

# Nested pagination: last 50 rows among the first 100 by PK
ls /mnt/db/events/.first/100/.last/50/
```

## Path grammar and `PathType` resolution

`ParsePath` splits the mount path into segments and routes through `parseTablePath` → `processSegments`. Known capabilities are recognized by `isKnownCapability`; unknown dot-segments (for example `.git`) are treated as ordinary filenames via scan-ahead logic.

```mermaid
flowchart TD
  subgraph parse ["fs/path.go — ParsePath"]
    PP[ParsePath]
    PT[parseTablePath]
    PS[processSegments]
    PC[processCapability]
    PR[processRowOrColumn]
  end
  subgraph types ["ParsedPath.Type"]
    PCap[PathCapability — incomplete cap]
    PTbl[PathTable — filters/limits/order applied]
    PExp[PathExport — terminal export]
    PRow[PathRow / PathColumn]
  end
  subgraph runtime ["fs/operations.go"]
    OP[Operations.parsePath]
    RD[readDirWithParsed / readFileWithParsed]
    QP[FSContext.ToQueryParams]
    DB[db.QueryRowsPipeline]
  end
  PP --> PT --> PS
  PS -->|known cap| PC
  PS -->|no cap ahead| PR
  PC -->|listing only| PCap
  PC -->|filter/limit/order/columns| PTbl
  PC -->|.export/| PExp
  PR --> PRow
  OP --> PP
  PTbl --> RD
  PExp --> RD
  PTbl --> QP --> DB
```

### How `PathType` is chosen

| Path shape | `PathType` | Notes |
|------------|------------|-------|
| `/users` | `PathTable` | Empty `FSContext`; schema filled at runtime |
| `/users/.by/email` | `PathCapability` | `CapabilityDir=.by`, `CapabilityArg=email` |
| `/users/.by/email/foo@bar.com` | `PathTable` | Filter applied; type collapses from `PathCapability` |
| `/users/.first/10` | `PathTable` | Limit applied |
| `/users/.export/json` | `PathExport` | `IsTerminal=true`; no further caps |
| `/users/123/name` | `PathColumn` | Terminal row/column segment |
| `/users/.filter/active/.order/date/.last/10` | `PathTable` | Full pipeline in `Context` |

Incomplete capabilities (directory listing forms) keep `PathCapability` and set `CapabilityDir` / `CapabilityArg`. Once a filter value, limit, order, or column list is applied, `processBy` / `processFilter` / `processLimit` / `processOrder` / `processColumns` reset the type to `PathTable` while mutating `Context`.

`processRowOrColumn` runs only when no known capability appears ahead in the remaining segments. Hierarchical synth paths use scan-ahead: segments before `.history/` become `PrimaryKey` / `RawSubPath`, then capability processing continues.

### Reserved names

`IsCapabilityDirectory` in `constants.go` prevents names like `.filter` from being interpreted as column values during row/column parsing. The reserved set includes pipeline dirs plus `.info`, `.log`, `.savepoint`, and `.undo`.

## `FSContext` and chaining rules

`FSContext` (`internal/tigerfs/fs/context.go`) is immutable: each pipeline step returns a clone via `WithFilter`, `WithOrder`, `WithLimit`, `WithColumns`, or `WithTerminal`. `PipelineDepth` increments on each mutating step and drives `max_pipeline_depth` listing suppression.

Enforcement methods:

| Method | Rule |
|--------|------|
| `CanAddFilter()` | False after `.order/` or `.export/` |
| `CanAddOrder()` | False if already ordered (second `.order/` rejected at parse time) |
| `CanAddLimit(type)` | No `.first` after `.first`, no `.last` after `.last`, no limits after `.sample/` |
| `CanAddColumns()` | False after `.columns/` or `.export/` |
| `CanExport()` | False when `IsTerminal` |

`AvailableCapabilities()` drives which capability directories appear in `ls` after pipeline steps. After `.columns/`, only `.export/` is advertised.

### Allowed chaining (parent → children)

| Parent state | Next capabilities |
|--------------|-------------------|
| Table root | `.by`, `.filter`, `.order`, `.columns`, `.first`, `.last`, `.sample`, `.export` |
| After `.by/<col>/<val>/` or `.filter/<col>/<val>/` | Same as root (filters AND-combined) |
| After `.order/<col>/` | `.columns`, `.first`, `.last`, `.sample`, `.export` (no more filters) |
| After `.columns/col1,col2/` | `.export` only |
| After `.first/N/` or `.last/N/` | Filters, order, columns, other limit types, `.export` |
| After `.sample/N/` | Filters, order, columns, `.export` (no further limits) |
| After `.export/<fmt>` | None (terminal) |

### Disallowed combinations (parse-time errors)

| Pattern | Replacement / behavior |
|---------|------------------------|
| `.first/N/.first/M/` | Use single `.first/M/` |
| `.last/N/.last/M/` | Use single `.last/M/` |
| `.sample/N/.first|M` or `.sample/N/.last/M` | Use `.sample/M/` |
| `.order/a/.order/b/` | Rejected; only one order per path |
| `.columns/a,b/.columns/c,d/` | Rejected; merge column lists |
| `.by/` or `.filter/` after `.order/` | Rejected with hint: filters before order |
| `.export/.../.first/` | Export is terminal |

Post-limit filters are allowed: `.first/100/.filter/status/active/` applies the filter to the subquery result set.

### Nested pagination

When a second limit is added, `WithLimit` moves the current limit to `PreviousLimit` / `PreviousLimitType`. `FSContext.NeedsSubquery()` becomes true, and `db.buildNestedPipelineSQL` wraps the inner limit in a subquery before applying outer filters, order, and limit.

| Path | Meaning |
|------|---------|
| `.first/100/.last/50/` | Last 50 rows of the first 100 by PK (rows 51–100 of PK-ordered set) |
| `.last/100/.first/50/` | First 50 of the last 100 |
| `.first/1000/.sample/50/` | Random 50 drawn from the first 1000 |

Integration tests in `test/integration/pipeline_test.go` (`TestPipeline_NestedPagination`) verify `.first/10/.last/3/` and `.last/10/.first/3/` mount listings.

## SQL pushdown

At runtime, `readDirTable` checks `fsCtx.HasPipelineOperations()`. When true, it builds `params := fsCtx.ToQueryParams()`, fills `PKColumns` from metadata, applies `DirListingLimit` if no explicit limit was set, and calls `db.QueryRowsPipeline`. Export reads use `QueryRowsWithDataPipeline` with `selectPKOnly=false` so column projection and filters apply to the full row payload.

`db.QueryParams` (`internal/tigerfs/db/pipeline.go`) carries:

- `Filters` — AND-combined equalities (`column = $n`)
- `OrderBy` / `OrderDesc` — custom sort with PK tie-breakers
- `Limit` / `LimitType` — `LIMIT` plus default `ORDER BY` (PK ASC for `.first/`, PK DESC for `.last/`, `RANDOM()` for `.sample/`)
- `PreviousLimit` / `PreviousLimitType` — nested subquery when needed
- `Columns` — quoted column list instead of `SELECT *`

`FilterCondition.Indexed` (from `.by/` vs `.filter/`) does not change SQL semantics; both emit equality predicates. The flag is reserved for planning hints.

Simple (non-nested) pipeline SQL shape:

```sql
SELECT <cols or pk> FROM "schema"."table"
WHERE col1 = $1 AND col2 = $2
ORDER BY <order_col> ASC NULLS LAST, <pk cols>
LIMIT $n
```

Nested `.first/100/.last/50/` compiles to an inner `SELECT * ... ORDER BY pk ASC LIMIT 100` wrapped by an outer query with `ORDER BY pk DESC LIMIT 50`.

<Warning>
`.sample/` uses `ORDER BY RANDOM()` and can be expensive on large tables. Prefer indexed `.by/` paths and explicit `.first/` / `.last/` windows for predictable cost.
</Warning>

## `.by/` vs `.filter/`

Both add equality filters to `FSContext`. The difference is navigation and listing behavior, not the final filter SQL.

| Aspect | `.by/` | `.filter/` |
|--------|--------|------------|
| Column listing (`ls`) | Indexed columns only (`GetSingleColumnIndexes`) | All table columns |
| Value listing (`ls .by/col/`) | `DirListingLimit` (default 1000) | `DirFilterLimit` (default 100000); may surface `.table-too-large` |
| Filter flag | `Indexed: true` | `Indexed: false` |
| Typical use | Index-backed lookups | Ad-hoc filters on non-indexed columns |

Direct access without listing still works when value listing is blocked:

```bash
cat /mnt/db/large_events/.filter/type/click/.first/100/.export/json
```

On `tigerfs.<workspace>_log` tables, `.by/filename/` and `.filter/filename/` are rejected at parse time (`blockLogFilenameQuery`): log `filename` values contain `/`, which cannot appear in a single path segment. Use `.log/.by/file_id/<uuid>/` instead.

## Directory listings vs reads

| Operation | Pipeline behavior |
|-----------|-------------------|
| `ls` on `PathTable` with pipeline context | `QueryRowsPipeline` for row PKs; capability subdirs from `AvailableCapabilities()` |
| `ls` on `PathCapability` | Column/value/order listing helpers (`.by/`, `.filter/`, `.columns/`, `.order/`) |
| `cat` on `PathExport` | `QueryRowsWithDataPipeline` + format encoder |
| `cat` on row files under a pipeline path | Row read respects accumulated filters (via parsed context) |

Pagination directories (`.first/`, `.last/`, `.sample/`) appear in listings but show empty contents until you navigate to a numeric child (for example `.first/50/`). This avoids recursive tools (`find`, `rm -rf`, agents) walking infinite placeholder trees.

### `max_pipeline_depth`

<ParamField body="max_pipeline_depth" type="int" default="10">
Maximum chained pipeline operations before capability directories are hidden from `ls`. `0` means unlimited depth for listings. Parsing and explicit paths still work when depth is exceeded; only directory advertisements are suppressed. Default comes from `config.Config` / `~/.config/tigerfs/config.yaml`.
</ParamField>

When `fsCtx.PipelineDepth >= max_pipeline_depth`, `readDirTable` lists rows and `.info/` only—no further `.by/`, `.filter/`, etc. `.log/` and `.savepoint/` under file-first workspaces set `HideCapabilities` for the same recursion protection; pipeline segments remain reachable by full path.

## File-first workspace boundary

`Operations.rejectDataFirstCapOnSynthWorkspace` blocks `.by/`, `.filter/`, `.order/`, `.columns/`, `.first/`, `.last/`, `.sample/`, `.export/`, and related DDL on synth workspace mount paths (`/<view>/...`). Data-first pipeline access to the backing table is available through `/.tables/<view>/...` (schema `tigerfs`). Allowed workspace controls include `.info/`, `.history/`, `.log/`, `.savepoint/`, `.undo/`, and `.format/`.

## Configuration and errors

| Setting | Default | Role |
|---------|---------|------|
| `max_pipeline_depth` | `10` | Hide capability dirs in deep chains |
| `dir_listing_limit` | `1000` | Default row cap for pipeline `ls` when no `.first/` / `.last/` / `.sample/` |
| `dir_filter_limit` | `100000` | Threshold for `.filter/<col>/` value listing |

Invalid chains return `ErrInvalidPath` from `ParsePath` with a `Hint` field (surfaced via structured logging and POSIX `EINVAL` on FUSE/NFS). Examples: `"cannot add .filter/ after .order/"`, `"cannot add .first after .first"`.

## Implementation map

| Layer | Responsibility |
|-------|----------------|
| `fs/path.go` | `ParsePath`, `processCapability`, `PathType`, capability-specific parsers |
| `fs/context.go` | `FSContext`, chaining rules, `ToQueryParams()` |
| `fs/constants.go` | Directory name constants, `IsCapabilityDirectory` |
| `fs/operations.go` | `parsePath`, `readDirTable`, capability listing, synth/file-first gates |
| `db/pipeline.go` | SQL generation, `QueryRowsPipeline`, nested subqueries |

Production code should call `Operations.parsePath`, not bare `ParsePath`, so schema resolution and workspace policy apply before any database access.

## Related pages

<CardGroup>
  <Card title="Filesystem as API" href="/filesystem-as-api">
    Mount hierarchy, dot-directory control surface, and how operations map paths to SQL.
  </Card>
  <Card title="Data-first exploration" href="/data-first-exploration">
    Row/column reads, index navigation, pagination, PATCH writes, and export/import workflows.
  </Card>
  <Card title="Consistency and caching" href="/consistency-and-caching">
    No content cache on reads; stat/path cache keys must include full pipeline paths.
  </Card>
  <Card title="Configuration reference" href="/configuration-reference">
    `max_pipeline_depth`, `dir_listing_limit`, `dir_filter_limit`, and mount flags.
  </Card>
</CardGroup>

---

## 07. Synthesized apps

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

- Page Markdown: https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/07-synthesized-apps.md
- Generated: 2026-06-03T07:55:30.250Z

### 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>

---

## 08. Consistency and caching

> Cross-mount read freshness (no content cache), metadata/stat/path cache TTLs, invalidation on writes, and pipeline cache key isolation.

- Page Markdown: https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/08-consistency-and-caching.md
- Generated: 2026-06-03T07:56:00.066Z

### Source Files

- `CLAUDE.md`
- `internal/tigerfs/fs/metadata_cache.go`
- `internal/tigerfs/fs/path_cache.go`
- `docs/adr/014-query-reduction-caching.md`
- `internal/tigerfs/fs/operations.go`

---
title: "Consistency and caching"
description: "Cross-mount read freshness (no content cache), metadata/stat/path cache TTLs, invalidation on writes, and pipeline cache key isolation."
---

TigerFS treats PostgreSQL as the single source of truth across every mount of the same database: `ReadFile`, export reads, and column reads always query the database, while `Operations` in `internal/tigerfs/fs` caches only metadata (directory entries, stat sizes, catalog lists) with short TTLs and explicit invalidation on writes.

## Design principle

Multiple TigerFS processes can mount one database at the same time. The consistency contract is:

| Data class | Cached? | Cross-mount freshness |
|------------|---------|------------------------|
| Row/column/export **content** | Never | Always current (every read hits PostgreSQL) |
| Directory **listings** (`ReadDir`) | No result cache | Always current |
| **Stat** metadata (size, mode, mtime) | Yes, 2s TTL + write invalidation | Visible on other mounts within ~2s |
| **Path resolution** (parent → row ID) | Yes, 2s TTL + write invalidation | Same as stat cache |
| **Catalog** (schemas, tables, views) | Yes, configurable TTL | Default 10s; explicit invalidate on DDL/build |
| **Structural** (row counts, permissions, PK negatives) | Yes, slower TTL | Default 5m |
| **Column definitions** | Session cache until invalidate | Cleared with `MetadataCache.Invalidate()` |

<Warning>
Never cache row values or export payloads. A write on mount A must be visible to `cat` on mount B immediately. Only metadata may be stale for a bounded window.
</Warning>

## Cache tiers in `fs.Operations`

ADR-014 defines a three-tier query-reduction strategy. The shared `Operations` struct (`internal/tigerfs/fs/operations.go`) implements these plus auxiliary caches:

```mermaid
flowchart TB
  subgraph clients [Mount clients]
    FUSE[FUSE adapter]
    NFS[NFS adapter go-nfs]
  end

  subgraph ops [fs.Operations]
    T1[schemaOnce - Tier 1]
    MC[MetadataCache - Tier 2]
    SC[statCache - Tier 3]
    PC[pathCache]
    UC[undoCache]
  end

  PG[(PostgreSQL)]

  FUSE --> ops
  NFS --> ops
  T1 --> PG
  MC --> PG
  SC -.->|Stat only| PG
  PC -.->|resolve_path| PG
  UC -.->|preview only| PG
  ops -->|ReadFile ReadDir exports| PG
```

### Tier 1: Schema resolution (`sync.Once`)

`resolveSchema` fills empty `FSContext.Schema` from `current_schema()` once per mount via `schemaOnce`. This removes repeated `SELECT current_schema()` on every RPC (roughly 4–6 queries per operation before caching).

- **Invalidation:** None until mount restart.
- **Scope:** Default-schema paths such as `/users` (not explicit `/.schemas/foo/...` paths).

### Tier 2: `MetadataCache`

`MetadataCache` (`internal/tigerfs/fs/metadata_cache.go`) caches information_schema–style data in two TTL sub-tiers:

| Sub-tier | Config key | Default | Contents |
|----------|------------|---------|----------|
| Catalog | `metadata_refresh_interval` | `10s` | Schemas, table lists, view lists per schema |
| Structural | `structural_metadata_refresh_interval` | `5m` | Row count estimates (`pg_class.reltuples`), batch permissions |
| Per-table (no TTL) | — | Until invalidate | Column lists, primary keys (positive and negative cache) |

`Invalidate()` clears all tiers, PK/column maps, and per-schema lazy caches. Call sites include DDL commit (`write.go`), and synth `.build/` / `.format/` (`build.go`).

<ParamField body="metadata_refresh_interval" type="duration">
Catalog refresh interval. Env: `TIGERFS_METADATA_REFRESH_INTERVAL`. YAML under metadata section in config display.
</ParamField>

<ParamField body="structural_metadata_refresh_interval" type="duration">
Structural refresh interval. Env: `TIGERFS_STRUCTURAL_METADATA_REFRESH_INTERVAL`.
</ParamField>

### Tier 3: `statCache` (ReadDir-primed stat metadata)

`statCache` stores `Entry` metadata (name, `IsDir`, `Size`, `ModTime`, `Mode`) keyed by `schema + "\x00" + table`, with per-filename entries inside a `tableCache` bucket.

| Behavior | Detail |
|----------|--------|
| TTL | `statCacheTTL` = **2 seconds** (table bucket expires entirely) |
| Prime | `ReadDir` on tables, synth views, and row directories calls `statCache.prime` |
| Lookup | `Stat` checks cache before PostgreSQL (native rows, synth files, pipeline info/export **sizes**) |
| Negative cache | Missing synth/native files can be cached as negatives for up to 2s |
| Row columns | Keys use `schema\x00table/rowPK` prefix; `invalidate` clears table and all `table/` row-level buckets |

`ReadFile` and `readExportFile` **do not** use `statCache` for content—they always query PostgreSQL. Stat may cache the **size** of an export by running `readExportFile` once inside `statExport`, but a subsequent `cat` still fetches fresh data.

### `pathCache` (synth hierarchy)

For parent-pointer synth directories (ADR-017), `pathCache` maps `(parentID, filename) → row UUID` per table, with the same **2s** TTL as stat cache (`pathCacheTTL` in `path_cache.go`). Invalidated alongside `statCache` on directory structure changes (rename, move, delete, undo).

### `undoCache` (preview only)

Undo preview paths (`.undo/.../.info/summary`, affected-file listings) use a **2s** TTL cache over read-only preview queries. **Apply** (`.apply`) re-queries inside `ExecuteUndoTransaction` without using this cache. A narrow stale-preview window is possible if apply commits while a preview `SELECT` is in flight; apply correctness does not depend on cache freshness.

## Cross-mount read freshness

### Content reads (always fresh)

```text
Mount A: echo "new" > /mnt/a/app/note.md
Mount B: cat /mnt/b/app/note.md     → queries DB, sees "new" immediately
```

`readFileWithParsed` routes row/column/export/history reads to `db.GetRow`, `db.GetColumn`, pipeline export queries, or synth synthesis—none consult a content cache.

### Directory listings

`ReadDir` always lists from PostgreSQL (or pipeline SQL). It may **prime** `statCache` as a side effect, but the listing itself is not served from cache.

### Stat and `ls -l` staleness

NFS and FUSE issue many `Stat`/`GETATTR` calls per directory entry. Without caching, synth `ls -l` could trigger tens of full-table scans; with ReadDir-primed stat cache, one ReadDir plus cached Stats suffices (ADR-014).

Cross-mount visibility for **metadata**:

- Another mount’s write calls `statCache.invalidate(userSchema, table)` → immediate on that process.
- If invalidation is missed (wrong schema key—see below), negatives can survive up to **2s**.
- Another mount with no write: stat entries expire after **2s** TTL.

<Info>
macOS mounts use NFS `noac` so the **kernel** does not cache attributes for ~60s. TigerFS still serves GETATTR from its in-memory stat cache (2s). `noac` adds loopback round-trips but does not multiply SQL load.
</Info>

### FUSE kernel caches (separate layer)

Linux FUSE exposes tunable kernel attribute caching:

| Flag | Default | Role |
|------|---------|------|
| `attr_timeout` | `1s` | FUSE attribute cache |
| `entry_timeout` | `1s` | FUSE dentry cache |

These are independent of TigerFS’s `statCache`. They affect how long the **kernel** may reuse inode attributes, not whether TigerFS queries PostgreSQL for content.

## Write invalidation

Successful writes must drop cached metadata for the affected table so other mounts and subsequent Stats do not serve stale directory structure or false negatives.

### `statCache` and `pathCache`

`WriteFile` documents postconditions: stat/path caches for the table are invalidated (`write.go`). Typical call pattern:

```go
o.statCache.invalidate(fsCtx.Schema, fsCtx.TableName)
o.pathCache.invalidate(fsCtx.Schema, fsCtx.TableName)  // synth hierarchy ops
```

Also invalidated on: synth create/update/delete/rename, undo apply, and related synth ops (`synth_ops.go`, `undo.go`).

<Warning>
Pass the **user-facing schema** (the view/workspace schema), not `synth.TigerFSSchema`. Undo and synth paths resolve stats against the user schema; invalidating `tigerfs` leaves stale negatives on the workspace mount for up to 2s. `undo.go` documents this explicitly for apply.
</Warning>

### `MetadataCache`

| Trigger | Action |
|---------|--------|
| DDL `.commit` | `metaCache.Invalidate()` |
| `.build/` / `.format/` synth setup | `invalidateSynthCache()` + `metaCache.Invalidate()` |
| PK DDL | `InvalidatePrimaryKey(schema, table)` |
| TTL expiry | Automatic catalog/structural refresh on next access |

### `undoCache`

Cleared on undo apply (`undoCache.invalidate()`), together with stat/path invalidation under the user schema.

## Pipeline cache key isolation

Pipeline paths build SQL from `FSContext` (filters, order, limits, column projection). **Stat** for pipeline-scoped virtual files must not share cache entries with unfiltered paths on the same table.

TigerFS uses the **full mount path string** as the stat cache filename key for:

- **`.info/` files** — e.g. `.filter/active/true/.info/count` vs `.info/count` (`statInfoFile` / `cachedStat`)
- **`.export/<format>` files** — e.g. `.filter/in_stock/true/.export/json` vs `.export/json` (`statExport`)

Integration tests in `test/integration/command_cases.go` (`ExportAllProductsJSON` → filtered export → `ExportAllProductsJSON_AfterFiltered`) verify that a filtered export stat does not corrupt the unfiltered export size used by NFS `READ`.

<Check>
`cat` on any export path always runs `readExportFile` against live pipeline state. Stat cache only affects reported **size** for GETATTR; read content is unaffected.
</Check>

Table-level stat cache keys remain `schema\x00table` for row directories and synth filenames; pipeline state is not part of the table key—only paths that encode pipeline in the **filename key** (info/export) get isolation.

## What stays uncached or lightly cached

| Operation | Behavior |
|-----------|----------|
| `ReadFile` / `cat` | DB query every time |
| `ReadDir` / `ls` | DB (or pipeline) query; may prime stat cache |
| `readExportFile` | Pipeline-aware SQL every time |
| `readColumnFile` | `GetColumn` every time |
| Undo `.apply` | Fresh transaction, no undo preview cache |
| Synth targeted lookups | `RowExistsByColumns` / `GetRowByColumns` on stat miss (ADR-014) |

## Configuration reference

Defaults from `config.Init()` (`internal/tigerfs/config/config.go`):

```yaml
metadata_refresh_interval: 10s
structural_metadata_refresh_interval: 5m
attr_timeout: 1s      # FUSE only
entry_timeout: 1s     # FUSE only
```

Precedence: defaults → `~/.config/tigerfs/config.yaml` → `TIGERFS_*` env → CLI flags (see [Configuration reference](/configuration-reference)).

## Operational expectations

| Scenario | Expected behavior |
|----------|-------------------|
| Cross-mount `cat` after write | Immediate (no content cache) |
| Cross-mount `ls` after external INSERT | New row may appear up to ~2s late on stat-primed listings; direct `cat` on new path works immediately |
| Cross-mount DDL (new table) | Visible after catalog TTL (default 10s) or `metaCache.Invalidate()` from a TigerFS DDL commit on any mount |
| `ls -l` on synth dir | Fast after one ReadDir; Stats hit stat cache |
| Undo restore, `ls` on macOS NFS | `noac` + TigerFS invalidation; restored rows get fresh `modified_at` for client readdir |
| False `ENOENT` for existing file | Check debug logs for `statCache.isNegative hit`; suspect stale negative or wrong-schema invalidation |

## Debugging

Enable debug logging on the mount (`--debug`) and look for:

- `statCache.lookup hit` / `statCache.isNegative hit`
- `statCache.invalidate` with `schema` and `table`
- `Metadata cache catalog expired, refreshing`

Stress runs may log NFS monotonicity warnings on `.log/.last/N/.export/json`; those reflect macOS NFS client staleness, not TigerFS content caching (see project stress-test notes in `CLAUDE.md`).

## Related pages

<CardGroup>
  <Card title="Capability directories" href="/capability-directories">
    Pipeline path grammar and how FSContext drives SQL for exports and listings.
  </Card>
  <Card title="Platform backends" href="/platform-backends">
    FUSE vs NFS adapters, noac, and NFS write/stat interaction with stat cache.
  </Card>
  <Card title="Synthesized apps" href="/page-synthesized-apps">
    Synth ReadDir priming, pathCache, and targeted stat queries.
  </Card>
  <Card title="History, savepoints, and undo" href="/history-savepoints-undo">
    Undo preview cache, apply path freshness, and post-undo invalidation.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    Stale mounts, debug logging, and metadata refresh recovery.
  </Card>
  <Card title="Configuration reference" href="/configuration-reference">
    Full config keys including metadata and FUSE timeout fields.
  </Card>
</CardGroup>

---

## 09. Platform backends

> Linux FUSE vs macOS in-process NFS (go-nfs), write/commit model on NFS, mount registry, and adapter delegation to fs.Operations.

- Page Markdown: https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/09-platform-backends.md
- Generated: 2026-06-03T07:55:43.504Z

### Source Files

- `internal/tigerfs/fuse/adapter.go`
- `internal/tigerfs/nfs/handler.go`
- `internal/tigerfs/nfs/mount_darwin.go`
- `internal/tigerfs/mount/registry.go`
- `docs/spec.md`
- `docs/adr/001-fuse-library.md`

---
title: "Platform backends"
description: "Linux FUSE vs macOS in-process NFS (go-nfs), write/commit model on NFS, mount registry, and adapter delegation to fs.Operations."
---

TigerFS selects a VFS backend at compile time: Linux mounts through `hanwen/go-fuse/v2` (default `FUSE+Operations` via `fuse.MountOps`); macOS runs an in-process NFS v3 server (`willscott/go-nfs`) and attaches it with `mount_nfs` to `127.0.0.1`. Both paths construct the same `fs.Operations` instance and route path semantics, SQL, and error codes through that core—adapters only translate POSIX/FUSE or NFS/billy calls.

## Platform matrix

| OS | Backend | Library | Default mount entry | CLI flag |
| --- | --- | --- | --- | --- |
| Linux | FUSE | `github.com/hanwen/go-fuse/v2` | `fuse.MountOps` → `OpsFS` + `FSAdapter` | `--legacy-fuse` selects `fuse.Mount` (specialized node tree) |
| macOS | In-process NFS v3 | `github.com/willscott/go-nfs` | `nfs.Mount` → `Server` + `OpsFilesystem` | NFS-only on Darwin (`mount_linux.go` stubs NFS with an error) |
| Windows (spec) | WinFsp | Documented in spec; not the focus of current `cmd` build tags | — | — |

`mountFilesystem` is implemented in platform-specific `cmd/mount_*.go` files. `tigerfs version` reports the active backend (`Backend: NFS` on Darwin, FUSE on Linux).

<Note>
ADR-001 records why `bazil.org/fuse` was rejected: it failed to compile on macOS. Production FUSE uses `hanwen/go-fuse/v2` for explicit `syscall.Errno` mapping and cross-platform builds.
</Note>

## Architecture: shared core, thin adapters

```mermaid
flowchart TB
  subgraph clients["OS clients"]
    LinuxTools["Linux: VFS → FUSE"]
    MacTools["macOS: VFS → NFS client"]
  end

  subgraph adapters["Platform adapters"]
    FUSEAdapter["fuse.FSAdapter / OpsNode"]
    NFSServer["nfs.Server"]
    BillyFS["nfs.OpsFilesystem"]
    StableH["nfs.StableHandler"]
  end

  subgraph core["Shared filesystem logic"]
    Ops["fs.Operations"]
    DB["db.Client → PostgreSQL"]
  end

  LinuxTools --> FUSEAdapter
  MacTools --> NFSServer
  NFSServer --> StableH --> BillyFS
  FUSEAdapter --> Ops
  BillyFS --> Ops
  Ops --> DB
```

New filesystem behavior belongs in `internal/tigerfs/fs/`, not in adapter packages. The FUSE tree still contains a legacy specialized node hierarchy (`TableNode`, `PipelineNode`, `StagingNode`, etc.) used only with `--legacy-fuse`; the default Linux path uses a single `OpsNode` type that mirrors NFS’s one-adapter design.

## Linux: FUSE + Operations (default)

`fuse.MountOps` wires the stack:

1. `db.NewClient` connects to PostgreSQL.
2. `fs.NewOperations(cfg, dbClient)` with `SetMountPoint` / `SetUserID`.
3. `NewFSAdapter(ops)` converts `fs.Entry` / `fs.FSError` to FUSE types.
4. `newOpsNode(adapter)` is the root; child paths are derived from the inode tree (`currentPath()`), not cached strings, so renames stay consistent after `MvChild`.
5. `fs.Mount` with `AttrTimeout` / `EntryTimeout` from config (FUSE-only kernel caches).

`FSAdapter` implements `ReadDir`, `Stat`, `ReadFile`, `WriteFile`, `Delete`, `Mkdir`, and `Rename` by delegating to `fs.Operations`. `ErrorToErrno` maps `fs.ErrorCode` to POSIX errno and logs hints via zap (FUSE cannot return messages to callers).

### FUSE_INTERRUPT handling

Read and write entry points call `decoupleFromRequestCancel(ctx)` so advisory `FUSE_INTERRUPT` signals (for example from Go’s `SIGURG` under GC) do not cancel in-flight `pgx` queries and surface spurious `EIO`. Statement timeouts and pool close on unmount still bound work.

### Legacy FUSE (`--legacy-fuse`)

`fuse.Mount` builds `RootNode` and specialized nodes with direct DB paths for tables, pipelines, staging, and exports. Use only when debugging parity with the old tree; default mounts should stay on `MountOps`.

## macOS: in-process NFS v3

`nfs.Mount` (Darwin build tag):

1. Connects DB and creates `nfs.NewServer`.
2. `Server.Start` listens on `127.0.0.1:0` (ephemeral port, localhost only).
3. Serves RPCs with `nfs.Serve(listener, NewStableHandler(billyFS))`.
4. Runs `mount_nfs` against `127.0.0.1:/` with tuned options.

### `mount_nfs` options (behavioral)

| Option | Role |
| --- | --- |
| `vers=3`, `tcp` | NFS v3 over TCP to loopback server |
| `port` / `mountport` | Dynamic port from in-process server |
| `wsize=131072`, `rsize=131072` | 128KB chunks—fewer RPCs than macOS defaults; avoids 1MB+ same-process GC issues |
| `noac` | Disables client attribute cache so cross-mount/undo changes are visible; GETATTR still hits TigerFS stat cache (~2s TTL) |
| `soft`, `timeo=300`, `retrans=2` | Timeouts instead of indefinite hang on slow commits |
| `nolocks`, `locallocks` | go-nfs does not implement NLM |
| `noresvport` | Non-root mounts |

### `StableHandler` and READDIR consistency

`StableHandler` embeds `NullAuthHandler` and overrides handle encoding: paths fit in the 64-byte NFS v3 handle when possible (version 1, 4-byte aligned for macOS); longer paths use SHA-256 fallback with a small in-memory map.

A 5-second `readdirCache` keyed by path + verifier prevents `BadCookie` when go-nfs paginates READDIRPLUS with conservative byte estimates—critical for non-deterministic listings such as `.sample/` (`ORDER BY RANDOM()`).

### `OpsFilesystem` (billy → Operations)

`NewOpsFilesystem` wraps the same `fs.Operations` as FUSE. Directory ops (`ReadDir`, `Stat`, `Mkdir`, `Rename`, `Delete`) call through immediately. File writes use a persistent `cachedFile` map (ADR-010) because go-nfs has no real open state.

## NFS write and commit model

NFS v3 is stateless. go-nfs synthesizes **`Open → Write → Close` per WRITE RPC** because `billy.Filesystem` requires it. TigerFS commits on **`memFile.Close`** when the fabricated handle’s refCount reaches zero (and the buffer is dirty).

```text
NFS WRITE RPC #1          WRITE RPC #2          ...
    │                         │
    ▼                         ▼
OpenFile (cache)        OpenFile (ref++)
Write → buffer grow     Write → buffer grow
Close → commit full     Close → commit full buffer again
    buffer to DB            (O(n²) bytes written for n chunks)
```

| Topic | Behavior |
| --- | --- |
| Durability | Each Close with dirty data calls `ops.WriteFile`—data is in PostgreSQL after every RPC, not deferred to a final COMMIT |
| Stat during write | Dirty/truncated cache entries return in-flight size from memory (required for READ after WRITE before Close) |
| Empty Close | Skipped for truncate-before-write (SETATTR→WRITE) except DDL `sql` files |
| DDL triggers | `.test` / `.commit` / `.abort` fire on Close; `Chtimes` (touch) also fires but dedupes via `triggered` flag |
| Large sequential writes | Above `nfs_streaming_threshold` (default 10MB), buffer commits and clears mid-write |
| Random writes | Capped by `nfs_max_random_write_size` (default 100MB) |
| Crashed clients | Reaper every `nfs_cache_reaper_interval` (default 30s) force-commits idle entries after `nfs_cache_idle_timeout` (default 5m) |
| Mitigation | 128KB `wsize`/`rsize`; files under one chunk avoid O(n²) amplification |

<Warning>
Stress runs and heavy writes may log NFS monotonicity warnings (stale `.log/.last/N/.export/json` snapshots). These are treated as expected macOS NFS client behavior, not TigerFS data bugs. See the troubleshooting page.
</Warning>

Long-term direction in spec: patch go-nfs to return `UNSTABLE` on WRITE and flush on NFS COMMIT for O(n) semantics.

## FUSE vs NFS write path comparison

| Aspect | FUSE (`OpsFileHandle`) | NFS (`memFile` + cache) |
| --- | --- | --- |
| Buffer | Per-handle memory; flush on `Flush` | Shared `cachedFile` per path |
| Commit trigger | FUSE flush/close semantics | Every go-nfs Close (per WRITE RPC) |
| Read while writing | Standard FUSE open state | Read-only opens bypass cache and hit DB for formatted output |
| Create validation | `ValidateCreate` before handle | Same—rejects bare-path row creates that would confuse macOS dentry types |

## Mount registry

Long-lived `tigerfs mount` processes register themselves so `unmount`, `status`, `list`, `info`, and `fork` can find active mounts without scanning `/proc`.

| Field | Purpose |
| --- | --- |
| `mountpoint` | Absolute mount path |
| `pid` | Serving process; `ListActive` / `Cleanup` use signal 0 liveness |
| `database` | Sanitized connection string (no password) |
| `service_id`, `cli_backend` | Cloud backend metadata for `tigerfs info` / `fork` |
| `auto_created` | If true, mountpoint dir removed after unmount |
| `start_time` | Uptime display |

Persistence path (`mount.DefaultRegistryPath`):

- Linux: `~/.config/tigerfs/mounts.json`
- macOS: `~/Library/Application Support/tigerfs/mounts.json`

File mode `0600`; parent dir `0700`. `Register` replaces duplicate mountpoints; `Unregister` on graceful shutdown; stale PIDs removed by `Cleanup` or filtered in `ListActive`.

## Configuration surfaces

| Key / flag | Applies to |
| --- | --- |
| `attr_timeout`, `entry_timeout` / `--attr-timeout`, `--entry-timeout` | FUSE kernel caches only |
| `nfs_streaming_threshold`, `nfs_max_random_write_size`, `nfs_cache_reaper_interval`, `nfs_cache_idle_timeout` | NFS `OpsFilesystem` cache |
| `--legacy-fuse` | Linux only; legacy node tree |

macOS does not use FUSE timeout flags; attribute freshness relies on `noac` plus TigerFS metadata caches (see consistency page).

## Verification

<Steps>
<Step title="Confirm backend from CLI">

```bash
tigerfs version
```

Expect `Backend: NFS` on macOS and FUSE on Linux.

</Step>
<Step title="List registered mounts">

```bash
tigerfs list
tigerfs status /path/to/mountpoint
```

Active entries require a live PID in `mounts.json`.

</Step>
<Step title="Linux: compare default vs legacy">

```bash
tigerfs mount postgres://localhost/db /mnt/test
# vs
tigerfs mount postgres://localhost/db /mnt/test --legacy-fuse
```

Default logs `Using FUSE+Operations backend for Linux`.

</Step>
</Steps>

Integration tests auto-select `MountMethodFUSE` on Linux and `MountMethodNFS` on macOS (`test/integration/setup.go`); do not prefix tests with `TestNFS_` unless exercising NFS-only behavior.

## Related pages

<CardGroup>
<Card title="Installation" href="/installation">
Platform prerequisites: Linux FUSE group membership vs macOS NFS (no extra install).
</Card>
<Card title="Filesystem as API" href="/filesystem-as-api">
How `fs.Operations` maps mount paths to SQL—shared by both backends.
</Card>
<Card title="Consistency and caching" href="/consistency-and-caching">
Cross-mount freshness, stat/path cache TTLs, and why NFS uses `noac`.
</Card>
<Card title="CLI reference" href="/cli-reference">
`mount`, `unmount`, `list`, `status`, and `--legacy-fuse`.
</Card>
<Card title="Configuration reference" href="/configuration-reference">
FUSE timeouts and NFS cache tuning keys.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
Stale mounts, force unmount, NFS monotonicity warnings, `--debug`.
</Card>
<Card title="Develop and test" href="/develop-and-test">
Building, `go test`, and integration mount method detection.
</Card>
</CardGroup>

---

## 10. File-first workspaces

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

- Page Markdown: https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/10-file-first-workspaces.md
- Generated: 2026-06-03T07:56:46.406Z

### 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>

---

## 11. Data-first exploration

> Row/column reads, index navigation (.by), pagination (.first/.last/.sample), PATCH writes, import/export, and large-table safety limits.

- Page Markdown: https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/11-data-first-exploration.md
- Generated: 2026-06-03T07:57:32.192Z

### Source Files

- `docs/data-first.md`
- `internal/tigerfs/db/query.go`
- `internal/tigerfs/db/pipeline.go`
- `internal/tigerfs/fs/operations.go`
- `internal/tigerfs/format/convert.go`
- `test/integration/crud_test.go`

---
title: "Data-first exploration"
description: "Row/column reads, index navigation (.by), pagination (.first/.last/.sample), PATCH writes, import/export, and large-table safety limits."
---

Data-first mode maps each PostgreSQL table under the mount to a directory tree: primary keys become path segments, row bodies are readable as `.json`/`.csv`/`.tsv`/`.yaml` files or as per-column files, and dot-capability directories (`.by`, `.filter`, `.first`, `.export`, `.import`) compile into SQL in `internal/tigerfs/db/pipeline.go` and `internal/tigerfs/fs/operations.go`. Reads always query PostgreSQL; metadata caches do not store row content.

```text
/mnt/<schema>/<table>/
├── <pk>                    # Row directory (or <pk>.json / .tsv / …)
│   ├── <col>               # Numeric types: no extension
│   ├── <col>.txt           # text/varchar
│   ├── <col>.json          # json/jsonb
│   └── <col>.bin           # bytea
├── .by/                    # Indexed column navigation
├── .filter/                # Any-column equality filters
├── .first/  .last/  .sample/
├── .columns/  .order/  .export/
├── .import/                # Bulk load (not pipeline-composable)
└── .info/                  # ddl, schema, columns, count
```

<Info>
File-first workspaces (markdown/plaintext under `.build/`) reject workspace-level `.by`, `.filter`, and `.export` paths. Use `/.tables/<workspace>/` for data-first access to the backing table without bypassing history semantics on normal workspace files.
</Info>

## Prerequisites

- A mounted TigerFS database (see [Quickstart](/quickstart)).
- Default schema tables visible under the mount root (or an explicit schema path).
- For large tables: configure `dir_listing_limit`, `dir_filter_limit`, and `query_timeout` before relying on `ls` over millions of rows.

## Reading rows and columns

| Path pattern | Operation | Backend |
|--------------|-----------|---------|
| `/<table>/<pk>.json` | Full row, JSON | `db.GetRow` + `format.RowToJSON` |
| `/<table>/<pk>.tsv` / `.csv` / `.yaml` | Full row, tabular/YAML | `format.RowTo*` |
| `/<table>/<pk>/<col>` or `<col>.txt` | Single column | `db.GetColumn` + `format.ConvertValueToText` |
| `/<table>/.info/count` | Row count | Metadata query |
| `/<table>/.info/columns` | Column names (one per line) | Column cache |

```bash
ls /mnt/db/users/                    # PKs (capped by dir_listing_limit)
cat /mnt/db/users/42.json            # Entire row
cat /mnt/db/users/42/email.txt       # One column
cat /mnt/db/users/.info/count        # Total rows
```

Row reads go through `Operations.ReadFile` → `readRowFile` / `readColumnFile`, which call the database on every read. Column files append a trailing newline on output; empty writes to a column set NULL (`TestCRUDNullHandling`, `TestFSOperations_WriteFile_NullColumn`).

### Row-as-directory dot formats

Inside `/<table>/<pk>/`, dot-prefixed files expose the full row in alternate formats:

```bash
cat /mnt/db/users/42/.json
cat /mnt/db/users/42/.csv
```

## Index navigation with `.by/`

`.by/` exposes only columns that have indexes. Listing is index-backed and uses `dir_listing_limit` for distinct-value queries (same cap as table row listings in the shared operations path).

```bash
ls /mnt/db/orders/.by/                      # Indexed columns
ls /mnt/db/orders/.by/status/               # Distinct indexed values
ls /mnt/db/orders/.by/status/pending/       # Rows matching status=pending
cat /mnt/db/orders/.by/customer_id/7.json   # Row via index path
```

Each `.by/<column>/<value>/` segment adds an **indexed** filter (`FilterCondition.Indexed = true` in `db.QueryParams`). Filters AND-combine with prior `.by/` and `.filter/` segments.

### Composite indexes

Two equivalent styles when a multi-column index exists:

```bash
# Sequential (two filters)
.by/last_name/Smith/.by/first_name/John/

# Composite (single segment, matches index column order)
.by/last_name.first_name/Smith.John/
```

Composite syntax is preferred when it matches an existing composite index definition.

## Pagination: `.first/`, `.last/`, `.sample/`

Pagination capabilities appear as numeric subdirectories. They apply at the current pipeline context and push down to SQL (`LimitFirst`, `LimitLast`, `LimitSample` in `db/pipeline.go`).

| Capability | Default ordering | SQL effect |
|------------|------------------|------------|
| `.first/N/` | Primary key ascending | `ORDER BY pk ASC LIMIT N` |
| `.last/N/` | Primary key descending | `ORDER BY pk DESC LIMIT N` |
| `.sample/N/` | Random subset | `ORDER BY RANDOM() LIMIT N` |

```bash
ls /mnt/db/events/.first/100/              # First 100 PKs
ls /mnt/db/events/.last/50/                # Last 50 PKs
ls /mnt/db/events/.sample/25/              # Random 25 PKs
```

### Nested pagination

Chaining limits builds subqueries (`NeedsSubquery()` / `buildNestedPipelineSQL`):

| Path | Meaning |
|------|---------|
| `.first/100/.last/50/` | Last 50 of the first 100 rows (rows 51–100 by PK order) |
| `.last/100/.first/50/` | First 50 of the last 100 rows |
| `.first/1000/.sample/50/` | Random 50 drawn from the first 1000 |

Post-limit filters are allowed (e.g. `.first/100/.filter/status/active/` filters within the capped set).

### Ordering interaction

After `.order/<col>/`, only `.first/`, `.last/`, `.sample/`, `.columns/`, and `.export/` may follow. Use `.first/` for ascending sort results and `.last/` for descending on the ordered column.

## Pipeline queries and export

Capability segments between the table root and a row listing or export file form one pipeline. `FSContext` in `internal/tigerfs/fs/context.go` tracks filters, order, limits, column projection, and terminal state; `ToQueryParams()` feeds `db.QueryRowsPipeline` / `QueryRowsWithDataPipeline`.

```bash
cat /mnt/db/orders/.by/customer_id/42/.by/status/pending/.order/created_at/.last/10/.export/json
```

Rough SQL shape:

```sql
SELECT * FROM orders
WHERE customer_id = $1 AND status = $2
ORDER BY created_at DESC
LIMIT 10
```

### Terminal and redundant chains

Rules enforced at parse time (`path.go`, `FSContext.CanAdd*`):

| Rule | Example |
|------|---------|
| `.export/` is terminal | Nothing after `.export/json` |
| After `.columns/`, only `.export/` | `.columns/id,name/.filter/...` invalid |
| No double `.first/` or `.last/` | Use a single smaller limit |
| No limit after `.sample/` | Use `.sample/N/` with smaller N |
| No second `.order/` | Second replaces first; use one `.order/` |
| `.import/` not in pipelines | Bulk import is a separate write path |

Export formats under `.export/`: `csv`, `tsv`, `json`, `yaml`, plus `.with-headers/` variants for csv/tsv. Reading an export path runs the full pipeline and serializes all matching rows.

<Note>
Deep pipeline grammar, `PathType` resolution, and ADR-level SQL pushdown are documented on [Capability directories](/capability-directories).
</Note>

## `.by/` vs `.filter/`

| Aspect | `.by/` | `.filter/` |
|--------|--------|------------|
| Columns listed | Indexed columns only | All table columns |
| Value listing | Index scan, `dir_listing_limit` cap | `DISTINCT` query, `dir_filter_limit` cap |
| Performance | Predictable index use | May scan large tables |
| Typical use | Known indexed lookups | Ad-hoc equality on any column |

```bash
ls /mnt/db/users/.by/email/          # Fast value listing
ls /mnt/db/users/.filter/notes/      # Any column; may hit safety limits
```

For `.filter/<col>/` on very large tables, when the cached row estimate exceeds `dir_filter_limit` (default 100,000), legacy FUSE mounts surface a `.table-too-large` indicator file instead of enumerating distinct values. Direct paths still work:

```bash
cat /mnt/db/large_events/.filter/type/click/.first/100/.export/json
```

## Writing data (PATCH semantics)

Structured row formats use **PATCH** semantics: only columns present in the payload are updated; omitted columns are unchanged (`format.ParseJSON`, `ParseCSVWithHeader`, `ParseTSVWithHeader`; `Operations.writeRowFile` → `db.UpdateRow`).

| Write target | Semantics | Notes |
|--------------|-----------|-------|
| `/<pk>.json` / `.yaml` / `.csv` / `.tsv` | PATCH by column keys / header row | Insert if row missing; format suffix required for new rows |
| `/<pk>/<col>` or `<col>.txt` | Single-column replace | Empty content → NULL |
| `/<pk>` (no extension), existing row | PUT in schema column order | Bare path; TSV values in column order |
| `/<pk>` (no extension), new row | Rejected | Use `/<pk>.tsv` (or `.json`, etc.) to avoid NFS inode conflicts |

```bash
echo 'new@example.com' > /mnt/db/users/42/email.txt
echo '{"name":"Ada"}' > /mnt/db/users/42.json
echo -e 'email\tname\na@b.com\tAda' > /mnt/db/users/42.tsv
mkdir /mnt/db/users/99 && echo 'x@y.com' > /mnt/db/users/99/email.txt
rm -r /mnt/db/users/99/
```

Writes invalidate stat/path caches for the table schema (`statCache.invalidate`); cross-mount reads still see fresh row data from PostgreSQL.

<Warning>
Text primary keys ending in `.json`, `.csv`, `.tsv`, or `.yaml` are ambiguous with format extensions. Prefer the row-directory view or escape-aware tooling.
</Warning>

## Bulk import and export

Import is **write-only** under `.import/` and is not composable with pipeline segments.

| Mode path | Behavior |
|-----------|----------|
| `.import/.sync/<file>.csv` | Upsert: update existing PKs, insert new |
| `.import/.overwrite/<file>.csv` | Truncate table, then load |
| `.import/.append/<file>.csv` | Insert additional rows only |

Formats: `csv`, `tsv`, `json`, `yaml` under each mode; `.no-headers/` subpaths accept csv/tsv without a header row (column order from table schema).

```bash
cat > /mnt/db/staging/.import/.overwrite/users.csv <<'EOF'
id,name,email
1,Alice,alice@example.com
2,Bob,bob@example.com
EOF
```

Export reads use `.export/<fmt>` at the end of a pipeline (or on a bare table with implicit limits). Verified modes in integration tests: sync, append, overwrite (`TestFSOperations_Import_*`).

## Large-table safety limits

Configuration lives in `~/.config/tigerfs/config.yaml` and `TIGERFS_*` environment variables (`internal/tigerfs/config/config.go`).

| Key | Default | Effect |
|-----|---------|--------|
| `dir_listing_limit` | `1000` | Max PKs returned in `ls` on a table; max distinct values listed under `.by/<col>/` |
| `dir_filter_limit` | `100000` | Max distinct values fetched for `.filter/<col>/`; tables above estimate skip value listing (`.table-too-large` on legacy FUSE) |
| `dir_writing_limit` | `100000` | Bulk write safety threshold |
| `query_timeout` | `30s` | Statement timeout for queries |
| `max_pipeline_depth` | `10` | After this depth, pipeline capability dirs are hidden (rows still list) to stop runaway `find`/agent recursion |
| `no_filename_extensions` | `false` | Disable `.txt`/`.json`/`.bin` column suffixes |

```yaml
filesystem:
  dir_listing_limit: 1000
  dir_filter_limit: 100000
  query_timeout: 30s
```

```bash
export TIGERFS_DIR_LISTING_LIMIT=5000
export TIGERFS_DIR_FILTER_LIMIT=100000
export TIGERFS_QUERY_TIMEOUT=60s
```

<Steps>
<Step title="Explore a large table safely">
Use pagination instead of bare `ls`:

```bash
ls /mnt/db/events/.first/100/
cat /mnt/db/events/.first/100/.export/json
```
</Step>
<Step title="Filter without value listing">
When `ls .filter/col/` shows `.table-too-large`, jump directly to the value:

```bash
cat /mnt/db/events/.filter/type/click/.last/50/.export/csv
```
</Step>
<Step title="Verify row count">
```bash
cat /mnt/db/events/.info/count
```
</Step>
</Steps>

Legacy Linux FUSE (`legacy_fuse: true`) additionally refuses full table `ls` when `pg_class` row estimate exceeds `dir_listing_limit` (logged as "Table too large for directory listing"). The default shared `fs.Operations` path (including macOS NFS) returns up to `dir_listing_limit` PKs plus capability directories.

## File-first backing tables via `.tables/`

Synthesized workspaces store rows in the `tigerfs` schema. Data-first operations on that data use:

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

<Warning>
Writes under `.tables/<workspace>/` bypass workspace history triggers. They do not appear in `.log/`, `.history/`, or undo paths. Use the file-first workspace for normal edits; reserve `.tables/` for diagnostics and repairs.
</Warning>

## Verification signals

| Action | Expected signal |
|--------|-----------------|
| `cat …/<pk>.json` after edit | Updated keys only; other fields unchanged (PATCH) |
| `cat …/.info/count` after import overwrite | Count matches imported rows |
| `ls …/.by/<indexed_col>/` | Only indexed columns appear |
| Pipeline export | Single JSON array / CSV with header, no trailing capabilities |
| Blocked workspace pipeline | Error with hint mentioning `.tables/` |

Integration coverage: `test/integration/crud_test.go` (insert/select/update/delete, partial updates), `test/integration/pipeline_test.go` (`.by`, nested pagination, `.sample`), `test/integration/fs_operations_test.go` (import modes, PATCH via pipeline column write), `test/integration/synthesized_test.go` (workspace vs `.tables/` gates).

## Related pages

<CardGroup>
<Card title="Capability directories" href="/capability-directories">
Pipeline grammar, chaining matrix, SQL pushdown, and PathType resolution.
</Card>
<Card title="Data formats reference" href="/data-formats-reference">
TSV/CSV/JSON/YAML encoding, PATCH details, NULL rules, and type extensions.
</Card>
<Card title="File-first and data-first" href="/file-first-and-data-first">
When mounts are workspaces vs raw tables and how mode detection works.
</Card>
<Card title="Consistency and caching" href="/consistency-and-caching">
Why reads are always fresh and which metadata caches invalidate on writes.
</Card>
<Card title="Configuration reference" href="/configuration-reference">
Full config struct, env vars, and precedence.
</Card>
</CardGroup>

---

## 12. History, savepoints, and undo

> Enable history on workspaces, .history/.log paths, savepoint JSON, .undo preview/apply, per-user filters, auto-savepoints, and migration boundaries.

- Page Markdown: https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/12-history-savepoints-and-undo.md
- Generated: 2026-06-03T07:57:05.739Z

### Source Files

- `docs/history.md`
- `internal/tigerfs/fs/undo.go`
- `internal/tigerfs/fs/history.go`
- `internal/tigerfs/fs/auto_savepoint_test.go`
- `test/integration/undo_test.go`
- `docs/adr/016-undo-and-recovery.md`

---
title: "History, savepoints, and undo"
description: "Enable history on workspaces, .history/.log paths, savepoint JSON, .undo preview/apply, per-user filters, auto-savepoints, and migration boundaries."
---

History-enabled file-first workspaces get four tigerfs-schema companion tables (`<app>_history`, `<app>_log`, `<app>_savepoint`, `<app>_metadata`) plus workspace-level dot directories `.history/`, `.log/`, `.savepoint/`, and `.undo/`. A PostgreSQL BEFORE trigger archives row snapshots into `_history`; TigerFS writes `_log` entries on each create/edit/rename/delete/undo. Rollback uses preview-then-`touch .apply`, with optional per-user scoping and a post-migration undo boundary enforced via `_metadata`.

## Enable history on a workspace

History is a synth feature flag, not a separate product mode. Enable it at workspace creation or on an existing app.

<Steps>
<Step title="Create with history">
Write a `.build/` spec that includes `history` (with a format such as `markdown` or `txt`):

```bash
echo "markdown,history" > /mnt/db/.build/notes
```

TigerFS runs `GenerateBuildSQLWithFeatures`, creating the backing table, view comment (`tigerfs:md,history`), history hypertable + archive trigger, log hypertable, savepoint table, and metadata table.
</Step>
<Step title="Add history to an existing workspace">
```bash
echo "history" > /mnt/db/.build/notes
```

This uses `GenerateHistoryOnlySQL` to add history infrastructure and update the view comment.
</Step>
<Step title="Verify">
```bash
ls /mnt/db/notes/.history/
ls /mnt/db/notes/.log/
ls /mnt/db/notes/.savepoint/
ls /mnt/db/notes/.undo/
```
</Step>
</Steps>

<Warning>
History requires **TimescaleDB** (hypertables for `_history` and `_log`). It applies only to **file-first synth workspaces** with history enabled. Data-first tables have no history trigger. Writes that bypass the synth layer (for example direct edits under `.tables/`) are not logged.
</Warning>

Detection uses the view comment (`tigerfs:md,history`) or presence of a companion `_history` table (`ViewInfo.HasHistory`).

## Storage layout

```mermaid
flowchart TB
  subgraph mount["Workspace mount (file-first)"]
    files["notes/hello.md"]
    hist[".history/"]
    log[".log/"]
    sp[".savepoint/"]
    undo[".undo/"]
  end
  subgraph tigerfs["tigerfs schema"]
    src["notes backing table"]
    h["notes_history hypertable"]
    l["notes_log hypertable"]
    s["notes_savepoint"]
    m["notes_metadata"]
  end
  files --> src
  hist --> h
  log --> l
  sp --> s
  undo --> l
  undo --> h
  undo --> s
  src -->|"BEFORE trigger"| h
  src -->|"logSynthOp on write"| l
  m -->|"checkBoundary"| undo
```

| Table | Role |
|-------|------|
| `<app>_history` | Full row snapshots (`version_id` UUIDv7 PK, `operation` create/edit/rename/delete) |
| `<app>_log` | Operation metadata: `log_id`, `file_id`, `type`, `user_id`, denormalized `filename`, `version_id` → before-state |
| `<app>_savepoint` | Named bookmarks: `name` (PK), `savepoint_id` (UUIDv7), `user_id`, `description` |
| `<app>_metadata` | Workspace events; `history-format-migration` blocks undo before boundary |

Log entries are written by TigerFS (`logSynthOp`), not by database triggers. For edit/rename/delete/undo, `version_id` points at the history row captured immediately before the operation.

## Browse versions (`.history/`)

Per-directory `.history/` lists files that have snapshots at that level. Subdirectory history is scoped by `parent_id` (ADR-017 parent-pointer model).

| Path | Returns |
|------|---------|
| `workspace/.history/` | Filenames with history (+ `.by/` at workspace root) |
| `workspace/.history/file.md/` | `.id` plus version IDs, newest first |
| `workspace/.history/file.md/.id` | Stable row UUID (`file_id`) |
| `workspace/.history/file.md/<version_id>` | Full synthesized file at that version |
| `workspace/.history/.by/<uuid>/` | Same versions keyed by row UUID (rename-safe) |

Version directory names use the UUIDv7 display form: `2026-04-07T143000.123Z-zzz0063hd8e5r42` (UTC ms timestamp + base36 entropy). Listing sorts chronologically; export JSON still uses standard hex UUIDs.

Cross-rename tracking: `file_id` is stable; history follows the row even when the leaf `filename` changes.

## Operation log (`.log/`)

`.log/` is data-first on `tigerfs.<app>_log`. Standard pipeline paths work: `.last/N`, `.first/N`, `.by/<col>/<val>`, `.filter/`, `.order/`, `.export/json`, format suffixes.

```bash
cat /mnt/db/notes/.log/.last/10/.export/json
ls /mnt/db/notes/.log/.by/user_id/agent-7/.last/5/
ls /mnt/db/notes/.log/.by/file_id/<uuid>/
ls /mnt/db/notes/.log/.by/type/edit/.last/10/
```

<Note>
Atomic editors (vim, VS Code, many agent write tools) often replace files via temp + rename, producing **2–3 log entries per logical save**. Undo a group by anchoring at the log entry before the group, or use `.undo/to-id/<log_id>/`.
</Note>

### Diff symlinks

Each log entry directory exposes:

| Symlink | Target |
|---------|--------|
| `before` | `.history/<path>/<version_id>` from this entry's `version_id`; `/dev/null` for `create` |
| `after` | Next log entry's before-state, or live file |
| `current` | Live workspace path; `/dev/null` if deleted |

```bash
diff -u --color /mnt/db/notes/.log/<log_id>/before /mnt/db/notes/.log/<log_id>/after
```

`filename` in the log is the **full path at operation time** (historically correct). It is not a `.log/.by/` index column — use `.history/<file>/.id` then `.log/.by/file_id/<uuid>/`.

## Savepoints (`.savepoint/`)

Named bookmarks in the operation timeline. Creating a savepoint does not change workspace files; deleting a savepoint does not remove log or history data.

```bash
echo '{"description":"Before refactoring"}' > /mnt/db/notes/.savepoint/before-refactor.json
cat /mnt/db/notes/.savepoint/before-refactor/description
ls /mnt/db/notes/.savepoint/.last/5/
```

Writes require a **format suffix** (`.json`, `.tsv`, `.csv`, `.yaml`). TSV uses a `description` column; JSON can be `{}` for a minimal bookmark.

| Field | Source |
|-------|--------|
| `name` | Filename without suffix (primary key) |
| `savepoint_id` | UUIDv7 at insert time (undo ordering: `log_id > savepoint_id`) |
| `user_id` | Mount identity when set |
| `description` | User text or auto-savepoint message |

Rename with `mv` updates `name` only; `savepoint_id` is unchanged, so `.undo/to-savepoint/<name>/` references remain valid.

## Auto-savepoints

Before each logged write, TigerFS compares wall-clock time to the last write on that workspace (`schema.table` key). If the gap exceeds the configured interval, it inserts an auto-savepoint **before** the new log entry.

Naming:

- `auto-<user_id>-20260408T143000Z` when `--user-id` / `TIGERFS_USER_ID` is set
- `auto-20260408T143000Z` for anonymous mounts

<ParamField body="auto_savepoint_interval" type="duration" default="30m">
Inactivity threshold. Set to `0` to disable.
</ParamField>

| Source | Key / flag |
|--------|------------|
| Config file | `auto_savepoint_interval: 30m` |
| Environment | `TIGERFS_AUTO_SAVEPOINT_INTERVAL=30m` |
| CLI | `--auto-savepoint-interval 30m` |

Auto-savepoints are skipped on the **first write after mount** (no prior session to separate).

## User identity and per-user undo

Identity is a single mount-level string used for new log and savepoint rows.

| Precedence | Source |
|------------|--------|
| 1 | `--user-id` |
| 2 | `TIGERFS_USER_ID` |
| 3 | Anonymous (`NULL` in log) |

```bash
cat /mnt/db/.info/user          # read
echo "agent-9" > /mnt/db/.info/user   # change mid-session
```

<Info>
Use **separate mounts per agent** on the same database when possible. Each mount gets its own `user_id` and in-memory state; PostgreSQL MVCC provides cross-mount consistency.
</Info>

Per-user undo adds `/.by/user_id/<user_id>/` under a `.undo/to-savepoint/` or `.undo/to-id/` path before `.apply`. Only that user's operations after the anchor are reversed; other users' log entries are ignored in the undo query.

<Warning>
If two users interleave edits on the **same file**, per-user undo still rolls the file back to the state before that user's first post-anchor operation — which can revert the other user's interleaved changes on that file (rollback semantics).
</Warning>

## Undo (`.undo/`)

Preview-then-apply: inspect `.info/summary` and the preview tree (or `.log/` symlinks for single ops), then `touch` `.apply`. The full undo runs in one PostgreSQL transaction.

### Modes

| Mode | Path | Effect |
|------|------|--------|
| Single operation | `.undo/id/<log_id>/` | Reverse one log entry (one file) |
| To log entry | `.undo/to-id/<log_id>/` | Reverse all ops **after** that `log_id` per file |
| To savepoint | `.undo/to-savepoint/<name>/` | Reverse all ops **after** `savepoint_id` per file |

`<log_id>` accepts raw UUID or display name (`2026-04-08T143015.234Z-...`).

Default listing under `.undo/id/`, `.undo/to-id/`, and `.undo/to-savepoint/` is **100** most recent targets (`undo_list_limit` / `--undo-list-limit`).

### Preview and apply

```bash
cat /mnt/db/notes/.undo/to-savepoint/before-refactor/.info/summary
diff -ru /mnt/db/notes/.undo/to-savepoint/before-refactor /mnt/db/notes/ -x '.*'
touch /mnt/db/notes/.undo/to-savepoint/before-refactor/.apply
```

Multi-file preview trees include only affected paths. Files whose first post-anchor operation was `create` appear as symlinks to `/dev/null` (will be removed on apply). Restored files show synthesized content from history (`version_id`).

`.undo/id/<log_id>/` has **no** preview tree — use `.log/<log_id>/before` and `after` for diffs.

### Pipeline + `.apply`

Filters on `.undo/...` paths narrow what gets undone; preview matches apply scope. `.apply` works with `.all/`, `.last/N`, `.first/N`, `.by/`, `.filter/`, but **not** with `.sample/` (returns `EINVAL`).

Examples:

```bash
touch /mnt/db/notes/.undo/to-savepoint/x/.filter/type/delete/.apply
touch /mnt/db/notes/.undo/to-savepoint/x/.by/user_id/agent-7/.apply
touch /mnt/db/notes/.undo/to-savepoint/x/.last/5/.apply
```

### Semantics

- **Rollback**, not surgical revert: restores full row state from history.
- **Undo of undo**: undo ops are logged as `type=undo`; undoing them uses the same paths (with `undo-of-undo` dispatch via history `operation`).
- **Idempotent** undo-to-savepoint: repeating apply toward the same anchor yields the same data state (new log/history rows each time).

On success, TigerFS logs `undo applied` at Info with file counts; caches (`stat`, `path`, `undo`) invalidate under the **user schema** (where the view lives).

## Migration undo boundary

Workspaces upgraded from pre–parent-pointer history (v0.6 → v0.7) receive one `history-format-migration` row in `<app>_metadata` when `tigerfs migrate` runs. Pre-migration history kept **lossy `parent_id`** (filled from current source, not historical placement), so undo of old edits could restore files to the wrong directory.

| Surface | Pre-boundary behavior |
|---------|----------------------|
| `.history/`, `.log/` | Read/browse still works |
| `.undo/` | Refused with **EPERM** (`ErrPermission`) when target `log_id` or `savepoint_id` sorts **before** the marker `entry_id` |

The refusal message includes the marker's `description` as the structured hint (check stderr / JSON logs on FUSE/NFS).

Fresh installs with v0.7 history have an empty metadata table — no boundary, undo works for all entries.

<Tip>
Verify migration and boundary behavior with `tigerfs migrate --describe` / `--dry-run` and integration coverage in `test/integration/migrate_test.go` (`verifyMigrationUndoBoundary`).
</Tip>

## Configuration reference

| Setting | Default | Purpose |
|---------|---------|---------|
| `user_id` / `--user-id` / `TIGERFS_USER_ID` | empty | Attributed log/savepoint rows |
| `auto_savepoint_interval` | `30m` | Session gap auto-savepoint (`0` = off) |
| `undo_list_limit` | `100` | Default `.undo/` target listing |

## Limitations and operations notes

| Topic | Constraint |
|-------|------------|
| Database | TimescaleDB hypertables + compression (7-day chunks) |
| Scope | File-first synth with `history` only |
| DDL | Undo does not reverse schema changes |
| Storage | Every edit adds a history row (compression mitigates) |
| Concurrency | Last writer wins; undo can overwrite concurrent edits |
| Direct table access | `.tables/` writes bypass logging |

Reserved workspace-level names (cannot be used as file names): `.history`, `.log`, `.savepoint`, `.undo`, plus standard capability dirs.

## Quick reference

| Goal | Command / path |
|------|----------------|
| Enable history | `echo "markdown,history" > .build/<app>` |
| List versions | `ls <app>/.history/<file>/` |
| Row UUID | `cat <app>/.history/<file>/.id` |
| Recent ops | `cat <app>/.log/.last/10/.export/json` |
| Diff one change | `diff -u <app>/.log/<id>/before <app>/.log/<id>/after` |
| Create savepoint | `echo '{"description":"..."}' > <app>/.savepoint/name.json` |
| Preview undo | `cat <app>/.undo/to-savepoint/name/.info/summary` |
| Apply undo | `touch <app>/.undo/to-savepoint/name/.apply` |
| Per-user undo | `touch <app>/.undo/to-savepoint/name/.by/user_id/X/.apply` |
| Set user | `echo "agent-7" > /.info/user` |

## Related pages

<CardGroup>
<Card title="File-first workspaces" href="/file-first-workspaces">
Create workspaces, markdown/plaintext I/O, and `.tables/` backing access.
</Card>
<Card title="Capability directories" href="/capability-directories">
Pipeline grammar used by `.log/` and `.savepoint/` listings.
</Card>
<Card title="Migrate workspaces" href="/migrate-workspaces">
`migrate --describe` / `--dry-run`, history-format upgrades, and boundary verification.
</Card>
<Card title="Consistency and caching" href="/consistency-and-caching">
Why undo invalidates stat/path caches and never caches read content.
</Card>
<Card title="Error codes" href="/error-codes">
Mapping `ErrPermission` / `EINVAL` from `.undo/` to errno and zap hints.
</Card>
</CardGroup>

---

## 13. Agent skills

> Bundled skills/tigerfs pack, install-time staging to agent config dirs, SKILL.md navigation (files, data, ops, recipes), and mount-time copy behavior.

- Page Markdown: https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/13-agent-skills.md
- Generated: 2026-06-03T07:57:27.235Z

### Source Files

- `skills/tigerfs/SKILL.md`
- `skills/tigerfs/files.md`
- `skills/tigerfs/data.md`
- `skills/tigerfs/ops.md`
- `scripts/install.sh`
- `scripts/test-install.sh`

---
title: "Agent skills"
description: "Bundled skills/tigerfs pack, install-time staging to agent config dirs, SKILL.md navigation (files, data, ops, recipes), and mount-time copy behavior."
---

TigerFS ships a portable `skills/tigerfs/` instruction pack (open `SKILL.md` format) that teaches coding agents how to use mounted PostgreSQL as files. Release archives bundle the pack via Goreleaser; `scripts/install.sh` copies it into detected agent config directories or stages it under `~/.config/tigerfs/skills/tigerfs/`. The `tigerfs mount` command does not install skills—installation runs at binary install time (or via manual copy from a clone).

## Bundled pack layout

The repository maintains five markdown files under `skills/tigerfs/`:

| File | Role |
|------|------|
| `SKILL.md` | Entry skill: mode detection, mount layout, safe editing, quick reference, anti-patterns, links to detail files |
| `files.md` | File-first: `.build/`, frontmatter, `.tables/`, history, log, savepoints, undo |
| `data.md` | Data-first: `.info/`, pipeline paths, PATCH writes, import/export, DDL staging |
| `ops.md` | CLI: `mount`, `create`, `fork`, `status`, `unmount`, config |
| `recipes.md` | Workflow and application patterns (kanban, knowledge base, session context, multi-agent undo) |

`SKILL.md` frontmatter registers the skill for agent loaders:

```yaml
name: tigerfs
description: How to discover, read, write, and search data in a TigerFS-mounted PostgreSQL database using file tools.
```

<Note>
Skill packs are plain files—no model provider or hosted service is required. Any agent that loads `SKILL.md` from a filesystem or catalog path can use the same pack.
</Note>

## Release bundling

Goreleaser includes the pack in every release archive:

```yaml
files:
  - skills/tigerfs/*
```

After `curl -fsSL https://install.tigerfs.io | sh`, the extracted tarball contains `skills/tigerfs/` beside the `tigerfs` binary. `install.sh` runs skill installation immediately after copying the binary.

## Install-time copy and staging

`scripts/install.sh` implements all automated skill placement. Flow:

```mermaid
sequenceDiagram
  participant User
  participant install_sh as install.sh
  participant Archive as release tar.gz
  participant Stage as ~/.config/tigerfs/skills
  participant Agent as agent skills dir

  User->>install_sh: curl | sh
  install_sh->>Archive: extract tigerfs + skills/tigerfs
  install_sh->>install_sh: install_skills(skills_src)
  alt stdout is TTY or TIGERFS_INTERACTIVE=1
    install_sh->>User: agent menu (read /dev/tty)
    User->>install_sh: 1..N, a, or s
    opt choice a or N
      install_sh->>Agent: copy_skills_to_agent (rm -rf tigerfs, cp -r)
    opt choice s or no agents
      install_sh->>Stage: stage_skills
    end
  else piped / non-interactive
    install_sh->>Stage: stage_skills
    install_sh->>User: print cp -r hints for detected agents
  end
```

### Interactive vs non-interactive

<ParamField body="trigger" type="condition">
Runs `interactive_install_skills` when stdout is a terminal (`-t 1`) **or** `TIGERFS_INTERACTIVE=1`. Otherwise calls `stage_skills` only.
</ParamField>

| Mode | Trigger | Result |
|------|---------|--------|
| Interactive | TTY or `TIGERFS_INTERACTIVE=1` | Menu: install to one/all detected agents, skip (stage only), or stage when none detected |
| Non-interactive | `curl … \| sh` with piped stdin | Skills copied to `~/.config/tigerfs/skills/tigerfs/`; script prints `cp -r` commands per detected agent |

Interactive prompts read from `/dev/tty` so `curl | sh` can still ask questions without consuming the install pipe. Tests force interactivity with `TIGERFS_INTERACTIVE=1` and a fake stdin file (`scripts/test-install.sh`).

### `copy_skills_to_agent` behavior

For each selected agent:

1. `rm -rf "$agent_skills_dir/tigerfs"` — full replace on upgrade (removes stale files such as retired skill filenames).
2. `mkdir -p "$agent_skills_dir"`
3. `cp -r "$src" "$agent_skills_dir/tigerfs"`

Expected files after install (asserted in `scripts/test-install.sh`): `SKILL.md`, `files.md`, `data.md`, `recipes.md`, `ops.md`.

### Staging directory

`stage_skills` writes the same tree to:

```
~/.config/tigerfs/skills/tigerfs/
```

If agent directories exist under `$HOME`, the script prints per-agent copy lines, for example:

```bash
cp -r ~/.config/tigerfs/skills/tigerfs ~/.claude/skills/tigerfs
```

If no agents are detected, it prints a generic example (`~/.claude/skills/`).

<Warning>
README and CHANGELOG mention skills “automatically installed at mount time.” The `tigerfs mount` implementation (`internal/tigerfs/cmd/mount.go`) has no skill-copy hook—only `scripts/install.sh` performs automated installation today.
</Warning>

## Supported coding agents

Detection checks for a marker directory under `$HOME`. Installation targets the agent-specific skills path:

| Agent | Detection dir | Install path (under `$HOME`) |
|-------|---------------|------------------------------|
| Claude Code | `.claude` | `.claude/skills/tigerfs/` |
| Cursor | `.cursor` | `.cursor/skills/tigerfs/` |
| Codex CLI | `.codex` | `.agents/skills/tigerfs/` |
| Gemini CLI | `.gemini` | `.gemini/skills/tigerfs/` |
| Windsurf | `.codeium/windsurf` | `.codeium/windsurf/skills/tigerfs/` |
| Antigravity | `.gemini/antigravity` | `.gemini/antigravity/skills/tigerfs/` |
| Kiro | `.kiro` | `.kiro/steering/tigerfs/` |

Menu choices: `a` = all detected, `s` = skip (stage only), `1`…`N` = one detected agent (display order, not fixed agent index).

## SKILL.md navigation model

`SKILL.md` is the hub agents load first. It branches on **mode** and **task**, then defers to sibling files.

### Mode routing

| Signal on path | Mode | Detail file |
|----------------|------|-------------|
| `.md` / `.txt` files, workspace layout | File-first | [files.md](files.md) |
| `.info/` at table root | Data-first | [data.md](data.md) |

### Task routing from SKILL.md

| User intent | Routed to |
|-------------|-----------|
| Workspaces, frontmatter, history, undo paths | `files.md` |
| Row/column access, pipelines, bulk export, DDL | `data.md` |
| Kanban, knowledge base, session context, safe exploration | `recipes.md` (Recipes 1–7; SKILL.md maps intents to recipe numbers) |
| Mount, create, fork, cloud backends | `ops.md` |

### Cross-cutting sections in SKILL.md

- **Directory structure** — file-first tree (`.history`, `.log`, `.savepoint`, `.undo`, `.tables/`, `.build/`).
- **Safe editing** — savepoint JSON, preview via `.undo/…/.info/summary`, apply via `touch …/.apply`, multi-entry atomic-rename log groups.
- **Quick reference** — tool-oriented table (`Read`, `Write`, `Glob`, `Grep`, `Bash`).
- **Directory scanning safety** — never recurse `.log/`, `.by/`, `.export/`, etc.; use targeted paths.
- **Anti-patterns** — e.g. do not use `Write`/`Edit` to revert content; use `.undo/`; kanban uses directories + `mv`, not `status` frontmatter.

Detail files link back to SKILL.md for shared workflows (e.g. `files.md` → “Common Workflows” for undo steps).

## Install paths compared

<Tabs>
  <Tab title="curl | sh">
    Binary + `skills/tigerfs/` from archive; `install.sh` stages or copies to agent dirs.
  </Tab>
  <Tab title="git clone / go install">
    Pack lives at `skills/tigerfs/` in the repo. No automatic copy to `~/.claude/skills`—point the agent at the repo path or run `cp -r skills/tigerfs ~/.claude/skills/tigerfs` (or equivalent).
  </Tab>
  <Tab title="tigerfs mount">
    Mounts PostgreSQL only; does not update agent skill directories.
  </Tab>
</Tabs>

<Steps>
  <Step title="Install or stage skills">
    Run `curl -fsSL https://install.tigerfs.io | sh` (interactive TTY shows the agent menu), or copy from `~/.config/tigerfs/skills/tigerfs/` after a non-interactive install.
  </Step>
  <Step title="Verify files">
    Confirm `SKILL.md`, `files.md`, `data.md`, `recipes.md`, and `ops.md` exist under your agent’s `tigerfs/` skill directory.
  </Step>
  <Step title="Mount and use">
    `tigerfs mount CONNECTION MOUNTPOINT` — agents apply loaded skills when operating on the mount path.
  </Step>
</Steps>

Optional verification for the install script itself:

```bash
./scripts/test-install.sh
```

## Portable integration (BYOC/BYOK)

- **Source of truth**: markdown in `skills/tigerfs/`, not embedded in the `tigerfs` binary.
- **Distribution**: release tarball, git tree, or staged `~/.config/tigerfs/skills/tigerfs/`.
- **Agent binding**: filesystem copy into each product’s skills folder (or symlink). Grok-Wiki and other doc generators can treat the pack as a repository-local catalog entry without tying to one LLM vendor.

Reload or restart the agent session after upgrading skill files so new instructions (e.g. undo log grouping) take effect.

## Related pages

<CardGroup>
  <Card title="Installation" href="/installation">
    curl installer, platform prerequisites, and first binary install.
  </Card>
  <Card title="Quickstart" href="/quickstart">
    First mount and exploration after skills are in place.
  </Card>
  <Card title="Workflow recipes" href="/workflow-recipes">
    Kanban, knowledge base, and session patterns from recipes.md.
  </Card>
  <Card title="File-first workspaces" href="/file-first-workspaces">
    Workspaces, frontmatter, and .tables/ backing access.
  </Card>
  <Card title="History, savepoints, and undo" href="/history-savepoints-undo">
    .history, .log, .savepoint, and .undo behavior the skills teach.
  </Card>
  <Card title="CLI reference" href="/cli-reference">
    mount, create, fork, and other commands documented in ops.md.
  </Card>
</CardGroup>

---

## 14. Cloud backends

> tiger: and ghost: prefix resolution, create/fork/list flows, default_backend config, and Tiger CLI credential requirements.

- Page Markdown: https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/14-cloud-backends.md
- Generated: 2026-06-03T07:58:08.451Z

### Source Files

- `internal/tigerfs/backend/resolve.go`
- `internal/tigerfs/backend/tiger.go`
- `internal/tigerfs/backend/ghost.go`
- `internal/tigerfs/cmd/create.go`
- `internal/tigerfs/cmd/fork.go`
- `docs/adr/013-backend-prefix-scheme.md`

---
title: "Cloud backends"
description: "tiger: and ghost: prefix resolution, create/fork/list flows, default_backend config, and Tiger CLI credential requirements."
---

TigerFS connects to Tiger Cloud and Ghost by shelling out to their CLIs (`tiger`, `ghost`) through `internal/tigerfs/backend`. A `tiger:ID` or `ghost:ID` reference is parsed by `backend.Resolve()`, the backend returns a `postgres://` connection string, and mount/create/fork commands use that string while the mount registry stores `cli_backend` and `service_id` for later `info` and `fork` lookups.

## Prefix scheme

All cloud-backed references use an explicit prefix (ADR-013). `backend.Resolve()` is the single parser:

| Input pattern | `Resolve()` result | Caller behavior |
|---------------|-------------------|-----------------|
| `tiger:ID` | `TigerBackend`, ID after colon | Shell out to `tiger` CLI |
| `ghost:ID` | `GhostBackend`, ID after colon | Shell out to `ghost` CLI |
| `tiger:` or `ghost:` (empty ID) | Backend + empty ID | Auto-generate name (create/fork only) |
| `postgres://...` | `nil` backend, full URI | Direct PostgreSQL connection |
| Any other string | `nil` backend, bare string | `create`/`fork` use `default_backend`; `mount` does **not** |

**Path vs name:** Only arguments that **start with** `.` or `/` are filesystem paths (`isPath()` in fork; mount arg 2 is always a path). Names may contain slashes (e.g. `project/my-db`) without being treated as paths.

<Warning>
`mount` does not apply `default_backend` to bare IDs. You must use `tiger:ID`, `ghost:ID`, or `postgres://...`. A single argument without a prefix is interpreted as a **mountpoint** (connection from config/env), not a cloud service ID.
</Warning>

```text
  User ref                    backend.Resolve()          Next step
  ---------                   -----------------          ---------
  tiger:abc123        -->     TigerBackend, "abc123"     tiger db connection-string ...
  ghost:mydb          -->     GhostBackend, "mydb"       ghost connect ...
  postgres://h/db     -->     nil, full URI              connect directly
  my-db (create)      -->     nil, "my-db"               ForName(default_backend)
```

## Backend interface

`internal/tigerfs/backend` defines one interface implemented by `TigerBackend` and `GhostBackend`:

| Method | Purpose |
|--------|---------|
| `GetConnectionString(ctx, id)` | Resolve service ID → `postgres://` (with credentials) |
| `GetInfo(ctx, id)` | Live service metadata |
| `Create(ctx, opts)` | Provision new service |
| `Fork(ctx, sourceID, opts)` | Clone existing service |

`ForName("tiger"|"ghost")` maps config/registry `cli_backend` values to implementations. Empty name returns `ErrNoBackend`; unknown names error.

Credentials are **not** stored in TigerFS config for cloud mounts. Each operation invokes the backend CLI; connection strings are used immediately and registry entries store **sanitized** database URLs only.

## Tiger Cloud vs Ghost

| Behavior | Tiger CLI | Ghost CLI |
|----------|-----------|-----------|
| Connection string | `tiger db connection-string <id> --with-password` | `ghost connect <id>` (password from `~/.pgpass`) |
| Service info | `tiger service get <id> --output json` | `ghost list --json` (filter by ID; no per-ID get) |
| Create | `tiger service create [--name <n>] --output json --with-password` | `ghost create [--name <n>] --wait --json`, then `ghost connect` |
| Fork | `tiger service fork <id> --now [--name <n>] --output json --with-password` | `ghost fork <id> [--name <n>] --wait --json`, then `ghost connect` |
| Wait behavior | Waits by default (~30m); TigerFS does not pass `--no-wait` | Requires explicit `--wait` (TigerFS always passes it) |
| Fork strategy | Always `--now` | No strategy flag (current state) |
| Create/fork conn string | Included in JSON when `--with-password` | Fetched in a second `ghost connect` call |

TigerFS does not validate service names; the backend CLI owns format and error messages.

## Mount flow

```mermaid
sequenceDiagram
  participant User
  participant Mount as tigerfs mount
  participant Resolve as backend.Resolve
  participant BE as TigerBackend / GhostBackend
  participant CLI as tiger / ghost CLI
  participant Reg as mounts.json registry

  User->>Mount: tiger:ID [MOUNTPOINT]
  Mount->>Resolve: Resolve(connStr)
  Resolve-->>Mount: Backend, serviceID
  Mount->>BE: GetConnectionString(ctx, serviceID)
  BE->>CLI: connection-string / connect
  CLI-->>BE: postgres://...
  BE-->>Mount: connStr
  Mount->>Mount: mountFilesystem(connStr)
  Mount->>Reg: Register(cli_backend, service_id)
```

**Arguments:**

- Two args: `CONNECTION` + `MOUNTPOINT` (connection may be `tiger:`, `ghost:`, or `postgres://`).
- One arg with prefix: connection ref; mountpoint defaults to `{default_mount_dir}/{id}` (default `/tmp`).

On success, `registerMount()` writes `service_id` and `cli_backend` (`tiger` or `ghost`) to the mount registry for `info`, `fork`, and `status`.

## Create flow

```bash
tigerfs create [BACKEND:]NAME [MOUNTPOINT] [--no-mount] [--json]
```

| `NAME` argument | Backend | Service name |
|-----------------|---------|--------------|
| `tiger:my-db` | Tiger | `my-db` |
| `ghost:my-db` | Ghost | `my-db` |
| `my-db` | `default_backend` | `my-db` |
| `tiger:` or omitted (with default) | From prefix/config | Auto-generated |

After `b.Create()`:

- Without `--no-mount`: spawns detached `tigerfs mount {cli}:{serviceID} {mountpoint}` (`startMountProcess`).
- Mountpoint: explicit arg, else `{default_mount_dir}/{name}`.
- `--no-mount`: prints `tigerfs mount {cli}:{id} /path/...` hint.

<ParamField body="--no-mount" type="boolean">
Create the cloud service but do not start a background mount process.
</ParamField>

<ParamField body="--json" type="boolean">
Emit create result including `backend`, `connection_string`, and `no_mount`.
</ParamField>

## Fork flow

```bash
tigerfs fork SOURCE [DEST] [--name NAME] [--no-mount] [--json]
```

**SOURCE resolution (first argument):**

| SOURCE form | Resolution |
|-------------|------------|
| Path (starts with `.` or `/`) | Mount registry lookup → `cli_backend` + `service_id` |
| `tiger:ID` / `ghost:ID` | `backend.Resolve()` |
| Bare name | `default_backend` + name as service ID |
| `postgres://` mount (no cloud metadata) | Error: cannot fork — no cloud service |

**DEST resolution (optional second argument):**

| DEST form | Service name | Mountpoint |
|-----------|--------------|------------|
| Path | `--name` or basename of path | Absolute path |
| Bare name | DEST (or `--name`) | `{baseDir}/{name}` |
| Omitted | Auto (CLI) | After fork: `{baseDir}/{result.Name}` |

`baseDir` is `default_mount_dir`, or the parent directory of the source mountpoint when SOURCE was a path.

Fork requires a cloud-backed source. Raw `postgres://` mounts have empty `cli_backend` in the registry and cannot be forked through TigerFS.

## List, status, and info

| Command | Scope |
|---------|--------|
| `tigerfs list` | Active **local** mountpoints (one per line, scripting) |
| `tigerfs status` | Human-readable mount registry (PID, database, backend, uptime) |
| `tigerfs info [MOUNTPOINT]` | Registry entry + live `GetInfo()` from backend CLI when `cli_backend` and `service_id` are set |

TigerFS does not list cloud services globally. Use `tiger` / `ghost` CLIs for provider-side inventory; Ghost `GetInfo` internally runs `ghost list --json` and filters by ID.

## default_backend configuration

<ParamField body="default_backend" type="string">
Cloud backend for **bare names** on `create` and `fork` only. Values: `tiger`, `ghost`, or empty (unset).
</ParamField>

<ParamField body="default_mount_dir" type="string">
Base directory for auto-derived mountpoints. Default: `/tmp`.
</ParamField>

```yaml
# ~/.config/tigerfs/config.yaml
default_backend: tiger   # or ghost
default_mount_dir: /tmp
```

Environment: `TIGERFS_DEFAULT_BACKEND`, `TIGERFS_DEFAULT_MOUNT_DIR`.

| Scenario | Backend source |
|----------|----------------|
| `tigerfs create tiger:my-db` | `tiger:` prefix |
| `tigerfs create my-db` | `default_backend` (error if unset) |
| `tigerfs fork tiger:abc` | `tiger:` prefix |
| `tigerfs fork /mnt/prod` | Registry `cli_backend` on that mount |
| `tigerfs mount tiger:ID` | Prefix (not `default_backend`) |
| `tigerfs mount postgres://...` | Direct (no backend) |

There is no `--backend` flag; the prefix (or config for bare names on create/fork) is the only selector.

## CLI prerequisites and credentials

### Install CLIs

| Backend | Install hint (from TigerFS errors) |
|---------|-------------------------------------|
| Tiger | https://github.com/timescale/tiger-cli |
| Ghost | https://ghost.build |

TigerFS checks `exec.LookPath` before every backend call. Missing binary → `ErrCLINotFound` with install URL.

### Authenticate

TigerFS delegates auth to the backend CLI. On auth-related stderr (`not authenticated`, `login`, `unauthorized`, `401`), errors map to `ErrNotAuthenticated` with:

- Tiger: `tiger auth login`
- Ghost: `ghost auth login` (TigerFS message; Ghost docs may use `ghost login`)

**Tiger Cloud — interactive:**

```bash
tiger auth login
```

**Tiger Cloud — headless / CI (Tiger CLI reads these; bind via config or env):**

```bash
export TIGER_PUBLIC_KEY=<public-key>
export TIGER_SECRET_KEY=<secret-key>
export TIGER_PROJECT_ID=<project-id>
tiger auth login
```

Config equivalents: `tiger_public_key`, `tiger_secret_key`, `tiger_project_id` in `~/.config/tigerfs/config.yaml` (passed through Viper; used by `tiger auth login`, not stored as DB passwords by TigerFS).

**Ghost:**

- Run Ghost login flow so `ghost connect` succeeds.
- Ensure `~/.pgpass` contains credentials Ghost expects (Ghost does not use Tiger’s `--with-password`).

### Legacy config mount path

If `mount` is invoked with **no** connection argument (mountpoint only), `db.ResolveConnectionString()` may use legacy `tiger_service_id` / `TIGER_SERVICE_ID` to call `TigerBackend.GetConnectionString()`. Prefer explicit `tiger:ID` on the command line for new workflows.

## Mount registry

Cloud-backed mounts persist metadata in `~/.config/tigerfs/mounts.json` (XDG paths on other platforms):

| Field | Meaning |
|-------|---------|
| `service_id` | Tiger or Ghost service/database ID |
| `cli_backend` | `tiger` or `ghost` |
| `database` | Sanitized connection string (no password) |
| `auto_created` | TigerFS created mountpoint dir (removed on unmount) |

`fork` from a mountpoint path and `info` depend on these fields. Direct `postgres://` mounts leave `cli_backend` and `service_id` empty.

## Error handling

| Condition | Sentinel / message |
|-----------|-------------------|
| CLI not in PATH | `ErrCLINotFound` + install URL |
| Auth failure | `ErrNotAuthenticated` + `run '<cli> auth login' first` |
| No backend for bare create/fork | `no backend specified` + config hint |
| Fork non-cloud mount | `cannot fork — filesystem has no cloud service` |
| Service missing | `service not found` (from CLI stderr) |
| Other CLI failure | `{cli} CLI error: ...` (stderr preserved) |

Passwords never land in TigerFS YAML for cloud backends. Registry and logs use `db.SanitizeConnectionString()`.

## Command quick reference

<CodeGroup>

```bash title="Mount cloud service"
tigerfs mount tiger:SERVICE_ID /mnt/prod
tigerfs mount ghost:DB_ID              # mountpoint → /tmp/DB_ID
```

```bash title="Create and auto-mount"
tigerfs create tiger:my-project
tigerfs create ghost:my-project /mnt/dev
tigerfs create my-project              # needs default_backend
tigerfs create tiger: --no-mount
```

```bash title="Fork"
tigerfs fork /mnt/prod my-fork
tigerfs fork tiger:abc123 /mnt/staging
tigerfs fork /mnt/prod --name experiment --no-mount
```

```bash title="Inspect"
tigerfs list
tigerfs status
tigerfs info /mnt/prod
tigerfs info --json
```

</CodeGroup>

## Related pages

<CardGroup>
<Card title="Quickstart" href="/quickstart">
First mount with `postgres://` or `tiger:` and success signals.
</Card>
<Card title="CLI reference" href="/cli-reference">
Full cobra command surface for mount, create, fork, list, status, and info.
</Card>
<Card title="Configuration reference" href="/configuration-reference">
`default_backend`, `default_mount_dir`, Tiger env vars, and precedence.
</Card>
<Card title="Connection reference" href="/connection-reference">
`postgres://` URIs, TLS, and password resolution after the CLI returns a conn string.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
Auth errors, stale mounts, and errno recovery.
</Card>
</CardGroup>

---

## 15. DDL staging

> .create/.modify/.delete staging dirs, sql/.test/.commit control files, index DDL via .indexes/, grace period, and errno on validation failures.

- Page Markdown: https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/15-ddl-staging.md
- Generated: 2026-06-03T07:58:30.850Z

### Source Files

- `internal/tigerfs/fs/ddl.go`
- `internal/tigerfs/fs/staging.go`
- `internal/tigerfs/fuse/control_files.go`
- `docs/adr/003-ddl-staging-pattern.md`
- `test/integration/ddl_test.go`
- `docs/spec.md`

---
title: "DDL staging"
description: ".create/.modify/.delete staging dirs, sql/.test/.commit control files, index DDL via .indexes/, grace period, and errno on validation failures."
---

TigerFS applies schema changes through in-memory DDL staging sessions exposed as directories under `/.create/`, `/<table>/.modify/`, `/<table>/.delete/`, and related paths. Each session holds staged SQL plus trigger files (`sql`, `.test`, `test.log`, `.commit`, `.abort`); `fs.DDLManager` validates with `BEGIN`/`ROLLBACK` and commits with a real `Exec`, while FUSE and NFS adapters map `fs.FSError` codes to POSIX errno and log hints to stderr.

<Note>
DDL staging is not row staging. `StagingManager` in `internal/tigerfs/fs/staging.go` tracks partial `INSERT`s when you `mkdir` a new primary-key directory under a table. DDL staging uses `DDLManager` in `internal/tigerfs/fs/ddl.go` and only affects schema objects.
</Note>

## Supported operations and paths

All DDL types share the same control-file workflow (ADR-003). Path resolution is `PathDDL` in `internal/tigerfs/fs/path.go`.

| Object | Create | Modify | Delete |
|--------|--------|--------|--------|
| Table | `/.create/<name>/` | `/<table>/.modify/` | `/<table>/.delete/` |
| Index | `/<table>/.indexes/.create/<name>/` | — | `/<table>/.indexes/<name>/.delete/` |
| Schema | `/.schemas/.create/<name>/` | — | `/.schemas/<name>/.delete/` |
| View | `/.views/.create/<name>/` | — | `/.views/<name>/.delete/` |

Existing indexes also expose read-only metadata at `/<table>/.indexes/<name>/.schema` (generated `CREATE INDEX` DDL, not a staging directory).

```text
/mnt/db/
├── .create/orders/          sql  .test  test.log  .commit  .abort
├── users/
│   ├── .modify/             (auto-session on first access)
│   ├── .delete/
│   └── .indexes/
│       ├── .create/email_idx/
│       └── email_idx/.delete/
├── .schemas/.create/app/
└── .views/.create/active_users/
```

## Control files

| File | Type | Read | Write / touch |
|------|------|------|----------------|
| `sql` | Content | Staged SQL, or generated template if empty | Stage DDL; resets validation |
| `.test` | Trigger | Empty | Run `ExecInTransaction` (rolled back) |
| `test.log` | Content | Last validation output | Read-only (`ENOENT` until first test) |
| `.commit` | Trigger | Empty | Execute staged SQL |
| `.abort` | Trigger | Empty | Mark session completed (cancel) |

Trigger files must be opened for **write** (for example `touch` or `os.Create`), not read-only open — integration tests document that read-only open does not fire the write path.

Comment stripping uses `ExtractSQL` / `IsEmptyOrCommented`: lines starting with `--` and block comments `/* … */` are removed before test or commit. Comment-only `sql` is rejected at test/commit with a message in `test.log` or commit error text.

## Workflow

<Steps>
<Step title="Start a session">

For **new** root-level objects (`/.create/<name>/`, `/.schemas/.create/`, `/.views/.create/`, index create under `.indexes/.create/`), run `mkdir`:

```bash
mkdir /mnt/db/.create/orders
mkdir /mnt/db/users/.indexes/.create/email_idx
```

For **modify**, **delete**, and index delete paths on existing objects, TigerFS **auto-creates** a session on first `ls`, `stat`, or read of `sql` — no `mkdir` required.

</Step>
<Step title="Edit sql">

```bash
cat /mnt/db/.create/orders/sql          # template with commented examples
vi /mnt/db/.create/orders/sql           # or echo/redirection
```

Writing `sql` clears the `Validated` flag. Vim/emacs swap files are stored in-memory on the session (`ExtraFiles`), not on disk.

</Step>
<Step title="Validate (optional)">

```bash
touch /mnt/db/.create/orders/.test
cat /mnt/db/.create/orders/test.log
```

A failed validation updates `test.log` but the touch itself **succeeds** at the filesystem layer (no errno for bad SQL). Check `test.log` and stderr JSON logs (`logging.Warn` / `logging.Info`).

</Step>
<Step title="Commit or abort">

```bash
touch /mnt/db/.create/orders/.commit     # applies DDL, invalidates metadata cache
# or
touch /mnt/db/.create/orders/.abort      # cancels; sql reverts to template on read
```

On commit failure the session stays active so you can fix `sql` and retry. On success the session is marked `Completed` and kept in memory for the grace period.

</Step>
</Steps>

Script-style one-liner (no `.test`):

```bash
mkdir /mnt/db/.create/orders && \
  echo 'CREATE TABLE orders (id serial PRIMARY KEY)' > /mnt/db/.create/orders/sql && \
  touch /mnt/db/.create/orders/.commit
```

## Session lifecycle and grace period

```mermaid
stateDiagram-v2
    [*] --> Active: mkdir or auto-create
    Active --> Active: write sql
    Active --> Active: touch .test
    Active --> Completed: touch .commit success
    Active --> Completed: touch .abort
    Completed --> Reaped: grace period elapsed
    Reaped --> [*]
    Active --> Active: commit failure
```

| State | Behavior |
|-------|----------|
| Active | SQL and control files fully functional |
| Completed | After successful commit or abort; trigger touches are **no-ops** |
| Reaped | Lazy removal on `ListSessionEntries` / `FindSessionByName` after grace elapsed |

<ParamField body="ddl_grace_period" type="duration" default="30s">
How long completed sessions remain visible so NFS/FUSE post-close `stat`/`readdir` do not fail. Set in `~/.config/tigerfs/config.yaml` or `TIGERFS_DDL_GRACE_PERIOD`. Zero means use the 30s default (`config.DDLGracePeriod` → `NewDDLManager`).
</ParamField>

Staged SQL lives **in memory per mount**. Unmount drops all sessions. This is intentional — no stale DDL on disk.

## Creating sessions: mkdir rules

| Situation | errno | Hint (stderr) |
|-----------|-------|----------------|
| Duplicate active session | `EEXIST` | Abort existing session first |
| Empty name `/.create/` | `EINVAL` | Use `mkdir /.create/<name>` |
| Unknown operation in path | `EINVAL` | Malformed DDL path |
| Access `sql` without session (root create) | `ENOENT` | `mkdir /.create/<name>` |

Replacing a **completed** root-level create session: `mkdir` removes the old session and starts fresh. Completed table-level auto-sessions are replaced silently on next access.

## errno and logging on validation failures

FUSE/NFS only return errno codes; details go to **stderr** via structured zap logs (`logging.Error` with `hint`).

| Operation | Condition | errno | Where detail appears |
|-----------|-----------|-------|-------------------|
| Touch `.test` | Bad SQL | *(success)* | `test.log`, stderr warn |
| Touch `.test` | No / comment-only SQL | *(success)* | `test.log` |
| Read `test.log` | Before any test | `ENOENT` | Hint: run `touch …/.test` |
| Touch `.commit` | Empty / comment-only SQL | `EIO` | stderr + error message |
| Touch `.commit` | PostgreSQL error | `EIO` | stderr; session preserved |
| Write `sql` | Session already completed | `EPERM` | Hint: new `mkdir` |
| `mkdir` duplicate active | `EEXIST` | stderr hint |
| `rm` control file or staging dir | — | `EACCES` | Use `.abort` |
| Read `test.log` open for write | — | `EACCES` | Read-only file |

<Warning>
Do not infer validation success from the exit code of `touch .test`. Always read `test.log` or stderr. Commit failures use `EIO` — read TigerFS logs when the shell only prints `Input/output error`.
</Warning>

Example stderr after failed commit:

```json
{"message":"DDL commit failed for create/orders","hint":"session=… error: …"}
```

## Index DDL

Indexes use the same control files under the owning table:

```bash
mkdir /mnt/db/users/.indexes/.create/email_idx
echo 'CREATE INDEX email_idx ON users (email)' > /mnt/db/users/.indexes/.create/email_idx/sql
touch /mnt/db/users/.indexes/.create/email_idx/.test
cat /mnt/db/users/.indexes/.create/email_idx/test.log
touch /mnt/db/users/.indexes/.create/email_idx/.commit

# Drop
echo 'DROP INDEX email_idx' > /mnt/db/users/.indexes/email_idx/.delete/sql
touch /mnt/db/users/.indexes/email_idx/.delete/.commit
```

`DDLParentTable` is set from path context so templates reference the correct table. Primary key indexes are omitted from `.indexes/` listings (row paths remain the access model).

## Implementation map

| Layer | Role |
|-------|------|
| `fs/path.go` | Parses `PathDDL`, `DDLOp`, `DDLFile`, object type |
| `fs/ddl.go` | `DDLManager`: sessions, test, commit, templates, grace reaper |
| `fs/write.go` | `mkdirDDL`, `writeDDLFile`, metadata cache invalidation on commit |
| `fs/operations.go` | `ensureDDLSession`, directory listings, stable mtimes |
| `fuse/adapter.go` | `ErrorToErrno` + hint logging |
| `fuse/control_files.go` | Legacy FUSE-only nodes when `legacy_fuse: true` |

Default mounts route through `fs.Operations`; both Linux FUSE and macOS NFS use the same DDL semantics.

## Templates

Reading `sql` before writing returns object-specific commented templates (`generateCreateTemplate`, `generateModifyTemplate`, `generateDeleteTemplate` in `ddl.go`). Templates encourage uncommenting real statements rather than executing comments.

## Verification

Integration coverage in `test/integration/ddl_test.go` exercises table create/delete, `ALTER` via `.modify`, index and schema cycles, view create/delete, valid/invalid `.test`, abort clearing staged content, and script workflow.

```bash
go test -run TestDDL_ ./test/integration/...
go test -run 'TestMkdir_PathDDL|TestWriteDDLFile' ./internal/tigerfs/fs/...
```

After create/delete, allow a short pause (or cache TTL) before `stat` on new table paths — tests sleep ~500ms for metadata visibility.

## 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="Error codes" href="/error-codes">
`fs.ErrorCode` to errno mapping and reading stderr hints.
</Card>
<Card title="Configuration reference" href="/configuration-reference">
`ddl_grace_period` and mount-level settings.
</Card>
<Card title="Develop and test" href="/develop-and-test">
Build, `go test`, and integration testcontainers workflow.
</Card>
</CardGroup>

---

## 16. CLI reference

> All cobra commands (mount, unmount, stop, status, info, list, create, fork, migrate, test-connection, config, version), flags, and argument patterns.

- Page Markdown: https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/16-cli-reference.md
- Generated: 2026-06-03T08:00:10.112Z

### Source Files

- `internal/tigerfs/cmd/root.go`
- `internal/tigerfs/cmd/mount.go`
- `internal/tigerfs/cmd/migrate.go`
- `internal/tigerfs/cmd/config.go`
- `docs/spec.md`
- `internal/tigerfs/cmd/cmd_test.go`

---
title: "CLI reference"
description: "All cobra commands (mount, unmount, stop, status, info, list, create, fork, migrate, test-connection, config, version), flags, and argument patterns."
---

The `tigerfs` binary is a Cobra application rooted at `internal/tigerfs/cmd`: `cmd.Execute` builds `buildRootCmd`, runs `PersistentPreRunE` (logging + `config.Init`), then dispatches subcommands. Mount is a blocking long-lived process; `create` and `fork` optionally spawn a detached `tigerfs mount` child. Active mounts are recorded in a JSON registry (default `~/.config/tigerfs/mounts.json`) for `status`, `list`, `info`, `unmount`, and `stop`.

## Command tree

```text
tigerfs                          # root: persistent logging/config flags
├── mount [CONNECTION] [MOUNTPOINT]
├── unmount MOUNTPOINT
├── stop PID
├── status [MOUNTPOINT]
├── info [MOUNTPOINT]
├── list
├── create [BACKEND:]NAME [MOUNTPOINT]
├── fork SOURCE [DEST]
├── migrate [CONNECTION]
├── test-connection [CONNECTION]
├── config
│   ├── show
│   ├── validate
│   └── path
└── version
```

<Info>
`mount` is documented as the primary workflow, but the root command does not register a Cobra default command. Invoke `tigerfs mount …` explicitly (not `tigerfs /mnt/db` alone).
</Info>

## Global flags

Applied on every subcommand via root `PersistentPreRunE` (flags bind to Viper at run time).

| Flag | Default | Effect |
|------|---------|--------|
| `--config-dir` | `~/.config/tigerfs` (or `XDG_CONFIG_HOME` / `%APPDATA%`) | Config search path |
| `--log-level` | `warn` | `debug`, `info`, `warn`, `error` |
| `--log-file` | (empty → stderr) | Log file path |
| `--log-format` | `text` | `text` or `json` |
| `--log-sql-params` | `false` | Log SQL bind parameters (may leak secrets) |

Configuration merge order for runtime behavior: built-in defaults → `config.yaml` → `TIGERFS_*` and bound `PG*` / `TIGER_*` env vars → command-specific flag overrides in each command’s `RunE`.

## Connection argument patterns

Many commands accept an optional `CONNECTION` positional argument. Resolution rules differ by command.

### Prefix scheme (mount, migrate)

| Form | Meaning |
|------|---------|
| `tiger:ID` | Tiger Cloud service; CLI fetches `postgres://…` via backend |
| `ghost:ID` | Ghost database; same pattern |
| `postgres://…` | Direct URL (two-arg mount: connection + mountpoint) |
| (omitted) | See per-command resolution below |

`backend.Resolve` treats only `tiger:` and `ghost:` as cloud backends. A bare name (no prefix) is not a connection string; callers use `default_backend` in config (`create`, `fork`) or treat a single non-prefixed arg as a mountpoint path (`mount`).

### `db.ResolveConnectionString` (test-connection, migrate)

When `CONNECTION` is omitted:

1. Explicit argument if provided  
2. `tiger_service_id` / `TIGER_SERVICE_ID` in config  
3. `host` from config (including `PGHOST`)

`mount` does **not** call `ResolveConnectionString`. It resolves `tiger:` / `ghost:` via `backend.GetConnectionString` only when the connection arg is non-empty after `resolveMountArgs`.

## `mount`

**Use:** `tigerfs mount [CONNECTION] [MOUNTPOINT]`  
**Args:** `cobra.RangeArgs(1, 2)` — one or two positionals.

Blocks until SIGINT/SIGTERM (from `cmd/tigerfs/main.go` cancel) or external unmount. Registers the mount in the registry on success; unregisters on exit. Auto-creates missing mountpoint directories and removes them on unmount only when TigerFS created the directory.

### Argument resolution

| Args | `CONNECTION` | `MOUNTPOINT` |
|------|--------------|--------------|
| `CONN MNT` | First arg | Second arg |
| `tiger:ID` or `ghost:ID` | Prefix ref | `default_mount_dir/ID` (default `/tmp/ID`) |
| Single path or name | Empty | That arg (e.g. `/mnt/db`, `./db`) |

<Warning>
A single `postgres://…` positional is parsed as a **mountpoint**, not a connection. Use two args: `tigerfs mount postgres://user@host/db /mnt/db`.
</Warning>

### Mount flags (applied in `RunE`)

| Flag | Default | Maps to / behavior |
|------|---------|-------------------|
| `--schema` | `""` | `cfg.DefaultSchema` when set |
| `--no-filename-extensions` | `false` | `cfg.NoFilenameExtensions` |
| `--query-timeout` | `0` | `cfg.QueryTimeout` when &gt; 0 |
| `--dir-filter-limit` | `0` | `cfg.DirFilterLimit` when &gt; 0 |
| `--max-pipeline-depth` | `0` | `cfg.MaxPipelineDepth` only if flag changed |
| `--legacy-fuse` | `false` | Linux: legacy FUSE tree vs `fs.Operations` (default) |
| `--insecure-no-ssl` | `false` | `cfg.InsecureNoSSL` |
| `--user-id` | `""` | `cfg.UserID`, else `TIGERFS_USER_ID` |
| `--auto-savepoint-interval` | `0` | `cfg.AutoSavepointInterval` when non-zero; must be ≥ 0 |
| `--undo-list-limit` | `0` | `cfg.UndoListLimit` when &gt; 0 |

### Mount flags (registered, not wired in `RunE`)

These appear in `--help` but are not passed into `cfg` or the mount layer today:

| Flag | Default | Note |
|------|---------|------|
| `--read-only` | `false` | Logged only |
| `--max-ls-rows` | `10000` | Use `dir_listing_limit` in config (default 1000) |
| `--foreground` | `false` | Mount always blocks in the foreground process |

### Platform backends

| OS | Backend |
|----|---------|
| Linux | FUSE (`fuse.MountOps` default; `--legacy-fuse` → `fuse.Mount`) |
| macOS | In-process NFS (`nfs.Mount`) |

### Examples

```bash
# Cloud: auto mountpoint under /tmp
tigerfs mount tiger:abcde12345

# Cloud + explicit path
tigerfs mount ghost:fghij67890 /mnt/db

# Direct URL (two positionals required)
tigerfs mount postgres://user@host:5432/mydb /mnt/db

# Undo identity + debug logs
tigerfs mount --user-id agent-1 --log-level debug postgres://localhost/mydb /mnt/db
```

## `unmount`

**Use:** `tigerfs unmount MOUNTPOINT`  
**Args:** Exactly one mountpoint (resolved to absolute path).

| Flag | Shorthand | Default | Description |
|------|-----------|---------|-------------|
| `--force` | `-f` | `false` | Force unmount (`fusermount -uz` on Linux, `diskutil unmount force` on macOS) |
| `--timeout` | `-t` | `30` | Seconds to wait for graceful shutdown |

**Behavior:** Looks up registry → `SIGTERM` to registered PID → waits → falls back to OS unmount. Cleans registry entry and removes auto-created mountpoint dirs when applicable. Prints `Successfully unmounted <path>`.

## `stop`

**Use:** `tigerfs stop PID`  
**Args:** Positive integer PID.

| Flag | Shorthand | Default |
|------|-----------|---------|
| `--timeout` | `-t` | `30` |

Sends `SIGTERM`, waits for exit, cleans registry if the PID matches a registered mount. Equivalent to unmount when you have the PID from `status` instead of the path.

## `status`

**Use:** `tigerfs status [MOUNTPOINT]`  
**Args:** Zero or one.

| Mode | Output |
|------|--------|
| No args | Table: `MOUNTPOINT`, `DATABASE`, `PID`, `STATUS`, `UPTIME` for active mounts (stale entries cleaned first) |
| With mountpoint | Detail: mountpoint, database, backend, service ID, PID, status (`active` / `stale`), started time, uptime |

Empty registry: `No active TigerFS mounts`.

## `list`

**Use:** `tigerfs list`  
**Args:** None.

One mountpoint path per line (no header). Stale registry entries are cleaned before listing. Intended for scripting (`xargs`, loops).

## `info`

**Use:** `tigerfs info [MOUNTPOINT]`  
**Args:** Zero or one.

| Flag | Description |
|------|-------------|
| `--json` | JSON `backend.MountInfo` |

Without `MOUNTPOINT`: requires exactly one active mount; errors if zero or multiple (lists suggested commands). For cloud-backed mounts (`CLIBackend` + `ServiceID`), calls the backend CLI for live service metadata.

## `create`

**Use:** `tigerfs create [BACKEND:]NAME [MOUNTPOINT]`  
**Args:** `cobra.MaximumNArgs(2)`.

| Flag | Description |
|------|-------------|
| `--no-mount` | Create service only; print manual mount hint |
| `--json` | JSON result including `backend`, `no_mount` |

**Backend resolution:** `tiger:NAME`, `ghost:NAME`, or bare `NAME` with `default_backend` in config. Bare `tiger:` / `ghost:` auto-generates a service name.  
**Mount:** Unless `--no-mount`, runs detached `tigerfs mount <backend>:<serviceID> <mountpoint>`. Default mountpoint: `default_mount_dir/<name>` (often `/tmp/<name>`).

## `fork`

**Use:** `tigerfs fork SOURCE [DEST]`  
**Args:** One or two.

| Flag | Description |
|------|-------------|
| `--name` | Override fork service name |
| `--no-mount` | Fork only |
| `--json` | JSON fork result |

**SOURCE**

| Form | Resolution |
|------|------------|
| Path starting with `/` or `.` | Mount registry lookup (requires cloud-backed mount) |
| `tiger:ID`, `ghost:ID` | Backend + service ID |
| Bare name | `default_backend` |

**DEST** (optional): path → mount there (basename used as name unless `--name`); bare name → service name and mount at `baseDir/NAME` (`baseDir` is parent of source mountpoint when SOURCE was a path, else `default_mount_dir`).

## `migrate`

**Use:** `tigerfs migrate [CONNECTION]`  
**Args:** `cobra.MaximumNArgs(1)`. Uses `db.ResolveConnectionString`. 60s command timeout.

| Flag | Description |
|------|-------------|
| `--describe` | List pending migrations and items |
| `--dry-run` | Print SQL without executing |
| `--schema` | Schema to migrate (default: `current_schema()`) |
| `--insecure-no-ssl` | Disable TLS enforcement for remote hosts |

**Registered migrations (order fixed):**

| Name | Summary |
|------|---------|
| `move-backing-tables` | Move synth backing tables from `_name` in user schema to `tigerfs.name` |
| `relational-directories` | Add `parent_id` parent-pointer directory model |
| `parent-dir-mtime-trigger` | Parent directory `modified_at` trigger for NFS listings |

Default mode runs each pending migration in a transaction. Idempotent when nothing pending: `No pending migrations.`

## `test-connection`

**Use:** `tigerfs test-connection [CONNECTION]`  
**Args:** `cobra.MaximumNArgs(1)`. 30s timeout. Uses `db.ResolveConnectionString`.

**Output:** PostgreSQL version (trimmed), database, user, schema count, base table count (user schemas only).

## `config`

**Use:** `tigerfs config <subcommand>`

### `config show`

Merged effective config as YAML (passwords omitted). Sections: `connection`, `tiger_cloud`, `backend`, `filesystem`, `query`, `metadata`, `nfs`, `ddl`, `logging`, `advanced`.

### `config validate`

Reads `~/.config/tigerfs/config.yaml` (or `--config-dir`). Missing file is OK (“Using default configuration”). Validates YAML, unmarshaling, port/pool/dir limits, `default_format`, `binary_encoding`, `log_level`.

### `config path`

Prints config file path; notes if file does not exist.

## `version`

**Use:** `tigerfs version`

Prints: `Version`, `BuildTime`, `GitCommit` (ldflags in release builds; `dev` / `unknown` in dev), Go version, `GOOS`/`GOARCH`.

## Mount lifecycle

```mermaid
stateDiagram-v2
  [*] --> Starting: tigerfs mount
  Starting --> Registered: register mounts.json
  Registered --> Serving: fs.Serve blocks
  Serving --> Unregistering: SIGTERM or unmount
  Unregistering --> [*]: unregister + optional rmdir
```

## Operational commands (quick reference)

| Goal | Command |
|------|---------|
| Mount cloud DB | `tigerfs mount tiger:SERVICE_ID` |
| List mountpoints | `tigerfs list` |
| Inspect mounts | `tigerfs status` |
| Cloud service details | `tigerfs info --json /path` |
| Test credentials | `tigerfs test-connection postgres://…` |
| Graceful teardown | `tigerfs unmount /path` or `tigerfs stop PID` |
| Provision + mount | `tigerfs create tiger:my-db` |
| Clone + mount | `tigerfs fork /mnt/prod my-fork` |
| Schema upgrades | `tigerfs migrate postgres://… --dry-run` |

## Error and exit behavior

- Root and subcommands set `SilenceUsage: true` on failure (no usage spam; message on stderr via logging).
- Invalid args (bad PID, missing mount, multiple mounts for bare `info`) return descriptive errors from `RunE`.
- FUSE/NFS operational errors surface as POSIX errno to tools; detailed hints are written via `logging.Error` / `Warn` (see error-codes page).

<Note>
`cmd_test.go` verifies command names, help text, and core flag registration. When `--help` disagrees with runtime wiring, trust the `RunE` implementation in `internal/tigerfs/cmd/*.go`.
</Note>

## Related pages

<CardGroup>
  <Card title="Configuration reference" href="/configuration-reference">
    Config file fields, env vars, and precedence beyond CLI flags.
  </Card>
  <Card title="Connection reference" href="/connection-reference">
    postgres:// URIs, PG* env, TLS, passwords, and pool settings.
  </Card>
  <Card title="Cloud backends" href="/cloud-backends">
    tiger: and ghost: resolution, create/fork, and Tiger CLI auth.
  </Card>
  <Card title="Migrate workspaces" href="/page-migrate-workspaces">
    migrate --describe/--dry-run and history-format upgrade boundaries.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    Stale mounts, force unmount, and --log-level debug.
  </Card>
</CardGroup>

---

## 17. Configuration reference

> Config struct fields, ~/.config/tigerfs/config.yaml, TIGERFS_* env vars, precedence, TLS enforcement, and mount-specific overrides.

- Page Markdown: https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/17-configuration-reference.md
- Generated: 2026-06-03T07:59:01.625Z

### Source Files

- `internal/tigerfs/config/config.go`
- `internal/tigerfs/config/config_test.go`
- `internal/tigerfs/cmd/config.go`
- `docs/spec.md`
- `internal/tigerfs/db/tls.go`

---
title: "Configuration reference"
description: "Config struct fields, ~/.config/tigerfs/config.yaml, TIGERFS_* env vars, precedence, TLS enforcement, and mount-specific overrides."
---

TigerFS loads a single `config.Config` value per process via Viper (`internal/tigerfs/config`), merges defaults with `~/.config/tigerfs/config.yaml` and environment variables, then applies command-specific overrides (notably on `mount`) before passing the struct into `db.NewClient` and `fs.Operations`. Use `tigerfs config show` to inspect the effective merged settings; never read Viper directly outside the `cmd` package.

## Configuration flow

```mermaid
flowchart TB
  subgraph sources [Sources lowest to highest]
    D[Init defaults in config.Init]
    F["config.yaml in GetDefaultConfigDir"]
    E["TIGERFS_* / PG* / TIGER_* env"]
    R["Root persistent flags bound in PersistentPreRunE"]
  end
  subgraph load [Per command]
    I[config.Init in root PersistentPreRunE]
    L[config.Load into Config struct]
  end
  subgraph mount [Mount only]
    M[Mount RunE patches cfg fields]
    C[db.NewClient enforceSSLMode + pool]
    O[fs.Operations + FUSE/NFS adapter]
  end
  D --> F --> E --> R --> I --> L
  L --> M --> C --> O
```

<Note>
`config.Init()` runs on every command via the root command’s `PersistentPreRunE`. Subcommands call `config.Load()` to obtain a `*config.Config`. Application code should use that struct, not `viper` directly.
</Note>

## Precedence

Later layers override earlier ones for keys Viper manages:

| Priority | Source | Examples |
|----------|--------|----------|
| 1 (lowest) | Built-in defaults | `port: 5432`, `dir_listing_limit: 1000` |
| 2 | Config file | `~/.config/tigerfs/config.yaml` |
| 3 | Environment | `TIGERFS_LOG_LEVEL`, `PGHOST`, `TIGER_SERVICE_ID` |
| 4 | Root persistent CLI flags | `--log-level`, `--config-dir` (bound to Viper before `Init`) |
| 5 | Mount-time patches | `--schema`, `--insecure-no-ssl`, `--user-id` (applied to `cfg` after `Load`) |
| 6 (connection) | Connection string | Password in URL; `sslmode` transformed at connect |

**`TIGERFS_*` vs `PG*`:** For the same logical key (e.g. port), `TIGERFS_PORT` wins over `PGPORT` when both are set.

**Password resolution** (separate from Viper; in `db.resolvePassword`):

1. Password embedded in the connection string  
2. `PGPASSWORD`  
3. `TIGERFS_PASSWORD`  
4. `password_command` from config  
5. `~/.pgpass` (pgx automatic)

## Config file location and format

### Path resolution

| Platform / variable | Directory |
|---------------------|-----------|
| Default (Unix) | `~/.config/tigerfs/` |
| `XDG_CONFIG_HOME` set | `$XDG_CONFIG_HOME/tigerfs/` |
| Windows `APPDATA` | `%APPDATA%/tigerfs/` |

File name: `config.yaml`. A missing file is not an error.

```bash
tigerfs config path    # print path; note if file missing
tigerfs config show    # merged effective config (YAML, secrets omitted)
tigerfs config validate
```

<Warning>
`config.Init()` always calls `viper.AddConfigPath(GetDefaultConfigDir())`, which follows `XDG_CONFIG_HOME` / `APPDATA` / home — not the `config_dir` field after load. To relocate the config directory, set `XDG_CONFIG_HOME` (or `APPDATA` on Windows). The `config_dir` / `TIGERFS_CONFIG_DIR` value is stored on `Config` and shown in `config show`, but does not change where Viper searches for `config.yaml` today.
</Warning>

### YAML shape

Keys are **flat snake_case** matching `mapstructure` tags on `Config`. Example:

```yaml
# Connection
host: localhost
port: 5432
user: myuser
database: mydb
default_schema: public
pool_size: 10
pool_max_idle: 5
password_command: "op read op://vault/db/password"
insecure_no_ssl: false

# Backend
default_backend: tiger
default_mount_dir: /tmp

# Filesystem
dir_listing_limit: 1000
dir_writing_limit: 100000
trailing_newlines: true
no_filename_extensions: false
attr_timeout: 1s
entry_timeout: 1s
default_format: tsv
binary_encoding: raw
max_pipeline_depth: 10

# Query safety
query_timeout: 30s
dir_filter_limit: 100000

# Metadata caches (catalog vs structural)
metadata_refresh_interval: 10s
structural_metadata_refresh_interval: 5m

# NFS write buffers (macOS)
nfs_streaming_threshold: 10485760
nfs_max_random_write_size: 104857600
nfs_cache_reaper_interval: 30s
nfs_cache_idle_timeout: 5m

# DDL
ddl_grace_period: 30s

# Logging
log_level: warn
log_file: ""
log_format: text
log_sql_params: false

# Identity / undo
user_id: agent-7
auto_savepoint_interval: 30m
undo_list_limit: 100

# Linux FUSE
legacy_fuse: false
```

Do not put passwords in the config file; use `.pgpass`, env vars, or `password_command`.

## Environment variables

Viper uses prefix `TIGERFS` with `AutomaticEnv()` (e.g. `TIGERFS_DIR_LISTING_LIMIT` → `dir_listing_limit`). Duration env values accept Go duration strings (`30s`, `1m`, `5m`).

### PostgreSQL standard (bound in `config.Init`)

| Variable | Config field |
|----------|----------------|
| `PGHOST` | `host` |
| `PGPORT` | `port` |
| `PGUSER` | `user` |
| `PGDATABASE` | `database` |
| `PGPASSWORD` | (password resolution only) |

### Tiger Cloud (headless / Docker)

| Variable | Config field |
|----------|----------------|
| `TIGER_SERVICE_ID` | `tiger_service_id` |
| `TIGER_PUBLIC_KEY` | `tiger_public_key` |
| `TIGER_SECRET_KEY` | `tiger_secret_key` |
| `TIGER_PROJECT_ID` | `tiger_project_id` |

### Common `TIGERFS_*` variables

| Environment variable | Field | Default |
|---------------------|-------|---------|
| `TIGERFS_CONFIG_DIR` | `config_dir` | from `GetDefaultConfigDir()` |
| `TIGERFS_DEFAULT_SCHEMA` | `default_schema` | `""` (inherit PG `current_schema`) |
| `TIGERFS_DIR_LISTING_LIMIT` | `dir_listing_limit` | `1000` |
| `TIGERFS_DIR_WRITING_LIMIT` | `dir_writing_limit` | `100000` |
| `TIGERFS_DIR_FILTER_LIMIT` | `dir_filter_limit` | `100000` |
| `TIGERFS_QUERY_TIMEOUT` | `query_timeout` | `30s` |
| `TIGERFS_MAX_PIPELINE_DEPTH` | `max_pipeline_depth` | `10` (`0` = unlimited) |
| `TIGERFS_TRAILING_NEWLINES` | `trailing_newlines` | `true` |
| `TIGERFS_NO_FILENAME_EXTENSIONS` | `no_filename_extensions` | `false` |
| `TIGERFS_ATTR_TIMEOUT` | `attr_timeout` | `1s` |
| `TIGERFS_ENTRY_TIMEOUT` | `entry_timeout` | `1s` |
| `TIGERFS_METADATA_REFRESH_INTERVAL` | `metadata_refresh_interval` | `10s` |
| `TIGERFS_STRUCTURAL_METADATA_REFRESH_INTERVAL` | `structural_metadata_refresh_interval` | `5m` |
| `TIGERFS_DEFAULT_FORMAT` | `default_format` | `tsv` |
| `TIGERFS_BINARY_ENCODING` | `binary_encoding` | `raw` |
| `TIGERFS_POOL_SIZE` | `pool_size` | `10` |
| `TIGERFS_POOL_MAX_IDLE` | `pool_max_idle` | `5` |
| `TIGERFS_PASSWORD` | (password resolution) | — |
| `TIGERFS_PASSWORD_COMMAND` | `password_command` | — |
| `TIGERFS_DEFAULT_BACKEND` | `default_backend` | `""` |
| `TIGERFS_DEFAULT_MOUNT_DIR` | `default_mount_dir` | `/tmp` |
| `TIGERFS_INSECURE_NO_SSL` | `insecure_no_ssl` | `false` |
| `TIGERFS_USER_ID` | `user_id` | `""` |
| `TIGERFS_AUTO_SAVEPOINT_INTERVAL` | `auto_savepoint_interval` | `30m` (`0` disables) |
| `TIGERFS_UNDO_LIST_LIMIT` | `undo_list_limit` | `100` |
| `TIGERFS_LOG_LEVEL` | `log_level` | `warn` |
| `TIGERFS_LOG_FILE` | `log_file` | `""` (stderr) |
| `TIGERFS_LOG_FORMAT` | `log_format` | `text` |
| `TIGERFS_LOG_SQL_PARAMS` | `log_sql_params` | `false` |
| `TIGERFS_LEGACY_FUSE` | `legacy_fuse` | `false` |
| NFS / DDL keys | `nfs_*`, `ddl_grace_period` | see defaults table |

## `Config` struct reference

All fields live in `internal/tigerfs/config/config.go`. Grouped by concern:

### Connection and TLS

| Field | YAML / env | Default | Role |
|-------|------------|---------|------|
| `Host` | `host` / `PGHOST` | empty | Server hostname |
| `Port` | `port` / `PGPORT` / `TIGERFS_PORT` | `5432` | TCP port |
| `User` | `user` / `PGUSER` | empty | DB user |
| `Database` | `database` / `PGDATABASE` | empty | Database name |
| `Password` | — | empty | Rarely set via file; prefer env/pgpass |
| `DefaultSchema` | `default_schema` | `""` | Flatten schema to mount root |
| `PoolSize` | `pool_size` | `10` | Max pool connections |
| `PoolMaxIdle` | `pool_max_idle` | `5` | Min idle connections |
| `PasswordCommand` | `password_command` | empty | Shell command → password on stdout |
| `InsecureNoSSL` | `insecure_no_ssl` / `--insecure-no-ssl` | `false` | Skip remote TLS enforcement |

Tiger Cloud credential fields: `TigerCloudServiceID`, `TigerCloudPublicKey`, `TigerCloudSecretKey`, `TigerCloudProjectID` (env `TIGER_*`).

### Backend and mount paths

| Field | Default | Role |
|-------|---------|------|
| `DefaultBackend` | `""` | `tiger` or `ghost` for bare service IDs |
| `DefaultMountDir` | `/tmp` | Base dir when `tiger:ID` / `ghost:ID` omit mountpoint |

### Filesystem behavior

| Field | Default | Role |
|-------|---------|------|
| `DirListingLimit` | `1000` | Max rows for `ls`, `.by/`, exports without explicit limit |
| `DirWritingLimit` | `100000` | Bulk import row cap |
| `TrailingNewlines` | `true` | Append `\n` on column/count reads |
| `NoFilenameExtensions` | `false` | Disable type-based extensions (`.json`, `.txt`, …) |
| `AttrTimeout` | `1s` | FUSE attribute cache (Linux only) |
| `EntryTimeout` | `1s` | FUSE entry cache (Linux only) |
| `MaxPipelineDepth` | `10` | Hide deeper pipeline capabilities (`0` = unlimited) |
| `DefaultFormat` | `tsv` | Row-as-file default (`tsv`, `csv`, `json`) |
| `BinaryEncoding` | `raw` | BYTEA encoding (`raw`, `base64`, `hex`) |

### Query safety

| Field | Default | Role |
|-------|---------|------|
| `QueryTimeout` | `30s` | Per-connection `statement_timeout` + context timeout |
| `DirFilterLimit` | `100000` | Row threshold before `.filter/` value listing is restricted |

### Metadata TTLs

| Field | Default | Role |
|-------|---------|------|
| `MetadataRefreshInterval` | `10s` | Catalog cache (schemas, tables, views) |
| `StructuralMetadataRefreshInterval` | `5m` | PKs, permissions, row counts |

### NFS (macOS write path)

| Field | Default | Role |
|-------|---------|------|
| `NFSStreamingThreshold` | 10 MiB | Buffer size → streaming commit |
| `NFSMaxRandomWriteSize` | 100 MiB | Max random-write buffer |
| `NFSCacheReaperInterval` | `30s` | Stale entry check interval |
| `NFSCacheIdleTimeout` | `5m` | Idle force-commit |

### DDL, logging, identity, FUSE mode

| Field | Default | Role |
|-------|---------|------|
| `DDLGracePeriod` | `30s` | Completed DDL session visibility |
| `LogLevel` | `warn` | Zap level (`debug`, `info`, `warn`, `error`) |
| `LogFile` | empty | File path; empty → stderr |
| `LogFormat` | `text` | `text` or `json` |
| `LogSQLParams` | `false` | Log bind parameters (sensitive) |
| `UserID` | empty | Per-mount undo/log identity |
| `AutoSavepointInterval` | `30m` | Inactivity auto-savepoint (`0` off) |
| `UndoListLimit` | `100` | Default `.undo/` listing cap |
| `LegacyFuse` | `false` | Linux: legacy FUSE tree vs shared `fs.Operations` |
| `ConfigDir` | `GetDefaultConfigDir()` | Recorded config directory path |

## TLS enforcement

Applied in `db.enforceSSLMode` when opening the pool (`db.NewClient`), using `cfg.InsecureNoSSL`.

```text
                    enforceSSLMode(connStr, InsecureNoSSL)
                              |
         +--------------------+--------------------+
         |                    |                    |
    localhost            remote host          insecure_no_ssl
  (127.0.0.1, ::1,      sslmode missing        true → no change
   unix socket)         or disable/prefer      + warn log
         |                    |
  no sslmode →            → sslmode=require
  sslmode=disable         (require/verify-* kept)
  explicit sslmode kept
```

<ParamField body="insecure_no_ssl" type="bool">
When `true` (`--insecure-no-ssl`, `TIGERFS_INSECURE_NO_SSL`, or YAML), remote connections keep the connection string’s `sslmode` unchanged and log a warning. Default is enforced TLS for non-localhost hosts.
</ParamField>

| Host class | No `sslmode` in URI | `sslmode=disable` / `prefer` | `require` / `verify-ca` / `verify-full` |
|------------|-------------------|------------------------------|----------------------------------------|
| Localhost / Unix socket | Adds `disable` | Unchanged | Unchanged (explicit override allowed) |
| Remote | Adds `require` | Replaced with `require` | Unchanged |

Localhost detection: `localhost`, `127.0.0.1`, `::1`, empty host, or host paths starting with `/`.

## Mount-specific overrides

Each `tigerfs mount` process loads config once, then patches the struct before `mountFilesystem`. These flags override file/env for that mount only:

| Flag | Config field | Behavior |
|------|--------------|----------|
| `--schema` | `DefaultSchema` | Set when non-empty |
| `--no-filename-extensions` | `NoFilenameExtensions` | Set `true` when passed |
| `--query-timeout` | `QueryTimeout` | Applied when `> 0` |
| `--dir-filter-limit` | `DirFilterLimit` | Applied when `> 0` |
| `--max-pipeline-depth` | `MaxPipelineDepth` | Applied only if flag **changed** (allows `0`) |
| `--legacy-fuse` | `LegacyFuse` | Linux legacy FUSE backend |
| `--insecure-no-ssl` | `InsecureNoSSL` | Disables TLS enforcement |
| `--user-id` | `UserID` | Flag → `TIGERFS_USER_ID` → value from config file |
| `--auto-savepoint-interval` | `AutoSavepointInterval` | Applied when `!= 0` (`>= 0` required) |
| `--undo-list-limit` | `UndoListLimit` | Applied when `> 0` |

<Info>
`--max-ls-rows` is declared on the mount command (default `10000`) but is **not** wired to `DirListingLimit` in `mount.go`; use `dir_listing_limit` in config or `TIGERFS_DIR_LISTING_LIMIT` instead (default `1000`).
</Info>

Root persistent flags (all subcommands):

| Flag | Viper key | Default |
|------|-----------|---------|
| `--config-dir` | `config_dir` | `GetDefaultConfigDir()` |
| `--log-level` | `log_level` | `warn` |
| `--log-file` | `log_file` | stderr |
| `--log-format` | `log_format` | `text` |
| `--log-sql-params` | `log_sql_params` | `false` |

`--debug` is not a separate config key; use `--log-level debug`.

### Per-mount identity

`UserID` tags undo/log entries and drives per-user filters on `.log/` and `.undo/.by/user_id/`. It is session-scoped (lost on remount). Precedence at mount: `--user-id` → `TIGERFS_USER_ID` → `user_id` from config file → anonymous.

## Validation rules

`tigerfs config validate` checks the on-disk file (syntax + unmarshal) and:

| Field | Rule |
|-------|------|
| `port` | 1–65535 |
| `pool_size` | ≥ 1 |
| `pool_max_idle` | ≥ 0 |
| `dir_listing_limit` | ≥ 1 |
| `default_format` | `tsv`, `csv`, or `json` |
| `binary_encoding` | `raw`, `base64`, or `hex` |
| `log_level` | `debug`, `info`, `warn`, or `error` |

## Inspecting configuration

<Steps>
<Step title="Show effective config">
Run after exporting env vars or editing the file:

```bash
tigerfs config show
```

Sensitive fields (`password`, Tiger secret keys) are omitted from output.
</Step>
<Step title="Validate on-disk file">
```bash
tigerfs config validate
```
</Step>
<Step title="Mount with overrides">
```bash
export TIGERFS_QUERY_TIMEOUT=45s
tigerfs mount --schema public --user-id agent-7 \
  postgres://user@db.example.com/mydb /mnt/db
```
</Step>
</Steps>

<Check>
`config show` reflects Viper merge (defaults + file + env + root flags). Mount-only flag patches apply only to that mount process after `Load()`.
</Check>

## Related pages

<CardGroup>
<Card title="CLI reference" href="/cli-reference">
Cobra commands, mount flags, and argument patterns beyond config keys.
</Card>
<Card title="Connection reference" href="/connection-reference">
`postgres://` URIs, `.pgpass`, pool behavior, and connection-string `sslmode` details.
</Card>
<Card title="Cloud backends" href="/cloud-backends">
`default_backend`, `tiger:` / `ghost:` prefixes, and Tiger Cloud credentials.
</Card>
<Card title="Consistency and caching" href="/consistency-and-caching">
How `metadata_refresh_interval` and FUSE `attr_timeout` / `entry_timeout` affect freshness.
</Card>
<Card title="Platform backends" href="/platform-backends">
FUSE vs NFS: which config keys apply per platform.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
TLS errors, stale metadata, and `--log-level debug`.
</Card>
</CardGroup>

---

## 18. Data formats reference

> TSV, CSV, JSON, YAML row encoding, PATCH semantics, NULL representation, type extensions (.txt/.json/.bin), and binary_encoding options.

- Page Markdown: https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/18-data-formats-reference.md
- Generated: 2026-06-03T08:00:03.124Z

### Source Files

- `internal/tigerfs/format/json.go`
- `internal/tigerfs/format/csv.go`
- `internal/tigerfs/format/yaml.go`
- `internal/tigerfs/format/convert.go`
- `internal/tigerfs/fs/extensions.go`
- `test/integration/format_test.go`

---
title: "Data formats reference"
description: "TSV, CSV, JSON, YAML row encoding, PATCH semantics, NULL representation, type extensions (.txt/.json/.bin), and binary_encoding options."
---

TigerFS serializes PostgreSQL rows through the `format` package and exposes them as mount paths: row-as-file (`123.tsv`, `123.json`), row-as-directory column files (`email.txt`, `avatar.bin`), and pipeline exports under `.export/`. Reads always query the database; writes route through `fs.Operations` and `format.Parse*` helpers with either PATCH (format suffix) or PUT (bare path) semantics.

## Format overview

| Format | Row read path | Row write (PATCH) | Bulk export (default) | Bulk import |
|--------|---------------|-------------------|----------------------|-------------|
| TSV | `table/pk.tsv` or `table/pk/.tsv` | Header line + value line | No header row | Header row required (or `.no-headers/`) |
| CSV | `table/pk.csv` or `table/pk/.csv` | Header line + value line | No header row (RFC 4180 quoting) | Header row required (or `.no-headers/`) |
| JSON | `table/pk.json` or `table/pk/.json` | Object keys = columns | Pretty-printed array | JSON array of objects |
| YAML | `table/pk.yaml` or `table/pk/.yaml` | `key: value` document | Multi-document (`---` per row) | Multi-document YAML |

Default row serialization when no extension is given is TSV. Config key `formats.default_format` (`default_format` in `Config`) accepts `tsv`, `csv`, or `json` (default `tsv`); YAML is selected only by the `.yaml` path suffix.

```text
  PostgreSQL row
        │
        ▼
  format.RowTo* / format.RowsTo*     ← read (export, row file, column text)
        │
        ▼
  mount path bytes

  mount write bytes
        │
        ▼
  format.Parse* / parseWriteData
        │
        ▼
  db.UpdateRow / InsertRow / Import*
```

## Row-as-file encoding

### TSV (default)

**Read:** One line, tab-separated values in **schema column order**, trailing newline, **no header row**. NULL → empty field (consecutive tabs for adjacent NULLs).

**Write (`.tsv` suffix, PATCH):** Exactly two lines after trimming trailing empty lines: line 1 = tab-separated column names, line 2 = tab-separated values. Only columns listed in the header are updated. More than two non-empty lines returns a parse error.

**Bare path (no extension, PUT):** Single line of tab-separated values in full schema column order (all columns). Used for paths like `table/123` without a format suffix; new row inserts require a format suffix on macOS NFS.

### CSV

Same layout as TSV but comma-delimited and **RFC 4180** quoting: fields containing `,`, `"`, or newlines are double-quoted; internal quotes are doubled.

**Read:** Headerless, schema order, NULL = empty field.

**Write (`.csv`, PATCH):** Header row + value row via `csv.Reader` (max two records).

### JSON

**Read:** Single compact object per row (`json.Encoder` with `SetEscapeHTML(false)`). NULL → JSON `null`. Booleans are JSON `true`/`false` (not PostgreSQL `t`/`f`). Numbers and strings use JSON-native types where possible (`NormalizeForJSON`).

**Multi-row export** (`.export/json`): Indented JSON array (`SetIndent("", "  ")`), one object per row.

**Write (`.json`, PATCH):** Any JSON object; only keys present are updated. Omitted keys are left unchanged (not set to NULL). Explicit `null` sets NULL.

### YAML

**Read:** Document starts with `---`, then `column: value` lines per row. NULL → literal `null`. Scalars use `ConvertValueToText` then YAML quoting rules (reserved words like `null`, `true`, special characters, leading/trailing space).

**Multi-row export:** Each row is a separate document concatenated (each begins with `---`).

**Write (`.yaml`, PATCH):** Parsed as a single mapping; keys determine updated columns (same PATCH model as JSON).

## PATCH vs PUT write semantics

<Note>
All paths with an explicit format extension (`.tsv`, `.csv`, `.json`, `.yaml`) use **PATCH**: only named columns change; others keep their stored values.
</Note>

| Path pattern | Semantics | Parser |
|--------------|-----------|--------|
| `table/pk.json`, `table/pk.yaml` | PATCH | `ParseJSON`, `ParseYAML` |
| `table/pk.tsv`, `table/pk.csv` | PATCH | `ParseTSVWithHeader`, `ParseCSVWithHeader` |
| `table/pk` (no extension) | PUT — all columns in schema order | `ParseTSV` + schema column list |

Empty body with a format suffix is valid for insert paths (row created with PK from path and column defaults).

New rows via bare path without a format suffix are rejected (`newBarePathRejection`) to avoid NFS inode conflicts; use `.tsv`, `.json`, etc. PK columns from the path are merged into INSERT when omitted from the body.

**Example (PATCH JSON):**

```bash
echo '{"email":"new@example.com"}' > /mount/users/123.json
# UPDATE users SET email = ... WHERE id = 123; name unchanged
```

**Example (PATCH TSV):**

```bash
printf 'email\tname\nnew@example.com\tNew Name\n' > /mount/users/123.tsv
```

## NULL representation

| Surface | NULL on read | NULL on write | Notes |
|---------|--------------|---------------|-------|
| TSV / CSV row or export | Empty field | Empty field in value row | `,,` or `\t\t` between values |
| JSON row / export | `null` | `null` or omitted key | Omitted key on PATCH does **not** clear the column |
| YAML row / export | `null` | `null` | |
| Column file | 0-byte file | `rm column` sets NULL | Writing `""` stores empty string, not NULL |
| Bulk import | Empty field / JSON `null` | Same parsers as export | |

## Value conversion (`ConvertValueToText`)

Shared text rules for TSV, CSV, YAML scalars, and column files:

| PostgreSQL type | Text form |
|-----------------|-----------|
| `boolean` | `t` / `f` |
| `timestamp` / `time` | RFC3339Nano |
| `json` / `jsonb` | Compact JSON string |
| Arrays | JSON array string |
| `uuid` | Standard UUID string |
| `numeric` (pgtype) | JSON-marshaled decimal when available |
| `NULL` | Empty string |

JSON row encoding uses `NormalizeForJSON` instead, preserving JSON booleans and numeric types where compatible.

## Column filename extensions

Type-driven extensions map PostgreSQL types to suffixes; parameterized types strip the length suffix first (`varchar(255)` → `.txt`).

| PostgreSQL type | Extension | Example filename |
|-----------------|-----------|------------------|
| `text`, `varchar`, `char`, `bpchar` | `.txt` | `name.txt` |
| `json`, `jsonb` | `.json` | `metadata.json` |
| `xml` | `.xml` | `config.xml` |
| `bytea` | `.bin` | `avatar.bin` |
| `geometry`, `geography` | `.wkb` | `location.wkb` |
| Integer, numeric, boolean, date, arrays, etc. | (none) | `age`, `tags` |

`FindColumnByFilename` accepts either `column` or `column.ext` when the extension matches the column type. Disable extensions with `--no-filename-extensions` or `no_filename_extensions: true` (exact column names only).

Within a row directory, dot-prefixed format aliases expose the full row: `.json`, `.csv`, `.tsv`, `.yaml` (same encoding as top-level row files).

## `binary_encoding` configuration

<ParamField body="binary_encoding" type="string" default="raw">
BYTEA representation for filesystem I/O. Valid values: `raw`, `base64`, `hex`. Set in `~/.config/tigerfs/config.yaml` under `formats.binary_encoding`, via `TIGERFS_BINARY_ENCODING`, or validated at config load.
</ParamField>

| Value | Intended use |
|-------|----------------|
| `raw` | BYTEA column files contain raw bytes (default; matches current column read/write) |
| `base64` | Config-valid; not wired in `format` or `fs.Operations` read/write paths today |
| `hex` | Config-valid; not wired in `format` or `fs.Operations` read/write paths today |

For binary column files today, read and write pass through byte content directly (`.bin` suffix is a naming hint). Use external tools (`base64`, `xxd`) to transform raw bytes when needed.

<Warning>
Synthesized file-first workspaces use a separate `encoding` column (`utf8` | `base64`) on backing tables in the `tigerfs` schema — not the mount-level `binary_encoding` setting.
</Warning>

## Bulk export and import

### Export (`.export/`)

`readExportFile` serializes pipeline-filtered rows:

| Path | Output |
|------|--------|
| `table/.export/json` | `RowsToJSON` — indented array |
| `table/.export/csv` | `RowsToCSV` — data rows only |
| `table/.export/.with-headers/csv` | `RowsToCSVWithHeaders` |
| `table/.export/tsv` (or default) | `RowsToTSV` or `RowsToTSVWithHeaders` |
| `table/.export/yaml` | `RowsToYAML` — multi-document |

Pipeline context (`.filter/`, `.order/`, `.first/`, `.columns/`, etc.) applies before serialization. Default row cap uses `dir_listing_limit` when no `.first`/`.last` limit is set.

### Import (`.import/`)

| Mode | Path segment | Behavior |
|------|--------------|----------|
| Append | `.import/.append/<fmt>` | Insert only; PK conflict fails |
| Sync | `.import/.sync/<fmt>` | Upsert by primary key |
| Overwrite | `.import/.overwrite/<fmt>` | Delete all rows, then insert |

Parsers: `ParseCSVBulk`, `ParseTSVBulk`, `ParseJSONBulk`, `ParseYAMLBulk` (header/document rules as above). `.import/.../.no-headers/csv` or `.no-headers/tsv` uses schema column order via `ParseCSVBulkNoHeaders` / `ParseTSVBulkNoHeaders`. Default import format when unspecified is `csv`.

## Primary key and path caveats

Text primary keys that end with `.json`, `.csv`, `.tsv`, or `.yaml` are parsed as format extensions (e.g. PK `config` + format `json` for path `config.json`). Integer and UUID keys are unaffected.

Row files (`1.json`) are reachable by direct path but omitted from `ls` at the table level to keep listings small; row directories (`1/`) appear in listings.

## Configuration quick reference

| Key | Env var | Default | Applies to |
|-----|---------|---------|------------|
| `formats.default_format` | `TIGERFS_DEFAULT_FORMAT` | `tsv` | Default row format preference (`tsv`, `csv`, `json`) |
| `formats.binary_encoding` | `TIGERFS_BINARY_ENCODING` | `raw` | BYTEA (`raw`, `base64`, `hex` — see above) |
| `filesystem.no_filename_extensions` | `TIGERFS_NO_FILENAME_EXTENSIONS` | `false` | Column extension mapping |
| `filesystem.trailing_newlines` | `TIGERFS_TRAILING_NEWLINES` | `true` | Column and `.info/count` reads |

## Related pages

<CardGroup>
  <Card title="Data-first exploration" href="/data-first-exploration">
    Row and column reads, PATCH writes, import/export paths, and pipeline pagination.
  </Card>
  <Card title="Capability directories" href="/capability-directories">
    `.export/`, `.import/`, `.columns/`, and pipeline chaining that shapes bulk format output.
  </Card>
  <Card title="Filesystem as API" href="/filesystem-as-api">
    Mount hierarchy, row-as-file vs row-as-directory, and path-to-SQL mapping.
  </Card>
  <Card title="Configuration reference" href="/configuration-reference">
    Full `Config` fields, precedence, and environment variable names.
  </Card>
</CardGroup>

---

## 19. Connection reference

> postgres:// URIs, PG* env vars, password_command, .pgpass, sslmode=require rules, localhost exemptions, and pool settings.

- Page Markdown: https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/19-connection-reference.md
- Generated: 2026-06-03T08:02:25.722Z

### Source Files

- `internal/tigerfs/db/connection.go`
- `internal/tigerfs/db/password.go`
- `internal/tigerfs/db/tls.go`
- `internal/tigerfs/cmd/test_connection.go`
- `docs/spec.md`

---
title: "Connection reference"
description: "postgres:// URIs, PG* env vars, password_command, .pgpass, sslmode=require rules, localhost exemptions, and pool settings."
---

TigerFS opens PostgreSQL through `db.NewClient`, which resolves credentials, enforces TLS on connection strings, and builds a `pgxpool` pool used by every mount backend. Connection strings come from CLI arguments, cloud backend CLIs (`tiger:`, `ghost:`), or standard `PG*` / `TIGERFS_*` configuration; `tigerfs test-connection` additionally uses `db.ResolveConnectionString` to assemble a URL from config when no argument is passed.

## Connection string formats

TigerFS accepts the same connection string shapes as [pgx](https://github.com/jackc/pgx): URL form and libpq key-value form.

| Format | Example | Notes |
|--------|---------|-------|
| URL | `postgres://user@host:5432/dbname` | Preferred for mounts; supports query params (`?sslmode=require`) |
| URL (alternate scheme) | `postgresql://user@host/db` | Treated identically to `postgres://` |
| Key-value | `host=db.example.com user=myuser dbname=mydb` | Parsed for TLS host extraction and `sslmode` injection |

URL structure:

```text
postgres://[user[:password]@]host[:port][/dbname][?param=value&...]
```

<Warning>
Passwords embedded in URLs appear in process listings and logs. `db.SanitizeConnectionString` strips URL passwords and masks `password=` in key-value strings before logging; prefer `user@host` URLs plus a separate secret source.
</Warning>

## How TigerFS picks a connection target

Resolution differs by command.

### Mount (`tigerfs mount`)

| Input | `connStr` passed to `NewClient` | Mountpoint |
|-------|----------------------------------|------------|
| `tigerfs mount postgres://user@host/db /mnt` | `postgres://user@host/db` | `/mnt` |
| `tigerfs mount tiger:ID` | Resolved `postgres://…` from Tiger CLI | `<default_mount_dir>/ID` |
| `tigerfs mount /mnt` (no URL) | `""` (empty; pgx uses `PG*` env vars) | `/mnt` |

Cloud prefixes are resolved in `cmd/mount.go` via `backend.Resolve` → `GetConnectionString` before connect.

### `test-connection` and `migrate`

These commands call `db.ResolveConnectionString` when no explicit argument is given:

1. Explicit connection argument (if any)
2. `TIGER_SERVICE_ID` / `connection.tiger_service_id` → Tiger Cloud CLI
3. Config `host` (from `PGHOST` / config file) → builds `postgres://{user}@{host}:{port}/{database}`

If none apply, resolution fails with an error asking for a connection string, Tiger service ID, or `PGHOST`.

```mermaid
sequenceDiagram
  participant CLI as tigerfs CLI
  participant Resolve as db.ResolveConnectionString
  participant NC as db.NewClient
  participant Pool as pgxpool

  CLI->>Resolve: explicit / config / Tiger Cloud
  Resolve-->>CLI: postgres URL or error
  CLI->>NC: connStr + Config
  NC->>NC: resolvePassword
  NC->>NC: enforceSSLMode
  NC->>Pool: ParseConfig, Ping
  Pool-->>NC: ready
```

<Note>
For remote hosts, pass an explicit `postgres://user@remote-host:5432/db` on **mount** so `enforceSSLMode` can read the hostname from the URL. An empty mount connection string is treated as localhost for TLS defaults (see [TLS enforcement](#tls-enforcement)); pgx may still connect using `PGHOST` from the environment.
</Note>

## PostgreSQL environment variables

Viper binds standard libpq variables into `config.Config` at startup (`config.Init`):

| Variable | Config field | Default when unset |
|----------|--------------|-------------------|
| `PGHOST` | `host` | (empty) |
| `PGPORT` | `port` | `5432` |
| `PGUSER` | `user` | (empty) |
| `PGDATABASE` | `database` | (empty) |
| `PGPASSWORD` | `password` | (empty; not used directly by `NewClient`) |

TigerFS-prefixed overrides use `TIGERFS_` + map key (e.g. `TIGERFS_PORT` overrides `PGPORT` when both are set).

Additional Tiger connection env vars:

| Variable | Purpose |
|----------|---------|
| `TIGERFS_PASSWORD` | Database password (after `PGPASSWORD` in precedence) |
| `TIGERFS_PASSWORD_COMMAND` | Shell command whose stdout is the password |
| `TIGERFS_POOL_SIZE` | Maps to `pool_size` (default `10`) |
| `TIGERFS_POOL_MAX_IDLE` | Maps to `pool_max_idle` (default `5`) |
| `TIGERFS_INSECURE_NO_SSL` | Skips remote TLS enforcement when `true` |
| `TIGER_SERVICE_ID` | Legacy Tiger Cloud service fallback in `ResolveConnectionString` |

<Info>
The `password` field exists on `Config` for display and viper binding, but `db.resolvePassword` reads `PGPASSWORD` / `TIGERFS_PASSWORD` from the environment only—not `cfg.Password`. Do not put passwords in `config.yaml`; use `.pgpass`, env vars, or `password_command`.
</Info>

## Password resolution

`db.NewClient` resolves secrets before `pgxpool.ParseConfig`:

```mermaid
flowchart TD
  A[Connection string already contains password?] -->|yes| B[pgx uses URL password as-is]
  A -->|no| C{PGPASSWORD set?}
  C -->|yes| D[Inject into URL / append key-value]
  C -->|no| E{TIGERFS_PASSWORD set?}
  E -->|yes| D
  E -->|no| F{password_command configured?}
  F -->|yes| G[Run command, inject stdout]
  F -->|no| H[pgx reads ~/.pgpass]
```

| Priority | Source | Behavior |
|----------|--------|----------|
| 1 (implicit) | Password in `postgres://user:pass@…` | Left unchanged; env/command not consulted |
| 2 | `PGPASSWORD` | Injected into URL; wins over `TIGERFS_PASSWORD` and `password_command` |
| 3 | `TIGERFS_PASSWORD` | Injected when `PGPASSWORD` unset |
| 4 | `password_command` | Executed when env passwords unset |
| 5 | `~/.pgpass` | Handled by pgx when no injected password |

### `password_command`

Configure in `~/.config/tigerfs/config.yaml`:

```yaml
connection:
  password_command: "vault kv get -field=password secret/prod-db"
```

Or via `TIGERFS_PASSWORD_COMMAND`.

<ParamField body="password_command" type="string">
Shell command run to fetch the database password. First token is the executable; remaining tokens are arguments (`strings.Fields`—no quoted-argument parsing). Stdout is trimmed; must be non-empty. Context timeout: **5 seconds**. Non-zero exit includes stderr in the error.
</ParamField>

<RequestExample>

```bash
export TIGERFS_PASSWORD_COMMAND='pass show tigerfs/prod'
tigerfs test-connection postgres://app@db.example.com/prod
```

</RequestExample>

### `.pgpass`

TigerFS does not parse `.pgpass` itself. When no password is injected, [pgx](https://github.com/jackc/pgx) reads `~/.pgpass` using the usual libpq rules.

```text
hostname:port:database:username:password
```

```bash
chmod 0600 ~/.pgpass
tigerfs mount postgres://myuser@localhost:5432/mydb /mnt/db
```

### Password injection rules

- **URL:** Requires `user@host`; replaces or adds `user:password@`.
- **Key-value:** Appends `password=…` only if `password=` is absent; existing key-value passwords are not overwritten.

If `PGPASSWORD` is set but the connection string has no `user@host` segment, injection fails with `connection string has no user@host format`.

## TLS enforcement

`db.enforceSSLMode` runs on every `NewClient` call after password handling.

### Remote hosts (non-localhost)

| Current `sslmode` | Action |
|-------------------|--------|
| (missing) | Add `sslmode=require` |
| `disable`, `prefer` | Replace with `sslmode=require` |
| `require`, `verify-ca`, `verify-full` | Unchanged |

### Localhost exemption

Hosts treated as local (no forced `require`):

- `localhost`, `127.0.0.1`, `::1`, empty host
- Unix socket paths (host starts with `/`)

When no `sslmode` is present on a local connection, TigerFS adds **`sslmode=disable`** (avoids pgx `prefer` handshake issues on `ssl=off` clusters). An explicit `sslmode` in the connection string is always preserved on localhost (e.g. `sslmode=require` on `localhost` stays as written).

### Disabling enforcement

<ParamField body="insecure_no_ssl" type="boolean" default="false">
CLI: `--insecure-no-ssl` on `mount` and `migrate`. Config: `connection.insecure_no_ssl` or `TIGERFS_INSECURE_NO_SSL=true`. Skips TLS rewriting; logs a warning for remote hosts.
</ParamField>

<Warning>
`--insecure-no-ssl` allows plaintext connections to remote databases. Use only for local development or trusted networks.
</Warning>

## Connection pool settings

Each mount daemon holds one `pgxpool.Pool` for the lifetime of the process.

| Setting | Config key | Env var | Default | pgx mapping |
|---------|------------|---------|---------|-------------|
| Max connections | `connection.pool_size` | `TIGERFS_POOL_SIZE` | `10` | `MaxConns` |
| Min idle connections | `connection.pool_max_idle` | `TIGERFS_POOL_MAX_IDLE` | `5` | `MinConns` |

Validation (`tigerfs config` write path): `pool_size` ≥ 1; `pool_max_idle` ≥ 0.

Additional pool behavior in `db.NewClient`:

- **Ping:** Caller context must succeed before the client is returned.
- **Pool creation:** Uses `context.Background()` so short mount timeouts do not cancel background pool maintenance.
- **`AfterConnect`:** Sets PostgreSQL `statement_timeout` from `query_timeout` when configured (default 30s in config).
- **Tracing:** Optional SQL debug tracing via `log_sql_params`.
- **Shutdown:** `Client.Close()` closes the pool on unmount.

Concurrent filesystem operations share the pool; server-side poolers (PgBouncer, etc.) work transparently.

<Tip>
Size `pool_size` to your database `max_connections` budget across all TigerFS mounts on the same instance. Each mount process opens up to `pool_size` connections.
</Tip>

## Verify connectivity

```bash
tigerfs test-connection postgres://user@localhost:5432/mydb
```

With only environment configuration:

```bash
export PGHOST=localhost PGDATABASE=mydb PGUSER=myuser
tigerfs test-connection
```

Successful output includes PostgreSQL version, current database/user, and counts of accessible schemas and tables (system/internal schemas excluded).

On failure, check stderr for structured zap hints (TLS, `password_command`, parse errors) before errno from mount tools.

## Security practices

| Practice | Recommendation |
|----------|----------------|
| Production secrets | `~/.pgpass` or `password_command` |
| Development / CI | `PGPASSWORD` or `TIGERFS_PASSWORD` |
| Remote encryption | Explicit `postgres://…` URL; avoid relying on empty-string mount + `PGHOST` for TLS defaults |
| Config file | Never store `password:` in `config.yaml` |
| Logging | Passwords stripped via `SanitizeConnectionString` in registry and debug logs |

## Related pages

<CardGroup>
  <Card title="Configuration reference" href="/configuration-reference">
    Full config file layout, precedence, and TigerFS-specific env vars.
  </Card>
  <Card title="Cloud backends" href="/cloud-backends">
    tiger: and ghost: prefix resolution and CLI credential requirements.
  </Card>
  <Card title="CLI reference" href="/cli-reference">
    mount, test-connection, migrate flags and argument patterns.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    Connection failures, TLS errors, and errno recovery.
  </Card>
</CardGroup>

---

## 20. Error codes

> fs.ErrorCode to POSIX errno mapping, structured zap hints on failure, constraint violations, and reading stderr when tools report EINVAL/EACCES.

- Page Markdown: https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/20-error-codes.md
- Generated: 2026-06-03T08:01:19.438Z

### Source Files

- `internal/tigerfs/fs/errors.go`
- `internal/tigerfs/fs/errors_test.go`
- `internal/tigerfs/fuse/control_files.go`
- `internal/tigerfs/logging/logging.go`
- `docs/spec.md`

---
title: "Error codes"
description: "fs.ErrorCode to POSIX errno mapping, structured zap hints on failure, constraint violations, and reading stderr when tools report EINVAL/EACCES."
---

TigerFS surfaces filesystem failures as POSIX `errno` values to shells and tools (`mv`, `cp`, `echo > file`), while the shared `fs` package classifies errors with backend-agnostic `ErrorCode` values wrapped in `*FSError`. FUSE and NFS adapters translate `FSError` to `syscall.Errno` or `os` errors; structured detail—including optional `Hint` fields—is written to **stderr** via zap before the syscall returns.

## Error flow

FUSE and NFS cannot return arbitrary error strings to callers. The design logs human-oriented detail first, then returns a standard errno.

```mermaid
sequenceDiagram
    participant Tool as Shell / tool
    participant Adapter as fuse.FSAdapter or nfs.OpsFilesystem
    participant Ops as fs.Operations
    participant DB as PostgreSQL

    Tool->>Adapter: VFS op (write, rename, stat)
    Adapter->>Ops: ReadDir / Stat / WriteFile / ...
    Ops->>DB: SQL (when needed)
    DB-->>Ops: success or driver error
    Ops-->>Adapter: nil or *FSError
    Note over Adapter: logging.Error / Debug<br/>with hint, message, cause
    Adapter-->>Tool: errno (e.g. EINVAL, EACCES)
```

On Linux (FUSE), `FSAdapter.ErrorToErrno` is the canonical mapping used by migrated `OpsNode` paths and adapter helpers. macOS (NFS) maps a subset of codes to `os.ErrNotExist`, `os.ErrPermission`, and `os.ErrInvalid`; other `FSError` codes may collapse differently at the NFS layer.

## `ErrorCode` and `FSError`

`ErrorCode` is an `int` enum defined in `internal/tigerfs/fs/errors.go`. Each constant documents its intended POSIX mapping in comments; adapters implement that mapping in `fuse/adapter.go`.

| `ErrorCode` | Constant | POSIX errno | Typical causes |
|-------------|----------|-------------|----------------|
| 0 | `ErrNone` | success (`0`) | No error |
| 1 | `ErrNotExist` | `ENOENT` | Missing path, row, column, or DDL session |
| 2 | `ErrPermission` | `EACCES` | Read-only view, reserved TigerFS name, immutable `.history/`, undo across migration boundary, DB privilege denial |
| 3 | `ErrInvalidPath` | `EINVAL` | Malformed pipeline path, wrong capability order, invalid DDL path |
| 4 | `ErrInvalidFormat` | `EINVAL` | Unparseable row file (JSON/TSV/CSV/YAML) |
| 5 | `ErrInvalidOperation` | `EPERM` | Operation not allowed (e.g. `Readlink` on non-symlink) |
| 6 | `ErrReadOnly` | `EROFS` | Write on read-only mount |
| 7 | `ErrNotEmpty` | `ENOTEMPTY` | `rmdir` on non-empty directory |
| 8 | `ErrAlreadyExists` | `EEXIST` | Path or row already exists (`ErrExists` alias) |
| 9 | `ErrIO` | `EIO` | DB connection/query failure, unexpected SQL error, many wrapped `Cause` errors |
| 10 | `ErrInternal` | `EIO` | Unexpected internal failure |
| 11 | `ErrNotImplemented` | `ENOSYS` | Feature not implemented |
| 12 | `ErrInvalidArgument` | `EINVAL` | Invalid argument value (grouped with path/format errors in FUSE) |

`FSError` carries programmatic and user-facing fields:

| Field | Role |
|-------|------|
| `Code` | `ErrorCode` for adapter mapping |
| `Message` | Short description; also `error.Error()` when no `Cause` |
| `Cause` | Underlying error (`Unwrap` for `errors.Is` / `As`) |
| `Hint` | Actionable guidance logged to stderr before errno return |

Constructors such as `NewNotExistError`, `NewPermissionError`, and `NewInvalidPathError` populate `Message`; `NewInvalidPathError` also copies `reason` into `Hint`.

<Note>
`ErrNotExist` is treated as a normal outcome for many lookups (tab completion, `stat` before `mkdir`). `ErrorToErrno` logs it at **debug** only, not `Error`, to avoid noise.
</Note>

## FUSE mapping (`ErrorToErrno`)

`fuse.FSAdapter.ErrorToErrno` converts `*FSError` to `syscall.Errno` and applies logging rules:

| Condition | Log level | Fields |
|-----------|-----------|--------|
| `Code == ErrNotExist` | `logging.Debug` | `message`, optional `hint`, `cause` |
| `Hint != ""` (other codes) | `logging.Error` | `message`, `hint`, `cause` |
| `Cause != nil`, no hint | `logging.Debug` | `message`, `cause` |
| Otherwise | (no log) | — |

Mapping switch (default unknown codes → `EIO`):

```
ErrNotExist          → ENOENT
ErrPermission        → EACCES
ErrInvalidPath,
  ErrInvalidFormat,
  ErrInvalidArgument → EINVAL
ErrInvalidOperation  → EPERM
ErrReadOnly          → EROFS
ErrNotEmpty          → ENOTEMPTY
ErrAlreadyExists     → EEXIST
ErrIO, ErrInternal   → EIO
ErrNotImplemented    → ENOSYS
(default)            → EIO
```

Unit tests in `fuse/adapter_test.go` lock these mappings for the covered codes.

## NFS mapping differences

`nfs.OpsFilesystem` does not call `ErrorToErrno`. For `Stat`, `ReadDir`, and `Rename` it maps:

| `ErrorCode` | Go error returned |
|-------------|-------------------|
| `ErrNotExist` | `os.ErrNotExist` |
| `ErrPermission` | `os.ErrPermission` |
| `ErrInvalidPath`, `ErrInvalidArgument` | `os.ErrInvalid` |
| Other (including `ErrIO`, `ErrInvalidFormat`) | Often `os.ErrNotExist` on directory ops, or wrapped `fmt.Errorf` on writes |

Create/Open paths log `message`, `hint`, and `cause` at `Error` and return `os.ErrInvalid` when `ValidateCreate` rejects a path (e.g. bare row path without format suffix on macOS).

<Warning>
Do not assume NFS and FUSE return identical errno for every `FSError`. Prefer stderr logs and `Hint` when diagnosing macOS mounts.
</Warning>

## PostgreSQL and constraint violations

The specification (`docs/spec.md`) documents the user-visible contract: standard errno at the syscall boundary, full PostgreSQL detail in logs.

| PostgreSQL / domain situation | Spec errno | `ErrorCode` in shared `fs` layer (when used) |
|------------------------------|------------|-----------------------------------------------|
| Row not found | `ENOENT` | `ErrNotExist` |
| Table/view privilege denied | `EACCES` | `ErrPermission` |
| NOT NULL / FK / check violation | `EACCES` (generic message to tool) | Often `ErrIO` with `Cause` = driver error on `WriteFile`/`InsertRow`; legacy FUSE nodes may map substring `"constraint"` → `EACCES` |
| Connection / timeout / other SQL | `EIO` | `ErrIO` + `Cause` |

**Pre-insert validation:** `db.ValidateConstraints` checks NOT NULL (no default) and UNIQUE before some incremental row commits (`fuse/partial.go`). Violations return Go errors with messages like `NOT NULL constraint violation on column email`; these are logged at `Error` in partial-row commit paths.

**Legacy FUSE paths** (still present alongside `fs.Operations`):

- `TableNode` delete/rmdir: errors containing `foreign key` or `constraint` → `EACCES`
- `RowDirectoryNode` unlink on NOT NULL column without default → `EACCES`
- `RowFileNode` flush failures → `EIO` (constraint text only in logs)

**Shared `fs.Operations` path:** many database failures become `ErrIO` with the driver error in `Cause` (e.g. `failed to insert row`). Tools see `EIO`; read stderr for `null value in column ... violates not-null constraint` and similar.

<Info>
Example spec workflow: `echo 'Name' > /mnt/db/users/125/name` when `email` is NOT NULL may return exit code **13** (`EACCES`) on some code paths, while the mount process logs the real constraint message on stderr.
</Info>

## `Hint` field patterns

`Hint` is set at the point of failure so adapters can log it before returning errno. Common sources:

| Area | Example hint |
|------|----------------|
| Path parsing (`fs/path.go`) | `filters must come before .order/ in the path` |
| Row create validation | `retry as "1.json", "1.tsv", or "1.csv" (bare-path writes conflict ...)` |
| DDL staging | `run validation with: touch /.create/<name>/.test` |
| Synth reserved names | `choose a different filename to avoid collision with TigerFS control directories` |
| History / undo | `history files are archived versions and cannot be modified`; migration boundary text from metadata `Description` |
| File-first routing | `use mount/.tables/<table>/... for data-first access to the backing table` |

Reserved TigerFS capability names (`.filter`, `.history`, `.undo`, etc.) return `ErrPermission` → `EACCES`, matching the spec reserved-name rule.

## Logging and reading stderr

The `tigerfs` CLI initializes zap in `logging.Init` from `--log-level` (`debug`, `info`, `warn`, `error`; default `warn`). Logs go to **stderr** unless `--log-file` is set. `--log-format json` emits structured lines; production-style configs use a compact JSON shape with a `message` key.

<Steps>
<Step title="Reproduce with verbose logging">
Run the mount in the foreground with debug logging:

```bash
tigerfs mount --foreground --log-level debug postgres://host/db /mnt/db
```

</Step>
<Step title="Run the failing command in another terminal">
```bash
mv 1-foo-o.md a-foo-o.md
# or: echo 'x' > /mnt/db/users/125/name
echo $?   # 13 = EACCES, 22 = EINVAL
```

</Step>
<Step title="Read the mount process stderr">
Look for JSON or text lines immediately before the tool’s errno message:

```json
{"message":"invalid path ...","hint":"limit must be a positive integer"}
```

When `Hint` is set, `ErrorToErrno` logs at `Error` with `hint` as a zap field. The shell only prints the generic errno string (e.g. `Invalid argument`, `Permission denied`).

</Step>
</Steps>

| Tool exit / errno | Value | Often means |
|-------------------|-------|-------------|
| `EINVAL` | 22 | Bad path grammar, bad format, invalid create path, NFS `ValidateCreate` rejection |
| `EACCES` | 13 | Permission, read-only view, reserved name, constraint (legacy paths), undo boundary |
| `EPERM` | 1 | `ErrInvalidOperation` (wrong op type) |
| `ENOENT` | 2 | Missing file/row/column |
| `EIO` | 5 | DB/network failure; check `Cause` in logs |
| `ENOSYS` | 38 | Not implemented |

<Tip>
FUSE returns errno only—never SQL text. Always correlate the user command with the **tigerfs** process stderr from the same time window, not only the shell’s one-line `mv: ...` message.
</Tip>

## errno quick reference for agents

When automating against a mount:

1. Treat non-zero exit codes as `errno`, not as PostgreSQL error codes.
2. On `EINVAL` or `EACCES`, search mount stderr for `"hint"` or `"message"` (JSON) or the plain message (text format).
3. Prefer fixing path grammar and capability order (see capability directories docs) before retrying writes.
4. On `EIO`, inspect `Cause` in logs for connection, timeout, or constraint strings from the driver.
5. Use `ErrNotExist` outcomes for existence checks without treating them as mount failures.

## Related pages

<CardGroup>
<Card title="Filesystem as API" href="/filesystem-as-api">
Path types, dot-directories, and how operations become SQL.
</Card>
<Card title="Capability directories" href="/capability-directories">
Pipeline grammar errors that surface as EINVAL with path hints.
</Card>
<Card title="DDL staging" href="/ddl-staging">
`.test` / `.commit` validation errno and staging control files.
</Card>
<Card title="History, savepoints, and undo" href="/history-savepoints-undo">
Undo boundary refusals (ErrPermission / EACCES) and migration metadata.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
Stale mounts, `--log-level debug`, and common errno recovery.
</Card>
<Card title="Develop and test" href="/develop-and-test">
`adapter_test.go`, integration tests, and error regression coverage.
</Card>
</CardGroup>

---

## 21. Demo environment

> scripts/demo workflows (Docker vs macOS native), seed data, demo.sh start/shell/stop, and sample exploration commands.

- Page Markdown: https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/21-demo-environment.md
- Generated: 2026-06-03T08:01:27.061Z

### Source Files

- `scripts/demo/demo.sh`
- `scripts/demo/README.md`
- `scripts/demo/init.sql`
- `scripts/demo/seed.sh`
- `test/integration/demo_data.go`

---
title: "Demo environment"
description: "scripts/demo workflows (Docker vs macOS native), seed data, demo.sh start/shell/stop, and sample exploration commands."
---

The `scripts/demo/` tree provides a self-contained TigerFS playground: PostgreSQL loads relational seed data from `init.sql`, `demo.sh` mounts the database, and `seed.sh` creates synthesized file-first apps through the real `.build/` mount path.

## Layout

```text
scripts/demo/
  demo.sh                 # start | stop | status | restart | shell
  init.sql                # data-first tables (PostgreSQL init)
  seed.sh                 # file-first apps (via mount)
  README.md
  docker/
    docker-compose.yml    # postgres + tigerfs (FUSE)
    Dockerfile            # TigerFS image + tooling
  mac/
    docker-compose.yml    # postgres only (TigerFS on host)
```

## Runtime modes

`demo.sh` picks a mode from flags or the host OS. Darwin defaults to macOS native NFS; other platforms default to Docker FUSE.

| Mode | Flag | PostgreSQL | TigerFS | Mount path | Connection string |
|------|------|------------|---------|------------|-------------------|
| Docker | `--docker` (default on Linux) | Container | Container (FUSE) | `/mnt/db` (inside `tigerfs`) | `postgres://demo:demo@postgres:5432/demo` |
| macOS native | `--mac` (default on macOS) | Container on `localhost:5432` | Host binary (`bin/tigerfs`) | `/tmp/tigerfs-demo` | `postgres://demo:demo@localhost:5432/demo` |

Both modes mount with `tigerfs mount --insecure-no-ssl` (demo-only TLS bypass for localhost/Docker). Pass `--debug` on any command to add `--log-level debug` to the mount.

```mermaid
sequenceDiagram
  participant User
  participant demo_sh as demo.sh
  participant Compose as docker compose
  participant PG as postgres (timescaledb-ha:pg18)
  participant TFS as TigerFS
  participant Seed as seed.sh

  User->>demo_sh: start
  demo_sh->>Compose: up -d
  Compose->>PG: init.sql on first boot
  demo_sh->>PG: wait pg_isready
  demo_sh->>TFS: mount CONN_STR MOUNTPOINT
  demo_sh->>Seed: seed.sh MOUNTPOINT
  Seed->>TFS: .build/* + file writes
```

## Commands

Run all commands from `scripts/demo/` (repository-relative paths below assume that cwd).

<Steps>
<Step title="Start">
```bash
./demo.sh start
# or: ./demo.sh start --docker
# or: ./demo.sh start --mac
```

Starts PostgreSQL, waits for init (`PostgreSQL init process complete` in logs, then `pg_isready`), mounts TigerFS, and runs `seed.sh`. Exits with an error if the mount is already active.
</Step>
<Step title="Explore">
On macOS, use the host mount directly:

```bash
ls /tmp/tigerfs-demo/
cat /tmp/tigerfs-demo/users/1.json
```

In Docker mode, enter the container at the mount root:

```bash
./demo.sh shell
# working directory: /mnt/db
ls
cat users/1.json
```
</Step>
<Step title="Stop">
```bash
./demo.sh stop
```

Docker: `umount` inside the container, then `docker compose down`. macOS: `pkill` tigerfs for the mountpoint, `umount` / `diskutil unmount force` on `/tmp/tigerfs-demo`, then stop the postgres container.
</Step>
</Steps>

| Command | Behavior |
|---------|----------|
| `start` | Compose up, wait for DB, mount, seed file-first apps |
| `stop` | Unmount TigerFS, tear down containers / processes |
| `status` | Report container and mount state |
| `restart` | `stop`, pause, `start` |
| `shell` | Interactive shell at mount root (`/mnt/db` or `/tmp/tigerfs-demo`) |

<Warning>
`start` refuses to run if TigerFS is already mounted at the mode’s mountpoint. Run `./demo.sh stop` first.
</Warning>

## Seed data

Demo content splits into two phases: SQL at database init, filesystem writes after mount.

### Data-first (`init.sql`)

Loaded by PostgreSQL via `docker-entrypoint-initdb.d/init.sql` on first container start (`timescale/timescaledb-ha:pg18`, user/password/database `demo`).

| Object | Rows / notes |
|--------|----------------|
| `users` | 1,000 — `SERIAL` PK, composite index on `(last_name, first_name)` |
| `categories` | 10 — `TEXT` slug PK |
| `products` | 200 — `SERIAL` PK, FK to `categories` |
| `orders` | 8,000 — `UUID` PK with `uuidv7()` default |
| `product_inventory` | ~600 — composite PK `(product_id, warehouse)` |
| `product_views` | ~50,000 — TimescaleDB hypertable with columnstore |
| `active_users` | Updatable view on `users` |
| `order_summary` | JOIN view (orders + users + products + categories) |

Also creates schema `tigerfs`, enables TimescaleDB, and indexes for `.by/` navigation (`idx_products_category`, `idx_orders_*`, `idx_users_name`, `idx_orders_status_date`, etc.).

File-first apps are intentionally **not** in `init.sql`; comments in that file point to `seed.sh`.

### File-first (`seed.sh`)

Argument: mount path (`seed.sh <mountpoint>`). Invoked by `demo.sh` after a successful mount.

| App | `.build/` spec | Content |
|-----|----------------|---------|
| `blog/` | `markdown,history` | 5 posts in `tutorials/`, `deep-dives/`; savepoints; history via rewrites |
| `docs/` | `markdown,history` | 4 pages in `getting-started/`, `reference/` |
| `snippets/` | `txt,history` | 3 plain-text files in `meetings/` |

Each app uses `echo 'format,history' > "$MOUNT/.build/<name>"` then `mkdir` and `cat` heredocs into `.md` or `.txt` files. Blog and docs pages are written twice to populate `.history/`.

## Sample exploration

### Data-first tables

```bash
# Row read
cat users/1.json

# Pipeline / export
cat products/.by/category/electronics/.export/json

# Sampling
ls orders/.sample/10/

# Composite index path (users)
ls users/.by/last_name.first_name/Smith/

# Views
cat active_users/.export/json
ls order_summary/.first/5/
```

### File-first apps

```bash
ls blog/
cat blog/hello-world.md
cat docs/getting-started/installation.md
cat snippets/todo.txt

# History (blog and docs)
ls docs/.history/getting-started/installation.md/
ls blog/.savepoint/
```

On macOS after `./demo.sh start --mac`, prefix paths with `/tmp/tigerfs-demo/`. In Docker `./demo.sh shell`, paths are relative to `/mnt/db`.

## Docker image details

The `tigerfs` service image (`scripts/demo/docker/Dockerfile`) builds `cmd/tigerfs` on Go 1.25, installs FUSE3, `psql`, and optional agent tooling: Node.js, `@anthropic-ai/claude-code`, `tiger-cli`, and `ghost`. The container runs **privileged** for FUSE.

<Info>
To use Claude Code inside the Docker demo, set `ANTHROPIC_API_KEY` before `start`, then `./demo.sh shell` and run `claude`.
</Info>

Compose wires `ANTHROPIC_API_KEY` from the host environment into the `tigerfs` service.

## Prerequisites

| Mode | Requirements |
|------|----------------|
| Docker | Docker with Compose v2, daemon running, privileged FUSE in container |
| macOS | macOS, Docker Desktop, Go 1.21+ (builds `bin/tigerfs` if missing) |

## Integration tests vs demo dataset

`test/integration/demo_data.go` seeds a **smaller, deterministic** dataset for automated tests (`DefaultDemoConfig`: 100 users, 20 products, 200 orders, fixed `BaseTimestamp`). It mirrors the same table shapes and `.by/` indexes but is not invoked by `demo.sh`. Full demo scale and `uuidv7()` orders live only in `init.sql`.

Use the demo environment for manual exploration; use `seedDemoData` in integration tests for CI-stable assertions.

## Troubleshooting

| Symptom | Mitigation |
|---------|------------|
| Port `5432` in use (macOS) | Stop local PostgreSQL (`brew services stop postgresql`), inspect with `lsof -i :5432` |
| Mount stuck at `/tmp/tigerfs-demo` | `./demo.sh status --mac`; `diskutil unmount force /tmp/tigerfs-demo`; then `./demo.sh stop` |
| Docker mount failed | Check `docker compose -f scripts/demo/docker/docker-compose.yml ps` and container logs |
| `Go is not installed` (macOS) | Install Go 1.21+ or pre-build `bin/tigerfs` at repo root |

PostgreSQL init runs only on a **fresh** volume. To reload `init.sql`, remove the compose volume (`docker compose down -v`) before `start`.

## Verification signals

After `./demo.sh start`:

- **macOS:** `mount | grep /tmp/tigerfs-demo` shows the NFS mount; `ls /tmp/tigerfs-demo` lists `users`, `products`, `categories`, `orders`, `blog`, `docs`, `snippets`.
- **Docker:** `./demo.sh status` reports containers `RUNNING` and mount `MOUNTED` at `/mnt/db`; `docker compose exec tigerfs ls /mnt/db` lists the same top-level dirs.

`seed.sh` ends with a summary of created apps; missing directories usually mean mount or seed failed earlier in `start`.

## Related pages

<CardGroup>
<Card title="Quickstart" href="/quickstart">
First mount and explore workflows that reference `demo.sh` on Docker and macOS.
</Card>
<Card title="Installation" href="/installation">
Platform prerequisites (FUSE, NFS, Docker) before running the demo.
</Card>
<Card title="File-first workspaces" href="/file-first-workspaces">
How `blog/`, `docs/`, and `snippets/` map to backing tables and `.build/` specs.
</Card>
<Card title="Data-first exploration" href="/data-first-exploration">
Row reads, `.by/`, `.export/`, `.sample/`, and pipeline paths on seeded tables.
</Card>
<Card title="History, savepoints, and undo" href="/history-savepoints-undo">
`.history/`, `.savepoint/`, and version browsing on seeded markdown apps.
</Card>
<Card title="Synthesized apps" href="/synthesized-apps">
`.build/` format strings (`markdown,history`, `txt,history`) used by `seed.sh`.
</Card>
</CardGroup>

---

## 22. Workflow recipes

> Copy-paste file-first patterns: kanban (todo/doing/done + mv), knowledge base, session context, from skills/tigerfs/recipes.md.

- Page Markdown: https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/22-workflow-recipes.md
- Generated: 2026-06-03T08:01:15.039Z

### Source Files

- `skills/tigerfs/recipes.md`
- `skills/tigerfs/files.md`
- `docs/file-first.md`
- `README.md`
- `test/integration/fs_operations_test.go`

---
title: "Workflow recipes"
description: "Copy-paste file-first patterns: kanban (todo/doing/done + mv), knowledge base, session context, from skills/tigerfs/recipes.md."
---

TigerFS file-first workflows are defined in `skills/tigerfs/recipes.md` and implemented as synthesized markdown workspaces: write a format string to `mount/.build/<name>`, use normal file tools (`Write`, `Glob`, `Grep`, `mv`, `rm`, `mkdir`), and optionally enable `history` for `.history/`, `.log/`, `.savepoint/`, and `.undo/`. Kanban and similar apps treat **directories as state** and **`mv` as transitions** (updating the backing `filename` column); savepoint/undo recipes use the same dot-directories documented in the file-first skill pack.

## Prerequisites

| Requirement | Detail |
|-------------|--------|
| Mount | `tigerfs mount CONNECTION MOUNTPOINT` (see CLI reference) |
| Workspace creation | `echo '<format>' > mount/.build/<workspace>` |
| History (optional) | Append `,history` to the format (e.g. `markdown,history`) |
| Multi-agent identity | `--user-id <id>` or `TIGERFS_USER_ID` at mount; runtime: `echo 'agent-7' > mount/.info/user` |

<Note>
Examples below use `mount/` as the mount root. Shell users can substitute `/mnt/db/` or your actual mountpoint. Agent examples use tool-style paths (`Write`, `Glob`, `Bash`) from the bundled skill pack.
</Note>

## Recipe map

| # | Name | Format | Primary tools |
|---|------|--------|---------------|
| 1 | Safe agent exploration | `markdown,history` | `.savepoint/`, `.undo/to-savepoint/` |
| 2 | Compare approaches | `markdown,history` | Savepoint + undo-to-savepoint |
| 3 | Multi-agent selective undo | `markdown,history` | `--user-id`, `.log/.by/user_id/`, per-user `.apply` |
| 4 | Task board (kanban) | `markdown,history` | `mkdir`, `mv`, `Glob`, `Grep` |
| 5 | Knowledge base | `markdown,history` | Topic dirs, `Grep`, `.history/` |
| 6 | Session context | `markdown` | Dated `.md` files, `status` frontmatter |
| 7 | Activity log | `markdown` | Append-only timestamped files |

Workflow patterns (1–3) cover safe editing; application patterns (4–7) cover durable workspaces agents build on mounts.

---

## Workflow patterns

### Recipe 1: Safe agent exploration

Create a named savepoint before uncertain work, explore, preview, then keep or revert.

<Steps>
<Step title="Create savepoint">

```bash
echo '{"description":"Before investigating bug #42"}' > mount/notes/.savepoint/before-investigation.json
```

Savepoint files need a format suffix (`.json` preferred). With `--user-id` set, `user_id` is injected automatically.

</Step>
<Step title="Work">

Edit, create, or delete files under the workspace as usual.

</Step>
<Step title="Preview">

```bash
cat mount/notes/.undo/to-savepoint/before-investigation/.info/summary
cd mount/notes && diff -ru .undo/to-savepoint/before-investigation . -x '.*'
```

</Step>
<Step title="Keep or revert">

Keep: do nothing (changes are already committed).

Revert (destructive — confirm with the user first):

```bash
touch mount/notes/.undo/to-savepoint/before-investigation/.apply
```

</Step>
</Steps>

**Auto-savepoints:** After an inactivity gap (default **30 minutes**), the next write creates `auto-<user>-<timestamp>` (or `auto-<timestamp>` without user id). List with `Glob "mount/notes/.savepoint/auto-*"`. Disable with `--auto-savepoint-interval 0`; override default with e.g. `--auto-savepoint-interval 15m`.

### Recipe 2: Compare approaches with savepoints

Branch implementations without losing either result.

```bash
# Baseline
echo '{"description":"Clean baseline"}' > mount/app/.savepoint/baseline.json

# Implement approach A, then bookmark
echo '{"description":"DFS implementation complete"}' > mount/app/.savepoint/after-dfs.json

# Reset to baseline for approach B
touch mount/app/.undo/to-savepoint/baseline/.apply

# Implement B (now current)

# Compare: read summary for A's bookmarked state
# mount/app/.undo/to-savepoint/after-dfs/.info/summary

# Keep B: no action. Keep A instead:
touch mount/app/.undo/to-savepoint/after-dfs/.apply
```

Undo-to-savepoint restores the tree **as of that savepoint**, including work done after it (e.g. approach B), so you can switch winners without manual file surgery.

### Recipe 3: Multi-agent selective undo

Separate mounts or identities tag operations in `.log/`; undo can filter by `user_id`.

```bash
# Distinct identities (same database, different mounts)
tigerfs mount --user-id agent-research  postgres://... /mnt/research
tigerfs mount --user-id agent-implement postgres://... /mnt/implement

# Shared sprint bookmark
echo '{"description":"Sprint start"}' > mount/notes/.savepoint/sprint-start.json

# Inspect one agent's recent ops
# Read mount/notes/.log/.by/user_id/agent-research/.last/10/.export/json

# Undo only that agent's changes since the savepoint
touch mount/notes/.undo/to-savepoint/sprint-start/.by/user_id/agent-research/.apply
```

<Warning>
If two agents edit the **same file**, per-user undo reverts the file to its state before that agent's **first** edit on it — which can also remove the other agent's interleaved edits on that file. Use separate files or savepoints when agents share paths.
</Warning>

---

## Application patterns

### Recipe 4: Task board (kanban)

Use for todo lists, kanban, project trackers, and shared queues. **Directories are states; files are items; `mv` is the transition; `author` tracks ownership.**

<Warning>
Do **not** model column state with a `status` frontmatter field. State lives in the directory path (`todo/`, `doing/`, `done/`). The bundled `skills/tigerfs/SKILL.md` enforces this for agents building boards.
</Warning>

#### Setup

```bash
echo 'markdown,history' > mount/.build/tasks
mkdir mount/tasks/todo mount/tasks/doing mount/tasks/done
```

#### Lifecycle

```text
mount/tasks/
├── todo/     ← pending
├── doing/    ← in progress (claim via mv)
└── done/     ← completed
```

```mermaid
stateDiagram-v2
    [*] --> todo: Write new .md
    todo --> doing: mv (claim)
    doing --> done: mv (complete)
    done --> [*]
```

Renaming a markdown file updates the `filename` column in the backing `tigerfs` table (same as `mv` on disk).

#### Add a task

```markdown
---
title: Fix Auth Bug
priority: high
---

The login endpoint returns 500 when session cookie is expired.
```

Path: `mount/tasks/todo/fix-auth-bug.md`

#### Claim and complete

```bash
mv mount/tasks/todo/fix-auth-bug.md mount/tasks/doing/fix-auth-bug.md
# optional: set author: your-name in frontmatter

mv mount/tasks/doing/fix-auth-bug.md mount/tasks/done/fix-auth-bug.md
```

#### Inspect the board

```bash
ls mount/tasks/todo/*.md
ls mount/tasks/doing/*.md
ls mount/tasks/done/*.md
grep -r "author:" mount/tasks/doing/*.md
```

#### Custom columns

Any directory name works: `backlog/`, `sprint/`, `review/`, `shipped/`. Add columns with `mkdir`; move cards with `mv`.

#### History

```bash
ls mount/tasks/.history/doing/fix-auth-bug.md/
```

Shows moves and edits when `history` was enabled at workspace creation.

### Recipe 5: Knowledge base with history

Topic directories replace tags-as-state; moving a file recategorizes it.

#### Setup

```bash
echo 'markdown,history' > mount/.build/kb
mkdir mount/kb/architecture mount/kb/debugging mount/kb/conventions
```

#### Store a decision

```markdown
---
title: Chose JWT Over Server Sessions
author: alice
confidence: high
---

## Decision
Use JWT tokens instead of server-side sessions.

## Reasoning
- Stateless -- no session store
- Works across multiple server instances
```

Path: `mount/kb/architecture/chose-jwt.md`

#### Recategorize

```bash
mv mount/kb/debugging/null-bytes.md mount/kb/conventions/null-bytes.md
```

#### Search

```bash
grep -r "authentication" mount/kb/
grep -r "confidence: high" mount/kb/*.md
```

#### Suggested frontmatter (extra headers / columns)

| Key | Example values | Purpose |
|-----|------------------|---------|
| `confidence` | `high`, `medium`, `low` | Certainty |
| `source` | free text | Provenance |
| `supersedes` | filename | Replaces an older note |

Keys without dedicated columns are stored in the `headers` JSONB column (full-replace on each write). Known columns like `title` and `author` keep old values when omitted.

#### Track evolution

```bash
ls mount/kb/.history/architecture/chose-jwt.md/
```

### Recipe 6: Session context (resuming work)

Lightweight continuity across agent sessions — uses **`status` frontmatter** here (unlike kanban).

#### Setup

```bash
echo 'markdown' > mount/.build/sessions
```

#### End of session

```markdown
---
title: Auth Refactor
status: in-progress
---

## Completed
- Migrated to JWT
- Updated /src/auth/middleware.ts

## Next Steps
- Implement refresh token rotation
```

Path: `mount/sessions/2026-02-24-auth-refactor.md` (date + topic naming).

#### Start of next session

```bash
ls mount/sessions/*.md
grep -r "status: in-progress" mount/sessions/*.md
cat mount/sessions/2026-02-24-auth-refactor.md
```

History is optional for session notes; add `,history` to the build format if you need versioned session files.

### Recipe 7: Activity log

Append-only, one file per event; safe for concurrent writers.

#### Setup

```bash
echo 'markdown' > mount/.build/activity
```

#### Log an event

Filename pattern: `YYYY-MM-DDTHHMMSS.mmmZ-short-description.md`

```markdown
---
author: agent-a
type: fix
---

Fixed the auth bug in login endpoint. Changed session cookie handling to check expiry before validating.
```

Path example: `mount/activity/2026-03-21T150000.000Z-fixed-auth-bug.md`

#### Review

```bash
ls mount/activity/*.md
ls mount/activity/2026-03-21*
grep -r "author: agent-a" mount/activity/
grep -r "type: fix" mount/activity/
```

---

## Format strings reference

| Build command | Workspace behavior |
|---------------|-------------------|
| `echo 'markdown' > mount/.build/name` | YAML frontmatter + body |
| `echo 'markdown,history' > mount/.build/name` | Above + `.history/`, `.log/`, `.savepoint/`, `.undo/` |
| `echo 'plaintext' > mount/.build/name` | Body only, no frontmatter |
| `echo 'history' > mount/.build/name` | Add history to existing workspace |

Backing tables live under `mount/.tables/<workspace>/` for data-first export, indexes, and schema inspection.

---

## Agent vs shell command styles

The skill pack documents two equivalent styles:

| Action | Agent (skill pack) | Shell |
|--------|-------------------|-------|
| Create workspace | `Bash "echo 'markdown,history' > mount/.build/tasks"` | `echo "markdown,history" > /mnt/db/.build/tasks` |
| List pending | `Glob "mount/tasks/todo/*.md"` | `ls /mnt/db/tasks/todo/` |
| Transition card | `Bash "mv mount/tasks/todo/x.md mount/tasks/doing/x.md"` | `mv /mnt/db/tasks/todo/x.md /mnt/db/tasks/doing/x.md` |
| Revert workspace | `Bash "touch mount/notes/.undo/to-savepoint/name/.apply"` | `touch /mnt/db/notes/.undo/to-savepoint/name/.apply` |

<Tip>
Natural-language requests ("set up a kanban with todo/doing/done") map to Recipe 4 via the bundled `skills/tigerfs/SKILL.md` routing table — no need to memorize paths if the agent has TigerFS skills installed at mount time.
</Tip>

---

## Verification signals

| Check | Expected |
|-------|----------|
| Workspace exists | `ls mount/tasks/` shows `todo/`, `doing/`, `done/` |
| Card moved | File only under new column directory; `mv` preserves frontmatter and body |
| Savepoint listed | `ls mount/notes/.savepoint/` includes your `.json` name |
| Undo preview | `.undo/to-savepoint/<name>/.info/summary` describes affected files |
| Per-user log | `.log/.by/user_id/<id>/.last/N/.export/json` returns JSON rows |
| Auto-savepoint | After gap + write, `auto-*` entry appears under `.savepoint/` |

On failure, TigerFS logs structured hints to stderr (errno alone is often insufficient). Use `tigerfs mount --debug` for full operation traces.

---

## Related pages

<CardGroup>
<Card title="File-first workspaces" href="/file-first-workspaces">
Create workspaces, frontmatter semantics, directories as state, and `.tables/` access.
</Card>
<Card title="History, savepoints, and undo" href="/history-savepoints-undo">
`.history/`, `.log/`, savepoint JSON, preview/apply undo, and per-user filters.
</Card>
<Card title="Agent skills" href="/agent-skills">
Bundled `skills/tigerfs` pack, SKILL.md routing to recipes, mount-time install.
</Card>
<Card title="Synthesized apps" href="/synthesized-apps">
`.build/` formats, backing tables in `tigerfs` schema, and column mapping.
</Card>
<Card title="Quickstart" href="/quickstart">
First mount, `ls`/`cat`, and demo workflows.
</Card>
</CardGroup>

---

## 23. Migrate workspaces

> tigerfs migrate --describe/--dry-run, history-format migrations, pre-boundary undo EPERM, and release verification against test/integration migrate tests.

- Page Markdown: https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/23-migrate-workspaces.md
- Generated: 2026-06-03T08:02:17.214Z

### Source Files

- `internal/tigerfs/cmd/migrate.go`
- `test/integration/migrate_test.go`
- `docs/release-process.md`
- `docs/adr/019-undo-boundary-via-metadata-table.md`
- `internal/tigerfs/fs/undo_boundary_test.go`

---
title: "Migrate workspaces"
description: "tigerfs migrate --describe/--dry-run, history-format migrations, pre-boundary undo EPERM, and release verification against test/integration migrate tests."
---

The `tigerfs migrate` command (`internal/tigerfs/cmd/migrate.go`) connects to a PostgreSQL database, runs an ordered list of self-detecting migrations per user schema, and supports inspection (`--describe`), SQL preview (`--dry-run`), or transactional execution. Migrations upgrade synth workspaces from older TigerFS layouts (user-schema `_app` tables, path-encoded filenames, missing triggers) to the current model: backing tables in `tigerfs`, `parent_id` directories, and optional undo-boundary metadata after the `relational-directories` step.

## When to run migrate

Run migrate when a database was created with an older TigerFS release and still has pending structural changes. Fresh workspaces built via `/.build/` on a current release typically need no migration.

| Signal | Meaning |
|--------|---------|
| `tigerfs migrate … --describe` lists migrations | Work is pending |
| Output is `No pending migrations.` | Schema matches current TigerFS |
| Mount works but nested paths or undo behave oddly on an old DB | Likely skipped `relational-directories` |
| Undo of old log entries fails with permission errors after upgrade | Expected post `history-format-migration` boundary |

<Warning>
Migrate runs DDL in transactions per migration. Take a database backup before applying on production data. Use `--dry-run` first on shared or production databases.
</Warning>

## Command reference

```bash
tigerfs migrate [CONNECTION] [--describe] [--dry-run] [--schema SCHEMA] [--insecure-no-ssl]
```

<ParamField body="CONNECTION" type="string">
Optional connection. Prefix selects backend: `tiger:ID`, `ghost:ID`, or `postgres://…`. If omitted, resolution uses config, env, and `PG*` variables (same as other TigerFS commands).
</ParamField>

<ParamField body="--describe" type="boolean">
List pending migration names, summaries, and affected workspace/table identifiers. Does not execute SQL.
</ParamField>

<ParamField body="--dry-run" type="boolean">
Print planned SQL (semicolon-terminated statements) without executing. Database state is unchanged.
</ParamField>

<ParamField body="--schema" type="string">
User schema to scan (default: database `current_schema()` / `search_path`).
</ParamField>

<ParamField body="--insecure-no-ssl" type="boolean">
Allow non-TLS remote connections (same semantics as other TigerFS commands).
</ParamField>

Default mode (no flags) executes each pending migration inside a single transaction, prints `Running migration: <name>`, then `Migrated N views` per migration. Command timeout is 60 seconds.

<Steps>
<Step title="Inspect pending work">

```bash
tigerfs migrate postgres://user@host/dbname --describe
```

Example output shape:

```text
move-backing-tables: Move backing tables from user schema to tigerfs schema
  - _myapp
relational-directories: Upgrade directory structure for improved performance and undo support
  - myapp
```

</Step>

<Step title="Preview SQL">

```bash
tigerfs migrate postgres://user@host/dbname --dry-run
```

Expect `CREATE SCHEMA`, `ALTER TABLE … SET SCHEMA`, `ALTER TABLE … RENAME`, view recreation, and—for `relational-directories`—`parent_id`, history/log column renames, and metadata `INSERT` for the undo boundary.

</Step>

<Step title="Apply">

```bash
tigerfs migrate postgres://user@host/dbname
```

Re-run `--describe` until you see `No pending migrations.`

</Step>
</Steps>

## Migration pipeline

Migrations run in a fixed order; each has `Detect` (catalog queries) and `Plan` (SQL generation). Later migrations assume earlier ones completed where relevant.

```text
  ┌─────────────────────┐     ┌──────────────────────────┐     ┌────────────────────────────┐
  │ move-backing-tables │ ──► │ relational-directories   │ ──► │ parent-dir-mtime-trigger   │
  │ user._app → tigerfs │     │ parent_id, history/log   │     │ parent directory mtime     │
  │ .app view           │     │ metadata boundary row    │     │ AFTER trigger on children  │
  └─────────────────────┘     └──────────────────────────┘     └────────────────────────────┘
```

| Name | Summary | Detect criteria (abbrev.) |
|------|---------|---------------------------|
| `move-backing-tables` | Move `_name` backing tables to `tigerfs.name`, recreate user-schema view | `_name` table + `tigerfs:` view comment with synth format; not already in `tigerfs` |
| `relational-directories` | Path-encoded filenames → `parent_id` model; history/log column renames; metadata table + boundary | `tigerfs` backing table exists, no `parent_id` column |
| `parent-dir-mtime-trigger` | POSIX-style directory `modified_at` on child changes | Has `parent_id`, missing `trg_<app>_parent_mtime` |

All three are idempotent: detection skips already-migrated objects; re-running with nothing pending prints `No pending migrations.`

### move-backing-tables

Targets legacy layout: backing table `_<app>` in the user schema and view `<app>` with a `tigerfs:` comment. For each match, migration:

1. Ensures `tigerfs` schema exists
2. Moves and renames `_<app>` → `tigerfs.<app>`
3. Drops and recreates the user-schema view pointing at `tigerfs`
4. If `<app>_history` exists in the user schema, moves and renames it to `tigerfs.<app>_history`

Skips views whose comment resolves to native format without history (`FormatNative && !History`).

### relational-directories

Upgrades apps already in `tigerfs` (post move-backing-tables) from ADR-011 path-encoded filenames to ADR-017 `parent_id` directories. Per app, in one transaction:

- Adds `parent_id`, populates it from old path hierarchy, strips filenames to leaf names
- Switches `id` default to `uuidv7()`, adds FK and `UNIQUE NULLS NOT DISTINCT (parent_id, filename, filetype)`
- Recreates the user-schema view (PostgreSQL `SELECT *` views do not pick up new columns automatically)
- If history exists: renames `id`→`file_id`, `_history_id`→`version_id`, `_operation`→`operation`; maps `UPDATE`/`DELETE` to `edit`/`delete`; rebuilds archive trigger
- If log exists: renames `history_id`→`version_id`; maps log types `insert`→`create`, `update`→`edit`; updates CHECK constraint
- Creates `tigerfs.<app>_metadata` if missing and inserts exactly one `history-format-migration` row (`WHERE NOT EXISTS` guard)

<Info>
The boundary `INSERT` runs at the end of this migration so its `entry_id` (UUIDv7) sorts after every pre-migration `log_id`. That lexical ordering is what the undo engine uses to block pre-boundary targets.
</Info>

### parent-dir-mtime-trigger

Adds `GenerateParentDirMtimeTriggerSQL` for apps on the parent-pointer model so directory `modified_at` updates when children are inserted, deleted, or moved—important for NFS directory listing freshness.

## History-format migration and undo boundary

ADR-019 introduces `tigerfs.<app>_metadata` as a sibling of `_history`, `_log`, and `_savepoint`. The `relational-directories` migration inserts one row:

| Field | Value |
|-------|--------|
| `subject` | `history-format-migration` (`synth.SubjectHistoryFormatMigration`) |
| `description` | User-facing hint when undo is refused |
| `payload` | `{"from":"0.6","to":"0.7","reason":"parent-pointer model"}` |

**Why undo is blocked:** migration sets history `parent_id` from the *current* source row, not the historical directory at edit time. Pre-migration log entries remain structurally valid but undo could restore content while leaving the file in the wrong directory.

```mermaid
sequenceDiagram
    participant User
    participant Mount as fs.Operations / mount
    participant Undo as undo.go checkBoundary
    participant Meta as tigerfs.app_metadata

    User->>Mount: .undo/.../.apply (target log_id)
    Mount->>Undo: checkBoundary(schema, app, targetLogID)
    Undo->>Meta: cached metadata rows (mount lifetime)
    alt targetLogID < blocker.entry_id
        Undo-->>Mount: FSError ErrPermission + Hint=description
        Mount-->>User: EPERM (FUSE/NFS) + stderr hint
    else targetLogID >= blocker.entry_id
        Undo-->>Mount: nil (proceed to undo SQL)
    end
```

### Pre-boundary undo: EPERM and hints

`checkBoundary` in `internal/tigerfs/fs/undo.go` runs before `ExecuteUndoSingle`, `ExecuteUndoToLogID`, and `ExecuteUndoToSavepoint` database work. Rules:

| Condition | Result |
|-----------|--------|
| No metadata rows (fresh 0.7+ install) | Allow undo (fast path) |
| `targetLogID < entry_id` for a blocking subject | Refuse: `ErrPermission`, `Hint` = row `description` |
| `targetLogID == entry_id` | Allow (boundary row itself is not a blocked target) |
| `targetLogID > entry_id` | Allow |

Blocking subjects are hard-coded in `blockingSubjects` (currently only `history-format-migration`). FUSE maps `ErrPermission` to `syscall.EPERM`; NFS maps to `os.ErrPermission`.

<RequestExample>

```bash
# After upgrading a v0.6 workspace — undo of an old log id
touch /mnt/db/myapp/.undo/id/<pre-migration-log-uuid>/.apply
```

</RequestExample>

<ResponseExample>

```text
# stderr (structured logging) includes the metadata description as hint
# shell
apply: Operation not permitted   # EPERM
```

</ResponseExample>

Pre-migration history remains readable via `.log/` and `.history/`; only `.undo/` apply paths refuse. Post-migration edits, savepoints, and undo-to-log-id targets after the marker behave normally (covered in `verifyMigrationUndoBoundary` in integration tests).

<Check>
Fresh installs: `/.build/` creates an empty `_metadata` table. With zero rows, `checkBoundary` returns immediately and undo is unrestricted (`TestSynth_FreshInstall_MetadataFastPath`).
</Check>

## Release verification

When a release adds or changes migrations, extend the release checklist (`docs/release-process.md` §1b):

<Steps>
<Step title="Start Postgres 18 + TimescaleDB">

```bash
docker run --rm -d --name migrate-test -e POSTGRES_PASSWORD=test \
  -p 15433:5432 timescale/timescaledb-ha:pg18
sleep 5
```

</Step>

<Step title="Seed pre-release fixture (if applicable)">

Use a database snapshot or SQL fixture representing the previous release layout before running migrate.

</Step>

<Step title="Run migrate describe → dry-run → apply">

```bash
CONN='postgres://postgres:test@127.0.0.1:15433/postgres?sslmode=disable'

tigerfs migrate "$CONN" --describe
tigerfs migrate "$CONN" --dry-run
tigerfs migrate "$CONN"
```

</Step>

<Step title="Run integration migrate tests">

```bash
TEST_DATABASE_URL="$CONN" go test -count=1 -timeout 600s ./test/integration/... -run '^TestSynth_Migrate'
```

Tests in `test/integration/migrate_test.go`:

| Test | Covers |
|------|--------|
| `TestSynth_MigrateDetectAndExecute` | `move-backing-tables`: describe, dry-run, execute, idempotency |
| `TestSynth_MigrateWithHistory` | Backing + `_history` table move |
| `TestSynth_MigrateAddParentPointer` | `relational-directories`, FS ops on migrated tree, undo boundary block/allow |
| `TestSynth_MigrateAddParentDirMtimeTrigger` | Trigger install and mtime behavior |
| `TestSynth_MigrateParentDirMtimeTrigger_NotNeeded` | Fresh build skips trigger migration |
| `TestSynth_FreshInstall_MetadataFastPath` | Empty metadata, undo allowed |

Unit tests for boundary logic: `go test ./internal/tigerfs/fs/... -run TestCheckBoundary` (`undo_boundary_test.go`).

For v0.7-style releases, manually confirm pre-boundary `.undo/…/.apply` returns EPERM with the metadata `description` as the logged hint.

</Step>
</Steps>

<Note>
`docs/release-process.md` suggests `-run TestMigrate`; the repository’s migrate integration tests use the `TestSynth_Migrate` prefix. Use `-run '^TestSynth_Migrate'` (or a narrower name) to match actual test names.
</Note>

## Troubleshooting

| Symptom | Likely cause | Action |
|---------|----------------|--------|
| `No pending migrations.` but mount errors on old DB | Wrong `--schema` | Pass `--schema` for the schema that owns `tigerfs:` views |
| `migration …: failed to execute SQL` | DDL conflict, permissions, or extension missing | Read failed SQL in error; fix DB state; restore from backup if needed |
| Undo EPERM with long hint after upgrade | Pre-boundary log target | Use `.log/<id>/before` or `.history/`; do not `.undo/` pre-migration ids |
| Duplicate boundary rows | Should not happen | `INSERT … WHERE NOT EXISTS` keeps one marker; if count ≠ 1, inspect `tigerfs.<app>_metadata` manually |
| `relational-directories` not listed | Tables still in user schema | Run `move-backing-tables` first (pipeline order) |
| History-enabled app missing TimescaleDB | Hypertable expectations | Install TimescaleDB extension before migrate on history workspaces |

## Related pages

<CardGroup>
<Card title="History, savepoints, and undo" href="/history-savepoints-undo">
`.log`, `.history`, `.undo`, savepoints, and how the migration boundary interacts with undo.
</Card>
<Card title="CLI reference" href="/cli-reference">
Full `migrate` entry alongside mount, build, and connection commands.
</Card>
<Card title="File-first workspaces" href="/file-first-workspaces">
Workspace layout before and after `parent_id` migration.
</Card>
<Card title="Develop and test" href="/develop-and-test">
`go test`, integration testcontainers, and pre-commit checks.
</Card>
<Card title="Error codes" href="/error-codes">
Mapping `fs.ErrPermission` to EPERM and reading stderr hints.
</Card>
</CardGroup>

---

## 24. Develop and test

> Build commands, go test ./... and integration testcontainers, pre-commit checks, stress runner, and release-process checklist.

- Page Markdown: https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/24-develop-and-test.md
- Generated: 2026-06-03T08:02:22.164Z

### Source Files

- `CLAUDE.md`
- `docs/release-process.md`
- `test/integration/main_test.go`
- `test/stress/README.md`
- `scripts/pre-commit`
- `.github/workflows/test.yml`

---
title: "Develop and test"
description: "Build commands, go test ./... and integration testcontainers, pre-commit checks, stress runner, and release-process checklist."
---

TigerFS is a Go module (`github.com/timescale/tigerfs`, Go 1.25.1) built from `cmd/tigerfs` into `bin/tigerfs`. Local verification spans unit tests under `internal/tigerfs/`, mount-based integration tests in `test/integration/` (PostgreSQL via local env or testcontainers), optional Docker FUSE runs via `scripts/test-docker.sh`, the `tigerfs-stress` binary in `test/stress/`, and a release checklist in `docs/release-process.md` that pairs Postgres 18 + TimescaleDB with GoReleaser tagging.

## Prerequisites

| Requirement | Used by |
|-------------|---------|
| Go 1.25+ (see `go.mod`) | Build, all `go test` |
| PostgreSQL / Docker | DB unit tests (`internal/tigerfs/db/`), integration, release |
| Linux: FUSE (`/dev/fuse`, `libfuse-dev`) | Native integration mounts, `scripts/test-docker.sh`, CI |
| macOS: NFS (`/sbin/mount_nfs`) | Default integration mount on Darwin |
| Docker daemon | testcontainers fallback, stress runner, docker-FUSE stress |

<Note>
Many `internal/tigerfs/db/` tests call `t.Skip` when `PGHOST` / `TEST_DATABASE_URL` are unset. CI and release flows always set those variables so DB tests actually run.
</Note>

## Build

Primary binary:

```bash
go build -o bin/tigerfs ./cmd/tigerfs
```

Build all packages (matches CI):

```bash
go build -v ./...
```

Stress runner (separate main under `test/stress`):

```bash
go build -o bin/tigerfs-stress ./test/stress
```

Release snapshot (no tag push):

```bash
goreleaser release --snapshot --clean
./dist/tigerfs_darwin_arm64*/tigerfs version
```

GoReleaser builds `tigerfs` for `linux` and `darwin` × `amd64`/`arm64` with `CGO_ENABLED=0`, embedding version metadata via ldflags.

## Unit tests

### Full tree

```bash
go test ./...                    # everything, including integration
go test -race ./...              # race detector
go test -run TestName ./path     # single test
```

### Package-scoped (no integration)

Exclude mount tests when iterating quickly:

```bash
PACKAGES=$(go list ./... | grep -v /test/integration)
go test -race $PACKAGES
```

### DB-connected unit tests

Point at a running Postgres (CI uses port 5432; release doc uses 15432):

```bash
PGHOST=127.0.0.1 PGPORT=5432 PGUSER=postgres PGPASSWORD=test PGDATABASE=postgres \
TEST_DATABASE_URL='postgres://postgres:test@127.0.0.1:5432/postgres?sslmode=disable' \
  go test -count=1 -timeout 300s ./internal/tigerfs/...
```

`-count=1` disables the Go test cache so skipped tests do not silently stay skipped from a prior run.

### Synth tests

Filter all synthesized-app tests across packages:

```bash
go test ./internal/tigerfs/fs/synth/...
go test -run "^TestSynth_" ./internal/tigerfs/fs/... ./test/integration/...
```

FUSE-layer tests that need a live mount are skipped in-package with a pointer to `test/integration/`; filesystem behavior belongs in integration tests.

## Integration tests

Package: `test/integration/`. Tests mount TigerFS against real PostgreSQL and exercise `os.ReadFile`, `os.Rename`, pipelines, synth workspaces, DDL, history/undo, and more.

### Database provisioning

`TestMain` in `main_test.go`:

1. Probes local PostgreSQL (`TEST_DATABASE_URL` or `PG*` vars, with Unix-socket detection when no host/password).
2. If local PG is unavailable and Docker is reachable, starts one shared **TimescaleDB HA pg18** container via testcontainers-go.
3. Each test isolates data with unique schemas (`setupLocalTestDB` / `setupLocalTestDBEmpty`).

```bash
go test -count=1 -timeout 600s ./test/integration/...
go test -count=1 -timeout 600s ./test/integration/... -run TestMount_ListTables
```

If neither local PG nor Docker is available, tests skip with `Neither local PostgreSQL nor Docker available`.

### Mount method matrix

| Host OS | Default mount | Override |
|---------|---------------|----------|
| Linux | FUSE (`/dev/fuse`) | — |
| macOS | NFS (in-process `go-nfs`) | `TEST_MOUNT_METHOD=docker` runs tests in a Linux FUSE container |

Platform-specific test prefixes (use only when behavior is mount-specific):

| Prefix | Meaning |
|--------|---------|
| `TestMount_`, `TestWrite_`, `TestDDL_`, … | Generic; runs on FUSE or NFS |
| `TestNFS_` | NFS-only; skipped on Linux/FUSE |
| `TestFUSE_` | FUSE-only; skipped on macOS/NFS |

### Docker FUSE integration (Linux container)

For Linux-native FUSE from any host (including macOS):

```bash
./scripts/test-docker.sh                      # all integration tests
./scripts/test-docker.sh -run TestPipeline    # filter
./scripts/test-docker.sh -v -timeout 10m      # extra go test flags
./scripts/test-docker.sh -out results.log     # custom log path
```

Uses `test/docker/docker-compose.test.yml`; tears down compose on exit.

## Pre-commit checks

Install the git hook:

```bash
./scripts/install-hooks.sh
```

Hook script `scripts/pre-commit` runs on every commit (skip with `git commit --no-verify`):

<Steps>
<Step title="Format">
`gofmt -l .` must be empty (otherwise run `go fmt ./...`).
</Step>
<Step title="Vet">
`go vet ./...`
</Step>
<Step title="Unit tests with race">
`go test -race` on all packages **except** `test/integration`.
</Step>
</Steps>

Full maintainer gate before commit (from project guidance — includes integration and mod tidy):

```bash
go fmt ./... && go vet ./... && go test ./... && go mod tidy
```

<Warning>
The pre-commit hook does **not** run integration tests or `go mod tidy`. Release and CI cover those surfaces.
</Warning>

## CI workflows

### `test.yml` (default on PR and `main` push)

| Step | Command / behavior |
|------|------------------|
| Service | `timescale/timescaledb:latest-pg18` on port 5432 |
| FUSE | `apt-get install fuse libfuse-dev` |
| Tidy check | `go mod tidy` + `git diff --exit-code go.mod go.sum` |
| Build | `go build -v ./...` |
| Vet | `go vet ./...` |
| Unit tests | `-race -timeout 300s -coverprofile=coverage.txt`, packages excluding `test/integration`, with `PGHOST` / `TEST_DATABASE_URL` set |
| Integration | Only when `workflow_dispatch` input `run_integration: true` |
| Coverage | Codecov upload on PR / `main` |

Trigger integration tests manually:

```bash
gh workflow run test.yml --ref "$(git branch --show-current)" -f run_integration=true
```

### `stress.yml`

Path-filtered on PRs touching `fs/`, `db/`, `fuse/`, migrate, or stress scripts. Runs `./scripts/stress-docker.sh` on Ubuntu with FUSE inside Docker:

| Trigger | Iterations |
|---------|------------|
| PR | 200 |
| Weekly schedule (Mon 06:17 UTC) | 1000 |
| `workflow_dispatch` | Configurable input |

Flags: `--validate-every 1 --large-files --many-files`. Failure dumps upload from `test/stress/docker/host-out/`.

### `release.yml`

Pushing a `v*.*.*` tag runs GoReleaser `release --clean` and publishes binaries; non-prerelease tags also sync `install.sh` and `latest.txt` to S3.

## Stress runner (`tigerfs-stress`)

Standalone binary — **no TigerFS internal imports**. It drives a mounted workspace through `os.*` calls, maintains an in-memory MD5 model, and validates after every op (and around undo).

```text
tigerfs-stress ── os.WriteFile ──> kernel mount ──> TigerFS ──> PostgreSQL
       │                                    │
       └── ValidateWorkspace() ◄── os.ReadFile + hash compare
```

| Mode | Where tigerfs runs | Mount |
|------|-------------------|-------|
| `bin/tigerfs-stress start` on macOS | Host | NFS |
| Same on Linux | Host | FUSE |
| `./scripts/stress-docker.sh` | Linux container | FUSE |

Common commands:

```bash
bin/tigerfs-stress start
bin/tigerfs-stress start --seed 42 --iterations 50
bin/tigerfs-stress start --large-files --many-files --iterations 100 --validate-every 5
bin/tigerfs-stress stop
./scripts/stress-docker.sh --seed 42 --iterations 200
```

Stress package unit tests (no Docker/TigerFS):

```bash
go test ./test/stress/...
```

<Info>
Long macOS NFS runs may print `[warn iter ...] readLatestLogID regressed` lines. The runner retries and continues; these reflect NFS snapshot timing, not TigerFS data loss. See the troubleshooting page for interpretation.
</Info>

Exit codes: `0` pass, `1` verification failure, `2` infrastructure failure. Validation failures write diagnostic dumps under `/tmp/tigerfs-stress-*` (or `test/stress/docker/host-out/` in docker mode) and leave infrastructure up for inspection.

## Release checklist

Follow `docs/release-process.md` end-to-end before tagging.

<Steps>
<Step title="Start Postgres 18 + TimescaleDB">
```bash
docker run --rm -d --name tigerfs-release-pg -e POSTGRES_PASSWORD=test \
  -p 15432:5432 timescale/timescaledb-ha:pg18
sleep 5
```
</Step>
<Step title="Run full test suite">
```bash
go fmt ./... && go vet ./... && go mod tidy

PGHOST=127.0.0.1 PGPORT=15432 PGUSER=postgres PGPASSWORD=test PGDATABASE=postgres \
TEST_DATABASE_URL='postgres://postgres:test@127.0.0.1:15432/postgres?sslmode=disable' \
  go test -count=1 -timeout 300s ./internal/tigerfs/...

go test -count=1 -timeout 600s ./test/integration/...
./scripts/test-docker.sh -v -timeout 300s
```
</Step>
<Step title="Migration verification (when migrations ship)">
Seed a pre-release workspace, then `tigerfs migrate --describe`, `--dry-run`, apply, and re-run `go test ... -run TestMigrate` against the migrated DB.
</Step>
<Step title="CHANGELOG and checklist">
Add a user-facing section to `CHANGELOG.md` (replace `YYYY-MM-DD` with the release date). Mark tasks in `docs/implementation/implementation-tasks-checklist.md`.
</Step>
<Step title="Snapshot build">
`goreleaser release --snapshot --clean` and smoke-test `./dist/.../tigerfs version`.
</Step>
<Step title="Commit, tag, push">
```bash
git add CHANGELOG.md README.md docs/
git commit -m "docs: prepare vX.Y.Z release"
git tag vX.Y.Z
git push origin main
git push origin vX.Y.Z
```
Edit the GitHub release body to match `CHANGELOG.md` (GoReleaser auto-changelog is a starting point only).
</Step>
<Step title="Stop release container">
`docker stop tigerfs-release-pg`
</Step>
</Steps>

## Quick reference

| Goal | Command |
|------|---------|
| Build CLI | `go build -o bin/tigerfs ./cmd/tigerfs` |
| Fast unit loop | `go test -race $(go list ./... \| grep -v /test/integration)` |
| Integration (local) | `go test -count=1 -timeout 600s ./test/integration/...` |
| Integration (FUSE in Docker) | `./scripts/test-docker.sh` |
| Install pre-commit hook | `./scripts/install-hooks.sh` |
| Stress (native) | `go build -o bin/tigerfs-stress ./test/stress && bin/tigerfs-stress start` |
| Stress (FUSE in Docker) | `./scripts/stress-docker.sh` |
| Release snapshot | `goreleaser release --snapshot --clean` |

```mermaid
flowchart TB
  subgraph local["Local developer"]
    fmt["go fmt / go vet"]
    unit["go test internal/..."]
    integ["go test test/integration/..."]
    docker["scripts/test-docker.sh"]
    stress["bin/tigerfs-stress"]
  end
  subgraph pg["PostgreSQL"]
    localPG["Local PG / PG* env"]
    tc["testcontainers shared container"]
  end
  subgraph ci["GitHub Actions"]
    testyml["test.yml"]
    stressyml["stress.yml"]
    rel["release.yml on v* tag"]
  end
  fmt --> unit
  unit --> localPG
  integ --> localPG
  integ --> tc
  docker --> integ
  stress --> pg
  testyml --> unit
  stressyml --> stress
  rel --> rel
```

## Related pages

<CardGroup>
<Card title="Installation" href="/installation">
Platform prerequisites (FUSE, NFS, Docker) before running tests locally.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
Stress monotonicity warnings, `--debug` logging, and errno recovery.
</Card>
<Card title="Migrate workspaces" href="/migrate-workspaces">
Release-time `tigerfs migrate` verification and `TestMigrate` integration coverage.
</Card>
<Card title="Demo environment" href="/demo-environment">
`demo.sh` workflows for manual exploration separate from automated tests.
</Card>
</CardGroup>

---

## 25. Troubleshooting

> Stale mounts, force unmount, NFS monotonicity warnings in stress runs, logging --debug, .refresh metadata clear, and common errno recovery.

- Page Markdown: https://grok-wiki.com/public/docs/timescale-tigerfs-60719456a5c3/pages/25-troubleshooting.md
- Generated: 2026-06-03T08:03:54.747Z

### Source Files

- `internal/tigerfs/cmd/unmount.go`
- `internal/tigerfs/cmd/stop.go`
- `test/stress/README.md`
- `internal/tigerfs/logging/logging.go`
- `docs/spec.md`
- `CHANGELOG.md`

---
title: "Troubleshooting"
description: "Stale mounts, force unmount, NFS monotonicity warnings in stress runs, logging --debug, .refresh metadata clear, and common errno recovery."
---

TigerFS surfaces failures through POSIX errno on the mount, structured JSON logs on stderr, and a JSON mount registry under `~/.config/tigerfs/mounts.json`. Most operational issues—stale registry rows, busy unmounts, metadata that lags external DDL, stress-run NFS warnings, and opaque `EINVAL`/`EACCES` from shell tools—are diagnosed from those three channels plus the `tigerfs` lifecycle commands (`status`, `list`, `unmount`, `stop`).

## Symptom map

| Symptom | First checks | Typical fix |
|---------|--------------|-------------|
| `tigerfs status` shows a mount you killed | Registry PID stale | `tigerfs status` or `list` (auto-`Cleanup`) |
| `unmount: device is busy` | Open files under mountpoint | Close processes, then `tigerfs unmount --force` |
| `ls` missing a new table | Metadata cache TTL | Wait for `metadata_refresh_interval`, or remount |
| Tool reports `EINVAL` with no detail | TigerFS stderr | Re-run with `--log-level debug` |
| `[warn iter N] readLatestLogID regressed` | Stress run on macOS NFS | Expected; see monotonicity section |

```text
  shell tool (mv, cat, ls)
        │
        ▼
  FUSE / NFS adapter  ──► errno (ENOENT, EINVAL, …)
        │
        ▼
  fs.Operations       ──► FSError + zap hint on stderr
        │
        ▼
  PostgreSQL
```

## Stale mount registry entries

The mount registry records mountpoint, PID, database label, and start time. Entries survive crashes when TigerFS exits without `unmount` or `stop`.

`Register` replaces duplicate mountpoints when a prior process died without cleanup. `Cleanup()` removes entries whose PID is no longer running. `ListActive()` returns only live processes.

| Command | Registry behavior |
|---------|-------------------|
| `tigerfs status` | Calls `Cleanup()`, then lists active mounts |
| `tigerfs list` | Same cleanup; prints mountpoints only (scripting) |
| `tigerfs status /path/to/mount` | Shows `active` or `stale` for that entry’s PID |
| `tigerfs unmount MOUNT` | Unmounts, then `Unregister` (best-effort) |

<Steps>
<Step title="Confirm registry vs reality">

```bash
tigerfs status
# or
tigerfs status /mnt/db
```

`active` means the PID responds to a signal probe. `stale` means the registry row outlived the process.

</Step>
<Step title="Refresh the registry">

```bash
tigerfs list    # cleanup + active mountpoints
tigerfs status  # human-readable table
```

Both commands invoke `registry.Cleanup()` before listing.

</Step>
<Step title="Remove a ghost mountpoint path">

If the directory still exists but nothing is mounted:

```bash
# macOS
diskutil unmount force /mnt/db 2>/dev/null || true
# Linux FUSE
fusermount -uz /mnt/db 2>/dev/null || true
```

`unmount` treats “not currently mounted” / “not mounted” as success.

</Step>
</Steps>

<Note>
A stale registry row does not mean the filesystem is still mounted. Check the mount with `mount | grep tigerfs` (Linux) or `mount` (macOS) before force-unmounting.
</Note>

## Force unmount and stuck mounts

`unmount` tries graceful shutdown first: look up the registry entry, send `SIGTERM`, poll every 100ms until the context timeout (default 30s), then fall back to OS unmount.

| Platform | Normal | Force (`-f` / `--force`) |
|----------|--------|---------------------------|
| macOS | `umount MOUNT` | `diskutil unmount force MOUNT` |
| Linux | `fusermount -u MOUNT` | `fusermount -uz MOUNT` (lazy) |
| Other | `umount MOUNT` | `umount -f MOUNT` |

<ParamField body="timeout" type="int" default="30">
Seconds to wait for the TigerFS process to exit after `SIGTERM` before system unmount.
</ParamField>

<RequestExample>

```bash
# Graceful (registry SIGTERM, then system unmount)
tigerfs unmount /mnt/db

# Busy mountpoint
tigerfs unmount --force /mnt/db

# Known PID instead of path
tigerfs stop 12345
```

</RequestExample>

`stop PID` sends `SIGTERM`, waits for exit, and unregisters the mountpoint if found in the registry. Use when you have the PID from `tigerfs status` but not the path.

<Warning>
Force unmount can strand open file handles in client processes. Close editors, shells with `cd` into the mount, and long-running jobs before `--force`.
</Warning>

### Stress-test teardown

`tigerfs-stress` uses the same platform rules: SIGTERM with a 5s grace window, then `Kill`, then `diskutil unmount force` (macOS) or `umount` (Linux). For a stuck stress run from another terminal:

```bash
bin/tigerfs-stress stop
# Docker FUSE mode (match info file env):
TIGERFS_STRESS_INFO_FILE=/out/tigerfs-stress.info \
  docker compose -f test/stress/docker/docker-compose.yml \
  exec -e TIGERFS_STRESS_INFO_FILE=/out/tigerfs-stress.info \
  stress /usr/local/bin/tigerfs-stress stop
```

## Debug logging (`--log-level debug`)

FUSE and NFS adapters return errno only. TigerFS logs the underlying reason (and optional `hint` fields) to stderr before returning.

| Level | Config / flag | Output style |
|-------|---------------|--------------|
| `warn` (default) | `log_level: warn` | Minimal production JSON |
| `debug` | `--log-level debug` | Development: colors, timestamps, caller |

<RequestExample>

```bash
# Foreground mount with full diagnostics
tigerfs mount --foreground --log-level debug postgres://host/db /mnt/db

# Any subcommand inherits persistent flags
tigerfs --log-level debug status

# Config file or env (viper)
# ~/.config/tigerfs/config.yaml: log_level: debug
# export TIGERFS_LOG_LEVEL=debug
```

</RequestExample>

At `debug` or `info`, logging uses `zap.NewDevelopmentConfig` (colored levels, ISO8601 timestamps). At `warn` and `error`, output is compact production JSON with `message` as the primary key.

Stress runs pass debug through to the child TigerFS process:

```bash
bin/tigerfs-stress start --debug --iterations 10
# equivalent: --log-level debug on the tigerfs binary
```

TigerFS process stdout/stderr from stress land in `tigerfs.log` in the working directory.

<Tip>
Reproduce the failing operation with debug enabled on a foreground mount. Search stderr for `"hint"` — synth validation, DDL staging, and undo paths emit structured guidance there.
</Tip>

## Metadata cache refresh

Metadata (schemas, tables, views, PKs, permissions) is cached in-process with tiered TTLs. Row **content** is not cached; stale symptoms usually affect directory listings or stat sizes, not `cat` on a known path.

| Cache tier | Config key | Default TTL |
|------------|------------|-------------|
| Catalog (schemas, tables, views) | `metadata_refresh_interval` | 10s |
| Structural (row counts, permissions, PKs) | `structural_metadata_refresh_interval` | 5m |
| Stat / path (per-table listings) | Fixed in code | 2s |
| Undo preview | Fixed in code | 2s |

`MetadataCache.Invalidate()` clears all tiers and forces a DB refresh on next access. It runs after DDL commits, workspace build operations, and similar write paths.

### Manual clear via `.refresh`

`docs/spec.md` documents a mount-root control file:

```bash
echo 1 > /mnt/db/.refresh
```

Writing to `.refresh` is specified to clear cached metadata (table lists, columns, indexes, permissions). The active `fs.Operations` root listing currently exposes `.build`, `.create`, `.info`, `.schemas`, `.tables`, and `.views` — not `.refresh`. If `ls -a` at the mount root does not show `.refresh`, use the workarounds below instead of expecting that path to exist.

<Steps>
<Step title="Wait for catalog TTL">

After external `CREATE TABLE` / `DROP` outside TigerFS, new names appear after `metadata_refresh_interval` (default 10s) unless a TigerFS write path already called `Invalidate()`.

</Step>
<Step title="Tune refresh interval">

```yaml
# ~/.config/tigerfs/config.yaml
metadata_refresh_interval: 5s
structural_metadata_refresh_interval: 2m
```

Or `TIGERFS_METADATA_REFRESH_INTERVAL` / `TIGERFS_STRUCTURAL_METADATA_REFRESH_INTERVAL` where supported by your config loader.

</Step>
<Step title="Remount for a hard reset">

```bash
tigerfs unmount /mnt/db
tigerfs mount postgres://host/db /mnt/db
```

</Step>
</Steps>

<Info>
Direct file access (`cat /mount/table/row/col`) always queries PostgreSQL. If `cat` works but `ls` on the parent directory is wrong, suspect metadata or kernel directory cache (often 1–2s on FUSE), not row content staleness.
</Info>

## NFS monotonicity warnings (stress runs)

On macOS, TigerFS serves the mount through an in-process NFS v3 server (`go-nfs`). The stress harness (`bin/tigerfs-stress`) reads `.log/.last/N/.export/json` over NFS. After heavy undo operations, the macOS NFS client can return a **stale** snapshot where the newest `log_id` is older than one already observed (UUIDv7 ordering makes that impossible for a true latest value).

The runner wraps reads in `readLatestLogIDMonotonic`:

- Up to **25** retries, **100ms** apart (~2.5s budget)
- On recovery: stderr warning `[warn iter N] readLatestLogID regressed after …; recovered after …`
- On exhaustion: keeps the prior known-good `lastLogID` and logs that recovery failed

<Warning>
These warnings are **expected** during stress runs. They indicate NFS/kernel attribute caching below TigerFS, not application data loss. Do not treat them as TigerFS bugs to fix; the run continues with the last known-good log ID.
</Warning>

End-of-run summary (stdout):

```text
=== Monotonicity Warnings ===
Total:     N regressions across M ops (x.xx%)
Recovered: … / … (… kept prior lastLogID after retry exhaustion)
```

To compare NFS vs FUSE behavior, run the same seed natively on macOS (NFS) and via `./scripts/stress-docker.sh` (Linux FUSE in Docker).

| Observation | Interpretation |
|-------------|----------------|
| Warnings only on macOS native | NFS client / go-nfs path |
| Same seed fails validation | Use failure dump under `/tmp/tigerfs-stress-failure-*` or `test/stress/docker/host-out/` |
| Seed does not replay | NFS timing can diverge PRNG consumption; rely on dump artifacts |

## Common errno recovery

Shell tools show only errno. Map symptoms using stderr JSON and this table.

### `fs.ErrorCode` → POSIX (FUSE adapter)

| `FSError` code | errno | Typical cause |
|----------------|-------|----------------|
| `ErrNotExist` | `ENOENT` | Missing path, row, or staging entry |
| `ErrPermission` | `EACCES` | Grants, constraints, reserved dotfile |
| `ErrInvalidPath`, `ErrInvalidFormat`, `ErrInvalidArgument` | `EINVAL` | Bad path, malformed PATCH/JSON, synth numbering |
| `ErrInvalidOperation` | `EPERM` | Operation not allowed on path type |
| `ErrReadOnly` | `EROFS` | Read-only mount |
| `ErrNotEmpty` | `ENOTEMPTY` | `rmdir` on non-empty directory |
| `ErrAlreadyExists` | `EEXIST` | Create when path exists |
| `ErrIO`, `ErrInternal` | `EIO` | DB connection, timeout, unexpected SQL |
| `ErrNotImplemented` | `ENOSYS` | Unimplemented feature |

### PostgreSQL errors → errno (user-visible)

| Database condition | errno | Where detail lives |
|--------------------|-------|-------------------|
| Row not found | `ENOENT` | stderr at `debug` |
| Permission / NOT NULL / FK violation | `EACCES` | stderr `ERROR` with SQL detail |
| Connection refused / timeout | `EIO` | stderr + `tigerfs test-connection` |

<RequestExample>

```bash
# User sees only:
echo 'x' > /mnt/db/users/1/email
# email: Permission denied

# Check exit code
echo $?    # 13 = EACCES on Linux

# TigerFS stderr (run mount with --log-level debug):
# {"message":"NOT NULL constraint violation",...,"hint":"..."}
```

</RequestExample>

### Recovery patterns

| errno | Check | Action |
|-------|-------|--------|
| `ENOENT` | Path spelling, PK segment, pipeline depth | `ls` parent; read stderr hint for synth paths |
| `EINVAL` | Format suffix (`.json`, `.tsv`), task numbers, undo order | stderr `hint`; undo **newest first** |
| `EACCES` | Table grants, NOT NULL, reserved `.name` | Fix SQL or pick non-reserved filename |
| `EPERM` | Read-only mount, history path, DDL policy | `tigerfs mount` without `--read-only` |
| `EIO` | Network, `sslmode`, pool exhaustion | `tigerfs test-connection URL`; TLS flags |
| `EBUSY` (unmount) | Processes using mount | `lsof +D /mnt/db`; `unmount --force` |

<Note>
`ErrNotExist` is logged at **debug** level to avoid noise from tab completion and speculative `stat`. For missing-file investigations, always use `--log-level debug`.
</Note>

### Cross-mount consistency

If mount A wrote data and mount B still shows wrong directory entries within ~2s, suspect stat/path cache TTL or metadata catalog TTL—not stale row reads. Writes should call `statCache.invalidate(schema, table)` and `pathCache.invalidate(schema, table)` with the **user** schema (not internal `tigerfs` schema keys). A schema-key mismatch leaves stale cache entries for up to 2 seconds.

## Quick reference

| Goal | Command |
|------|---------|
| List live mounts | `tigerfs status` |
| Scriptable mountpoints | `tigerfs list` |
| Graceful stop | `tigerfs unmount /mnt/db` |
| Force stop | `tigerfs unmount -f /mnt/db` |
| Stop by PID | `tigerfs stop PID` |
| Verbose server logs | `tigerfs --log-level debug mount --foreground …` |
| Stress debug | `bin/tigerfs-stress start --debug` |
| Stop stress infra | `bin/tigerfs-stress stop` |

## Related pages

<CardGroup>
<Card title="Error codes" href="/error-codes">
Full errno mapping, constraint behavior, and reading stderr hints.
</Card>
<Card title="Consistency and caching" href="/consistency-and-caching">
Cross-mount freshness rules, cache TTLs, and invalidation on writes.
</Card>
<Card title="CLI reference" href="/cli-reference">
`mount`, `unmount`, `stop`, `status`, `list`, and global flags.
</Card>
<Card title="Configuration reference" href="/configuration-reference">
`metadata_refresh_interval`, `log_level`, and env precedence.
</Card>
<Card title="Platform backends" href="/platform-backends">
Linux FUSE vs macOS NFS write/commit behavior.
</Card>
<Card title="Develop and test" href="/develop-and-test">
Stress runner, integration tests, and pre-commit checks.
</Card>
</CardGroup>

---