Data files

ctx.data gives functions and cron jobs a simple, file-native persistence layer under /data/. It’s the fastest way to store settings, drafts, text, and simple event logs without provisioning a database.

Use Data files when your application state naturally belongs in a text, JSON, or JSONL file and writes are infrequent. For structured application data, queries, relationships, counters, or concurrent updates, use a Database.

Data files are ordinary site files: visible to authenticated tooling, but never served on public hosted URLs.

Storage usage and billing

File storage is measured across your account by file size and time retained. Static files, /data files, function source, and cron source count, including files on archived sites. Deleting a file stops its contribution. Historical revisions are currently excluded from customer storage usage. The retention and pricing policy for retained history is under review.

Free includes 1 GB; monthly Builder includes 25 GB. New Builder subscriptions include storage overage billing. For activated subscriptions, overage costs $0.030666… per GB-month after the included allowance, billed with your subscription. One GB is 1,073,741,824 bytes; a month is your actual subscription billing period. Request costs are included in this storage rate.

The allowance is applied once to the period’s size × time usage. For example, 100 GB retained for an entire Builder billing period produces 75 GB-month of overage, or $2.30 before taxes and discounts. Activation starts measurement from that point forward; earlier usage is not backfilled.

See Usage → File storage in the dashboard for stored bytes, included storage, estimated overage, and whether billing is active. Existing subscriptions without storage billing attached remain inactive.

Helpers

Inside a function or cron handler, reach for ctx.data:

// Text
await ctx.data.read('/data/notes.txt')
await ctx.data.write('/data/notes.txt', 'hello')
await ctx.data.delete('/data/notes.txt')
await ctx.data.info('/data/notes.txt')

// JSON
await ctx.data.readJson('/data/settings.json')
await ctx.data.writeJson('/data/settings.json', { theme: 'dark' })

// JSONL (append-only logs)
await ctx.data.appendJsonl('/data/events.jsonl', { type: 'created' })
await ctx.data.tailJsonl('/data/events.jsonl', 100)

Extension rules

The structured helpers require matching file extensions:

Helper Required extension
readJson, writeJson .json
appendJsonl, tailJsonl .jsonl
read, write any

Patterns

JSON document

A single evolving document — settings, a small record, aggregate state:

export async function handler(req: Request, ctx: InterfoldContext) {
  const settings = await ctx.data
    .readJson('/data/settings.json')
    .catch(() => ({ theme: 'light' }))

  return Response.json(settings)
}

JSONL event log

Append-only records — form submissions, analytics, audit trails:

export async function handler(req: Request, ctx: InterfoldContext) {
  await ctx.data.appendJsonl('/data/signups.jsonl', {
    email: (await req.json()).email,
    at: new Date().toISOString(),
  })

  const recent = await ctx.data.tailJsonl('/data/signups.jsonl', 20)
  return Response.json({ recent })
}

Concurrency

Data file writes create file revisions. The Files API and MCP can reject a stale replacement when given the current version ID. ctx.data uses version checks for its read-modify-write operations. For shared state needing transactions across records, use a Database. For logs, drafts, settings, and other low-contention state, ctx.data is ideal.

Inspecting data

Data lives at /data/* and is not a public URL. Read it with authenticated tooling:

interfold files ls /data --site <site-id>
interfold files cat /data/settings.json --site <site-id>
On this page