Great for vibe-coded apps

Text hosting
with an API.

Write in the browser, get a URL, and read or update it from any app, script or AI agent. The easy place for a vibe-coded app to keep its settings, lists and data.

Free
Two pages, up to 1,000 characters each
$0
Pro
Ten pages, up to 10,000 characters each
$10 a year
k7qm2xt9bw
Groceries
- oat milk
- coffee beans
- lemons

Ideas
- weekly newsletter
- dark mode
{
  "theme": "light",
  "banner": "New docs",
  "sites": [
    "lobste.rs",
    "hn"
  ],
  "paused": false
}
Your app GET /f/k7qm… Script POST /append AI agent PUT /f/k7qm…
$ curl https://fetchext.arashtaher.com/f/k7qm2xt9bw
Groceries
- oat milk
- coffee beans
fetchext/llms.txt text/plain · 8225 bytes Raw
# Fetchext

> Fetchext hosts small plain text files and exposes them over a tiny HTTP API.
> Apps use the file as a simple database (for example a JSON document of todos),
> and the owner can open https://fetchext.arashtaher.com/edit at any time to read or fix the data by hand.
> No SDK is needed: it is plain HTTP with a bearer key.

This document teaches an AI coding assistant everything needed to use the API.

## Concepts

- **File URL**: `https://fetchext.arashtaher.com/f/{id}`, where `{id}` is a random 10-character string. The owner copies it from https://fetchext.arashtaher.com/edit.
- **Content**: UTF-8 plain text, served exactly as stored. The size limit depends on the owner's plan:
  1024 bytes on Free, 10240 bytes on Pro. Every write response tells you the limit in `max_bytes`.
- **Visibility**: a *public* file can be read by anyone with the URL. A *private* file needs a key to read.
  Writing always needs the read-write key.
- **Keys**: each file has two keys, created on https://fetchext.arashtaher.com/edit:
  - read-only key `txt_ro_...`: can read the file (also when private);
  - read-write key `txt_rw_...`: can read, replace and append.
  Send a key as `Authorization: Bearer <key>`.
- **ETag**: every successful response carries an `ETag` header (for example `"7"`) that changes whenever
  the content changes. Use it to avoid overwriting someone else's change (see below).

## Endpoints

| Method | Path | Key | Body | Result |
|--------|------|-----|------|--------|
| GET | `/f/{id}` | none if public, else either key | | the raw text, `text/plain; charset=utf-8` |
| PUT | `/f/{id}` | read-write | the new full content | replaces the content |
| POST | `/f/{id}/append` | read-write | one line of text | appends it as a new line, atomically |

- `GET` supports `If-None-Match: "<etag>"` and answers `304 Not Modified` when nothing changed.
- `PUT` supports `If-Match: "<etag>"` and answers `412 Precondition Failed` when the file changed since.
- `PUT` and `append` answer `200` with JSON: `{"etag": "\"8\"", "bytes": 123, "max_bytes": 1024}`.
- `append` adds a newline before the line if the file does not end with one, and one after it.
  Trailing newlines in the body are ignored.
- CORS is open to any origin, so browser apps can call the API directly.

## Errors

Errors are JSON with a message and a hint on how to fix the request:

```json
{"error": "This key is read-only", "hint": "Send the read-write key in the Authorization header as 'Bearer txt_rw_...'. ..."}
```

| Status | Meaning |
|--------|---------|
| 400 | malformed request (bad `If-Match`, empty append body, content that is not UTF-8 text) |
| 401 | missing or invalid key |
| 403 | the key is read-only and the request writes, or the file is an extra one beyond the owner's plan (read-only) |
| 404 | no file with this id |
| 412 | `If-Match` did not match: the file changed since you read it |
| 413 | the content would exceed the file's size limit (`max_bytes`) |
| 429 | rate limited: wait the number of seconds in `Retry-After` |

Read the `hint` field and follow it.

## Examples with curl

```sh
export FETCHEXT_URL="https://fetchext.arashtaher.com/f/YOUR_FILE_ID"
export FETCHEXT_READ_KEY="txt_ro_..."
export FETCHEXT_WRITE_KEY="txt_rw_..."

# Read (add -i to see the ETag header)
curl -i -H "Authorization: Bearer $FETCHEXT_READ_KEY" "$FETCHEXT_URL"

# Replace the whole file, only if it is still at version "7"
curl -X PUT "$FETCHEXT_URL" \
  -H "Authorization: Bearer $FETCHEXT_WRITE_KEY" \
  -H 'If-Match: "7"' \
  --data-binary '{"todos": [{"text": "Buy milk", "done": false}]}'

# Append one line
curl -X POST "$FETCHEXT_URL/append" \
  -H "Authorization: Bearer $FETCHEXT_WRITE_KEY" \
  --data-binary '{"at": "2026-01-01T10:00:00Z", "event": "signup"}'
```

Use `--data-binary`, not `-d`: `-d` strips newlines.

## Examples with fetch (JavaScript)

```js
const URL = process.env.FETCHEXT_URL;
const READ_KEY = process.env.FETCHEXT_READ_KEY;
const WRITE_KEY = process.env.FETCHEXT_WRITE_KEY;

async function read() {
  const res = await fetch(URL, { headers: { Authorization: `Bearer ${READ_KEY}` } });
  if (!res.ok) throw new Error((await res.json()).hint);
  return { text: await res.text(), etag: res.headers.get("ETag") };
}

async function write(text, etag) {
  const res = await fetch(URL, {
    method: "PUT",
    headers: { Authorization: `Bearer ${WRITE_KEY}`, "If-Match": etag },
    body: text,
  });
  if (res.status === 412) return null; // changed meanwhile: read again and retry
  if (!res.ok) throw new Error((await res.json()).hint);
  return res.headers.get("ETag");
}
```

## Pattern 1: the whole file is one JSON document

Best for small state that changes in place (settings, a todo list, a shopping list).

1. `GET` the file, keep the `ETag`. Parse it with `JSON.parse` (treat an empty file as your initial value).
2. Change the data in memory.
3. `PUT` the new JSON with `If-Match: <etag>`.
4. On `412`, someone else wrote first: go back to step 1 and re-apply your change. Retry a few times.

```js
async function update(change) {
  for (let attempt = 0; attempt < 5; attempt++) {
    const { text, etag } = await read();
    const data = text.trim() ? JSON.parse(text) : { todos: [] };
    change(data);
    if (await write(JSON.stringify(data, null, 2), etag)) return data;
  }
  throw new Error("Too many concurrent updates");
}

await update((data) => data.todos.push({ text: "Buy milk", done: false }));
```

Pretty-print the JSON (`JSON.stringify(data, null, 2)`) so the owner can edit it by hand.
Owners may also edit the file in the browser, so parse defensively and handle invalid JSON gracefully.

## Pattern 2: one record per line, with append

Best for logs, events, guestbooks, form submissions: data that is only added.

- Write: `POST /f/{id}/append` with one record, e.g. a JSON object on a single line (JSON Lines).
  Appends are atomic, so concurrent writers never lose lines and no `If-Match` is needed.
- Read: `GET` the file, split on `\n`, skip empty lines, parse each line.
- Clean up: when `bytes` gets close to `max_bytes`, `PUT` a trimmed version (with `If-Match`).

```js
await fetch(`${URL}/append`, {
  method: "POST",
  headers: { Authorization: `Bearer ${WRITE_KEY}` },
  body: JSON.stringify({ at: new Date().toISOString(), name: "Ada" }),
});

const records = (await read()).text.split("\n").filter(Boolean).map((line) => JSON.parse(line));
```

## Avoiding lost writes with ETag and If-Match

Two clients that read, change and `PUT` at the same time would otherwise overwrite each other.
Always send the `ETag` from your last read as `If-Match` on `PUT`. The server only writes if the file
is still at that version, otherwise it answers `412` with the current `ETag`. Never retry a `412`
blindly with the new ETag: read the file again and re-apply your change to the new content.
Without `If-Match`, a `PUT` overwrites unconditionally. `append` needs no `If-Match`.

To poll cheaply, send `If-None-Match` with the last ETag; a `304` means nothing changed.

## Limits

- File size: 1024 bytes of UTF-8 text on Free, 10240 on Pro; read `max_bytes`
  from any write response. Larger writes get `413` and change nothing. Design for the small limit:
  store compact data and prune old entries.
- Rate limits: 60 requests per minute per key and 120 per IP address.
  Over the limit you get `429` with a `Retry-After` header (seconds).
- 2 files per account on Free, 10 on Pro; each has its own URL and keys.

## Security: keep the read-write key out of frontend code

Anything shipped to the browser, including environment variables bundled into frontend code
(`NEXT_PUBLIC_*`, `VITE_*`, ...), is **visible to everyone** who opens the app. Whoever has the
read-write key can replace or wipe the file.

- Keep keys in environment variables, never commit them.
- Call the API with the read-write key from a server, an API route or a serverless function.
- In frontend-only apps, only use a read-only key or a public file for reading, or accept that
  anyone can change the data (fine for a personal prototype, not for real users).
- If a key leaks, the owner regenerates it on https://fetchext.arashtaher.com/edit; the old key stops working at once.