Storage: local filesystem
The same
Storagecontract, backed by a directory on disk.
Use this for development, tests that want real I/O, and single-node deployments where shared object storage would be overkill.
Install
sh
pnpm add @nwire/storage @nwire/storage-fsWire it in
ts
import { createApp } from "@nwire/app"
import { storagePlugin } from "@nwire/storage"
import { fsStorage } from "@nwire/storage-fs"
export const app = createApp({
appName: "my-app",
plugins: [storagePlugin({
storage: fsStorage({ baseDir: "./var/uploads" }),
})],
})The baseDir will be created automatically on first use (and at every readiness probe). Keys map to files under it: posts/2025/cover.png → ./var/uploads/posts/2025/cover.png.
Metadata
Content-type and arbitrary metadata are persisted in a sidecar .meta.json next to each file. Disable this if you don't need it:
ts
fsStorage({ baseDir: "./var/uploads", persistMetadata: false })Limitations
- No presigned PUT URLs. Clients can't upload directly to a server file path —
url(key, { method: "put" })throwsStorageUnsupportedError. Mount an HTTP upload endpoint instead and stream intostorage.put(...). - No sharing across nodes. Each replica sees only its own disk.
- No CDN integration. Front it with a real CDN only after moving to S3.
When to use it
| Scenario | Use fs? |
|---|---|
| Local dev | ✅ |
| Tests with real I/O | ✅ |
| Single VPS / Raspberry Pi | ✅ |
| Multi-node deployment | ❌ — switch to S3 |
| Need presigned URLs | ❌ — switch to S3 |
Key safety
Keys are resolved relative to baseDir and rejected if they'd escape it. storage.put("../escape.txt", ...) throws — the adapter refuses to write outside its sandbox.