# Platform backends

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

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

## Source Files

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