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 workspaceVersions
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"}' -- untouchedReading & 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
| Property | Type | Description |
|---|---|---|
| isFile | boolean | True if regular file |
| isDirectory | boolean | True if directory |
| isSymbolicLink | boolean | True if symlink (only from lstat) |
| mode | number | Unix permission bits (default: 0o644) |
| size | number | File size in bytes |
| mtime | Date | Last 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"