Filesystem

PgFileSystem mirrors the Node.js fs API. All operations are transactional, scoped to a workspace, and subject to configurable limits.

Workspace Isolation

Every PgFileSystem instance is scoped to a workspace. Workspaces are isolated via PostgreSQL Row-Level Security. Each transaction sets SET LOCAL app.workspace_id before executing any query. If you omit workspaceId, a random UUID is generated.

// Each workspace is fully isolated
const ws1 = new PgFileSystem({ db: sql, workspaceId: "tenant-a" })
const ws2 = new PgFileSystem({ db: sql, workspaceId: "tenant-b" })

await ws1.writeFile("/data.txt", "tenant A data")
await ws2.exists("/data.txt") // false, different workspace

Versions

Each instance is also scoped to a version inside an active version root (default: "main"). The workspace root is versioned by default, and directories can be made independently versioned with mkdir(path, { versioned: true }). See Versioning for the full API.

const fs = new PgFileSystem({ db: sql, workspaceId: "app" })
await fs.mkdir("/database", { versioned: true })

const dbMain = await fs.versioned("/database")
await dbMain.writeFile("/config.json", '{"env":"staging"}')

const dbDraft = await dbMain.fork("draft")
await dbDraft.writeFile("/config.json", '{"env":"prod"}')

await dbMain.readFile("/config.json") // '{"env":"staging"}' -- untouched

Reading & Writing

// Write a text file (creates parent directories automatically)
await fs.writeFile("/docs/guide.md", "# Getting Started")

// Append to a file (creates if missing)
await fs.appendFile("/docs/guide.md", "\nMore content...")

// Read entire file as string
const content = await fs.readFile("/docs/guide.md")

// Read a byte range (useful for large files)
const chunk = await fs.readFileRange("/docs/guide.md", {
  offset: 0,
  limit: 1024,
})

// Read a line range (text files only — slicing happens in Postgres)
const { content: head, total } = await fs.readFileLines("/docs/guide.md", {
  offset: 1,
  limit: 50,
})
// content: lines 1-50 joined by "\n", no trailing newline
// total:   total line count of the file (wc -l semantics)

// Read as binary
const buffer = await fs.readFileBuffer("/docs/image.png")

Directories

// Create directory (with recursive option)
await fs.mkdir("/docs/images", { recursive: true })

// Create an independent version root at a directory
await fs.mkdir("/database", { versioned: true })
const database = await fs.versioned("/database")

// List directory names
const names = await fs.readdir("/docs")
// ["guide.md", "images"]

// List with type info (avoids extra stat calls)
const entries = await fs.readdirWithFileTypes("/docs")
// [{ name: "guide.md", isFile: true, isDirectory: false, isSymbolicLink: false }, ...]

// List with full stat info
const detailed = await fs.readdirWithStats("/docs")
// [{ name, isFile, isDirectory, isSymbolicLink, mode, size, mtime, symlinkTarget }, ...]

// Walk entire directory tree recursively
const tree = await fs.walk("/docs")
// [{ path: "/docs/guide.md", name: "guide.md", depth: 1, isFile: true, ... }, ...]

Copy, Move, Remove

// Copy file or directory
await fs.cp("/docs", "/backup", { recursive: true })

// Move / rename
await fs.mv("/backup/guide.md", "/archive/guide.md")

// Remove file
await fs.rm("/archive/guide.md")

// Remove directory recursively (force ignores ENOENT)
await fs.rm("/archive", { recursive: true, force: true })

Stat & Existence

// Check existence
const exists = await fs.exists("/docs/guide.md")

// Get file stats (follows symlinks)
const stat = await fs.stat("/docs/guide.md")
// { isFile: true, isDirectory: false, isSymbolicLink: false,
//   mode: 420, size: 18, mtime: Date }

// Get stats without following symlinks
const lstat = await fs.lstat("/link")

// Resolve symlinks to canonical path
const real = await fs.realpath("/link")

FsStat

PropertyTypeDescription
isFilebooleanTrue if regular file
isDirectorybooleanTrue if directory
isSymbolicLinkbooleanTrue if symlink (only from lstat)
modenumberUnix permission bits (default: 0o644)
sizenumberFile size in bytes
mtimeDateLast modified time

Workspace Usage

getUsage() reports workspace-wide storage usage and active-version-root logical usage. Pass a path to scope visible counts to a subtree. Stored blob bytes are deduplicated across the workspace, while logical bytes count the visible files and symlinks in the selected path.

const usage = await fs.getUsage()
const projectUsage = await fs.getUsage({ path: "/project" })

usage.logicalBytes     // visible bytes in fs.version
usage.referencedBlobBytes // deduped visible file blobs for the path
usage.storedBlobBytes  // deduplicated workspace blob bytes
usage.blobCount        // stored blob rows
usage.versions         // version labels in the active version root
usage.entryRows        // fs_entries rows in the active version root
usage.visibleNodes     // visible nodes in fs.version, including root
usage.limits           // { maxFiles, maxFileSize, maxWorkspaceBytes? }

Configure maxWorkspaceBytes to reject writes that would add a new blob beyond the workspace quota. Quota failures throw FsQuotaError with structured fields for UI and API responses.

const fs = new PgFileSystem({
  db: sql,
  workspaceId: "tenant-a",
  maxWorkspaceBytes: 100 * 1024 * 1024,
})

try {
  await fs.writeFile("/large.bin", bytes)
} catch (e) {
  if (e instanceof FsQuotaError) {
    e.code            // "ENOSPC"
    e.limit           // configured maxWorkspaceBytes
    e.current         // current stored blob bytes
    e.attemptedDelta  // bytes for the new unique blob
  }
}

Symlinks & Links

// Create symbolic link
await fs.symlink("/docs/guide.md", "/latest")

// Read symlink target
const target = await fs.readlink("/latest")
// "/docs/guide.md"

// Create hard link (copies content, shares no reference)
await fs.link("/docs/guide.md", "/docs/guide-copy.md")

Symlinks are resolved up to 40 levels deep before throwing ELOOP. Symlink targets are limited to 4096 characters.

Permissions & Timestamps

// Change file permissions
await fs.chmod("/docs/guide.md", 0o755)

// Update modification time
await fs.utimes("/docs/guide.md", new Date(), new Date())

Glob

// Match files using glob patterns
const tsFiles = await fs.glob("**/*.ts", { cwd: "/project/src" })
// ["/project/src/index.ts", "/project/src/utils/helpers.ts"]

Path Resolution

// Resolve relative paths
fs.resolvePath("/docs", "../images/logo.png")
// "/images/logo.png"

fs.resolvePath("/docs", "./guide.md")
// "/docs/guide.md"