Adapters

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-dist

Mounting

// 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 it

Screenshots 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

OptionTypeDefaultWhat it does
dirstring.instafix in process.cwd()Where history.jsonl and screenshots/ live
screenshotUrlPrefixstring/api/instafix/screenshotsURL prefix screenshots are served from — must match wherever you mount the screenshot route above
allowProductionbooleanfalseBypasses 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 when NODE_ENV === "production" (pass allowProduction: true to 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-prisma or @instafix/adapter-sqlite (on a persistent volume) instead. The generated route also doesn't pass apiKey/allowedOrigins — it's meant for localhost during development, not for taking feedback from real site visitors.
  • Reads are always fresh off disk — nothing is cached in memory across requests, so history.jsonl stays 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 InstaFixStore behavior (clientId dedup, filtering, pagination, StoreNotFoundError on missing update/delete) comes from the same createCollectionStore engine 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.

Edit on GitHub

On this page