Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion content/docs/(overview)/access.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,6 @@ Every key has a source of its own, chosen when it's made, such as `laptop` or `t

**You make a key** as the owner, or with a key that holds the `keys.mint` permission, and say what it holds. A key made by another key can hold only the permissions and maps its maker holds, and a key can be set to expire. **The maker's `enforcement_override` isn't carried over**: unless the new key names one, it follows the instance's levers ([Settings](/guides/settings) describes them), so it can list items that its maker's levers hide. [Manage keys](/guides/manage-keys) shows how to make, narrow and revoke keys.

**An app is never given a key.** It registers itself with the instance and sends the owner to approve it, as when you sign in to an app with your Google account. The owner sees each scope the app asks for, such as `core.note:read`, and the app's token holds only what the owner leaves ticked. A browser that signed in more than five minutes ago can approve reading only; write access and permissions need a recent sign-in. The approval stands until the owner revokes it, or until the app goes unused for a year. [Authenticate](/guides/authenticate) shows an app's side of it.
**An app is never given a key.** It registers itself with the instance and sends the owner to approve it, as when you sign in to an app with your Google account. The owner chooses what to allow for each item type the app asks for, such as `core.note:read`, at the level No access, Read only or Read and edit, and the app's token holds only what the owner chooses. An app that asks for all data can be given chosen types instead. Approving a write or a permission that the app doesn't already hold needs a sign-in from the last five minutes, which the page asks the owner to confirm. The approval stands until the owner revokes it, or until the app goes unused for a year. [Authenticate](/guides/authenticate) shows an app's side of it.

The rules behind this page are in the [keys and sign-in](https://github.com/withmarfa/marfa/blob/main/conformance/spec/keys-and-oauth.md), [authentication](https://github.com/withmarfa/marfa/blob/main/conformance/spec/authentication.md), [owner counts and exports](https://github.com/withmarfa/marfa/blob/main/conformance/spec/search-and-filters.md#search-and-filtersowner-count-export), and [instance claim](https://github.com/withmarfa/marfa/blob/main/conformance/spec/instance-claim.md) chapters of the contract.
10 changes: 9 additions & 1 deletion content/docs/(overview)/devices.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,19 @@ Each write the instance answers gets one verdict:
| `merged` | Another change had touched the same field, and the instance settled the two by the type's rule, such as keeping the later title. |
| `conflicted` | The two changes collided, and the instance kept both: the item keeps its value, and the write's value goes to a conflicted copy. |
| `refused` | The instance refused the write, for example because it broke the item's type. The device keeps its content so an app can offer it back. |
| `blocked` | Something has to change before the write can go, such as an expired credential. |
| `blocked` | Something has to change before the write can go, such as an expired credential, or the write keeps meeting a failure that other writes don't. |
| `dead` | The write failed five times in ways that count against it, such as an answer the device couldn't read. A lost connection or a server error never counts. |

**Only `refused`, `blocked` and `dead` need an app's attention.** An app decides what to do with each: send it again, withdraw it, or let the person decide. [Versions](/versions) explains how the instance settles two changes to one item.

A `blocked` write has one of six reasons: `credential_refused`, `key_spent`, `ancestor_unavailable`, `conflict_unresolved`, `awaiting_dependency` and `failing_alone`.

**A device sets a write aside as `failing_alone` when it keeps failing in the words of an outage while the server still answers other writes.** A gateway can refuse one write in those words, such as an HTML `413` page for a large upload, and that write would otherwise end every drain and hold back the writes behind it. After the write has failed in several drains over some minutes, the device tests it: it sends another write that doesn't depend on it and, if the server answers in a way that lets the device go on, sends the failing write once more. If that fails in the same way, the device blocks the write as `failing_alone` and carries on with the rest of the queue, and the writes that depend on it wait as `awaiting_dependency`. The exact rules are in the contract: [`queue-and-verdicts/stuck-write-suspect`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/queue-and-verdicts.md#queue-and-verdictsstuck-write-suspect) for when a write becomes a suspect, and [`queue-and-verdicts/stuck-write-test-refused`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/queue-and-verdicts.md#queue-and-verdictsstuck-write-test-refused) for an answer that ends the test.

Releasing the write gives it a fresh try, and withdrawing it refuses the writes that wait on it.

The queue shows the last answer a write met in this way. `marfa device queue` prints it after `last answer:`, as `HTTP <status>` with the start of the body when the body is text, or `no answer`.

## Folders

A device's working copy can also be a directory of files on your computer: **a folder**. Each item the folder holds is a file, a note becomes a Markdown file, and an edit to either the file or the item reaches the other.
Expand Down
2 changes: 2 additions & 0 deletions content/docs/(overview)/properties-metadata-and-edges.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ Content-Type: application/json
{ "tags": ["favorite"] }
```

**A tag can't be empty, blank or longer than 128 UTF-16 code units, and it can't be `.` or `..`.** Marfa refuses such a tag `400 validation_error` on every write that carries tags except a restore, and for `.` or `..` the message in `details.errors[]` names the tag. A path can't name `.` or `..`, because a URL parser drops them, so such a tag could never be removed with `DELETE /items/{id}/tags/{tag}`.

An extension is a JSON object stored on an item under a namespace, such as `com.example.reader`. Whoever writes a namespace decides what goes in it, and **one app's extension stays out of another's way**: a key reads and writes only the namespaces it was given.

```http
Expand Down
4 changes: 2 additions & 2 deletions content/docs/(overview)/quickstart.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: Start Marfa on your own computer, claim it, make a key, and write a

By the end of this page you'll have a Marfa instance running on your computer, with you as its owner and one note in it. You need two terminal windows: one for the server, one for everything else. This instance is for trying Marfa; [Deploy](/guides/deploy) covers one you keep.

**You need** macOS or Linux, with Git, [Node.js](https://nodejs.org) 22.12 or later and [pnpm](https://pnpm.io/installation). On Linux, also install [Rust](https://www.rust-lang.org/tools/install), to build the CLI, and the `acl` package, which the server uses to check who may reach its private socket.
**You need** macOS or Linux, with Git, [Node.js](https://nodejs.org) 22.14 or later and [pnpm](https://pnpm.io/installation). On Linux, also install [Rust](https://www.rust-lang.org/tools/install), to build the CLI, and the `acl` package, which the server uses to check who may reach its private socket.

<Steps>
<Step>
Expand Down Expand Up @@ -74,7 +74,7 @@ The CLI asks for a password for the owner, then for the setup code the server pr
</Tab>
<Tab value="In the browser">

Open `http://localhost:8600/setup`, enter the setup code the server printed, and choose the owner's email and password. **Claiming signs you in**, and setup goes on from there to add a passkey, name the instance, connect something and read how it's backed up.
Open `http://localhost:8600/setup`, enter the setup code the server printed, and choose the owner's email and password. **Claiming signs you in**, and setup goes on from there to add a passkey, name the instance, connect something and read how it's backed up. Done opens [Home](/guides/authenticate#start-from-home).

</Tab>
</Tabs>
Expand Down
32 changes: 32 additions & 0 deletions content/docs/(reference)/changelog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,38 @@ Each release of Marfa ships the server and its container image, the CLI for macO

**Until the first public release, Marfa does not upgrade a database in place.** A build refuses a database whose schema differs from its own or that holds retired tables or settings, and leaves the file unchanged. If the build refuses your database, [Upgrade](/guides/upgrade) covers exporting with the old build and restoring into fresh storage. A version change alone does not cause a refusal.

## v0.0.18

Released October 20, 2026. [v0.0.18 on GitHub](https://github.com/withmarfa/marfa/releases/tag/v0.0.18)

For running an instance:

- **The owner has a Home page at `/`.** A browser signed in as the owner sees the instance's name and address, what needs attention, a search for pages, and Sign out. A browser that isn't signed in gets a page that says the instance is running, with a Sign in link. An owner page under `/auth/owner/` sends a browser with no session to the sign-in page and returns it to the page it asked for, and an unclaimed instance sends it to `/setup`. A program that requests `/` still gets the description of the instance. [Start from Home](/guides/authenticate#start-from-home) covers it.
- **The owner manages API keys in the browser.** The API keys pages list the keys, create one, and edit or revoke one. A key's access starts off, and the secret shows once and is removed when the owner leaves the page. A save is refused if the key changed after the page opened. [Use the API keys page](/guides/manage-keys#use-the-api-keys-page) covers it.
- **The owner has Profile and Password and passkeys pages.** Adding, renaming or removing a passkey asks the owner to confirm it's them when the sign-in is more than five minutes old. Changing the password asks for the current password instead. A passkey name that is empty or too long is refused `400 validation_error` when the passkey is verified. [Authenticate](/guides/authenticate#sign-in-as-the-owner) covers it.
- **A folder that a different directory replaces stays listed.** A folder registry entry records the directory's identity. When another directory takes the folder's place, the entry stays, and the watch resumes when the directory the folder was admitted at returns. Marfa removes an entry when the directory at its path holds no folder state and isn't known to differ from the one admitted. [Sync a folder](/guides/sync-a-folder#a-missing-or-unreadable-directory) covers it.
- **Sign out stays beside the title on a phone.** On Home, the Sign out button no longer drops under the title and address.
- **Destructive actions sit in their own card, and Save appears only after a change.** Revoking a key and removing a passkey are in a bordered card of their own. A key's page shows Save and Discard, with a count of the changes, only once something has changed.
- **The server needs Node.js 22.14 or later.** The container image runs Node.js 24. [Deploy](/guides/deploy) covers it.

For apps and devices:

- **The owner chooses what an app gets.** The approval screen lists each type with No access, Read only or Read and edit, then the owner's profile and the powers over Marfa that are off until the owner turns them on. Three powers that can't be undone are marked. Access the app already holds shows as Already allowed. A request for all data also shows Choose types, and if the owner picks types the token names them instead of all data. If the request asks for access that the app doesn't already hold and the owner turns all of it off, the app gets `access_denied` and keeps what it held. A request for nothing but `openid` and `offline_access` offers nothing to turn off, so the owner can approve it. [What the owner chooses](/guides/authenticate#what-the-owner-chooses) covers it.
- **A device code is easier to type.** The device page shows a code as `XXXX-XXXX`, accepts it in any case with or without the hyphen, and a code ends after 10 minutes.
- **Approving or revoking asks to confirm in a dialog, not a redirect.** `POST /auth/confirm` takes the owner's password or a passkey and replaces the session, so the browser carries on at the same page. A password counts against the attempt windows: a wrong one answers `401 unauthorized`, and once the windows are spent the answer is `429 rate_limited` with `Retry-After`. A refused confirmation keeps the session. A program that calls a route that needs a recent sign-in gets `403 forbidden` with "Sign in again to perform this operation". `POST /auth/confirm` answers `401` to a request with no live owner session or with a key. A browser may end its own sign-in at any time. [When approval needs a recent sign-in](/guides/authenticate#when-approval-needs-a-recent-sign-in) covers it.
- **`narrow_only` keeps a key from gaining access.** `PATCH /keys/{id}` with `narrow_only: true` refuses a change that adds access, and the API keys pages send it. `POST /keys` answers `Cache-Control: no-store`, so the secret isn't cached. [Narrow a key](/guides/manage-keys#narrow-a-key) covers it.
- **Marfa keeps a whole number exact, or refuses it.** A `number` or `integer` value that Marfa can't serve back with the same digits, such as `1500000000000000001`, is refused `400 invalid_properties`, and `details.errors` names the field. A `url` value is trimmed and judged as the WHATWG URL Standard does. For a special scheme such as `http`, a host with a non-ASCII character must be valid IDNA, and a host with a forbidden code point is refused. [Field kinds](/catalog/field-kinds) covers it.
- **A tag of `.` or `..` is refused** with `400 validation_error`, as an empty, blank or over-long tag is. A restore still accepts what an archive holds.
- **Narrowing a webhook cancels the deliveries it no longer selects.** Pending deliveries that the narrowed subscription doesn't select end `canceled`. Changing the `url` or setting `active: false` cancels all of them, and widening revives none. [Change what a webhook sends](/guides/receive-webhooks#change-what-a-webhook-sends) covers it.
- **The CLI prints access in the words the approval page uses.** `marfa login`, `whoami`, and `keys list`, `current`, `create` and `update` print No access, Read only and Read and edit for each type, and name powers by their titles. `--json` is unchanged. [CLI](/cli#check-what-a-command-will-use) covers it.
- **A write that fails alone is set aside.** A device marks a queued write `blocked` with the reason `failing_alone` when it keeps failing in the words of an outage, over some minutes, while the server still answers other writes. It tests the write first by sending another write that doesn't depend on it, and sets the write aside only if it fails in the same way when sent once more. Its dependents wait as `awaiting_dependency`. `marfa device queue` prints the last answer the write met after `last answer:`. [Devices](/devices#verdicts) covers it, and [`queue-and-verdicts/stuck-write-suspect`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/queue-and-verdicts.md#queue-and-verdictsstuck-write-suspect) has the exact rule.
- **A file item keeps its bytes at a document name.** A file already bound to a file item, at a name ending `.md`, `.markdown` or `.txt`, is sent as bytes whatever they hold, with no frontmatter read. If it is empty it waits, and the rest of the scan goes on. A file at such a name that isn't bound to a file item is still read as a document. A `marfa_id` over 256 bytes is no id.
- **`--no-keychain` keeps the CLI off the keychain.** `--no-keychain` or `MARFA_NO_KEYCHAIN=1` makes every command treat the keychain as absent, and `marfa login --print-token` no longer reads or writes it. A command that sends a key from `--key` or `MARFA_API_KEY` and has no server named is refused `no_server`. [CLI](/cli#run-without-the-keychain) covers it.
- **The approval rows of two types with the same label are told apart.** When two item types share a label, such as `core.task` and `aic.ticket.task`, each shows its identifier under the label, and the type search matches the identifier.
- **A lock file the CLI can't write is refused with a message.** When the lock file opens read-only, the CLI stops with `<file> cannot be opened for writing, so it cannot be locked: check that it is writable`.
- **A repeat of a write whose first answer was large is refused, not run again.** Marfa keeps only the status of a first answer over 1,048,576 bytes, so a repeat under the same `Idempotency-Key` answers `422 idempotency_result_not_retained` with `details.original_status`, and writes nothing. A device counts a `2xx` status there as accepted, except for a create that names no `id`, which it refuses. [Write safely](/guides/write-safely#retry-with-an-idempotency-key) covers it.
- **A waiting edge edit or delete stays in view through a catch-up.** When a catch-up brings an item into a device's copy along with its edges, the device lays its own waiting writes over those edges, as hydration does, instead of showing the server's version until the server answers.

## v0.0.17

Released October 20, 2026. [v0.0.17 on GitHub](https://github.com/withmarfa/marfa/releases/tag/v0.0.17)
Expand Down
Loading
Loading