Filesystem adapter
No database at all — feedback and screenshots live as plain files under .instafix/, for a single developer working locally.
@instafix/adapter-fs is built for a different scenario than the other adapters: not "a client leaves feedback on a live site for a team to triage," but one developer watching an AI coding agent build a page, marking up what's wrong, and keeping a local, searchable history of the back-and-forth — without running a database.
Feedback (and any captured screenshots) are written under a .instafix/ folder at your project root — the same idea as .git: created on first write, plain text, meant to be read, diffed, or grepped directly.
npm install github:gnoopy/instafix#adapter-fs-distMounting
// app/api/instafix/route.ts — Next.js App Router
import { createInstaFixHandler, FsStore } from "@instafix/adapter-fs";
const store = new FsStore(); // writes to ./.instafix
export const { GET, POST, PATCH, DELETE, OPTIONS } = createInstaFixHandler({ store });FsStore also needs a small extra route to serve screenshots back over HTTP (the widget renders them as <img src>) — npx instafix init generates this for you when you pick the "Local history" option; if you're wiring things by hand, add:
// app/api/instafix/screenshots/[file]/route.ts
import { readFile } from "node:fs/promises";
import { join } from "node:path";
const SAFE_FILENAME = /^[A-Za-z0-9_-]+\.[A-Za-z0-9]+$/;
export async function GET(_request: Request, { params }: { params: Promise<{ file: string }> }) {
const { file } = await params;
if (!SAFE_FILENAME.test(file)) return new Response("Not found", { status: 404 });
try {
const bytes = await readFile(join(process.cwd(), ".instafix", "screenshots", file));
const contentType = file.endsWith(".png") ? "image/png" : "image/jpeg";
return new Response(new Uint8Array(bytes), { headers: { "Content-Type": contentType } });
} catch {
return new Response("Not found", { status: 404 });
}
}What's on disk
.instafix/
history.jsonl # one JSON object per line, newest last — grep/jq friendly
screenshots/
<clientId>.jpg # only present for feedback that captured a screenshot
outbox/
<id>.md # written when someone clicks "Agent에게" on a fix note
processed/ # ...and moved here once `instafix watch` delivers itScreenshots are named after the widget's client-generated id (available before the server assigns a record id), sanitized to a safe filename — anything that doesn't look like a plain id falls back to a random one.
FsStore options
| Option | Type | Default | What it does |
|---|---|---|---|
dir | string | .instafix in process.cwd() | Where history.jsonl and screenshots/ live |
screenshotUrlPrefix | string | /api/instafix/screenshots | URL prefix screenshots are served from — must match wherever you mount the screenshot route above |
allowProduction | boolean | false | Bypasses the NODE_ENV=production guard below — see why that's rarely what you want first |
There's no screenshotStorage option here, unlike the Prisma/SQLite adapters — writing the file is this adapter's storage, so there's nothing to plug in.
Behavior notes
- Refuses to start in production, on purpose.
FsStore's constructor throws whenNODE_ENV === "production"(passallowProduction: trueto override). This isn't about auth — it's that most hosts run each request in a fresh, disposable filesystem, so writes to.instafix/would silently disappear, and even a host with a real persistent disk puts the folder on a server, not the project checkout your coding agent has open — the entire "read it like.git" premise depends on the widget, the dev server, and the agent all sharing one machine. For real production feedback collection, use@instafix/adapter-prismaor@instafix/adapter-sqlite(on a persistent volume) instead. The generated route also doesn't passapiKey/allowedOrigins— it's meant forlocalhostduring development, not for taking feedback from real site visitors. - Reads are always fresh off disk — nothing is cached in memory across requests, so
history.jsonlstays the single source of truth even if you edit it by hand or read it from another process/script while the dev server is running. - Whether to commit
.instafix/is your call. Some projects want the history as a build log; others gitignore it. Neither is wrong — this adapter doesn't decide for you. - Every other
InstaFixStorebehavior (clientId dedup, filtering, pagination,StoreNotFoundErroron missing update/delete) comes from the samecreateCollectionStoreengine the memory and localStorage adapters use — see Writing an Adapter if you're curious how little code that engine leaves an adapter to write.
Pairing with the agent loop
This adapter is what unlocks the agent workflow beyond plain clipboard copying: npx @instafix/cli prompt/resolve read and rewrite history.jsonl directly, and the "Agent에게" handoff button writes straight into outbox/. Captured screenshots also resolve to a local path (.instafix/screenshots/<file>.jpg) a coding agent with file access can open directly, and captured console errors are included inline — so a single note carries the message, the exact DOM target, a screenshot reference, and any errors the change triggered.