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
4 changes: 2 additions & 2 deletions content/docs/(overview)/access.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,14 @@ A map names things at a level, `read` or `write`, and `write` includes reading.
| Edge | `edge_permissions` | Edge types | `"about": "read"` |
| Extension | `extension_permissions` | Extension namespaces | `"com.example.reader": "write"` |
| Metadata | `metadata_permissions` | Registering types and edge types | `"types": "write"` |
| Profile | `profile_permissions` | The owner's name, email and avatar | `"email": "read"` |
| Profile | `profile_permissions` | The owner's name, email and picture | `"email": "read"` |

A few rules apply to single maps:

- **The type map** matters most. Only it takes a third level, `none`, which shuts out a type its wildcard covers.
- **The metadata map** covers the definitions on your instance, not an item's tags and extensions.
- **The extension map** is a key's alone: an app reaches no extensions.
- **The profile map** can write only the owner's name and avatar, never the sign-in email.
- **The profile map** can write only the owner's name and picture, never the sign-in email.

## What a refusal reveals

Expand Down
2 changes: 1 addition & 1 deletion content/docs/(overview)/devices.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Reads come from the copy, with no connection needed. A file's bytes are there on

Sometimes a copy can't be brought up to date, for example after the device was away longer than the instance keeps events. The device then fills its copy again from scratch, and its **queued writes survive**.

A copy made by an earlier build of the core can have a different shape. The device **refuses to open it** with `wrong_schema`, deletes nothing, and says how many writes the file holds that the server hasn't taken, because only the build that made it can send them. If some are of a kind the current build no longer sends, it names the kind. [Troubleshooting](/guides/troubleshooting#staying-in-step) says what to do.
A copy made by an earlier build of the core can have a different shape. The device **refuses to open it** with `wrong_schema`, deletes nothing, and says how many writes the file holds that the server hasn't taken, because only the build that made it can send them. If some are of a kind the current build no longer sends, it names the kind. [Troubleshooting](/guides/troubleshooting#staying-in-step) says what to do. An app built on the core's Node or Swift binding can replace a refused copy that holds nothing unsent with an empty one.

## A write made offline

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

**Marfa doesn't 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; a version change alone doesn't cause a refusal. [Upgrade](/guides/upgrade) shows how to test an export and restore on fresh storage before switching builds.

## v0.0.26

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

**This release changes the database's shape, because the owner's picture is now a file the owner uploads and never a web address.** It refuses an existing v0.0.25 database without changing it: the `auth_user` table no longer has an `image` column, and a new `owner_picture` table holds the picture. Nothing is migrated. To move an instance that holds data, export it with v0.0.25, restore the export into a fresh v0.0.26 instance and upload the owner's picture again, because an archive doesn't carry it. [Upgrade](/guides/upgrade) covers the process.

For apps and the API reference (breaking):

- **Three operations store and serve the owner's picture.** [`PUT /owner/picture`](/api-reference/owner/replaceOwnerPicture) takes the picture from a direct owner sign-in of the last five minutes or from the private socket. [`PUT /profile/picture`](/api-reference/owner/replaceProfilePicture) takes it from a key or an app with profile write access for `avatar`, and answers `{ "ok": true }`. [`GET /owner/picture`](/api-reference/owner/getOwnerPicture) returns the bytes to the signed-in owner and to a credential with profile read access for `avatar`, and answers `404 not_found` while there is no picture. The body of an upload is the picture's bytes, sent as they are and not as `multipart/form-data`: a PNG, JPEG or WebP of 2 MiB or less, between 1 and 4096 pixels on each side. Marfa decides the kind from the bytes and ignores `Content-Type` and the file name. A file of another kind, an empty or damaged one, or one past 4096 pixels answers `400 validation_error`, one past 2 MiB answers `413 request_too_large`, and either leaves the previous picture in place.
- **`avatar` takes only `null` in an update.** [`PATCH /owner`](/api-reference/owner/updateOwner) and [`PATCH /profile`](/api-reference/owner/updateProfile) remove the picture when `avatar` is `null`. A web address, or any other value, answers `400 validation_error` and changes nothing. An app that set `avatar` to an address must upload the picture instead.
- **The owner's `avatar` is the picture's address on the instance.** It reads `https://<your instance>/owner/picture` while there is a picture and `null` while there is none, and the OIDC `picture` claim of an app that may read the avatar carries the same address. The picture isn't public: fetch it with the credential, as `GET /owner/picture` requires.

For using the CLI (breaking):

- **`--avatar` takes a picture file.** `marfa owner update --avatar <file>` and `marfa profile update --avatar <file>` upload the file where they took a web address. `--clear-avatar` removes the picture. The CLI refuses a path that isn't a file, or a file over 2 MiB, before it sends anything, and it sends the picture before the name or email, so a refusal leaves those as they were.
- **`marfa owner picture` saves the owner's picture.** `--output <path>` writes it to a file, and without the option the bytes go to standard output.
- **`marfa server install` says where a tunnel points.** For `cloudflare` and `proxy`, it names the address on this machine that the tunnel or proxy must target, and its summary repeats it as **Point at**, because a tunnel aimed anywhere else answers `502`. The summary names the machine that holds the folder and ends by pointing at `marfa docs guides/ai-agents`. With `--json`, the report gains `host` and `local_address`. `marfa --help` now ends by saying where the docs are. See [Choose how it's reached](/guides/install#choose-how-its-reached).

For the owner's console:

- **Profile uploads and removes the picture.** Under **Picture**, **Change** opens a dialog to choose a PNG, JPEG or WebP of 2 MiB or less, and **Remove picture** removes it. [Change the owner's profile](/guides/sign-in-as-the-owner#change-the-owners-profile) covers it.
- **List rows no longer underline when you point at them.**

For folders, apps and devices:

- **The core's Node and Swift bindings can replace a refused working copy.** `replaceRefused` replaces a file that `open` refuses with `wrong_schema` by an empty copy and opens it, but only when the file holds no write the server hasn't taken. The old file is removed, so what only it held, such as pins, is lost. [Troubleshooting](/guides/troubleshooting#staying-in-step) covers it.

## v0.0.25

Released October 22, 2026. [v0.0.25 on GitHub](https://github.com/withmarfa/marfa/releases/tag/v0.0.25)
Expand Down
4 changes: 3 additions & 1 deletion content/docs/(reference)/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -172,12 +172,14 @@ That's how the owner makes the first key in the [Quickstart](/quickstart). These
| --- | --- | --- |
| `marfa setup status`, `setup code`, `setup open` | Check whether the instance is claimed, replace the setup code, and open the browser handoff. | [Quickstart](/quickstart) |
| `marfa owner recover` | Set a new owner password. | [Sign in as the owner](/guides/sign-in-as-the-owner#recover-the-owners-password) |
| `marfa owner update` | Change the owner's email, name and picture address. | [Sign in as the owner](/guides/sign-in-as-the-owner#change-the-owners-profile) |
| `marfa owner update` | Change the owner's email and name, upload the owner's picture from a file with `--avatar`, or remove it with `--clear-avatar`. | [Sign in as the owner](/guides/sign-in-as-the-owner#change-the-owners-profile) |
| `marfa owner passkeys` | Add, list, rename and remove passkeys. | [Sign in as the owner](/guides/sign-in-as-the-owner#manage-passkeys-and-sign-ins) |
| `marfa items summaries` | List item summaries without opening item content. | [Access](/access#who-can-act) |
| `marfa items trash` | List, move, restore and purge items in the trash. | [Housekeeping](/guides/housekeeping#empty-the-bin) |
| `marfa sign-ins` | List, rename and end the sign-ins that can reach the server. | [Sign in as the owner](/guides/sign-in-as-the-owner#manage-passkeys-and-sign-ins) |

`marfa owner picture` writes the owner's picture to the file named by `--output`, or its bytes to standard output, and a key that may read the profile's `avatar` can run it too. A key or an app with profile write access changes the name and the picture with `marfa profile update`, which takes `--name`, `--avatar <file>` and `--clear-avatar`. [Sign in as the owner](/guides/sign-in-as-the-owner#change-the-owners-profile) shows both.

**The socket carries no key.** `--socket` can't be combined with `--url`, `--key`, `MARFA_API_URL` or `MARFA_API_KEY`. An operation that needs a key, such as `marfa events` or `marfa export`, ends with exit code 5, so run it with a key instead. `marfa login`, `logout`, `device`, `docs`, `types scaffold` and local folder commands refuse `--socket`. `marfa folders inventory` is the exception: it reads the owner’s reported folder copies through the socket.

## Exit codes
Expand Down
6 changes: 3 additions & 3 deletions content/docs/catalog/permissions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ A key's `profile_permissions` names `name`, `email`, `avatar`, or `*`. The wildc
| --- | --- | --- |
| `name` | `read`, `write` | The owner's display name. |
| `email` | `read` | Nothing. Only the owner can change the sign-in address. |
| `avatar` | `read`, `write` | The avatar URL, including clearing it. |
| `*` | `read`, `write` | Name and avatar only; email stays read-only. |
| `avatar` | `read`, `write` | The owner's picture: uploading a new one and removing it. |
| `*` | `read`, `write` | Name and picture only; email stays read-only. |

Profile write includes read. An explicit key grant for email write is refused, as is the OAuth scope `profile.email:write`. OAuth uses `profile.name:write` and `profile.avatar:write` for individual fields, or `profile:write` for both ([owner and delegated profile changes](https://github.com/withmarfa/marfa/blob/main/conformance/spec/instance-claim.md#instance-claimprofile-grant-closed)).
Profile write includes read. Read access to `avatar` lets a credential fetch the picture with [`GET /owner/picture`](/api-reference/owner/getOwnerPicture). An explicit key grant for email write is refused, as is the OAuth scope `profile.email:write`. OAuth uses `profile.name:write` and `profile.avatar:write` for individual fields, or `profile:write` for both ([owner and delegated profile changes](https://github.com/withmarfa/marfa/blob/main/conformance/spec/instance-claim.md#instance-claimprofile-grant-closed)).

[Access](/access) explains the maps, and [Manage keys](/guides/manage-keys) shows how to give a key its access.
2 changes: 1 addition & 1 deletion content/docs/guides/export-and-restore.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: Export and restore an archive
description: Export your items, edges, types and files to an archive, and restore it into the same instance or another, such as a new build.
---

An archive carries your content to another instance: items with their tags, extensions and earlier versions, edges, the types and edge types you registered, and file bytes. It leaves the instance's own setup behind, so it holds no keys, webhooks, configuration or audit log. It can move your data to a new build when you [upgrade](/guides/upgrade), but **test the restore on fresh storage first**: a different build may refuse the archive.
An archive carries your content to another instance: items with their tags, extensions and earlier versions, edges, the types and edge types you registered, and file bytes. It leaves the instance's own setup behind, so it holds no keys, webhooks, configuration, audit log or owner's picture. It can move your data to a new build when you [upgrade](/guides/upgrade), but **test the restore on fresh storage first**: a different build may refuse the archive.

An archive isn't a long-term backup, because only the build that wrote it is sure to read it. To recover an instance from a failed disk, [back up the data directory](/guides/back-up-and-restore) instead.

Expand Down
8 changes: 7 additions & 1 deletion content/docs/guides/install.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ marfa server install
```

```text
Options, all optional: --dir <folder> to install somewhere else, --port <port> for the port on this machine, --name <name> to run more than one, --external-settings when a secrets manager writes marfa.env. `marfa server install --help` lists them all.

Checking Docker...
How will your devices reach this server?
1. local: only this machine, at http://localhost
Expand All @@ -44,7 +46,7 @@ Setting the server to start when you log in...
Marfa <release> is running.

Address http://localhost:8600
Folder /Users/you/marfa-server
Folder /Users/you/marfa-server on your-mac
Backup none: no copy leaves this machine

Keep a copy of /Users/you/marfa-server/marfa.env somewhere safe, outside this folder. It holds the two secrets the data needs, and they are not stored anywhere else.
Expand All @@ -54,6 +56,8 @@ Open this link to choose the owner's email and password. It works once, for five
http://localhost:8600/setup#handoff=…

It starts when you log in, starting Docker first if it is not running, and again after a failure. `marfa server status` shows it.

To give an AI agent access to this server, run `marfa docs guides/ai-agents`.
```

</Terminal>
Expand Down Expand Up @@ -83,6 +87,8 @@ The way you reach the instance decides which caller address the server believes,

The instance gets a Docker network of its own, named for the install (`marfa-net` unless you pass `--name`), and the command reads its range from Docker. The network is made once and kept when the container is replaced, so the range stays the one the setting names. The port is 8600, or the next free one, unless you pass `--port`. It is published on the loopback address only, so only a proxy or tunnel on the same machine reaches it.

**For `cloudflare` and `proxy`, point the tunnel or proxy at `http://localhost:` and the port, on the machine that holds the folder.** The command says so when it asks for the address, and its summary repeats it as **Point at**, with the machine's name beside the folder. A tunnel aimed at another port, or running on another machine, makes the address answer `502`.

## What the command writes

The command keeps everything in one folder, `marfa-server` in your home directory unless you pass `--dir`:
Expand Down
16 changes: 13 additions & 3 deletions content/docs/guides/sign-in-as-the-owner.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ The list gives each sign-in's ID, kind, name and last use. A browser can't be re
When your sign-in is older than five minutes, these changes show a **Confirm it's you** dialog that asks for the owner's password or a passkey, then send the change again:

- Adding, renaming or removing a passkey.
- Changing the owner's name, email or picture address on the **Profile** page.
- Changing the owner's name, email or picture, or removing the picture, on the **Profile** page.
- Making, changing or revoking a key on the **API keys** page.
- Narrowing or disconnecting an app, renaming a sign-in, ending another sign-in, or signing out the other browsers or every browser.
- Moving, restoring, purging or emptying items through **Trash**, removing an unused custom type, retrying a webhook delivery, or restoring an archive.
Expand Down Expand Up @@ -78,9 +78,19 @@ Only the owner changes the sign-in email:
marfa --socket /srv/marfa/data/control/marfa.sock owner update --email owner@example.com --name "Reader"
```

In a browser, the **Profile** page uses [Update the owner](/api-reference/owner/updateOwner). Under **Picture**, choose **Change** to set an HTTP(S) picture address, or leave it blank to clear it. Marfa stores the address, not an uploaded picture. The email is stored in lowercase, needs no confirmation and becomes the address for password sign-in. Everything else the owner holds stays in place.
In a browser, the **Profile** page uses [Update the owner](/api-reference/owner/updateOwner). The email is stored in lowercase, needs no confirmation and becomes the address for password sign-in. Everything else the owner holds stays in place.

A key or an app with profile write access changes only the owner's **name or avatar**, with `marfa profile update --name "Reader"` or [Update the owner's profile](/api-reference/owner/updateProfile). **Email is never writable this way.** [Permissions](/catalog/permissions#access-to-profile-fields) lists the levels.
**The picture is a file you upload, never a web address.** It must be a PNG, JPEG or WebP of **2 MiB or less**, between 1 and 4096 pixels on each side. Marfa decides the kind from the file's bytes, ignores its name, and keeps the picture in its database. On the **Profile** page, choose **Change** under **Picture**, choose the file and select **Save**; **Remove picture** removes it. From a terminal, pass the file to `--avatar`:

```bash
marfa --socket /srv/marfa/data/control/marfa.sock owner update --avatar ./me.png
marfa --socket /srv/marfa/data/control/marfa.sock owner picture --output ./saved.png
marfa --socket /srv/marfa/data/control/marfa.sock owner update --clear-avatar
```

The CLI refuses a path that isn't a file, or a file over 2 MiB, before it sends anything. A file of another kind, or one that is empty or damaged, is refused `400 validation_error` and the previous picture stays. An API client sends the bytes with [Replace the owner's picture](/api-reference/owner/replaceOwnerPicture) and reads them with [Get the owner's picture](/api-reference/owner/getOwnerPicture).

A key or an app with profile write access changes only the owner's **name or picture**, with `marfa profile update --name "Reader"`, `marfa profile update --avatar ./me.png` or [Update the owner's profile](/api-reference/owner/updateProfile). An app uploads a picture with [Replace the picture as an app](/api-reference/owner/replaceProfilePicture), and an update can only remove one by setting `avatar` to `null`. **Email is never writable this way.** [Permissions](/catalog/permissions#access-to-profile-fields) lists the levels.

## Recover the owner's password

Expand Down
2 changes: 1 addition & 1 deletion content/docs/guides/troubleshooting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ A working copy is a file that one build of the core made, and a build with a dif

The message says how many writes the file holds that the server has not taken: waiting, blocked, refused or dead. Only the build that made the file can send or show them, so what you do depends on that count:

- **None.** Every write the file holds reached the server. Run `marfa device hydrate` to build a new working copy.
- **None.** Every write the file holds reached the server. Run `marfa device hydrate` to build a new working copy. An app built on the core's Node or Swift binding can call `replaceRefused` instead: it replaces the old file with an empty copy, so what only the old file held, such as pins, is lost.
- **Some.** Open the file with the build that made it, run `marfa device drain` to send the writes, and use `marfa device discard` on any it refuses that you don't want. Then hydrate a new copy with the newer build. Keep the old file until you have done this, because deleting it loses those writes.

If some of the writes are of a kind the current build no longer queues, the message names the kind and how many, such as `1 write of kind merge_metadata`. That one is a tag merge, which an instance at v0.0.24 or later answers `404 not_found`. Read the tags it carries with `marfa device queue`, and add them with `marfa items tag`.
Expand Down
Loading
Loading