---
title: Files and streaming
url: "https://native-sdk.dev/docs/files"
docs_index: /llms.txt
lastUpdated: 2026-11-03
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use raw file effects for user-owned files: imports, exports, recordings, images, CSV, and other blobs. App state belongs in [Model Persistence](/docs/persistence), the [Record Store](/docs/record-store), or [Relational SQLite](/docs/sqlite); secrets belong in credentials.

Every operation is still an effect. `update` returns a `Cmd`, the host performs I/O after the model commits, and results return as later `Msg` values. Session recording journals those results; streamed chunks live in the content-addressed `blobs/` directory beside the journal so replay never opens the original file.

The blob directory is deduplicated but currently has no quota or automatic GC; see [Automation: Session recording and blob growth](/docs/automation#session-recording-and-blob-growth).

## Small whole-file operations

`Cmd.readFile` and `Cmd.writeFile` remain convenient for payloads up to 1 MiB. An over-bound write is rejected. An over-bound read returns `truncated` rather than passing cut bytes as a successful file. `Cmd.appendFile` appends one payload up to 1 MiB, `Cmd.statFile` reports `{ exists, size, mtimeMs }` without reading the content, and `Cmd.deleteFile` removes one file.

```ts
return [model, Cmd.statFile(path, {
  ok: "file_stat",
  err: "file_failed",
})];

return [model, Cmd.deleteFile(path, {
  ok: "file_deleted",
  err: "file_failed",
})];
```

Deletion is file-only and never recursive: it reports `not_found` when the path is absent and `io_failed` for a directory or another OS refusal. If the final path component is a symlink, deletion removes the link rather than its target. File outcomes are closed and machine-readable: `ok`, `not_found`, `io_failed`, `truncated`, `rejected`, `cancelled`, `sink_missing`, `out_of_order`, and `disk_full`.

## Streaming reads

`Cmd.readFileStream` delivers zero or more 256-KiB chunks, followed by exactly one `done` message carrying the total byte count, or one `err` message.

Read-stream keys follow ordinary file semantics: reissuing a live key silently replaces the old read, and `Cmd.cancel(key)` silently drops it. Stale chunks from the retired generation cannot reach the replacement route.

```ts
return [model, Cmd.readFileStream(path, {
  key: "import",
  chunk: "import_chunk",
  done: "import_done",
  err: "import_failed",
})];
```

Streaming has its own four-slot budget and no total-size ceiling. Each individual chunk remains bounded, so one large import cannot consume the sixteen general spawn/fetch/whole-file slots.

## Atomic streaming writes

Open a sink, send one acknowledged chunk at a time, then close it. Close syncs the temporary file and atomically replaces the destination; before a successful close, the old destination stays visible and teardown removes the temporary file.

```ts
Cmd.writeFileStream("export", path, { ok: "sink_open", err: "export_failed" });
Cmd.writeFileChunk("export", bytes, { ok: "chunk_written", err: "export_failed" });
Cmd.writeFileClose("export", { ok: "export_done", err: "export_failed" });
```

A duplicate live sink rejects. A chunk or close issued before the previous acknowledgment returns `out_of_order`; a chunk or close with no open sink returns `sink_missing`. Chunks may be at most 1 MiB.

Write sinks report cancellation: `Cmd.cancel(key)` ends the sink through `err: cancelled` and removes its temporary file. A half-written export is never silently replaced or installed.

## Filesystem permission

Raw paths inside this app's resolved `data`, `config`, `cache`, `state`, `logs`, and `temp` directories need no permission. Any path outside those roots requires:

```zig
.permissions = .{ "filesystem" },
```

The runtime is authoritative. Before checking, it resolves the target when it exists, or resolves the deepest existing parent and normalizes the missing suffix. Existing symlinks are followed for authorization, so a symlink inside an app directory that points outside is external and requires the permission. Deletion still unlinks the final symlink itself after that check. `..` cannot escape an allowed root.

`native check` also reports NS1074 for certainly-external literal paths, but dynamic paths are decided only by the runtime. File pickers therefore require the `filesystem` permission because the selected user file is normally outside app-owned directories.

Directory enumeration, file watching, chmod/permissions, and per-path grant prompts are not part of this API.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)