From b0f174d251c965390f52cd96ce7e1769b93e74de Mon Sep 17 00:00:00 2001 From: August Cayzer Date: Sun, 25 Oct 2026 18:17:19 +0000 Subject: [PATCH 1/2] docs(build): replace the Guides tab with Build and Run --- .github/scripts/ci-changes.test.sh | 4 +- .github/scripts/read-live-commit.test.sh | 4 +- AGENTS.md | 2 +- STYLE.md | 14 +- content/AGENTS.md | 8 +- content/docs/(overview)/access.mdx | 10 +- content/docs/(overview)/connectors.mdx | 4 +- content/docs/(overview)/devices.mdx | 6 +- content/docs/(overview)/docs-mcp-server.mdx | 2 +- content/docs/(overview)/events.mdx | 4 +- content/docs/(overview)/files.mdx | 8 +- content/docs/(overview)/glossary.mdx | 24 +- content/docs/(overview)/how-marfa-works.mdx | 4 +- content/docs/(overview)/introduction.mdx | 2 +- content/docs/(overview)/items-and-types.mdx | 8 +- content/docs/(overview)/overview.mdx | 20 +- content/docs/(overview)/quickstart.mdx | 14 +- content/docs/(overview)/versions.mdx | 2 +- .../docs/(reference)/api-reference/index.mdx | 2 +- content/docs/(reference)/changelog.mdx | 216 +++++++++--------- content/docs/(reference)/cli.mdx | 32 +-- .../docs/(reference)/folder-watch-record.mdx | 2 +- content/docs/(reference)/swift-sdk.mdx | 14 +- content/docs/(reference)/typescript-sdk.mdx | 6 +- content/docs/{guides => build}/ai-agents.mdx | 2 +- .../docs/{guides => build}/authenticate.mdx | 6 +- .../docs/{guides => build}/change-a-type.mdx | 10 +- .../{guides => build}/connect-a-web-page.mdx | 10 +- .../connector-follow-changes.mdx | 12 +- .../connector-holds-and-retries.mdx | 4 +- .../connector-pull-and-push.mdx | 6 +- .../{guides => build}/connector-webhooks.mdx | 6 +- content/docs/{guides => build}/connector.mdx | 14 +- .../docs/{guides => build}/define-a-type.mdx | 6 +- .../docs/{guides => build}/handle-files.mdx | 2 +- content/docs/build/index.mdx | 17 ++ .../docs/{guides => build}/keep-in-sync.mdx | 0 .../{guides => build}/list-filter-search.mdx | 4 +- content/docs/{guides => build}/meta.json | 29 +-- .../move-items-to-another-type.mdx | 8 +- .../{guides => build}/raycast-extension.mdx | 10 +- .../docs/{guides => build}/read-a-view.mdx | 2 +- .../{guides => build}/receive-webhooks.mdx | 4 +- .../docs/{guides => build}/save-a-view.mdx | 4 +- content/docs/{guides => build}/swift-app.mdx | 6 +- .../docs/{guides => build}/write-safely.mdx | 0 .../docs/catalog/(types)/(system)/folder.mdx | 2 +- .../docs/catalog/(types)/(system)/view.mdx | 2 +- content/docs/catalog/(types)/types.mdx | 2 +- .../docs/catalog/(types)/your-own-types.mdx | 4 +- content/docs/catalog/connectors/github.mdx | 2 +- .../catalog/connectors/google-calendar.mdx | 4 +- content/docs/catalog/connectors/index.mdx | 2 +- .../catalog/connectors/readwise-reader.mdx | 2 +- content/docs/catalog/connectors/readwise.mdx | 2 +- content/docs/catalog/connectors/rss.mdx | 2 +- content/docs/catalog/connectors/todoist.mdx | 2 +- content/docs/catalog/edges.mdx | 2 +- content/docs/catalog/events.mdx | 2 +- content/docs/catalog/index.mdx | 2 +- content/docs/catalog/items.mdx | 2 +- content/docs/catalog/permissions.mdx | 2 +- content/docs/meta.json | 3 +- content/docs/{guides => run}/audit-log.mdx | 6 +- .../{guides => run}/back-up-and-restore.mdx | 6 +- content/docs/{guides => run}/deploy.mdx | 26 +-- .../{guides => run}/export-and-restore.mdx | 8 +- content/docs/{guides => run}/folder-files.mdx | 8 +- .../{guides => run}/folder-from-a-mac-app.mdx | 2 +- .../{guides => run}/folder-safeguards.mdx | 6 +- .../docs/{guides => run}/google-project.mdx | 4 +- content/docs/{guides => run}/housekeeping.mdx | 6 +- content/docs/run/index.mdx | 19 ++ content/docs/{guides => run}/install.mdx | 10 +- .../{guides => run}/keep-folders-running.mdx | 6 +- content/docs/{guides => run}/manage-keys.mdx | 12 +- content/docs/run/meta.json | 32 +++ .../docs/{guides => run}/run-a-connector.mdx | 8 +- content/docs/{guides => run}/settings.mdx | 22 +- .../{guides => run}/sign-in-as-the-owner.mdx | 20 +- .../docs/{guides => run}/sync-a-folder.mdx | 18 +- content/docs/{guides => run}/telemetry.mdx | 6 +- .../docs/{guides => run}/troubleshooting.mdx | 40 ++-- content/docs/{guides => run}/upgrade.mdx | 8 +- src/components/section-nav.test.ts | 8 +- src/components/section-nav.tsx | 3 +- src/lib/catalog.test.ts | 2 +- src/lib/command-check-allowlist.json | 10 +- src/lib/content-lint.test.ts | 20 +- src/lib/docs-api.test.ts | 2 +- src/lib/docs-map.test.ts | 18 +- src/lib/docs-search.test.ts | 66 +++--- src/lib/docs-structure.test.ts | 2 +- src/lib/docs-tools.test.ts | 20 +- src/lib/page-growth.test.ts | 8 +- src/lib/staleness-report.test.ts | 12 +- 96 files changed, 558 insertions(+), 499 deletions(-) rename content/docs/{guides => build}/ai-agents.mdx (98%) rename content/docs/{guides => build}/authenticate.mdx (94%) rename content/docs/{guides => build}/change-a-type.mdx (80%) rename content/docs/{guides => build}/connect-a-web-page.mdx (91%) rename content/docs/{guides => build}/connector-follow-changes.mdx (84%) rename content/docs/{guides => build}/connector-holds-and-retries.mdx (96%) rename content/docs/{guides => build}/connector-pull-and-push.mdx (96%) rename content/docs/{guides => build}/connector-webhooks.mdx (96%) rename content/docs/{guides => build}/connector.mdx (92%) rename content/docs/{guides => build}/define-a-type.mdx (96%) rename content/docs/{guides => build}/handle-files.mdx (99%) create mode 100644 content/docs/build/index.mdx rename content/docs/{guides => build}/keep-in-sync.mdx (100%) rename content/docs/{guides => build}/list-filter-search.mdx (98%) rename content/docs/{guides => build}/meta.json (52%) rename content/docs/{guides => build}/move-items-to-another-type.mdx (94%) rename content/docs/{guides => build}/raycast-extension.mdx (95%) rename content/docs/{guides => build}/read-a-view.mdx (96%) rename content/docs/{guides => build}/receive-webhooks.mdx (96%) rename content/docs/{guides => build}/save-a-view.mdx (92%) rename content/docs/{guides => build}/swift-app.mdx (97%) rename content/docs/{guides => build}/write-safely.mdx (100%) rename content/docs/{guides => run}/audit-log.mdx (94%) rename content/docs/{guides => run}/back-up-and-restore.mdx (91%) rename content/docs/{guides => run}/deploy.mdx (88%) rename content/docs/{guides => run}/export-and-restore.mdx (95%) rename content/docs/{guides => run}/folder-files.mdx (88%) rename content/docs/{guides => run}/folder-from-a-mac-app.mdx (94%) rename content/docs/{guides => run}/folder-safeguards.mdx (96%) rename content/docs/{guides => run}/google-project.mdx (96%) rename content/docs/{guides => run}/housekeeping.mdx (94%) create mode 100644 content/docs/run/index.mdx rename content/docs/{guides => run}/install.mdx (92%) rename content/docs/{guides => run}/keep-folders-running.mdx (95%) rename content/docs/{guides => run}/manage-keys.mdx (89%) create mode 100644 content/docs/run/meta.json rename content/docs/{guides => run}/run-a-connector.mdx (88%) rename content/docs/{guides => run}/settings.mdx (89%) rename content/docs/{guides => run}/sign-in-as-the-owner.mdx (88%) rename content/docs/{guides => run}/sync-a-folder.mdx (87%) rename content/docs/{guides => run}/telemetry.mdx (92%) rename content/docs/{guides => run}/troubleshooting.mdx (93%) rename content/docs/{guides => run}/upgrade.mdx (87%) diff --git a/.github/scripts/ci-changes.test.sh b/.github/scripts/ci-changes.test.sh index bbc63aad..260f293e 100644 --- a/.github/scripts/ci-changes.test.sh +++ b/.github/scripts/ci-changes.test.sh @@ -37,9 +37,9 @@ runs "a script inside an agent directory" false .agents/skills/x/helper.py runs "the issue and pull request templates" false .github/ISSUE_TEMPLATE/bug.yml .github/PULL_REQUEST_TEMPLATE.md # What must run. -runs "a docs page, which is built into the site" true content/docs/guides/items.mdx +runs "a docs page, which is built into the site" true content/docs/build/items.mdx runs "Markdown under content, which is built too" true content/docs/notes.md -runs "a sidebar file" true content/docs/guides/meta.json +runs "a sidebar file" true content/docs/build/meta.json runs "source" true src/lib/source.ts runs "a public asset" true public/logo.svg runs "the API spec" true openapi.json diff --git a/.github/scripts/read-live-commit.test.sh b/.github/scripts/read-live-commit.test.sh index 07e89aa3..ef5589d0 100644 --- a/.github/scripts/read-live-commit.test.sh +++ b/.github/scripts/read-live-commit.test.sh @@ -465,13 +465,13 @@ verdict() { # verdict ... if printf '%s\n' "$@" | paths_touch_target "$target"; then printf 'publish'; else printf 'skip'; fi } -check "docs: a content page publishes the docs site" publish "$(verdict docs content/docs/guides/x.mdx)" +check "docs: a content page publishes the docs site" publish "$(verdict docs content/docs/build/x.mdx)" check "docs: the status source does not" skip "$(verdict docs src/status/lib/state.ts)" check "docs: the status configs do not" skip "$(verdict docs vite.status.config.ts wrangler.status.jsonc tsconfig.status.json)" check "docs: a mixed change publishes it" publish "$(verdict docs src/status/lib/state.ts content/docs/x.mdx)" check "status: its own source publishes it" publish "$(verdict status src/status/lib/state.ts)" -check "status: a content page does not" skip "$(verdict status content/docs/guides/x.mdx)" +check "status: a content page does not" skip "$(verdict status content/docs/build/x.mdx)" check "status: a docs route does not" skip "$(verdict status src/routes/mcp.ts)" check "status: a shared component publishes it" publish "$(verdict status src/components/error-page.tsx)" check "status: a workflow change does not" skip "$(verdict status .github/workflows/publish.yml)" diff --git a/AGENTS.md b/AGENTS.md index de24b26c..24eae512 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,7 +12,7 @@ The source of the public documentation site at `docs.marfa.so` and the status pa ## Conventions - American English in code, comments and commits. Pages follow `STYLE.md`. -- Conventional Commits with a scope, such as `docs(guides):` or `fix(mcp):`. Work reaches `main` only through a pull request, one logical change each. +- Conventional Commits with a scope, such as `docs(build):` or `fix(mcp):`. Work reaches `main` only through a pull request, one logical change each. - Make worktrees in `.claude/worktrees/` (`git worktree add .claude/worktrees/ -b origin/main`). Once the work is merged, remove the worktree and its local branch (`git worktree remove`, then `git branch -D`, because git sees a squash-merged branch as unmerged) and run `git pull --ff-only` on `main`. If git refuses a removal, report it rather than forcing it. - TypeScript and ESM. pnpm with the committed lockfile, on Node 22.22.2 or later (24.15 or later on Node 24), which jsdom, the component tests' DOM, requires; TanStack Start, Vite and Vitest need only 22.12. CI uses Node 22. - This repository is public. Commit no internal hostname, private IP address, personal path, key or token, in a page or anywhere else. diff --git a/STYLE.md b/STYLE.md index 33df303e..eeca365f 100644 --- a/STYLE.md +++ b/STYLE.md @@ -13,9 +13,9 @@ How every page on this site is written. This file is the one place the writing r Write for a reader who is new to Marfa. Most know software; some don't, and more of them will as apps built on Marfa reach people. The Introduction and the concepts must make sense to both. Five readers come most often, and each needs something different first: - **Someone deciding whether Marfa is for them** needs to know what it is, what it's for and what it isn't: the Introduction, then How Marfa works. -- **An app builder** needs a credential, then reading and writing items, then the model: the Quickstart, the concepts, then the Build apps and agents guides. -- **Someone running an instance** needs to start it, claim it and keep it safe: the Quickstart, then the Run Marfa guides. -- **A connector author** needs what the instance keeps for a connector, then the tutorial and the connector guides. +- **An app builder** needs a credential, then reading and writing items, then the model: the Quickstart, the concepts, then Build. +- **Someone running an instance** needs to start it, claim it and keep it safe: the Quickstart, then Run. +- **A connector author** needs what the instance keeps for a connector, then the connector tutorials and guides in Build. - **An AI agent**, working for any of them, reads the same pages as Markdown, through the docs MCP server or `marfa docs`. A page that a newcomer can follow serves an agent too. Never write that Marfa is only for technical people. @@ -27,7 +27,8 @@ A page that a newcomer can follow serves an agent too. Never write that Marfa is | Overview | Get started: the Overview, an index of the docs; the Introduction; the Quickstart | Navigation, explanation, then a tutorial | | Overview | Concepts: How Marfa works, then one page per idea, then the Glossary | Explanation, then reference | | Overview | AI tools: the Docs MCP server and Docs as Markdown | How-to | -| Guides | Tutorials, Build apps and agents, Build connectors, Run Marfa (ending with Troubleshooting) | Tutorial, how-to | +| Build | Build on Marfa, an index of the tab; Tutorials; Build apps and agents; Build connectors | Navigation, tutorial, then how-to | +| Run | Run Marfa, an index of the tab; Set up; Keys and access; Folders; Connectors; Look after it (ending with Troubleshooting) | Navigation, then how-to | | Catalog | The Catalog, Items, Types (its own page, with the Core types and System types folders of one page per type under it, then Type rules and Field kinds), Edges, Events, Permissions, Connectors (its own page, with one page for each connector Marfa ships under it) | Reference | | Reference | The Reference landing page (`/reference`): a card for each of the SDKs, the CLI, the HTTP API, the Changelog and the contract | Reference | | Reference | SDKs and CLI: one page each, and the file the CLI keeps for other programs | How-to and reference for each client, then reference | @@ -37,7 +38,8 @@ A page that a newcomer can follow serves an agent too. Never write that Marfa is The tabs divide the work, and each thing is said once, with links between: - **Overview** explains what Marfa is and how it works, with two or three examples at most. -- **Guides** show how to do things. +- **Build** shows how to write an app, an agent or a connector on Marfa. +- **Run** shows how to run an instance and look after it, and how to use Marfa without writing code. - **Catalog** lists exactly what Marfa ships and explains nothing. - **Reference** covers the HTTP API's operations and errors, the SDKs, the CLI, the changelog and the contract. @@ -138,7 +140,7 @@ Use a component for what it's for, and only where it helps the reader. | `Accordion` | Entries a reader opens one at a time, such as troubleshooting. Never a reference list: a list a reader looks up is a plain table on its Catalog or Reference page | | `Files` | A directory a reader will see on disk | | `Callout` | Something that must interrupt: an action that can't be undone, a step that loses data, or the Overview's one-line warning that Marfa is early. Every callout has a bold `title` that says what happens, and a short body under it. It draws no icon. `type="warn"` is for a loss or something that can't be undone; a neutral note takes the default, `info` | -| `Cards` | Where to go next: the Overview, an index of the docs, and the end of a tutorial or of the Introduction | +| `Cards` | Where to go next: the Overview, an index of the docs, the Build and Run landing pages, and the end of a tutorial or of the Introduction | Write an object's fields as a Markdown table (field, type, description), not with `TypeTable`: a table reads the same on the page and in the Markdown copy. diff --git a/content/AGENTS.md b/content/AGENTS.md index 54e2e423..e33eecf5 100644 --- a/content/AGENTS.md +++ b/content/AGENTS.md @@ -6,10 +6,10 @@ This file sits above `content/docs/` on purpose: the docs collection publishes e ## Placing a page -- Each folder directly under `content/docs/` is a tab. `(overview)/` and `(reference)/` are folder groups, so their pages sit at the site root (`/files`, `/cli`). `guides/` and `catalog/` keep their names in the URL, and so does `(reference)/api-reference/`. The Catalog tab's Types pages sit in a folder group, `catalog/(types)/`, which the sidebar shows as a closed folder named Types. Its own page is `types.mdx` (`pagesIndex` in its `meta.json`, so the folder opens on `/catalog/types`), and it holds Type rules and Field kinds and two more folder groups, `(core)/` and `(system)/`, shown as closed folders, "Core types" and "System types". No group adds to an address: a type page stays at `/catalog/note`. -- A new page needs its file and an entry in its folder's `meta.json`. `content/docs/meta.json` orders the tabs. -- A slug in `(overview)/` or `(reference)/` must not repeat a slug in either group. It must not be a name the Worker answers at the root (`api`, `assets`, `mcp`, `llms`, `llms-full`, `docs-map`, `version`), or `docs`, which the docs tools strip as a prefix. -- The Catalog tab lists names and shapes and explains nothing. Each top-level type, core or system, has a page (`src/lib/catalog.test.ts` holds them to `src/generated/catalog/builtin-catalog.json`) that takes its title and description from the type and includes the type's generated file. A core type's page ends with one `## Example` item that the test checks; Marfa writes system items itself, so their pages have none. Add a type's page, in the folder of its family and in that folder's `meta.json`, when marfa ships a type, and leave explanation to the Overview and the Guides. Include `catalog/edge-types.md`, `permissions.md` and `event-types.md` on Catalog pages only; every other page links to the Catalog page. +- Each folder directly under `content/docs/` is a tab. `(overview)/` and `(reference)/` are folder groups, so their pages sit at the site root (`/files`, `/cli`). `build/`, `run/` and `catalog/` keep their names in the URL, and so does `(reference)/api-reference/`. The Catalog tab's Types pages sit in a folder group, `catalog/(types)/`, which the sidebar shows as a closed folder named Types. Its own page is `types.mdx` (`pagesIndex` in its `meta.json`, so the folder opens on `/catalog/types`), and it holds Type rules and Field kinds and two more folder groups, `(core)/` and `(system)/`, shown as closed folders, "Core types" and "System types". No group adds to an address: a type page stays at `/catalog/note`. +- A new page needs its file and an entry in its folder's `meta.json`. `content/docs/meta.json` orders the tabs. Build, Run and the Catalog each open on an `index.mdx` that their `meta.json` lists first; a page that belongs in Build (code against an instance) or Run (an instance and its care, with no code) goes in that folder, under the group a reader would name. +- A slug in `(overview)/` or `(reference)/` must not repeat a slug in either group. It must not be a name the Worker answers at the root (`api`, `assets`, `mcp`, `llms`, `llms-full`, `docs-map`, `version`) or a tab folder (`build`, `run`, `catalog`), or `docs`, which the docs tools strip as a prefix. +- The Catalog tab lists names and shapes and explains nothing. Each top-level type, core or system, has a page (`src/lib/catalog.test.ts` holds them to `src/generated/catalog/builtin-catalog.json`) that takes its title and description from the type and includes the type's generated file. A core type's page ends with one `## Example` item that the test checks; Marfa writes system items itself, so their pages have none. Add a type's page, in the folder of its family and in that folder's `meta.json`, when marfa ships a type, and leave explanation to the Overview, Build and Run. Include `catalog/edge-types.md`, `permissions.md` and `event-types.md` on Catalog pages only; every other page links to the Catalog page. - The header tabs come from `SECTIONS` in `src/components/section-nav.tsx`, not from `meta.json`. A new tab or a renamed landing page needs both. The Reference tab opens on `(reference)/reference.mdx` (`/reference`), the first entry in `(reference)/meta.json`, with a card for each destination below it. A new Reference page needs its entry in `(reference)/meta.json`, which the header reads, and a card there when it is a new kind of destination. - The header holds no text links. The Changelog and the contract sit in the Reference tab's Resources group, so a new destination goes in a sidebar group, not in `links` in `src/lib/layout.shared.tsx`. - The Reference tab's `meta.json` lifts the API pages into its own sidebar with `...api-reference`, and `src/lib/api-reference-tree.ts` finds the tab by that `meta.json` title. Renaming the title needs `REFERENCE_TITLE` changed there. diff --git a/content/docs/(overview)/access.mdx b/content/docs/(overview)/access.mdx index cfa316c2..1e590012 100644 --- a/content/docs/(overview)/access.mdx +++ b/content/docs/(overview)/access.mdx @@ -19,7 +19,7 @@ Through a **browser session**, the owner can list item summaries, manage the tra Trash summaries also show the deletion time and actor. The **Trash** page can move, restore, purge or empty items. Those writes require a sign-in from the last five minutes. Both listings apply the instance's source filtering; an owner browser session still cannot open an item's content. -Other item content operations need a key or an app token. If a request sends a credential alongside the owner's browser session, that credential alone decides its access. No key or app ever gets the owner's authority, whatever it holds. [Sign in as the owner](/guides/sign-in-as-the-owner) shows how the owner signs in, with a password or a passkey. +Other item content operations need a key or an app token. If a request sends a credential alongside the owner's browser session, that credential alone decides its access. No key or app ever gets the owner's authority, whatever it holds. [Sign in as the owner](/run/sign-in-as-the-owner) shows how the owner signs in, with a password or a passkey. Some owner actions need a **recent sign-in**, one from the last five minutes, such as making, changing or revoking a key, revoking an app, adding or changing a passkey, signing out every browser, or approving an app's request to write or to hold a permission. @@ -62,9 +62,9 @@ The Catalog's [Permissions](/catalog/permissions) page lists them all. ## How a key or an app gets access -You make a key as the owner, or with a key or an app that holds the `keys.mint` permission, and say what it holds. A key made by another credential can hold only the permissions and maps its maker holds, and can't outlive its maker's expiry. [Manage keys](/guides/manage-keys) shows how. +You make a key as the owner, or with a key or an app that holds the `keys.mint` permission, and say what it holds. A key made by another credential can hold only the permissions and maps its maker holds, and can't outlive its maker's expiry. [Manage keys](/run/manage-keys) shows how. -**Signing in by approval is the preferred way for an app to reach Marfa.** The app registers itself and sends the owner to approve it, and it holds only what the owner approved. [Authenticate](/guides/authenticate) shows the app's side. +**Signing in by approval is the preferred way for an app to reach Marfa.** The app registers itself and sends the owner to approve it, and it holds only what the owner approved. [Authenticate](/build/authenticate) shows the app's side. An app may also use a key. The owner can give it one, and an app that holds `keys.mint` can mint one within its own grant. A key an app mints stays within the app's grant and carries the app's `client_id`. @@ -77,10 +77,10 @@ The **Approve access** page, or the device approval page for an app with no brow - **Your profile:** only when the app asks for it. - **More control over Marfa:** each permission starts off. `instance.maintain`, `schema.write` and `items.purge` are marked because they can't be undone. -Approving with everything new off **is declining**: the app gets `access_denied` and keeps what it held. Approving a write or a permission the app doesn't hold needs a [recent sign-in](#who-can-act), so the page asks the owner to [confirm it's them](/guides/sign-in-as-the-owner#confirm-its-you), and a program without one gets `403 forbidden` and no code. +Approving with everything new off **is declining**: the app gets `access_denied` and keeps what it held. Approving a write or a permission the app doesn't hold needs a [recent sign-in](#who-can-act), so the page asks the owner to [confirm it's them](/run/sign-in-as-the-owner#confirm-its-you), and a program without one gets `403 forbidden` and no code. The owner can later **narrow the approved access**. Tokens and refresh chains that exceed the remaining grant stop working immediately, while covered tokens keep working. Giving the app wider access again requires fresh approval. -The approval stands until the owner revokes it, or until the app goes unused for the configured inactivity period. [Authenticate](/guides/authenticate) shows an app's side of it. +The approval stands until the owner revokes it, or until the app goes unused for the configured inactivity period. [Authenticate](/build/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 summaries](https://github.com/withmarfa/marfa/blob/main/conformance/spec/search-and-filters.md#owner-item-summaries), [owner trash management](https://github.com/withmarfa/marfa/blob/main/conformance/spec/items.md#owner-trash-management), [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. diff --git a/content/docs/(overview)/connectors.mdx b/content/docs/(overview)/connectors.mdx index 3646bebd..bc5dd89d 100644 --- a/content/docs/(overview)/connectors.mdx +++ b/content/docs/(overview)/connectors.mdx @@ -35,10 +35,10 @@ The state document, current problems and agreements belong to the key's **source Many vendors post a webhook when something changes, so a connector can act at once. The instance can give a connector an endpoint, a private address the vendor posts to. -The instance stores each delivery as it arrived, and **the connector does the rest**: it reads the deliveries, checks each came from the vendor, and marks them handled. [Receive vendor webhooks](/guides/connector-webhooks) shows how. +The instance stores each delivery as it arrived, and **the connector does the rest**: it reads the deliveries, checks each came from the vendor, and marks them handled. [Receive vendor webhooks](/build/connector-webhooks) shows how. ## Building one -[Build a connector](/guides/connector) walks through a connector from a template, and [Make a connector follow changes](/guides/connector-follow-changes) teaches it to archive what the vendor drops and report a failed run. The six connectors Marfa ships, listed in the [Catalog](/catalog/connectors), are working examples in [withmarfa/marfa-connectors](https://github.com/withmarfa/marfa-connectors), and [Run a connector](/guides/run-a-connector) runs any of them. A connector for a Google service signs in through a project you create yourself, as [Create a Google Cloud project for Google connectors](/guides/google-project) shows. +[Build a connector](/build/connector) walks through a connector from a template, and [Make a connector follow changes](/build/connector-follow-changes) teaches it to archive what the vendor drops and report a failed run. The six connectors Marfa ships, listed in the [Catalog](/catalog/connectors), are working examples in [withmarfa/marfa-connectors](https://github.com/withmarfa/marfa-connectors), and [Run a connector](/run/run-a-connector) runs any of them. A connector for a Google service signs in through a project you create yourself, as [Create a Google Cloud project for Google connectors](/run/google-project) shows. The rules behind this page are in the [connectors](https://github.com/withmarfa/marfa/blob/main/conformance/spec/connectors.md) and [inbound webhooks](https://github.com/withmarfa/marfa/blob/main/conformance/spec/inbound-webhooks.md) chapters of the contract. diff --git a/content/docs/(overview)/devices.mdx b/content/docs/(overview)/devices.mdx index 6672ce4f..8e949545 100644 --- a/content/docs/(overview)/devices.mdx +++ b/content/docs/(overview)/devices.mdx @@ -11,7 +11,7 @@ An app built on the [Swift SDK](/swift-sdk) is a device, and so is the [CLI](/cl A device chooses the **slice** of the instance it keeps: a list of types, a tier, and the edge types it holds. For example, every `core.note` and `core.task` in the library. It fills its copy once by reading every item in the slice, then keeps it current from the instance's [event stream](/events). To hold every type except `system.*`, declare `*` alone: the device reads the whole slice in one pass, with no need to read the catalog and name its types first. `*` beside `system.*` types adds them, and `*` beside any other type is refused `invalid` before anything is read. -Reads come from the copy, with no connection needed. A file's bytes are there once the device has fetched them. The core also [reads saved views](/guides/read-a-view), including sort order, groups and counts. **A view's item or group read needs the whole query held** and is refused with a reason if the copy cannot answer it completely. +Reads come from the copy, with no connection needed. A file's bytes are there once the device has fetched them. The core also [reads saved views](/build/read-a-view), including sort order, groups and counts. **A view's item or group read needs the whole query held** and is refused with a reason if the copy cannot answer it completely. **While a device fills its copy, it answers direct item reads from what has arrived.** A list or search without a saved view returns the items the copy holds so far and marks the answer `partial`; once the copy is whole, the same read is marked `complete`. A read by ID answers for an item that has arrived, and is refused `hydration_incomplete` for one that hasn't. Until the copy is whole, edge reads are refused `hydration_incomplete`, because an edge's two ends can arrive on different pages. A write to an item that has arrived, or a new item, is queued against the version the copy holds and sent once the copy is whole. This includes a file added on its own or attached to an item that has arrived: the device keeps its bytes and uploads them with the rest of the queue. A write to an item that hasn't arrived, or a file attached to one, is refused `hydration_incomplete`, because there is no version to base it on, and a drain is refused `hydration_incomplete` until the copy is whole. **Edge writes are refused `hydration_incomplete` until the copy is whole**, because a page still to arrive can bring the instance's edge in place of the one a write was made against. An edit that changes an item's body is still queued, and the device reads the body's links and embeds and makes their edges once the server answers the edit. The device's status reports how many items the fill has brought so far and, where the instance counted them, how many the slice held when the fill began, so an app can show how far the fill has come. Compare those two figures, not the count of items the copy holds, which also includes pinned items and creates that are still queued. The Swift SDK returns these answers as `Rows` with a `completeness`. @@ -37,7 +37,7 @@ The hydration **empties the copy first** if the instance's event log now ends be **For any other reason, the copy refuses reads and writes until a hydration, which empties it first.** Examples are another instance answering at the same address, or a credential whose reach changed. -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; the files the copy downloaded stay. A [folder](/guides/sync-a-folder#move-to-a-newer-cli) can replace its own copy itself. +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](/run/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; the files the copy downloaded stay. A [folder](/run/sync-a-folder#move-to-a-newer-cli) can replace its own copy itself. ## A write made offline @@ -86,6 +86,6 @@ The sixth reason, `awaiting_dependency`, marks a write waiting behind another th ## Folders -A device's working copy can also be **a folder**, a directory of files on your computer in which each item is a file, such as a note as a Markdown file. [Sync a folder](/guides/sync-a-folder) explains how a folder works and sets one up. +A device's working copy can also be **a folder**, a directory of files on your computer in which each item is a file, such as a note as a Markdown file. [Sync a folder](/run/sync-a-folder) explains how a folder works and sets one up. The rules behind this page are in the [device](https://github.com/withmarfa/marfa/blob/main/conformance/spec/device.md), [queue and verdicts](https://github.com/withmarfa/marfa/blob/main/conformance/spec/queue-and-verdicts.md) and [folders](https://github.com/withmarfa/marfa/blob/main/conformance/spec/folders.md) chapters of the contract. diff --git a/content/docs/(overview)/docs-mcp-server.mdx b/content/docs/(overview)/docs-mcp-server.mdx index c898bea2..8c047188 100644 --- a/content/docs/(overview)/docs-mcp-server.mdx +++ b/content/docs/(overview)/docs-mcp-server.mdx @@ -5,7 +5,7 @@ description: Connect an AI agent to these docs over MCP, so it searches and read The docs MCP server lets an agent search and read these docs while it works. It runs at `https://docs.marfa.so/mcp`, over the Streamable HTTP transport, and needs no credential. -It searches docs.marfa.so and can't reach your instance. To give an agent your data, give it a key and the CLI, as [Use Marfa with AI agents](/guides/ai-agents) shows. +It searches docs.marfa.so and can't reach your instance. To give an agent your data, give it a key and the CLI, as [Use Marfa with AI agents](/build/ai-agents) shows. ## Connect it diff --git a/content/docs/(overview)/events.mdx b/content/docs/(overview)/events.mdx index c75e706d..9cfe4541 100644 --- a/content/docs/(overview)/events.mdx +++ b/content/docs/(overview)/events.mdx @@ -21,7 +21,7 @@ A credential sees only the events about what it can read. The Catalog's [Events] The [event stream](/api-reference/event-stream/streamEvents) is a long-lived connection your app opens. Send your cursor, and Marfa replays every event after it, tells you when you've caught up, and then sends each new event as it happens. Resume from your cursor after a break, and you get every later event **exactly once**. -If you've been away longer than the log keeps events, which [Settings](/guides/settings) sets, your app reads its data again from the start. [Keep a copy in sync](/guides/keep-in-sync) shows how to follow it and handle each way it ends. The Swift SDK and the CLI follow the stream for you. +If you've been away longer than the log keeps events, which [Settings](/run/settings) sets, your app reads its data again from the start. [Keep a copy in sync](/build/keep-in-sync) shows how to follow it and handle each way it ends. The Swift SDK and the CLI follow the stream for you. ## Webhooks @@ -34,6 +34,6 @@ A webhook has no cursor and no replay: it delivers only events written after you | The event stream | An app needs every change, including what it missed while away | | A webhook | A service reacts to changes as they happen, such as sending a message when a task is done | -[Receive webhooks](/guides/receive-webhooks) shows how to register one and check each delivery. +[Receive webhooks](/build/receive-webhooks) shows how to register one and check each delivery. The rules behind this page are in the [events](https://github.com/withmarfa/marfa/blob/main/conformance/spec/events.md) and [reach](https://github.com/withmarfa/marfa/blob/main/conformance/spec/reach.md) chapters of the contract. diff --git a/content/docs/(overview)/files.mdx b/content/docs/(overview)/files.mdx index 0d7daf31..e6e4c8d6 100644 --- a/content/docs/(overview)/files.mdx +++ b/content/docs/(overview)/files.mdx @@ -33,7 +33,7 @@ Content-Type: application/json { "type": "core.file.image", "properties": { "title": "Holiday photo", "blob_ref": "", "mime_type": "image/jpeg" } } ``` -The same bytes uploaded again answer the same hash and are stored once, so two items can share a blob. [Handle files](/guides/handle-files) walks through uploading, downloading and replacing a file. +The same bytes uploaded again answer the same hash and are stored once, so two items can share a blob. [Handle files](/build/handle-files) walks through uploading, downloading and replacing a file. The figure shows a photo and a scanned receipt that name one blob, kept on the disk and, when one is added, in an object store. @@ -43,7 +43,7 @@ The figure shows a photo and a scanned receipt that name one blob, kept on the d **Knowing a hash isn't enough** to read the bytes. A credential reads a blob through an item that names it: if you can read a file item, you can read its blob. A hash anywhere else in an item counts too, such as an image link inside a note's Markdown body. -A reference counts only if whoever wrote it had uploaded the bytes or could already read them, so naming a hash you found somewhere gives you nothing. A file item can't name such a hash at all: the write is refused with `404 blob_not_found`. Upload the bytes with the same credential first, as [Handle files](/guides/handle-files#point-an-item-at-the-bytes) shows. +A reference counts only if whoever wrote it had uploaded the bytes or could already read them, so naming a hash you found somewhere gives you nothing. A file item can't name such a hash at all: the write is refused with `404 blob_not_found`. Upload the bytes with the same credential first, as [Handle files](/build/handle-files#point-an-item-at-the-bytes) shows. A blob you may not read answers `404 blob_not_found`, as a missing one does ([Access](/access#what-a-refusal-reveals)), even one you uploaded, until an item names it. @@ -51,10 +51,10 @@ A blob you may not read answers `404 blob_not_found`, as a missing one does ([Ac ## Where the copies live -Every instance stores its blobs on the disk beside the server. Add an **S3-compatible object store**, and the instance keeps a second copy of every blob there, checks the copies regularly, and replaces one that's missing or damaged. [Deploy](/guides/deploy#an-object-store) covers adding a store. +Every instance stores its blobs on the disk beside the server. Add an **S3-compatible object store**, and the instance keeps a second copy of every blob there, checks the copies regularly, and replaces one that's missing or damaged. [Deploy](/run/deploy#an-object-store) covers adding a store. The instance records each copy in its **copy record**. [List a blob's copies](/api-reference/blobs/listBlobCopies), or `marfa blobs copies `, answers the stores that hold a blob and when each copy was last found intact. [Delete one copy of a blob](/api-reference/blobs/deleteBlobCopy), or `marfa blobs delete-copy`, removes one store's copy where enough live copies remain, and needs the `blobs.manage` permission. -A blob stays as long as something names it, including an item in the bin or an earlier version of an item. Once nothing does, Marfa removes it after a grace period that [Settings](/guides/settings) sets. +A blob stays as long as something names it, including an item in the bin or an earlier version of an item. Once nothing does, Marfa removes it after a grace period that [Settings](/run/settings) sets. The rules behind this page are in the [blobs](https://github.com/withmarfa/marfa/blob/main/conformance/spec/blobs.md) and [stores](https://github.com/withmarfa/marfa/blob/main/conformance/spec/stores.md) chapters of the contract. diff --git a/content/docs/(overview)/glossary.mdx b/content/docs/(overview)/glossary.mdx index 67a1fa6c..a1705367 100644 --- a/content/docs/(overview)/glossary.mdx +++ b/content/docs/(overview)/glossary.mdx @@ -21,11 +21,11 @@ Marfa uses each word below in one sense only, in its code, its API and these doc | [**label**](/properties-metadata-and-edges#edges) | The heading a type or an edge type shows for itself. An edge type's label heads the source's end of an edge, such as `Children` for `parent-of`. | | [**reverse label**](/properties-metadata-and-edges#edges) | The heading an edge type shows for the target's end, such as `Parent`. Where an edge type names none, its label names both ends. | | [**collection**](/catalog/edges) | A flat container of items. An item joins one through an `in-collection` edge to an item whose type has the `container` role, such as an album. It can be in many, a container can't itself be a member, and deleting the container leaves its members. | -| [**view**](/guides/save-a-view) | A saved query with one sort and one grouping, kept on the instance as a `system.view` and shared by every app. How an app draws the items is the app's own. | +| [**view**](/build/save-a-view) | A saved query with one sort and one grouping, kept on the instance as a `system.view` and shared by every app. How an app draws the items is the app's own. | | [**file**](/files) | An item that names its bytes. | | [**blob**](/files) | A file's bytes, stored once under a hash of their content. | -| [**attachment**](/guides/handle-files) | A file attached to another item, such as a photo on a note, by an `attached-to` edge from the file. A device keeps its bytes while it holds the item, and a folder writes it beside the document, or where an embed says. | -| [**embed**](/guides/handle-files) | An attachment that the item's body shows inside the text, written `![](path)` or `![[file]]`. Embedding decides only whether the text shows the file: every attachment comes down with its item, embedded or not. | +| [**attachment**](/build/handle-files) | A file attached to another item, such as a photo on a note, by an `attached-to` edge from the file. A device keeps its bytes while it holds the item, and a folder writes it beside the document, or where an embed says. | +| [**embed**](/build/handle-files) | An attachment that the item's body shows inside the text, written `![](path)` or `![[file]]`. Embedding decides only whether the text shows the file: every attachment comes down with its item, embedded or not. | | [**state**](/items-and-types#an-items-life) | `active`, `archived` or `trashed`; `active` or `revoked` for a `system` item. | | [**tier**](/items-and-types#library-and-feed) | The **library**, what a person kept, or the **feed**, what arrived from elsewhere. | | [**version**](/versions) | A count of an item's changes, which a write names. | @@ -46,13 +46,13 @@ Marfa uses each word below in one sense only, in its code, its API and these doc | --- | --- | | [**owner**](/access#who-can-act) | The one person an instance belongs to, who decides what each key and app reaches. | | [**credential**](/access) | A key or an access token. | -| [**key**](/guides/manage-keys) | A credential beginning `marfa_k1_`, for a script, an agent or a connector. An app may use one too. | -| [**app**](/guides/authenticate) | A program that signs in with OAuth, which the owner approves. This is the preferred way for an app to reach Marfa; it may also use a key. | -| [**access token**](/guides/authenticate) | An app's credential, beginning `marfa_at_`. | -| [**scope**](/guides/authenticate#connect-an-app-with-oauth) | One thing an app asks for, such as `core.note:read`. | +| [**key**](/run/manage-keys) | A credential beginning `marfa_k1_`, for a script, an agent or a connector. An app may use one too. | +| [**app**](/build/authenticate) | A program that signs in with OAuth, which the owner approves. This is the preferred way for an app to reach Marfa; it may also use a key. | +| [**access token**](/build/authenticate) | An app's credential, beginning `marfa_at_`. | +| [**scope**](/build/authenticate#connect-an-app-with-oauth) | One thing an app asks for, such as `core.note:read`. | | [**map**](/access#what-a-credential-reaches) | The part of a credential that names the data it reaches, at `read` or `write`. | | [**permission**](/access#permissions) | The right to a group of operations. It opens no items. | -| [**lever**](/guides/settings#what-the-enforcement-levers-do) | A setting that holds chosen types to a stricter rule. | +| [**lever**](/run/settings#what-the-enforcement-levers-do) | A setting that holds chosen types to a stricter rule. | | [**source**](/items-and-types#where-an-item-came-from) | The program that created an item, set by the key that created it. | | [**`source_id`**](/items-and-types#where-an-item-came-from) | An item's own ID at its source. | | [**setup code**](/quickstart#claim-the-instance) | A one-time code for claiming a new instance. | @@ -63,7 +63,7 @@ Marfa uses each word below in one sense only, in its code, its API and these doc | --- | --- | | [**instance**](/how-marfa-works#the-instance) | One running Marfa server and its data, owned by one person. | | [**operation**](/api-reference) | One thing the API does, such as creating an item. | -| [**housekeeping**](/guides/housekeeping) | Work an instance does on itself on a schedule. | +| [**housekeeping**](/run/housekeeping) | Work an instance does on itself on a schedule. | | [**contract**](https://github.com/withmarfa/marfa/tree/main/conformance/spec) | The written rules Marfa follows, each checked by tests. | ## Devices and folders @@ -76,16 +76,16 @@ Marfa uses each word below in one sense only, in its code, its API and these doc | [**queue**](/devices#a-write-made-offline) | A device's writes waiting to be sent. | | [**drain**](/devices#a-write-made-offline) | One pass that sends the queue. | | [**verdict**](/devices#verdicts) | What became of a queued write, such as `accepted` or `dead`. | -| [**folder**](/guides/sync-a-folder) | A directory on a computer kept in step with the items a query chooses: an item the query matches is a file in it, and a file added there becomes an item. Its settings live on the instance as a `system.folder`, so every computer that follows it shares them, and each keeps its own working copy and queue. A device whose face is a directory. Not a view, which lists items and holds none as files. | +| [**folder**](/run/sync-a-folder) | A directory on a computer kept in step with the items a query chooses: an item the query matches is a file in it, and a file added there becomes an item. Its settings live on the instance as a `system.folder`, so every computer that follows it shares them, and each keeps its own working copy and queue. A device whose face is a directory. Not a view, which lists items and holds none as files. | ## Connectors | Word | What it means | | --- | --- | | [**connector**](/connectors) | A program that brings in records from another service. | -| [**hold**](/guides/connector-holds-and-retries) | What lets one copy of a connector write at a time. | +| [**hold**](/build/connector-holds-and-retries) | What lets one copy of a connector write at a time. | | [**agreement**](/connectors) | What a connector and its service last agreed about one item. | -| [**inbound endpoint**](/guides/connector-webhooks) | An address on an instance that a service posts to, for a connector to read. | +| [**inbound endpoint**](/build/connector-webhooks) | An address on an instance that a service posts to, for a connector to read. | ## The CLI diff --git a/content/docs/(overview)/how-marfa-works.mdx b/content/docs/(overview)/how-marfa-works.mdx index 1b88517d..ff6a5f2b 100644 --- a/content/docs/(overview)/how-marfa-works.mdx +++ b/content/docs/(overview)/how-marfa-works.mdx @@ -7,7 +7,7 @@ Marfa has one center, the **instance**, which holds your data. Everything else i ## The instance -An instance is one running Marfa server and the data it keeps. It belongs to one person, its **owner**, and its data lives on the machine the server runs on. [Deploy](/guides/deploy) shows how to run one. +An instance is one running Marfa server and the data it keeps. It belongs to one person, its **owner**, and its data lives on the machine the server runs on. [Deploy](/run/deploy) shows how to run one. The instance is the **source of truth**. Apps, devices and connectors read from it and write to it, and never talk to each other directly. When two of them change the same thing, the instance decides the outcome, and every copy comes into line with it. @@ -35,7 +35,7 @@ Every program that uses Marfa runs outside it and sends requests to its HTTP API - **The Swift SDK and the CLI** embed the **core**, a Rust engine that sends their requests and can keep a working copy of the data they need. A program that keeps a working copy is a **device**: it reads offline and sends its writes when it can reach the instance, as [Devices](/devices) explains. The Swift SDK also sends the operations only the instance can do, such as registering a type, straight to the API. - **The TypeScript SDK** sends each request straight to the API, and keeps no copy. -- **AI agents** use the CLI or the API like any script, with a key of their own, as [Use Marfa with AI agents](/guides/ai-agents) shows. +- **AI agents** use the CLI or the API like any script, with a key of their own, as [Use Marfa with AI agents](/build/ai-agents) shows. - **Connectors** are programs you run that bring in records from another service, such as Todoist, as [Connectors](/connectors) explains. diff --git a/content/docs/(overview)/introduction.mdx b/content/docs/(overview)/introduction.mdx index ef48542d..de7b398e 100644 --- a/content/docs/(overview)/introduction.mdx +++ b/content/docs/(overview)/introduction.mdx @@ -38,7 +38,7 @@ If you know Apple's platforms, Marfa is **like iCloud, but for structured data a Marfa keeps your data in one SQLite database file and a folder of files, so there's no database server to set up. Run it on your own computer, or deploy the ready-made template on Railway. -Your own devices reach it over your home network, or from anywhere over a private network such as Tailscale. To reach it from the internet, give it a public address on your own domain through a Cloudflare Tunnel. [Deploy](/guides/deploy) covers each. +Your own devices reach it over your home network, or from anywhere over a private network such as Tailscale. To reach it from the internet, give it a public address on your own domain through a Cloudflare Tunnel. [Deploy](/run/deploy) covers each. ## Coming soon diff --git a/content/docs/(overview)/items-and-types.mdx b/content/docs/(overview)/items-and-types.mdx index c399049e..0d73b215 100644 --- a/content/docs/(overview)/items-and-types.mdx +++ b/content/docs/(overview)/items-and-types.mdx @@ -22,7 +22,7 @@ Content-Type: application/json { "type": "core.task", "properties": { "description": "Call the dentist" } } ``` -Marfa ships types for what people keep most, such as notes, tasks, bookmarks and files. The Catalog's [Types](/catalog/types) page lists each with its fields. When none fits, register a type of your own, as [Define a type](/guides/define-a-type) shows. +Marfa ships types for what people keep most, such as notes, tasks, bookmarks and files. The Catalog's [Types](/catalog/types) page lists each with its fields. When none fits, register a type of your own, as [Define a type](/build/define-a-type) shows. A type's identifier is two or more lowercase segments separated by dots. The first segment, its namespace, says who adds types there: @@ -46,13 +46,13 @@ When your type should be readable as another type without taking on its properti ## Where an item came from -Every item records its **source**, the program that created it, such as `laptop` or `todoist`: the creating key's own source, or one that key claims. It never changes. An item can also carry a `source_id`, its own ID at that source, such as a task's ID in Todoist. Together they identify one record, so a connector that reads the record again updates the item instead of making a second one. [Manage keys](/guides/manage-keys#let-keys-share-a-source) shows how keys share a source. +Every item records its **source**, the program that created it, such as `laptop` or `todoist`: the creating key's own source, or one that key claims. It never changes. An item can also carry a `source_id`, its own ID at that source, such as a task's ID in Todoist. Together they identify one record, so a connector that reads the record again updates the item instead of making a second one. [Manage keys](/run/manage-keys#let-keys-share-a-source) shows how keys share a source. ## Library and feed Items sit in one of two tiers. The **library** holds what a person chose to keep. The **feed** holds what arrives in volume from elsewhere, as it came, such as the tasks a connector copies from Todoist. A listing returns both tiers unless it names one. -A feed item stays until someone acts on it, or until its type's feed window runs out and it moves to the trash. [Retention policy](/catalog/your-own-types#retention-policy) describes the window, and [Housekeeping](/guides/housekeeping) the job that applies it. To keep a feed item, a person or an app moves it to the library by [updating its `tier`](/api-reference/items/updateItem). It is the same item with a new tier, not a copy, and moving it back to the feed is the same update. +A feed item stays until someone acts on it, or until its type's feed window runs out and it moves to the trash. [Retention policy](/catalog/your-own-types#retention-policy) describes the window, and [Housekeeping](/run/housekeeping) the job that applies it. To keep a feed item, a person or an app moves it to the library by [updating its `tier`](/api-reference/items/updateItem). It is the same item with a new tier, not a copy, and moving it back to the feed is the same update. ## An item's life @@ -60,7 +60,7 @@ An item starts `active`. It can be archived, which sets it aside without deletin - **Archived** items stay readable by their `id`, but listings leave them out unless asked. - **Moving an item to the bin** also moves the items under it, such as a task's subtasks, and restoring it brings them back. -- **Purging** deletes an item and its history, for good. [Housekeeping](/guides/housekeeping#empty-the-bin) covers when the bin empties itself. +- **Purging** deletes an item and its history, for good. [Housekeeping](/run/housekeeping#empty-the-bin) covers when the bin empties itself. [Change an item's state](/api-reference/items/transitionItem) and [Purge an item](/api-reference/items/purgeItem) are the operations. diff --git a/content/docs/(overview)/overview.mdx b/content/docs/(overview)/overview.mdx index 047306a4..8369eadf 100644 --- a/content/docs/(overview)/overview.mdx +++ b/content/docs/(overview)/overview.mdx @@ -18,21 +18,21 @@ Your notes, tasks, bookmarks and files live in one instance, as typed items. You ## Build on Marfa -Apps and agents share the same data, through the Swift and TypeScript SDKs, the CLI or plain HTTP. +Apps and agents share the same data, through the Swift and TypeScript SDKs, the CLI or plain HTTP. [Build](/build) holds the tutorials and guides. -- [Build an app with Swift](/guides/swift-app): an iPhone and Mac app that works offline. -- [Build a Raycast extension](/guides/raycast-extension): save an item from Raycast, with a key that reaches only notes. -- [Use Marfa with AI agents](/guides/ai-agents): give an agent a key that reaches only what it needs. -- [Build a connector](/guides/connector): bring records from another service into Marfa. -- [Define a type](/guides/define-a-type): register a type of your own, with its fields. +- [Build an app with Swift](/build/swift-app): an iPhone and Mac app that works offline. +- [Build a Raycast extension](/build/raycast-extension): save an item from Raycast, with a key that reaches only notes. +- [Use Marfa with AI agents](/build/ai-agents): give an agent a key that reaches only what it needs. +- [Build a connector](/build/connector): bring records from another service into Marfa. +- [Define a type](/build/define-a-type): register a type of your own, with its fields. ## Run Marfa -An instance is one SQLite database file and a folder of files, on your Mac, a home server or Railway. +An instance is one SQLite database file and a folder of files, on your Mac, a home server or Railway. [Run](/run) covers starting one and looking after it. -- [Deploy](/guides/deploy): choose where it runs and how your devices reach it. -- [Back up and restore](/guides/back-up-and-restore): keep a copy of the data and secrets, and bring them back. -- [Troubleshooting](/guides/troubleshooting): the cause and the fix for the problems people meet first. +- [Deploy](/run/deploy): choose where it runs and how your devices reach it. +- [Back up and restore](/run/back-up-and-restore): keep a copy of the data and secrets, and bring them back. +- [Troubleshooting](/run/troubleshooting): the cause and the fix for the problems people meet first. ## Reference diff --git a/content/docs/(overview)/quickstart.mdx b/content/docs/(overview)/quickstart.mdx index 3e1af53b..6e2b684a 100644 --- a/content/docs/(overview)/quickstart.mdx +++ b/content/docs/(overview)/quickstart.mdx @@ -7,7 +7,7 @@ description: Start Marfa on your own computer, claim it, make a key, and write a - **You need** macOS or Linux, with Git, [Node.js](https://nodejs.org) 22.14 or later and [pnpm](https://pnpm.io/installation), and two terminal windows: one for the server, one for everything else. - **You'll have** a Marfa instance running on your computer, with you as its owner and one note in it. -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. This instance is for trying Marfa. [Deploy](/guides/deploy) covers one you keep. +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. This instance is for trying Marfa. [Deploy](/run/deploy) covers one you keep. @@ -79,7 +79,7 @@ The CLI asks for a password for the owner, then for the setup code the server pr -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/sign-in-as-the-owner#start-from-home). +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](/run/sign-in-as-the-owner#start-from-home). @@ -110,7 +110,7 @@ key 01a15f24-3ade-75f0-8862-b72a2e8e85ab (laptop); the plaintext is shown once, -The first line is the key, shown only this once. It can do everything the owner can grant, which suits a trial. Give an app or a script a narrower one, as [Manage keys](/guides/manage-keys) shows. +The first line is the key, shown only this once. It can do everything the owner can grant, which suits a trial. Give an app or a script a narrower one, as [Manage keys](/run/manage-keys) shows. @@ -163,7 +163,7 @@ marfa items create --type core.note --prop title="Reading list" --prop body="Sta -The CLI prints the note's `id`, which Marfa made, its type, when it was made and its title, then its properties. A new item starts at version 1. Add `--json` to see the whole item, as the [HTTP API](/api-reference/items/createItem) returns it; the [TypeScript SDK](/typescript-sdk#write-and-read-an-item) shows the same request from code. Copy the `id` for the next step. +The CLI prints the note's `id`, which Marfa made, its type, when it was made and its title, then its properties. A new item starts at version 1. Add `--json` to see the whole item, as the [HTTP API](/api-reference/items/createItem) returns it; the [TypeScript SDK](/typescript-sdk#call-an-operation) shows the same request from code. Copy the `id` for the next step. @@ -212,7 +212,7 @@ marfa: the server refused (409 version_conflict): Version 1 is stale; current ve The parts of Marfa, and how they fit together. - Give an agent a key of its own, and let it work with your notes. - An iPhone and Mac app that lists notes and works offline. - Run an instance you keep. + Give an agent a key of its own, and let it work with your notes. + An iPhone and Mac app that lists notes and works offline. + Run an instance you keep. diff --git a/content/docs/(overview)/versions.mdx b/content/docs/(overview)/versions.mdx index bd1e9696..a09d1008 100644 --- a/content/docs/(overview)/versions.mdx +++ b/content/docs/(overview)/versions.mdx @@ -30,7 +30,7 @@ A write based on an older version isn't refused for that alone. Marfa looks at w Had the other app changed the body too, the changes would collide, and Marfa would refuse your write with `409 version_conflict`, sending back the note as it is now for you to combine. -A write can also ask Marfa to settle a collision for it, with `conflict=auto`, by the rules of the item's type. For a note, the last writer's title wins, and **both bodies are kept**: yours goes into a conflicted copy, a new note linked to the original by a `derived-from` edge, so neither writer's text is lost. A [device](/devices) sends every update with `conflict=auto`. [Write safely](/guides/write-safely) shows both ways. +A write can also ask Marfa to settle a collision for it, with `conflict=auto`, by the rules of the item's type. For a note, the last writer's title wins, and **both bodies are kept**: yours goes into a conflicted copy, a new note linked to the original by a `derived-from` edge, so neither writer's text is lost. A [device](/devices) sends every update with `conflict=auto`. [Write safely](/build/write-safely) shows both ways. In the figure, you change the body of a note you read at version 3. Choose what another app changed meanwhile, and how you send your write, to see what Marfa answers and what the note holds. diff --git a/content/docs/(reference)/api-reference/index.mdx b/content/docs/(reference)/api-reference/index.mdx index 45187767..0a95677d 100644 --- a/content/docs/(reference)/api-reference/index.mdx +++ b/content/docs/(reference)/api-reference/index.mdx @@ -7,7 +7,7 @@ Everything an app, a script or a connector does with an instance goes through it ## Send a request -Send each request to your instance's address, such as `http://localhost:8600` for one on your own machine. Almost every operation takes a credential in the `Authorization` header: a key, which begins `marfa_k1_`, or an app's access token, which begins `marfa_at_`. [Access](/access) explains both, and [Manage keys](/guides/manage-keys) shows how to make a key. +Send each request to your instance's address, such as `http://localhost:8600` for one on your own machine. Almost every operation takes a credential in the `Authorization` header: a key, which begins `marfa_k1_`, or an app's access token, which begins `marfa_at_`. [Access](/access) explains both, and [Manage keys](/run/manage-keys) shows how to make a key. A request body is JSON, sent with `Content-Type: application/json`. This request creates a note: diff --git a/content/docs/(reference)/changelog.mdx b/content/docs/(reference)/changelog.mdx index 7969878f..134f2673 100644 --- a/content/docs/(reference)/changelog.mdx +++ b/content/docs/(reference)/changelog.mdx @@ -5,7 +5,7 @@ description: What each release of Marfa changed for someone running an instance Each release of Marfa ships the server and its container image, the CLI for macOS on Apple silicon and Intel and for Linux on x86_64 and aarch64 with a checksum file, a Homebrew formula for the CLI, and the TypeScript SDK on npm. This page lists every release, newest first, with what changed and a link to the full list on GitHub. The Swift SDK's own releases are listed after Marfa's. -**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. +**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](/run/upgrade) shows how to test an export and restore on fresh storage before switching builds. ## v0.0.37 @@ -15,7 +15,7 @@ Released October 25, 2026. [v0.0.37 on GitHub](https://github.com/withmarfa/marf For folders: -- **A folder replaces a store another build made.** Its next push, pull, scan or watch, including `marfa folders watch` with no directory, replaces the store when the old one names the folder and holds no write still to be sent and no answer the folder has yet to record, and says so on standard error. The new store keeps the folder's record of which file is which item, which files went missing, which writes and placements the server refused and which notes were purged, along with the folder's first sync and the server it was filled from, and is filled again from the server. The replacement changes no file and asks for no new first sync, and the command then sends what changed in the folder while the store was refused. While writes wait, the command leaves the store and the files as they are and names the count, so you can push them with the earlier CLI first. A write the server gave up on, or a refused write the folder doesn't keep against a file, such as the deletion of a file, also keeps the store, and a push leaves it, so remove the folder with the earlier CLI and add it again; a file whose deletion the server never took then comes back. See [Troubleshooting](/guides/troubleshooting#staying-in-step). +- **A folder replaces a store another build made.** Its next push, pull, scan or watch, including `marfa folders watch` with no directory, replaces the store when the old one names the folder and holds no write still to be sent and no answer the folder has yet to record, and says so on standard error. The new store keeps the folder's record of which file is which item, which files went missing, which writes and placements the server refused and which notes were purged, along with the folder's first sync and the server it was filled from, and is filled again from the server. The replacement changes no file and asks for no new first sync, and the command then sends what changed in the folder while the store was refused. While writes wait, the command leaves the store and the files as they are and names the count, so you can push them with the earlier CLI first. A write the server gave up on, or a refused write the folder doesn't keep against a file, such as the deletion of a file, also keeps the store, and a push leaves it, so remove the folder with the earlier CLI and add it again; a file whose deletion the server never took then comes back. See [Troubleshooting](/run/troubleshooting#staying-in-step). For working copies, **a copy that replaces one refused with `wrong_schema` keeps the file budget the old copy recorded**, whether a byte count or every file. If the old copy records no budget that can be read, the replacement still happens, with the default budget. @@ -23,7 +23,7 @@ For working copies, **a copy that replaces one refused with `wrong_schema` keeps Released October 25, 2026. [v0.0.36 on GitHub](https://github.com/withmarfa/marfa/releases/tag/v0.0.36) -**Send the writes a working copy holds before you upgrade the CLI or the core embedded in your app.** The working copy gains two columns, so v0.0.36 refuses a copy an earlier build made with `wrong_schema` and says how many writes it holds that the server hasn't taken. Run `marfa device drain` with the earlier build first; [Troubleshooting](/guides/troubleshooting#staying-in-step) says what to do with a refused copy. The server's database and the API keep the shapes they had in v0.0.35, so an instance needs no export to upgrade. +**Send the writes a working copy holds before you upgrade the CLI or the core embedded in your app.** The working copy gains two columns, so v0.0.36 refuses a copy an earlier build made with `wrong_schema` and says how many writes it holds that the server hasn't taken. Run `marfa device drain` with the earlier build first; [Troubleshooting](/run/troubleshooting#staying-in-step) says what to do with a refused copy. The server's database and the API keep the shapes they had in v0.0.35, so an instance needs no export to upgrade. For devices and apps: @@ -34,18 +34,18 @@ For devices and apps: Released October 24, 2026. [v0.0.35 on GitHub](https://github.com/withmarfa/marfa/releases/tag/v0.0.35) -**Change anything that sends view layouts before you upgrade.** A view is a `title`, a `query`, an optional `sort` and an optional `group`. A request that sends `layouts` is refused `400 validation_error`, and a view saved with `layouts` drops them the next time it is changed. In the CLI, `marfa views create` and `marfa views change` take `--sort` and `--group` in place of `--layouts`, `marfa device items list --layout` and `marfa device views groups --layout` are gone, and so is a group's `sum`. The server's database and the working copy keep the shapes they had in v0.0.34, so an instance needs no export to upgrade. See [Save a list as a view](/guides/save-a-view). +**Change anything that sends view layouts before you upgrade.** A view is a `title`, a `query`, an optional `sort` and an optional `group`. A request that sends `layouts` is refused `400 validation_error`, and a view saved with `layouts` drops them the next time it is changed. In the CLI, `marfa views create` and `marfa views change` take `--sort` and `--group` in place of `--layouts`, `marfa device items list --layout` and `marfa device views groups --layout` are gone, and so is a group's `sum`. The server's database and the working copy keep the shapes they had in v0.0.34, so an instance needs no export to upgrade. See [Save a list as a view](/build/save-a-view). For apps and the API: -- **A view clears its grouping with `group: null`.** Send it on `PATCH /views/{id}`, or run `marfa views change --version --group null`. A view's `filter` takes an `edge` term, a `backref` term and a `source` term, and a working copy answers a `backref` condition where it holds that edge type whole. See [Read a view offline](/guides/read-a-view). +- **A view clears its grouping with `group: null`.** Send it on `PATCH /views/{id}`, or run `marfa views change --version --group null`. A view's `filter` takes an `edge` term, a `backref` term and a `source` term, and a working copy answers a `backref` condition where it holds that edge type whole. See [Read a view offline](/build/read-a-view). - **Notes, articles and messages no longer require a `body`.** A note can be only a title, and a message only a file. An existing instance stops requiring it the next time it starts, with no migration. See [Items and types](/items-and-types). - **Every edge type names both of its ends.** `label` names the source end and the optional `reverse_label` names the target end. Without a `reverse_label`, the label names both ends; without a `label`, Marfa makes one from the ID. See [Edges](/catalog/edges). -- **Move every item of a type to another type in one step.** `POST /types/{id}/move`, or `marfa types move `, queues a bulk-action job that moves the items, the trash included. Once none is left, it changes the folders, views and webhooks that name the old type. `DELETE /types/{id}` now refuses while a folder, view or webhook names the type, and lists them in `details.settings`. See [Move items to another type](/guides/move-items-to-another-type). +- **Move every item of a type to another type in one step.** `POST /types/{id}/move`, or `marfa types move `, queues a bulk-action job that moves the items, the trash included. Once none is left, it changes the folders, views and webhooks that name the old type. `DELETE /types/{id}` now refuses while a folder, view or webhook names the type, and lists them in `details.settings`. See [Move items to another type](/build/move-items-to-another-type). For folders: -- **A folder writes every attached file beside its document**, whether or not the body embeds it, and lists the files no embed places under the document's `has-attachment` line. A file follows its document to another directory, and leaves with it. A second folder on the same Mac that holds the file keeps its own copy, and a folder pull no longer deletes and rewrites it on every pass. See [Files attached to a note](/guides/folder-files#files-attached-to-a-note). +- **A folder writes every attached file beside its document**, whether or not the body embeds it, and lists the files no embed places under the document's `has-attachment` line. A file follows its document to another directory, and leaves with it. A second folder on the same Mac that holds the file keeps its own copy, and a folder pull no longer deletes and rewrites it on every pass. See [Files attached to a note](/run/folder-files#files-attached-to-a-note). ## v0.0.34 @@ -61,7 +61,7 @@ For folders: Released October 24, 2026. [v0.0.33 on GitHub](https://github.com/withmarfa/marfa/releases/tag/v0.0.33) -**Upgrade the server to repair folders that still hold `search`.** A folder made in v0.0.29 or earlier can keep that setting on the instance, where changing the local YAML alone cannot remove it. Read its version with `marfa items get `, then run `marfa folders change --version --file settings.json`, naming the intended `query` in the JSON file and leaving out `search`. The accepted change removes the stored `search`; it does not translate it. The server's database, the HTTP API and the working copy keep the shapes they had in v0.0.32, so an instance needs no export to upgrade. See [Change a folder's settings](/guides/sync-a-folder#change-a-folders-settings). +**Upgrade the server to repair folders that still hold `search`.** A folder made in v0.0.29 or earlier can keep that setting on the instance, where changing the local YAML alone cannot remove it. Read its version with `marfa items get `, then run `marfa folders change --version --file settings.json`, naming the intended `query` in the JSON file and leaving out `search`. The accepted change removes the stored `search`; it does not translate it. The server's database, the HTTP API and the working copy keep the shapes they had in v0.0.32, so an instance needs no export to upgrade. See [Change a folder's settings](/run/sync-a-folder#change-a-folders-settings). For folders: @@ -77,7 +77,7 @@ For devices and apps: - **An aged-out copy keeps answering reads while it fills again.** After `catchup_too_old`, status reports `stale`, item lists and searches mark their answers `stale`, and reads by ID return held rows. Run `marfa device hydrate` with the same instance, reach and slice to refill in place. It updates rows as pages arrive and, only after the fill and replay finish, removes rows and edges it did not bring back, keeping pinned rows and those needed by waiting writes. An interrupted refill removes nothing and can resume. A changed instance, reach or slice, or a server restored behind the held cursor, requires a clearing fill. See [The working copy](/devices#the-working-copy). - **Queue creates and edits while a copy fills.** A first fill or a stale copy takes new items, files and writes to rows it holds, keeping each write's base version when a later page brings a newer row. A drain sends them once the copy is whole. Writes to unheld rows and direct edge writes are refused with `hydration_incomplete`; body links wait until the server answers the body write. See [The working copy](/devices#the-working-copy). -- **Operations that need a whole copy still wait.** A folder pull, a view's items, search or groups, and edge reads refuse a partial or stale copy with `hydration_incomplete`. A stale copy's drain and catch-up return `copy_expired` with `catchup_too_old` until its refill finishes. A follow waits for a refill already under way, but refuses a stale copy whose refill has not started. See [Troubleshooting](/guides/troubleshooting#staying-in-step). +- **Operations that need a whole copy still wait.** A folder pull, a view's items, search or groups, and edge reads refuse a partial or stale copy with `hydration_incomplete`. A stale copy's drain and catch-up return `copy_expired` with `catchup_too_old` until its refill finishes. A follow waits for a refill already under way, but refuses a stale copy whose refill has not started. See [Troubleshooting](/run/troubleshooting#staying-in-step). ## v0.0.31 @@ -87,7 +87,7 @@ Released October 24, 2026. [v0.0.31 on GitHub](https://github.com/withmarfa/marf For devices and apps: -- **Read saved views offline.** `marfa device views` lists held views, reads their settings and returns a layout's groups, counts and sums. `marfa device items list` and `marfa device search` take `--view` to select a saved view; their old `--folder` option is removed, with no alias. Item lists follow the layout's sort, and date groups use the device's time zone unless you name one. A query the copy cannot answer whole is refused with a reason. The core's Node and Swift bindings expose the same reads. See [Read a view offline](/guides/read-a-view). +- **Read saved views offline.** `marfa device views` lists held views, reads their settings and returns a layout's groups, counts and sums. `marfa device items list` and `marfa device search` take `--view` to select a saved view; their old `--folder` option is removed, with no alias. Item lists follow the layout's sort, and date groups use the device's time zone unless you name one. A query the copy cannot answer whole is refused with a reason. The core's Node and Swift bindings expose the same reads. See [Read a view offline](/build/read-a-view). - **Create, change and delete views through the core's Node and Swift bindings.** `createView`, `changeView` and `deleteView` send the write to the server at once and never queue it, so they need a connection. A change names the version it starts from and keeps the server's merge and conflict rules. An accepted create or change refreshes the held view when its read-back succeeds; a failed read-back leaves the write accepted, and a later catch-up can refresh the copy. An accepted delete removes the held view. Retry with the same idempotency key when an answer is lost. See [Views written through a copy](https://github.com/withmarfa/marfa/blob/main/conformance/spec/device.md#views-written-through-a-copy). - **An interrupted hydration resumes from the page it reached, including an every-type slice.** The next hydration of the same slice reads the pages left, then applies events since the fill began. It starts again when the instance, reach, slice or catalog changed, the event log cannot supply the needed history, or the server refuses the saved page cursor. The report gives `resumed`, the `restarted` reason, `pages` read in this call and `pages_total` across the fill. See [The working copy](/devices#the-working-copy). - **Keep attachments of held items offline.** Files attached to items the copy holds stay outside the file budget, including partial downloads, and a trim leaves them in place while a hydration is unfinished. After hydration and each catch-up, call `marfa device blobs fetch-kept`, or `fetchKept` in the core's Node or Swift binding, to fetch missing attachments smallest first. This fetch is explicit: watching or following does not run it. Status reports held attachments and why others are missing. When a file stops being an attachment, its bytes return to the ordinary cache. See [The working copy](/devices#the-working-copy). @@ -96,16 +96,16 @@ For devices and apps: Released October 24, 2026. [v0.0.30 on GitHub](https://github.com/withmarfa/marfa/releases/tag/v0.0.30) -**A folder's `search` setting is now `query`, and the old key is refused.** Change scripts that say `search` to use `query`, and use the query option in place of the old search option. A folder made before this release can still hold `search` on the instance: editing `.marfa/folder.yaml` alone cannot repair it, and v0.0.30's folder changes leave the old setting stored. Upgrade the server to v0.0.33 or later and follow [Change a folder's settings](/guides/sync-a-folder#change-a-folders-settings). The API changes the folder setting's shape and adds view operations. The server's database keeps the schema it had in v0.0.29, so an instance needs no export to upgrade. The working copy changes shape: send its waiting writes with the earlier build before upgrading, then follow [Troubleshooting](/guides/troubleshooting#staying-in-step) if it is refused with `wrong_schema`. +**A folder's `search` setting is now `query`, and the old key is refused.** Change scripts that say `search` to use `query`, and use the query option in place of the old search option. A folder made before this release can still hold `search` on the instance: editing `.marfa/folder.yaml` alone cannot repair it, and v0.0.30's folder changes leave the old setting stored. Upgrade the server to v0.0.33 or later and follow [Change a folder's settings](/run/sync-a-folder#change-a-folders-settings). The API changes the folder setting's shape and adds view operations. The server's database keeps the schema it had in v0.0.29, so an instance needs no export to upgrade. The working copy changes shape: send its waiting writes with the earlier build before upgrading, then follow [Troubleshooting](/run/troubleshooting#staying-in-step) if it is refused with `wrong_schema`. For folders: -- **A query can hold both tiers.** Set `query.tier` to `all` to write matching library and feed items as files. A new file without an explicit tier uses `defaults.tier`, or `library` when no default is set. Narrowing the query's tier keeps files whose items leave it as unmatched unless another folder takes them; it never deletes or trashes those items. `marfa folders create` and `marfa folders change` take `--query`. See [Sync a folder](/guides/sync-a-folder). -- **A remote edit no longer writes a moved file back into the folder it left.** If you move a file to another registered folder on the same Mac while another device changes its item, the old folder leaves it unwritten, so the destination keeps one item. It also waits when it cannot tell whether the file moved, such as when another folder cannot be read. Once the file is found nowhere, a change made elsewhere to content the file showed restores it while the query still holds it, even after the five-second grace or when you confirm a paused removal. An unchanged deleted file still sends its item to the bin. See [Folder safeguards](/guides/folder-safeguards#what-removes-a-file-or-an-item). +- **A query can hold both tiers.** Set `query.tier` to `all` to write matching library and feed items as files. A new file without an explicit tier uses `defaults.tier`, or `library` when no default is set. Narrowing the query's tier keeps files whose items leave it as unmatched unless another folder takes them; it never deletes or trashes those items. `marfa folders create` and `marfa folders change` take `--query`. See [Sync a folder](/run/sync-a-folder). +- **A remote edit no longer writes a moved file back into the folder it left.** If you move a file to another registered folder on the same Mac while another device changes its item, the old folder leaves it unwritten, so the destination keeps one item. It also waits when it cannot tell whether the file moved, such as when another folder cannot be read. Once the file is found nowhere, a change made elsewhere to content the file showed restores it while the query still holds it, even after the five-second grace or when you confirm a paused removal. An unchanged deleted file still sends its item to the bin. See [Folder safeguards](/run/folder-safeguards#what-removes-a-file-or-an-item). For apps and the API: -- **Save a list and its layouts as a view.** A `system.view` item holds a title, a query and layouts shared by apps. Create, change and delete it through `POST /views`, `PATCH /views/{id}` and `DELETE /views/{id}`, or `marfa views create`, `change` and `delete`; `marfa views get` reads it. The write operations require write access to `system.view` and accept an `Idempotency-Key`. A change names a version and merges settings nobody changed since; deleting a view purges it at once. Read its settings through the item operations: the view operations do not return the items its query selects. See [Save a list as a view](/guides/save-a-view). +- **Save a list and its layouts as a view.** A `system.view` item holds a title, a query and layouts shared by apps. Create, change and delete it through `POST /views`, `PATCH /views/{id}` and `DELETE /views/{id}`, or `marfa views create`, `change` and `delete`; `marfa views get` reads it. The write operations require write access to `system.view` and accept an `Idempotency-Key`. A change names a version and merges settings nobody changed since; deleting a view purges it at once. Read its settings through the item operations: the view operations do not return the items its query selects. See [Save a list as a view](/build/save-a-view). For devices: @@ -116,7 +116,7 @@ For devices: Released October 24, 2026. [v0.0.29 on GitHub](https://github.com/withmarfa/marfa/releases/tag/v0.0.29) -**The server adds listing indexes to an existing database when it opens it, so an instance needs no export to upgrade.** Its tables keep the shapes they had in v0.0.28. The read view is renamed to reach, with no alias for the old wire names. Upgrade clients with the server, and hydrate an existing working copy again: its old proof is no longer recognized, but its queued writes survive. See [Troubleshooting](/guides/troubleshooting#staying-in-step). +**The server adds listing indexes to an existing database when it opens it, so an instance needs no export to upgrade.** Its tables keep the shapes they had in v0.0.28. The read view is renamed to reach, with no alias for the old wire names. Upgrade clients with the server, and hydrate an existing working copy again: its old proof is no longer recognized, but its queued writes survive. See [Troubleshooting](/run/troubleshooting#staying-in-step). For using the CLI: @@ -125,8 +125,8 @@ For using the CLI: For running an instance: -- **The event log keeps changes for at least 30 days by default, up from seven.** A device away within that window can replay the log instead of filling its copy again because its cursor aged out. An explicit `MARFA_EVENT_LOG_RETENTION_HOURS` or `event_log_retention_hours` setting keeps its value, and `0` still keeps events without an age limit. See [Settings](/guides/settings). -- **Type-filtered item listings read pages in index order.** A listing sorted by creation, update or occurrence time can read and merge the matching types in order instead of sorting every matching item before answering a page. Listings without a type filter also gain indexes for creation and occurrence order. The result order and cursor format stay the same, so a cursor issued by the earlier build still continues. See [List, filter and search](/guides/list-filter-search). +- **The event log keeps changes for at least 30 days by default, up from seven.** A device away within that window can replay the log instead of filling its copy again because its cursor aged out. An explicit `MARFA_EVENT_LOG_RETENTION_HOURS` or `event_log_retention_hours` setting keeps its value, and `0` still keeps events without an age limit. See [Settings](/run/settings). +- **Type-filtered item listings read pages in index order.** A listing sorted by creation, update or occurrence time can read and merge the matching types in order instead of sorting every matching item before answering a page. Listings without a type filter also gain indexes for creation and occurrence order. The result order and cursor format stay the same, so a cursor issued by the earlier build still continues. See [List, filter and search](/build/list-filter-search). For devices and apps: @@ -136,7 +136,7 @@ For devices and apps: For apps and the API (breaking): -- **The read view is called reach.** The header is `X-Marfa-Reach`, the `stream_cursor` and `stream_live` frames carry `reach`, and the `409` refusal and final stream frame are `reach_changed`. The old names have no aliases. The server derives different proofs, and the core stores its proof under the new name, so an existing copy needs a hydration before it can be used again. See [Keep a copy in sync](/guides/keep-in-sync). +- **The read view is called reach.** The header is `X-Marfa-Reach`, the `stream_cursor` and `stream_live` frames carry `reach`, and the `409` refusal and final stream frame are `reach_changed`. The old names have no aliases. The server derives different proofs, and the core stores its proof under the new name, so an existing copy needs a hydration before it can be used again. See [Keep a copy in sync](/build/keep-in-sync). - **Image and media types declare a thumbnail.** `core.file.image` and `core.media` each carry an optional `thumbnail` field of the `thumbnail` kind, inherited by every subtype. The writer supplies it, so a device can show a preview without fetching the full file. A subtype cannot add a second thumbnail, and a type claiming compatibility with either must not declare the field as plain text. See [File](/catalog/file), [Media](/catalog/media) and [Type rules](/catalog/your-own-types). ## v0.0.28 @@ -147,7 +147,7 @@ Released October 23, 2026. [v0.0.28 on GitHub](https://github.com/withmarfa/marf For folders: -- **A sync no longer writes back a file you moved or deleted while it ran.** A pass scans the folder, sends what it found, then pulls. A file the scan read that is gone when the pull reaches its item was written back to its old path, so a moved file returned as a copy that became a second item, and a deleted file returned. The pull now leaves the file unwritten, counts it as unwritten and names it in the report, and the next scan decides: a moved file keeps its item, and a deleted file's item goes to the bin after the five-second grace. A file you delete before the folder first writes its `marfa_id` line into it keeps its delete the same way. A file gone from a path a scan no longer reaches, such as one the folder's `ignore` list now leaves out, is still written where its item wants it, because no scan can tell a move from a delete there. See [What removes a file or an item](/guides/folder-safeguards#what-removes-a-file-or-an-item). +- **A sync no longer writes back a file you moved or deleted while it ran.** A pass scans the folder, sends what it found, then pulls. A file the scan read that is gone when the pull reaches its item was written back to its old path, so a moved file returned as a copy that became a second item, and a deleted file returned. The pull now leaves the file unwritten, counts it as unwritten and names it in the report, and the next scan decides: a moved file keeps its item, and a deleted file's item goes to the bin after the five-second grace. A file you delete before the folder first writes its `marfa_id` line into it keeps its delete the same way. A file gone from a path a scan no longer reaches, such as one the folder's `ignore` list now leaves out, is still written where its item wants it, because no scan can tell a move from a delete there. See [What removes a file or an item](/run/folder-safeguards#what-removes-a-file-or-an-item). ## v0.0.27 @@ -158,14 +158,14 @@ Released October 23, 2026. [v0.0.27 on GitHub](https://github.com/withmarfa/marf For using the CLI: - **`marfa docs` helps after a search that finds nothing, reads one section of a page and prints a page safely.** A search with no hit prints that no page matches, then up to five pages whose title, path or description holds a word of the query, then the line that `marfa docs topics` lists every page. `--section ` prints one heading of a page and the sections inside it, and a heading the page lacks is refused with the new error code `docs_section_not_found`, exit code `1`, and the page's headings. On a terminal, a control character in a page is shown as an escape such as `\u{1b}` and no longer reaches the screen. With `--json`, a search with no hit prints the same empty `hits` list as before, and a page gains `section` only when `--section` is given. See [Docs as Markdown](/docs-as-markdown#search-from-a-terminal). -- **The login agent can take its key from a file in place of the keychain.** `marfa folders autostart on --key-file ` writes the file's path, and never what is in it, into the agent, and `marfa folders watch --key-file ` runs one watch the same way. The file must be a regular file with one name, owned by you and closed to anyone else, with `MARFA_API_URL` and `MARFA_API_KEY` lines, and outside every folder; a file that fails any of these is refused with exit code `1` and `invalid`, and `autostart on` writes nothing. The watch reads the file again each time it opens a folder, reads the keychain for no folder, and never sends the key to a folder of another server. `marfa folders autostart status` names the key file, or none. See [Use a key file instead of the keychain](/guides/keep-folders-running#use-a-key-file-instead-of-the-keychain). +- **The login agent can take its key from a file in place of the keychain.** `marfa folders autostart on --key-file ` writes the file's path, and never what is in it, into the agent, and `marfa folders watch --key-file ` runs one watch the same way. The file must be a regular file with one name, owned by you and closed to anyone else, with `MARFA_API_URL` and `MARFA_API_KEY` lines, and outside every folder; a file that fails any of these is refused with exit code `1` and `invalid`, and `autostart on` writes nothing. The watch reads the file again each time it opens a folder, reads the keychain for no folder, and never sends the key to a folder of another server. `marfa folders autostart status` names the key file, or none. See [Use a key file instead of the keychain](/run/keep-folders-running#use-a-key-file-instead-of-the-keychain). - **The CLI draws bold and color only for a person.** It draws on a stream that is a terminal when `NO_COLOR` is unset or empty, `TERM` isn't `dumb` and the command wasn't given `--json`. Piped, redirected or run with `--json`, a command writes no escape character, so output an agent reads is plain text. Each stream is judged alone. See [Color and piped output](/cli#color-and-piped-output). ## 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. +**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](/run/upgrade) covers the process. For apps and the API reference (breaking): @@ -177,16 +177,16 @@ For using the CLI (breaking): - **`--avatar` takes a picture file.** `marfa owner update --avatar ` and `marfa profile update --avatar ` 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 ` 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). +- **`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 build/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](/run/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. +- **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](/run/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. +- **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](/run/troubleshooting#staying-in-step) covers it. ## v0.0.25 @@ -196,7 +196,7 @@ Released October 22, 2026. [v0.0.25 on GitHub](https://github.com/withmarfa/marf For using the CLI: -- **`marfa server install --external-settings` reads quoted values as Docker Compose does.** A setting written as `NAME='value'` or `NAME="value"` now counts as `value`, without the quotes. The command checked the quotes as part of the value, so a file from a secrets manager that quotes its values, such as an address written as `'https://marfa.example.com'`, failed the check on the address even though the container reads the same line correctly. A value in double quotes also decodes the escapes Docker Compose decodes, such as `\n` and `\"`. A value in quotes that has no closing quote on its line is read as written. See [Keep the settings elsewhere](/guides/install#keep-the-settings-elsewhere). +- **`marfa server install --external-settings` reads quoted values as Docker Compose does.** A setting written as `NAME='value'` or `NAME="value"` now counts as `value`, without the quotes. The command checked the quotes as part of the value, so a file from a secrets manager that quotes its values, such as an address written as `'https://marfa.example.com'`, failed the check on the address even though the container reads the same line correctly. A value in double quotes also decodes the escapes Docker Compose decodes, such as `\n` and `\"`. A value in quotes that has no closing quote on its line is read as written. See [Keep the settings elsewhere](/run/install#keep-the-settings-elsewhere). ## v0.0.24 @@ -215,14 +215,14 @@ For using the CLI: For folders, apps and devices: -- **A working copy that an earlier build made is refused with `wrong_schema`.** The copy's queue has a new shape, because it no longer admits the `merge_metadata` write kind. The build never discards the copy. If the copy holds a write the server has not taken of a kind this build cannot send, such as a `merge_metadata` write, the refusal names the kind and how many, and the file keeps them. [Troubleshooting](/guides/troubleshooting#staying-in-step) shows what to do. +- **A working copy that an earlier build made is refused with `wrong_schema`.** The copy's queue has a new shape, because it no longer admits the `merge_metadata` write kind. The build never discards the copy. If the copy holds a write the server has not taken of a kind this build cannot send, such as a `merge_metadata` write, the refusal names the kind and how many, and the file keeps them. [Troubleshooting](/run/troubleshooting#staying-in-step) shows what to do. - **The core's Node and Swift bindings lose `mergeMetadata`.** `replaceMetadata` still writes a whole set of tags, and `addTag` adds one. ## v0.0.23 Released October 22, 2026. [v0.0.23 on GitHub](https://github.com/withmarfa/marfa/releases/tag/v0.0.23) -**This release renames a database table and breaks the API's operation names.** It refuses an existing v0.0.22 database without changing it, because `blob_locations` is now `blob_copies` and nothing is migrated. To move an instance that holds data, export it with v0.0.22, then restore the export into a fresh v0.0.23 instance. [Upgrade](/guides/upgrade) covers the process. +**This release renames a database table and breaks the API's operation names.** It refuses an existing v0.0.22 database without changing it, because `blob_locations` is now `blob_copies` and nothing is migrated. To move an instance that holds data, export it with v0.0.22, then restore the export into a fresh v0.0.23 instance. [Upgrade](/run/upgrade) covers the process. For apps and the API reference (breaking): @@ -235,18 +235,18 @@ For apps and the API reference (breaking): For running an instance and using the CLI: -- **`marfa server` runs a server in Docker on the same machine.** `install` asks how the server is reached and prints the link that claims it, and `upgrade`, `status`, `start` and `uninstall` look after it. `upgrade` does not move data, and `uninstall` keeps it unless you pass `--delete-data`. [Install with the CLI](/guides/install) covers it. +- **`marfa server` runs a server in Docker on the same machine.** `install` asks how the server is reached and prints the link that claims it, and `upgrade`, `status`, `start` and `uninstall` look after it. `upgrade` does not move data, and `uninstall` keeps it unless you pass `--delete-data`. [Install with the CLI](/run/install) covers it. - **The CLI comes for Linux and Intel Macs, with checksums.** A release carries four archives, `marfa---.tar.gz`, and one checksum file, `marfa--checksums.txt`. The release also writes a Homebrew formula to `withmarfa/homebrew-tap`, so `brew install withmarfa/tap/marfa` installs the CLI, and an install script picks the archive for the machine and checks it against the checksum file. [CLI](/cli#install) shows each route. - **Four CLI commands have new names.** `marfa types drift` is `marfa types retired`, `marfa types prune` is `marfa types delete-retired`, `marfa blobs locations` is `marfa blobs copies` and `marfa blobs delete-location` is `marfa blobs delete-copy`. The old names are gone. -- **An export by a key without `blobs.manage` names the files it could not carry.** The manifest's `missing_blobs` lists a file version whose bytes that key may not read, with a `null` MIME type and size, and `marfa export` then exits 1 and keeps the archive beside the path as `.incomplete`. A script that reads the path never gets an archive that lacks files. [Export and restore](/guides/export-and-restore) covers it. -- **The server refuses to start when its file folder is missing but the database records saved files there.** It names the folder and creates nothing there, so a drive that did not mount cannot become a new, empty store. Where an object store is configured, the bucket serves the copies and the start goes ahead. [Deploy](/guides/deploy) shows the exit status and what to restore. -- **The server image refuses to start on a database that may be older than its bucket's newest copy.** It exits 1 instead of replacing the newer copy. Set `MARFA_REPLACE_NEWER_REPLICA` only when you mean to replace it. A new file, `marfa.db-replica`, sits beside the database, so copy it with a volume backup. [Back up and restore](/guides/back-up-and-restore) lists the files. +- **An export by a key without `blobs.manage` names the files it could not carry.** The manifest's `missing_blobs` lists a file version whose bytes that key may not read, with a `null` MIME type and size, and `marfa export` then exits 1 and keeps the archive beside the path as `.incomplete`. A script that reads the path never gets an archive that lacks files. [Export and restore](/run/export-and-restore) covers it. +- **The server refuses to start when its file folder is missing but the database records saved files there.** It names the folder and creates nothing there, so a drive that did not mount cannot become a new, empty store. Where an object store is configured, the bucket serves the copies and the start goes ahead. [Deploy](/run/deploy) shows the exit status and what to restore. +- **The server image refuses to start on a database that may be older than its bucket's newest copy.** It exits 1 instead of replacing the newer copy. Set `MARFA_REPLACE_NEWER_REPLICA` only when you mean to replace it. A new file, `marfa.db-replica`, sits beside the database, so copy it with a volume backup. [Back up and restore](/run/back-up-and-restore) lists the files. For folders, apps and devices: -- **Folder syncing keeps one item when a file moves between folders.** A moved file that carries no ID, such as a file item or a `.txt` note, keeps its binding until the other folder takes it, and a file found by its identity or its bytes is read as the same item. [Folder safeguards](/guides/folder-safeguards) says what a move does. -- **The contract states every owner action that needs a recent sign-in.** That covers making, changing and revoking a key, signing out every browser, the trash operations, removing a custom type and retrying a webhook delivery. [Confirm it's you](/guides/sign-in-as-the-owner#confirm-its-you) lists them. -- **A working copy that an earlier build made is refused with `wrong_schema`.** The copy's folder journal has a new shape, and the build never discards the copy. [Troubleshooting](/guides/troubleshooting#staying-in-step) shows what to do. +- **Folder syncing keeps one item when a file moves between folders.** A moved file that carries no ID, such as a file item or a `.txt` note, keeps its binding until the other folder takes it, and a file found by its identity or its bytes is read as the same item. [Folder safeguards](/run/folder-safeguards) says what a move does. +- **The contract states every owner action that needs a recent sign-in.** That covers making, changing and revoking a key, signing out every browser, the trash operations, removing a custom type and retrying a webhook delivery. [Confirm it's you](/run/sign-in-as-the-owner#confirm-its-you) lists them. +- **A working copy that an earlier build made is refused with `wrong_schema`.** The copy's folder journal has a new shape, and the build never discards the copy. [Troubleshooting](/run/troubleshooting#staying-in-step) shows what to do. ## v0.0.22 @@ -261,7 +261,7 @@ For folders and app builders: Released October 21, 2026. [v0.0.21 on GitHub](https://github.com/withmarfa/marfa/releases/tag/v0.0.21) -**This release changes the database schema.** It refuses an existing v0.0.20 database without changing it. Keep the old build, database and an export made with that build. Test restoring the export into fresh storage before replacing the running instance; cross-build restore is not guaranteed. [Upgrade](/guides/upgrade) covers the process. +**This release changes the database schema.** It refuses an existing v0.0.20 database without changing it. Keep the old build, database and an export made with that build. Test restoring the export into fresh storage before replacing the running instance; cross-build restore is not guaranteed. [Upgrade](/run/upgrade) covers the process. For the owner's console: @@ -299,25 +299,25 @@ Released October 21, 2026. [v0.0.19 on GitHub](https://github.com/withmarfa/marf For running an instance: -- **The owner has a General page.** At `/auth/owner/general`, it shows the instance's name and the five retentions that can change while the server runs, and saves only the ones you change. [Change settings while it runs](/guides/settings#change-settings-while-it-runs) covers it. -- **The owner has a Health page.** At `/auth/owner/health`, it shows whether the database, saving changes, disk, file storage and any attached object store are working, and lists every housekeeping job with its last run, its error if it failed, and Run now. [Housekeeping](/guides/housekeeping) covers the jobs. -- **The owner has a Connected apps page.** At `/auth/owner/apps`, it lists the apps you approved, what each can reach and the keys it made. Disconnecting an app ends its sign-in at once, and its keys keep working unless you revoke them there too ([Revoke access](/guides/authenticate#revoke-access)). -- **The owner has a Browsers page.** At `/auth/owner/browsers`, it lists where you're signed in and signs out one other browser or all of them. [Manage passkeys and sign-ins](/guides/sign-in-as-the-owner#manage-passkeys-and-sign-ins) covers it. +- **The owner has a General page.** At `/auth/owner/general`, it shows the instance's name and the five retentions that can change while the server runs, and saves only the ones you change. [Change settings while it runs](/run/settings#change-settings-while-it-runs) covers it. +- **The owner has a Health page.** At `/auth/owner/health`, it shows whether the database, saving changes, disk, file storage and any attached object store are working, and lists every housekeeping job with its last run, its error if it failed, and Run now. [Housekeeping](/run/housekeeping) covers the jobs. +- **The owner has a Connected apps page.** At `/auth/owner/apps`, it lists the apps you approved, what each can reach and the keys it made. Disconnecting an app ends its sign-in at once, and its keys keep working unless you revoke them there too ([Revoke access](/build/authenticate#revoke-access)). +- **The owner has a Browsers page.** At `/auth/owner/browsers`, it lists where you're signed in and signs out one other browser or all of them. [Manage passkeys and sign-ins](/run/sign-in-as-the-owner#manage-passkeys-and-sign-ins) covers it. - **Disconnecting an app or signing out another browser needs a recent sign-in.** If yours is more than five minutes old, the page asks you to confirm it's you. -- **The owner has an Activity page.** At `/auth/owner/activity`, it lists the [audit log](/guides/audit-log) newest first and filters it by kind, time and text. -- **The owner has an All settings page.** At `/auth/owner/settings`, it lists every environment setting by group with its variable, whether it can change while the server runs, and its value now; a secret shows only whether it's set. [Every environment setting](/guides/settings#every-environment-setting) lists them. +- **The owner has an Activity page.** At `/auth/owner/activity`, it lists the [audit log](/run/audit-log) newest first and filters it by kind, time and text. +- **The owner has an All settings page.** At `/auth/owner/settings`, it lists every environment setting by group with its variable, whether it can change while the server runs, and its value now; a secret shows only whether it's set. [Every environment setting](/run/settings#every-environment-setting) lists them. - **Eight more settings count as secrets.** `S3_ENDPOINT`, `S3_ACCESS_KEY_ID`, `ERROR_WEBHOOK_URL`, `MARFA_HEARTBEAT_URL`, `OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`, `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` and `MARFA_POSTHOG_HOST` can carry a credential in an address, so no page shows their values and no startup message prints them. - **`MARFA_AUTH_BASE_URL` can't hold a user name, password or query string.** The server refuses one that does; the setting stays readable on the All settings page. -- **The owner has an Export and restore page.** At `/auth/owner/export`, it downloads an archive of everything, or of one type and the types under it, with or without file bytes, and restores one. [Export and restore an archive](/guides/export-and-restore#export-an-archive) covers it. +- **The owner has an Export and restore page.** At `/auth/owner/export`, it downloads an archive of everything, or of one type and the types under it, with or without file bytes, and restores one. [Export and restore an archive](/run/export-and-restore#export-an-archive) covers it. - **The owner has a Connectors page.** At `/auth/owner/connectors`, it lists each connector as Failing, Problems, Not heard from for over a day, or Running, shows its recent runs, and clears its saved state or removes it after you confirm. - **The Manage and Restore pages are gone.** `/auth/owner/manage` and `/auth/owner/restore` no longer exist, and nothing redirects from them. For apps and devices: -- **`PATCH /config` changes only the settings it names.** A setting in the body replaces the stored one, a setting set to `null` goes back to its default, and every other setting stays. `enforcement` is one setting, so naming it replaces all three levers. It takes the same fields and refusals as `PUT /config`. [Update the configuration](/guides/settings#update-the-configuration) covers it. +- **`PATCH /config` changes only the settings it names.** A setting in the body replaces the stored one, a setting set to `null` goes back to its default, and every other setting stays. `enforcement` is one setting, so naming it replaces all three levers. It takes the same fields and refusals as `PUT /config`. [Update the configuration](/run/settings#update-the-configuration) covers it. - **`marfa config update` sends `PATCH /config`**, with `--body` or `--file`. -- **`GET /audit` filters by `kind` and `q`.** `kind` is `access`, `data`, `settings` or `housekeeping`, and `q` keeps the entries that hold every word you give, in any order, ignoring ASCII case. Any other `kind` is refused `400 validation_error`. [Filter it](/guides/audit-log#filter-it) covers both. -- **`GET /export?format=archive&files=false` leaves out file bytes.** The manifest names each file it left out under `missing_blobs`, and a restore reports those the instance doesn't hold in `blobs_missing`. [Leave out the files](/guides/export-and-restore#leave-out-the-files) covers it. +- **`GET /audit` filters by `kind` and `q`.** `kind` is `access`, `data`, `settings` or `housekeeping`, and `q` keeps the entries that hold every word you give, in any order, ignoring ASCII case. Any other `kind` is refused `400 validation_error`. [Filter it](/run/audit-log#filter-it) covers both. +- **`GET /export?format=archive&files=false` leaves out file bytes.** The manifest names each file it left out under `missing_blobs`, and a restore reports those the instance doesn't hold in `blobs_missing`. [Leave out the files](/run/export-and-restore#leave-out-the-files) covers it. ## v0.0.18 @@ -325,17 +325,17 @@ Released October 20, 2026. [v0.0.18 on GitHub](https://github.com/withmarfa/marf 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 program that requests `/` still gets the description of the instance. [Start from Home](/guides/sign-in-as-the-owner#start-from-home) covers it. +- **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 program that requests `/` still gets the description of the instance. [Start from Home](/run/sign-in-as-the-owner#start-from-home) covers it. - **Owner pages send a browser with no session to sign in**, and back, or to `/setup` on an unclaimed instance. At `/`, such a browser sees that the instance is running, with a Sign in link. -- **The owner manages API keys in the browser.** The API keys pages list, create, edit and revoke keys; a new key's access starts off, and its secret shows once, until you leave the page. [Use the API keys page](/guides/manage-keys#use-the-api-keys-page) covers it. +- **The owner manages API keys in the browser.** The API keys pages list, create, edit and revoke keys; a new key's access starts off, and its secret shows once, until you leave the page. [Use the API keys page](/run/manage-keys#use-the-api-keys-page) covers it. - **A key's page refuses a save if the key changed** after the page opened. -- **The owner has Profile and Password and passkeys pages.** Changing a passkey asks you to confirm it's you if your sign-in is more than five minutes old, and changing the password asks for the current one. [Manage passkeys and sign-ins](/guides/sign-in-as-the-owner#manage-passkeys-and-sign-ins) covers them. +- **The owner has Profile and Password and passkeys pages.** Changing a passkey asks you to confirm it's you if your sign-in is more than five minutes old, and changing the password asks for the current one. [Manage passkeys and sign-ins](/run/sign-in-as-the-owner#manage-passkeys-and-sign-ins) covers them. - **A passkey name that is empty or too long is refused** `400 validation_error` when the passkey is verified. -- **A folder whose directory is replaced stays listed.** The folder registry records the directory's identity, so the watch resumes when the original directory returns; an entry goes only when its path holds no folder state and isn't known to be another directory. [Folder safeguards](/guides/folder-safeguards#a-missing-or-unreadable-directory) covers it. +- **A folder whose directory is replaced stays listed.** The folder registry records the directory's identity, so the watch resumes when the original directory returns; an entry goes only when its path holds no folder state and isn't known to be another directory. [Folder safeguards](/run/folder-safeguards#a-missing-or-unreadable-directory) covers it. - **Sign out stays beside the title on a phone.** On Home, it no longer drops below the title and address. - **Destructive actions sit in their own card.** Revoking a key and removing a passkey are in a bordered card of their own. - **Save appears only after a change.** A key's page shows Save and Discard, with a count of the changes, 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. +- **The server needs Node.js 22.14 or later.** The container image runs Node.js 24. [Deploy](/run/deploy) covers it. For apps and devices: @@ -347,12 +347,12 @@ For apps and devices: - **A wrong password at `POST /auth/confirm` counts against the sign-in limits.** It answers `401 unauthorized`, then `429 rate_limited` with `Retry-After` once the attempts are spent, and a refused confirmation keeps the session. - **`POST /auth/confirm` answers `401` without a live owner session**, and to a request that carries a key. - **A program refused for want of a recent sign-in gets `403 forbidden`**, with "Sign in again to perform this operation". A browser can always end its own sign-in. -- **`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. [Narrow a key](/guides/manage-keys#narrow-a-key) 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. [Narrow a key](/run/manage-keys#narrow-a-key) covers it. - **`POST /keys` answers `Cache-Control: no-store`**, so the new secret isn't cached. - **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. [Field kinds](/catalog/field-kinds) covers it. - **A `url` value is judged as the WHATWG URL Standard does.** It's trimmed first, and for a special scheme such as `http`, a non-ASCII host must be valid IDNA and a host with a forbidden code point is refused. - **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 pending deliveries it no longer selects.** They end `canceled`. Changing its `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. +- **Narrowing a webhook cancels the pending deliveries it no longer selects.** They end `canceled`. Changing its `url` or setting `active: false` cancels all of them, and widening revives none. [Change what a webhook sends](/build/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` show No access, Read only or Read and edit for each type, and name powers by their titles; `--json` is unchanged. [CLI](/cli) covers it. - **A queued write that fails alone is set aside.** When a write keeps failing like an outage while the server answers other writes, a device tests it by sending another write that doesn't depend on it, and blocks it as `failing_alone` only if it fails the same way when sent once more; the writes that depend on it wait as `awaiting_dependency`. [Devices](/devices#verdicts) covers it. - **`marfa device queue` prints the last answer a write met**, after `last answer:`. @@ -362,7 +362,7 @@ For apps and devices: - **A key sent with no server named is refused.** A command that sends a key from `--key` or `MARFA_API_KEY` without naming a server is refused `no_server`. - **Two types with the same label are told apart on the approval screen.** 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 ` 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.** 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. [Write safely](/guides/write-safely#retry-with-an-idempotency-key) covers it. +- **A repeat of a write whose first answer was large is refused.** 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. [Write safely](/build/write-safely#retry-with-an-idempotency-key) covers it. - **A device counts such a repeat's `2xx` status as accepted**, except for a create that names no `id`, which it refuses. - **A waiting edge edit or delete stays in view through a catch-up.** When a catch-up brings in an item with its edges, the device lays its own waiting writes over them, as hydration does, instead of showing the server's version until the server answers. @@ -372,26 +372,26 @@ Released October 20, 2026. [v0.0.17 on GitHub](https://github.com/withmarfa/marf For running an instance: -- **A blob folder that has gone reads as down, not empty.** When the disk store's folder or its `.marfa-store` marker is missing, as on an unmounted drive, `GET /health` reports `blob_storage` as `down` with status `503`. [Deploy](/guides/deploy#keep-it-running) covers the health check. +- **A blob folder that has gone reads as down, not empty.** When the disk store's folder or its `.marfa-store` marker is missing, as on an unmounted drive, `GET /health` reports `blob_storage` as `down` with status `503`. [Deploy](/run/deploy#keep-it-running) covers the health check. - **Housekeeping passes over a store it can't reach.** `blob-integrity` strikes none of its copies, counts it in `unreachable` and still checks the stores it can reach, replication carries on past it, and `enrichment-sweep` doesn't count the attempt against the file. - **A blob read answers `503 blob_store_unavailable` when no store returns the bytes** and a store that holds a copy can't answer, rather than `404 blob_not_found`. A restore that finds the disk store's marker missing, or naming another store, answers the same and writes nothing. -- **The bin purge keeps going through a backlog.** While items past the retention remain, `trash-purge` runs again at once instead of waiting for its interval. [Empty the bin](/guides/housekeeping#empty-the-bin) covers it. -- **A restore keeps an item as the archive holds it.** An item whose type the instance no longer registers, or that lacks a field its type later required, keeps its archived properties and `schema_version`; an item of a `core` or `system` type the build ships is still held to its shape. [Restore an archive](/guides/export-and-restore#restore-an-archive) covers it. +- **The bin purge keeps going through a backlog.** While items past the retention remain, `trash-purge` runs again at once instead of waiting for its interval. [Empty the bin](/run/housekeeping#empty-the-bin) covers it. +- **A restore keeps an item as the archive holds it.** An item whose type the instance no longer registers, or that lacks a field its type later required, keeps its archived properties and `schema_version`; an item of a `core` or `system` type the build ships is still held to its shape. [Restore an archive](/run/export-and-restore#restore-an-archive) covers it. - **A restore keeps an edge whose edge type was force-deleted**, and no longer judges an end of a force-deleted type against the edge type's constraints. Such an edge is still skipped for the other reasons, such as a missing end. - **An instance that holds such rows can be exported again.** An export no longer refuses them with `409 export_rows_unrestorable`. - **A restore refuses an archive that lacks one of its four files.** An archive without `manifest.json`, `items.ndjson`, `edges.ndjson` or `types.ndjson` is refused `400 validation_error`, naming each missing entry in the message and in `details.entries`, where before it answered `200` with counts of zero. -- **An upload or a restore has no total time limit.** `MARFA_UPLOAD_IDLE_TIMEOUT_MS`, 60000 milliseconds unless set, ends one that receives no bytes for that long with `408 request_timeout`, and nothing is stored. [Every environment setting](/guides/settings#every-environment-setting) lists the setting. +- **An upload or a restore has no total time limit.** `MARFA_UPLOAD_IDLE_TIMEOUT_MS`, 60000 milliseconds unless set, ends one that receives no bytes for that long with `408 request_timeout`, and nothing is stored. [Every environment setting](/run/settings#every-environment-setting) lists the setting. - **A restore's recent sign-in is judged when it starts.** A restore that starts within five minutes of the owner's sign-in isn't refused for finishing later; an older session is refused `403 forbidden` at once, and one that ends during the upload gets `401 unauthorized`. -- **`marfa export --output` keeps the last whole archive.** When the new archive lacks blobs, the CLI leaves the file at the path, keeps the new archive beside it as `.incomplete`, replacing only an earlier file of that name, and exits `1` with `blobs_missing`. The message says whether the path still holds an earlier file. If the CLI can't write that name, it keeps the archive in a hidden file beside the path and names it. [When an archive lacks a file's bytes](/guides/export-and-restore#when-an-archive-lacks-a-files-bytes) covers it. +- **`marfa export --output` keeps the last whole archive.** When the new archive lacks blobs, the CLI leaves the file at the path, keeps the new archive beside it as `.incomplete`, replacing only an earlier file of that name, and exits `1` with `blobs_missing`. The message says whether the path still holds an earlier file. If the CLI can't write that name, it keeps the archive in a hidden file beside the path and names it. [When an archive lacks a file's bytes](/run/export-and-restore#when-an-archive-lacks-a-files-bytes) covers it. - **Claiming in a browser signs the owner in.** A claim that carries the instance's own `Origin` leaves the browser signed in, which counts as a sign-in within the last five minutes, so adding a passkey needs no second prompt; a claim with no `Origin`, such as a script's, or a refused claim sets no session. [Claim the instance](/quickstart#claim-the-instance) covers it. - **A failed sign-in after a claim leaves the owner claimed**, to sign in at the page that follows. - **Setup runs as six steps:** the setup code, the owner's details, a passkey, a name for the instance, connecting, and backups. -- **The sign-in page says how long a lockout lasts.** When password sign-in or a device code lookup is refused for too many attempts, the page gives the wait in whole minutes, rounded up, and says that a passkey still signs in. [Troubleshooting](/guides/troubleshooting) covers it. -- **The sign-in page links to Forgot password,** at `/auth/forgot-password`, which gives the command that recovers the owner on the instance's machine. [Recover the owner's password](/guides/sign-in-as-the-owner#recover-the-owners-password) covers the command. +- **The sign-in page says how long a lockout lasts.** When password sign-in or a device code lookup is refused for too many attempts, the page gives the wait in whole minutes, rounded up, and says that a passkey still signs in. [Troubleshooting](/run/troubleshooting) covers it. +- **The sign-in page links to Forgot password,** at `/auth/forgot-password`, which gives the command that recovers the owner on the instance's machine. [Recover the owner's password](/run/sign-in-as-the-owner#recover-the-owners-password) covers the command. For apps and devices: -- **An export ends with an error when its credential ends.** If the key or app that started `GET /export` is revoked, expires, is disconnected, or is narrowed so it can't read what's left to send, the response ends with an error instead of ending cleanly, in both formats and while an archive sends a blob. A narrowing that takes away nothing left to send changes nothing. [What an export checks](/guides/export-and-restore#what-an-export-checks) covers it. +- **An export ends with an error when its credential ends.** If the key or app that started `GET /export` is revoked, expires, is disconnected, or is narrowed so it can't read what's left to send, the response ends with an error instead of ending cleanly, in both formats and while an archive sends a blob. A narrowing that takes away nothing left to send changes nothing. [What an export checks](/run/export-and-restore#what-an-export-checks) covers it. - **An archive export whose key is revoked before the response begins** gets `401 unauthorized`. ## v0.0.16 @@ -400,10 +400,10 @@ Released October 19, 2026. [v0.0.16 on GitHub](https://github.com/withmarfa/marf For running an instance: -- **An archive export is checked against a restore before it's sent.** Marfa checks the whole archive as a restore into an empty instance does, so the archive restores into an empty instance of the same build without skipping an item or an edge. [What an export checks](/guides/export-and-restore#what-an-export-checks) covers it. +- **An archive export is checked against a restore before it's sent.** Marfa checks the whole archive as a restore into an empty instance does, so the archive restores into an empty instance of the same build without skipping an item or an edge. [What an export checks](/run/export-and-restore#what-an-export-checks) covers it. - **An export that fails the check is refused.** A row that fails twice without changing answers `409 export_rows_unrestorable`, named in `details.row`; a row or type that changed meanwhile, or a second read that refused a different row, answers `503 export_unrestorable`, so export again. - **An export's temporary files count against `MARFA_DISK_RESERVE_BYTES`.** -- **Text that isn't well-formed UTF-16 is refused.** A string or property name with a lone surrogate, such as an emoji cut in the middle, is refused `400 validation_error`, and `details.errors[].path` names the text by its JSON pointer. A restore refuses an archive that holds one, and names the row. [Troubleshooting](/guides/troubleshooting) covers the refusal. +- **Text that isn't well-formed UTF-16 is refused.** A string or property name with a lone surrogate, such as an emoji cut in the middle, is refused `400 validation_error`, and `details.errors[].path` names the text by its JSON pointer. A restore refuses an archive that holds one, and names the row. [Troubleshooting](/run/troubleshooting) covers the refusal. - **Text extracted from a file is never cut through a character.** It's cut at `MARFA_ENRICHMENT_MAX_TEXT_CHARS` UTF-16 code units. - **The body checks apply whatever `Content-Type` a request states.** The UTF-16 check and the limit of 64 nested levels apply to every body except on `POST /blobs`, `POST /restore` and inbound webhooks. @@ -421,7 +421,7 @@ Released October 19, 2026. [v0.0.15 on GitHub](https://github.com/withmarfa/marf For running an instance: -- **A restore keeps the edges of items in the bin.** An archive made with `--state any` carries them, and a restore used to skip them as `item_not_found`. [Restore an archive](/guides/export-and-restore#restore-an-archive) covers it. +- **A restore keeps the edges of items in the bin.** An archive made with `--state any` carries them, and a restore used to skip them as `item_not_found`. [Restore an archive](/run/export-and-restore#restore-an-archive) covers it. - **`edges_skipped_reasons` names only four kinds of reason:** `already_present`, `endpoint_missing`, `malformed`, or the error code `POST /edges` would answer for that edge. - **A backup restores when its types refer to each other.** Types that name each other in `compatible_with`, or whose display hints, links or merge fields name fields they inherit, restore in any order. - **A key's first request no longer waits behind a long job.** Marfa records a key's last use in the background, so a first request during a restore or bulk job no longer fails with `503 write_contention`, and `last_used_at` can lag or stay `null` after a busy moment. @@ -431,53 +431,53 @@ For running an instance: For apps and devices: - **A read checks permission against the data it returns.** An item that changed type during a read could return metadata, such as a private tag, to a key that could no longer read it, and so could the link between an occurrence and its series. -- **Breaking another type's `compatible_with` claim is refused.** A replacement that breaks a dependent type's claim, or a deletion of a type others name in `compatible_with`, is refused `422 compatible_with_violation`, naming the dependents in `details.dependent_type_ids`, even with `force=true`. [Change the type](/guides/change-a-type#change-the-type) covers it. -- **Adding a parent or a compatibility target needs write permission on it.** Keeping one the type already has doesn't, so a connector can still update its type after its permissions narrow. [What your key needs](/guides/define-a-type#what-your-key-needs) covers it. +- **Breaking another type's `compatible_with` claim is refused.** A replacement that breaks a dependent type's claim, or a deletion of a type others name in `compatible_with`, is refused `422 compatible_with_violation`, naming the dependents in `details.dependent_type_ids`, even with `force=true`. [Change the type](/build/change-a-type#change-the-type) covers it. +- **Adding a parent or a compatibility target needs write permission on it.** Keeping one the type already has doesn't, so a connector can still update its type after its permissions narrow. [What your key needs](/build/define-a-type#what-your-key-needs) covers it. ## v0.0.14 Released October 19, 2026. [v0.0.14 on GitHub](https://github.com/withmarfa/marfa/releases/tag/v0.0.14) -**Before you update, if your container keeps its database in a bucket:** the replica moves from `db` at the bucket's root to `/db`. On its next start, an existing deployment streams its database to the new path and takes a full snapshot there, and the old `db/` objects are no longer read or pruned, so you can delete them once it has. **Every instance on a shared bucket needs its own `S3_PREFIX`**: before this release, they shared one replica, and a new volume of one could start on another's database. [With a bucket](/guides/back-up-and-restore#with-a-bucket) and [An object store](/guides/deploy#an-object-store) explain the prefix. +**Before you update, if your container keeps its database in a bucket:** the replica moves from `db` at the bucket's root to `/db`. On its next start, an existing deployment streams its database to the new path and takes a full snapshot there, and the old `db/` objects are no longer read or pruned, so you can delete them once it has. **Every instance on a shared bucket needs its own `S3_PREFIX`**: before this release, they shared one replica, and a new volume of one could start on another's database. [With a bucket](/run/back-up-and-restore#with-a-bucket) and [An object store](/run/deploy#an-object-store) explain the prefix. For running an instance: -- **Each release publishes the server image** to `ghcr.io/withmarfa/marfa-server`, for `linux/amd64` and `linux/arm64`, tagged with the release and `latest`. [Run it in a container](/guides/deploy#run-it-in-a-container) pulls it. +- **Each release publishes the server image** to `ghcr.io/withmarfa/marfa-server`, for `linux/amd64` and `linux/arm64`, tagged with the release and `latest`. [Run it in a container](/run/deploy#run-it-in-a-container) pulls it. - **An instance names its release.** `GET /` answers `release` beside `version`, and `marfa status` and `marfa whoami` show both; a build made from a clone has no `release`. - **The container's port is published on the loopback address only**, because Docker publishes a bare port on every interface. -- **A proxy on the container's machine needs `TRUSTED_PROXY_CIDRS`.** Set it to the container network's range, or every caller shares one address and sign-in lockouts apply to everyone together. [The address](/guides/deploy#the-address) explains it. -- **A webhook to a private address is refused when you register it.** A `url` whose host is, or resolves to, a private address, such as a Tailscale name, is refused `400 validation_error` naming `MARFA_WEBHOOK_ALLOW_PRIVATE_ADDRESSES`, and so is a change of `url` to one. [Receive deliveries on a private network](/guides/receive-webhooks#receive-deliveries-on-a-private-network) covers it. +- **A proxy on the container's machine needs `TRUSTED_PROXY_CIDRS`.** Set it to the container network's range, or every caller shares one address and sign-in lockouts apply to everyone together. [The address](/run/deploy#the-address) explains it. +- **A webhook to a private address is refused when you register it.** A `url` whose host is, or resolves to, a private address, such as a Tailscale name, is refused `400 validation_error` naming `MARFA_WEBHOOK_ALLOW_PRIVATE_ADDRESSES`, and so is a change of `url` to one. [Receive deliveries on a private network](/build/receive-webhooks#receive-deliveries-on-a-private-network) covers it. - **A delivery refused for its address is `dead_letter` at once** and not retried, so redeliver it after you allow private addresses. - **An outbound webhook reaches only global IPv6 addresses**, and other IPv6 blocks are refused. -- **Telemetry names the instance.** Every log record, span and reported exception carries `service.instance.id`, the `instance_id` that `GET /` answers. [Tell instances apart](/guides/telemetry#tell-instances-apart) covers it. +- **Telemetry names the instance.** Every log record, span and reported exception carries `service.instance.id`, the `instance_id` that `GET /` answers. [Tell instances apart](/run/telemetry#tell-instances-apart) covers it. - **`OTEL_RESOURCE_ATTRIBUTES` marks an instance** for the server and the CLI, such as `deployment.environment=staging` for a test one, and `OTEL_SERVICE_NAME` wins over a `service.name` in it. -- **The bare-metal start loads the telemetry preload.** [Run it without a container](/guides/deploy#run-it-without-a-container) shows it. -- **A backup names the files it lacks.** An archive leaves out a blob that no store holds whole and names it under `missing_blobs`. `marfa export` then exits `1` with `blobs_missing`, and a restore lists such files in `blobs_missing`, which makes `marfa restore` exit `1`. [When an archive lacks a file's bytes](/guides/export-and-restore#when-an-archive-lacks-a-files-bytes) covers it. +- **The bare-metal start loads the telemetry preload.** [Run it without a container](/run/deploy#run-it-without-a-container) shows it. +- **A backup names the files it lacks.** An archive leaves out a blob that no store holds whole and names it under `missing_blobs`. `marfa export` then exits `1` with `blobs_missing`, and a restore lists such files in `blobs_missing`, which makes `marfa restore` exit `1`. [When an archive lacks a file's bytes](/run/export-and-restore#when-an-archive-lacks-a-files-bytes) covers it. - **A cut `marfa export --output` leaves the path as it was** and exits `3`. - **A blob store that fails answers `503 blob_store_unavailable`.** -- **Headers named `Tailscale-` aren't stored** with a connector's inbound delivery. [Receive vendor webhooks](/guides/connector-webhooks) covers deliveries. +- **Headers named `Tailscale-` aren't stored** with a connector's inbound delivery. [Receive vendor webhooks](/build/connector-webhooks) covers deliveries. For apps and devices: -- **A file item names only bytes its writer uploaded or can already read.** Any other `blob_ref` is refused `404 blob_not_found`, where before it left a file nobody could download, so upload with the same credential first. [Point an item at the bytes](/guides/handle-files#point-an-item-at-the-bytes) covers it. +- **A file item names only bytes its writer uploaded or can already read.** Any other `blob_ref` is refused `404 blob_not_found`, where before it left a file nobody could download, so upload with the same credential first. [Point an item at the bytes](/build/handle-files#point-an-item-at-the-bytes) covers it. - **A working copy uploads again under its current key** when the key changed, and the refusal isn't kept against an `Idempotency-Key`. -- **`marfa folders push` says whether it finished.** It exits `3` when a write is left unsent or the catch-up fails, and `5` when the credential is refused. [Know whether a push finished](/guides/folder-safeguards#know-whether-a-push-finished) covers it. +- **`marfa folders push` says whether it finished.** It exits `3` when a write is left unsent or the catch-up fails, and `5` when the credential is refused. [Know whether a push finished](/run/folder-safeguards#know-whether-a-push-finished) covers it. - **`marfa folders push` doesn't wait out a rate limit.** It reports the writes as not delivered, names the wait and exits `3`; a watch waits on its own. - **A push counts files with an unanswered write as waiting.** - **`marfa folders restore` lists the files it couldn't write back.** - **A file moved to another folder and saved as UTF-16 stays the same item**, and a file that isn't text no longer holds up deletes in other folders. -- **`marfa keys keep` keeps any valid key**, including one that holds only `keys.mint`, `instance.read` or `audit.read`. [Give a key to a script or agent](/guides/manage-keys#give-a-key-to-a-script-or-agent) covers it. -- **`marfa events` exits by why it ended:** `1` for `cursor_ahead` and `catchup_too_old`, `3` for a stream that stopped early, and `5` for `credential_ended`. [When the stream ends](/guides/keep-in-sync#when-the-stream-ends) covers it. +- **`marfa keys keep` keeps any valid key**, including one that holds only `keys.mint`, `instance.read` or `audit.read`. [Give a key to a script or agent](/run/manage-keys#give-a-key-to-a-script-or-agent) covers it. +- **`marfa events` exits by why it ended:** `1` for `cursor_ahead` and `catchup_too_old`, `3` for a stream that stopped early, and `5` for `credential_ended`. [When the stream ends](/build/keep-in-sync#when-the-stream-ends) covers it. - **`marfa owner passkeys add` opens the ordinary sign-in page.** ## v0.0.13 Released October 18, 2026. [v0.0.13 on GitHub](https://github.com/withmarfa/marfa/releases/tag/v0.0.13) -- **An instance has a name.** Its live configuration holds a display name, Marfa by default, which the instance description returns and existing pages show; changing it needs no restart and keeps the instance ID. [Change settings while it runs](/guides/settings#change-settings-while-it-runs) covers it. -- **The owner can change their sign-in email and name.** `PATCH /owner` does it from a browser session signed in within the last five minutes, and `marfa owner update` through the private socket. [Change the owner's profile](/guides/sign-in-as-the-owner#change-the-owners-profile) covers it. +- **An instance has a name.** Its live configuration holds a display name, Marfa by default, which the instance description returns and existing pages show; changing it needs no restart and keeps the instance ID. [Change settings while it runs](/run/settings#change-settings-while-it-runs) covers it. +- **The owner can change their sign-in email and name.** `PATCH /owner` does it from a browser session signed in within the last five minutes, and `marfa owner update` through the private socket. [Change the owner's profile](/run/sign-in-as-the-owner#change-the-owners-profile) covers it. - **An email change needs no confirmation**, and the owner's password, passkeys, sessions, keys and app grants keep working. -- **A key or an app can change the profile's name and avatar.** `PATCH /profile` and `marfa profile update` need profile write access for each field sent, and email stays the owner's alone. [Change the owner's profile](/guides/sign-in-as-the-owner#change-the-owners-profile) covers it. +- **A key or an app can change the profile's name and avatar.** `PATCH /profile` and `marfa profile update` need profile write access for each field sent, and email stays the owner's alone. [Change the owner's profile](/run/sign-in-as-the-owner#change-the-owners-profile) covers it. - **A grant to write the email is refused**, and the approval screen names profile write access. - **A folder watch yields only to a command that asks for the folder.** Watches no longer take each other for such a command, so they don't take a folder from each other. @@ -493,9 +493,9 @@ For apps and devices: For running an instance: - **The owner can count and export items from a browser session**, as NDJSON or a restorable archive, with no key or recent sign-in. The source filter still applies, and a request that also sends a credential uses that credential's access alone. -- **The owner can list, rename and remove passkeys**, through the owner operations or the CLI over the private socket; a change in a browser needs a sign-in within the last five minutes. [Manage passkeys and sign-ins](/guides/sign-in-as-the-owner#manage-passkeys-and-sign-ins) covers it. +- **The owner can list, rename and remove passkeys**, through the owner operations or the CLI over the private socket; a change in a browser needs a sign-in within the last five minutes. [Manage passkeys and sign-ins](/run/sign-in-as-the-owner#manage-passkeys-and-sign-ins) covers it. - **`marfa owner passkeys add` registers a passkey** over the private socket, after a fresh browser sign-in. -- **`PORT=0` lets the operating system choose the port.** `MARFA_LISTEN_HOST` sets the bind address, an unspecified address when unset, and outside production an implicit authentication origin uses the port actually bound. [Every environment setting](/guides/settings#every-environment-setting) lists both. +- **`PORT=0` lets the operating system choose the port.** `MARFA_LISTEN_HOST` sets the bind address, an unspecified address when unset, and outside production an implicit authentication origin uses the port actually bound. [Every environment setting](/run/settings#every-environment-setting) lists both. - **Time windows don't move with the wall clock.** Health-probe reuse, key and OAuth usage stamps and error-notification throttling measure elapsed time; stored dates and event times still use the wall clock. - **Extension permissions recognize only `read` and `write`.** An explicit invalid entry denies access even when a wildcard grants it. [Access](/access) covers extension maps. @@ -503,14 +503,14 @@ For running an instance: Released October 18, 2026. [v0.0.11 on GitHub](https://github.com/withmarfa/marfa/releases/tag/v0.0.11) -**Before you update:** v0.0.11 adds a column to the items table, so **it refuses a database that v0.0.10 wrote** and leaves the file as it was; move your data with an export and a restore, as [Upgrade](/guides/upgrade) shows. Because v0.0.11 checks array elements, **a restore is refused `400 invalid_properties` if the archive holds an array element of the wrong kind**, so correct such items in v0.0.10 before you export. +**Before you update:** v0.0.11 adds a column to the items table, so **it refuses a database that v0.0.10 wrote** and leaves the file as it was; move your data with an export and a restore, as [Upgrade](/run/upgrade) shows. Because v0.0.11 checks array elements, **a restore is refused `400 invalid_properties` if the archive holds an array element of the wrong kind**, so correct such items in v0.0.10 before you export. For running an instance: -- **A type can say how long its feed items stay.** The `feed-expiry` housekeeping job, hourly unless `FEED_EXPIRY_INTERVAL_MS` says otherwise, moves feed items past that window to the trash, counting from when each arrived, which a refresh by natural key doesn't reset. [Housekeeping](/guides/housekeeping) covers the job and [Type rules](/catalog/your-own-types#retention-policy) the policy. +- **A type can say how long its feed items stay.** The `feed-expiry` housekeeping job, hourly unless `FEED_EXPIRY_INTERVAL_MS` says otherwise, moves feed items past that window to the trash, counting from when each arrived, which a refresh by natural key doesn't reset. [Housekeeping](/run/housekeeping) covers the job and [Type rules](/catalog/your-own-types#retention-policy) the policy. - **Feed expiry leaves some items alone.** It skips library and archived items and items brought back from the trash, and holds back an item whose delete would delete one that hasn't expired, or one an edge with `cascade_on_delete: block` protects. - **An expired item can be restored until the trash retention ends.** Each move is announced as `item.deleted` and audited as `items.feed_expired`. -- **The source filter narrows event streams and webhook deliveries.** A live or replayed stream and a webhook's deliveries leave out an item whose source the filter hides, and an edge whose source item it hides; a waiting delivery is settled unsent once its item is hidden. [What the enforcement levers do](/guides/settings#what-the-enforcement-levers-do) covers it. +- **The source filter narrows event streams and webhook deliveries.** A live or replayed stream and a webhook's deliveries leave out an item whose source the filter hides, and an edge whose source item it hides; a waiting delivery is settled unsent once its item is hidden. [What the enforcement levers do](/run/settings#what-the-enforcement-levers-do) covers it. - **A copy stream still sends a hidden item**, marked `listed: false`, so a device's working copy keeps it. - **The source filter judges an item with no source as `unknown`**, as every read already shows it. - **Two settings let a test harness control the server.** `MARFA_TEST_HOOKS` and `MARFA_TEST_CLOCK_OFFSET_MS` move the server's clock and set faults through the private socket. **Leave them off on any instance that holds data.** @@ -565,7 +565,7 @@ Released October 16, 2026. [v0.0.9 on GitHub](https://github.com/withmarfa/marfa For running an instance: -- **A web page can connect with one approval and no pasted key.** The discovery documents and `POST /auth/oauth2/register` answer any origin, and once the owner approves the page, the origins of its redirect URIs may call the API from a browser with no `CORS_ORIGINS` entry. [Connect a web page](/guides/connect-a-web-page) covers it. +- **A web page can connect with one approval and no pasted key.** The discovery documents and `POST /auth/oauth2/register` answer any origin, and once the owner approves the page, the origins of its redirect URIs may call the API from a browser with no `CORS_ORIGINS` entry. [Connect a web page](/build/connect-a-web-page) covers it. - **A registration that never receives a grant is removed 1 day after its registration or the last time its approval page was offered,** not 30 (`MARFA_DCR_CLIENT_RETENTION_DAYS`). - **A registration takes a body of at most 32,768 bytes** and at most 10 redirect URIs. - **Registering a client that uses `private_key_jwt` is refused** with `400 invalid_client_metadata`, and the discovery documents list the client authentication methods the instance accepts. @@ -577,26 +577,26 @@ For the API: - **An undeclared top-level key in a JSON body is refused.** The operation answers `400 validation_error` and lists the keys in `details.unknown_body_fields`, where `POST /items`, `POST /edges` and others dropped the key and answered success. - **Three kinds of operation treat unknown keys differently.** `PUT /items/{id}/extensions/{namespace}` and `POST /auth/oauth2/register` accept any key, and the operations that define a type or an edge type refuse one as `invalid_schema`. -- **A write can require the exact version you read.** `PATCH /items/{id}` takes `version_match: "exact"`, `POST /items/{id}/transition` a `version` query parameter, and `POST /edges` and `DELETE /edges/{id}` a `source_version` query parameter; a mismatch is refused `409 version_conflict`. [Send the version you read](/guides/write-safely#send-the-version-you-read) covers it. +- **A write can require the exact version you read.** `PATCH /items/{id}` takes `version_match: "exact"`, `POST /items/{id}/transition` a `version` query parameter, and `POST /edges` and `DELETE /edges/{id}` a `source_version` query parameter; a mismatch is refused `409 version_conflict`. [Send the version you read](/build/write-safely#send-the-version-you-read) covers it. - **A write can name the instance it was made for** in `X-Marfa-Instance-Id`. Another instance refuses it with `409 instance_changed` and writes nothing, so a write can't land on an instance that took over the address. -- **Tag, metadata and extension writes honor `Idempotency-Key`.** `POST /items/{id}/tags`, `DELETE /items/{id}/tags/{tag}`, `PUT` and `PATCH /items/{id}/metadata`, and `PUT` and `DELETE /items/{id}/extensions/{namespace}` answer a repeat from the first, and the matching CLI commands take `--idempotency-key`. [Retry with an Idempotency-Key](/guides/write-safely#retry-with-an-idempotency-key) covers it. +- **Tag, metadata and extension writes honor `Idempotency-Key`.** `POST /items/{id}/tags`, `DELETE /items/{id}/tags/{tag}`, `PUT` and `PATCH /items/{id}/metadata`, and `PUT` and `DELETE /items/{id}/extensions/{namespace}` answer a repeat from the first, and the matching CLI commands take `--idempotency-key`. [Retry with an Idempotency-Key](/build/write-safely#retry-with-an-idempotency-key) covers it. - **Edge reads follow the source filter.** `GET /edges`, `GET /items/{id}/edges`, `GET /items/{id}/backrefs` and an item's `edges` and `backrefs` leave out an edge whose source item the key's source filter hides, whatever the source of its target. - **`POST /edge-types` with `evolve_constraints: true` widens an edge type in place,** keeping its definition and its edges. It answers `200`, or `404` if the type isn't registered. - **The API description types every `version` as an integer**, for an item, an edge, a type and a version snapshot, so a generated client reads a whole number. For the CLI: -- **`marfa types scaffold ` prints a starting type definition** from an ID and its fields (`--field`, `--required`, `--parent`, `--description`), with no server or key. [Define a type](/guides/define-a-type) uses it. -- **`marfa folders watch` with no directory keeps every folder on the machine in step**, in one process. [Watch every folder](/guides/keep-folders-running#watch-every-folder) covers it. -- **`marfa folders autostart on` runs the watch from login**, as a launchd agent on macOS or a systemd user unit on Linux; `off` removes it and `status` reports it. [Sync from login](/guides/keep-folders-running#sync-from-login) covers it. -- **A folder command asks a running watch for the folder.** A command such as `marfa folders confirm` takes the folder and gives it back when it ends, and is refused `reading_handle` if the watch doesn't let go within 10 seconds. [Work on a folder while the watch runs](/guides/keep-folders-running#work-on-a-folder-while-the-watch-runs) covers it. +- **`marfa types scaffold ` prints a starting type definition** from an ID and its fields (`--field`, `--required`, `--parent`, `--description`), with no server or key. [Define a type](/build/define-a-type) uses it. +- **`marfa folders watch` with no directory keeps every folder on the machine in step**, in one process. [Watch every folder](/run/keep-folders-running#watch-every-folder) covers it. +- **`marfa folders autostart on` runs the watch from login**, as a launchd agent on macOS or a systemd user unit on Linux; `off` removes it and `status` reports it. [Sync from login](/run/keep-folders-running#sync-from-login) covers it. +- **A folder command asks a running watch for the folder.** A command such as `marfa folders confirm` takes the folder and gives it back when it ends, and is refused `reading_handle` if the watch doesn't let go within 10 seconds. [Work on a folder while the watch runs](/run/keep-folders-running#work-on-a-folder-while-the-watch-runs) covers it. - **A watch records its folders in `folder-watch.json`** for other programs on the machine to read. [Folder watch record](/folder-watch-record) describes it. - **`marfa folders add` and `marfa folders watch` warn about synced storage.** They warn once on standard error for a folder in iCloud Drive, a cloud service's synced location or a network file system, and go on. - **A refused `marfa folders add` leaves behind no directory it made.** -- **`marfa folders push --send-only` sends without touching files.** It sends what a folder holds and writes or removes no file in it. [Send without pulling](/guides/keep-folders-running#send-without-pulling) covers it. -- **`marfa folders unmatched ` lists files the folder's search no longer matches,** and `--remove` takes them away after a confirmation or with `--yes`. [Remove files a folder keeps but no longer holds](/guides/keep-folders-running#remove-files-a-folder-keeps-but-no-longer-holds) covers it. -- **A Markdown document's file name and title stay in step.** Renaming the file sets the title, and a new title renames a file whose name still matches the old title; a name you chose, or an empty title, leaves the file name as it is. [Edit on either side](/guides/folder-files#edit-on-either-side) covers it. -- **The CLI can send telemetry.** With `OTEL_EXPORTER_OTLP_ENDPOINT` set, it exports one trace for each command, and with `MARFA_POSTHOG_HOST` and `MARFA_POSTHOG_PROJECT_TOKEN` too, a failed command goes to PostHog error tracking; with none set, it sends nothing. [What the CLI sends](/guides/telemetry#what-the-cli-sends) covers it. +- **`marfa folders push --send-only` sends without touching files.** It sends what a folder holds and writes or removes no file in it. [Send without pulling](/run/keep-folders-running#send-without-pulling) covers it. +- **`marfa folders unmatched ` lists files the folder's search no longer matches,** and `--remove` takes them away after a confirmation or with `--yes`. [Remove files a folder keeps but no longer holds](/run/keep-folders-running#remove-files-a-folder-keeps-but-no-longer-holds) covers it. +- **A Markdown document's file name and title stay in step.** Renaming the file sets the title, and a new title renames a file whose name still matches the old title; a name you chose, or an empty title, leaves the file name as it is. [Edit on either side](/run/folder-files#edit-on-either-side) covers it. +- **The CLI can send telemetry.** With `OTEL_EXPORTER_OTLP_ENDPOINT` set, it exports one trace for each command, and with `MARFA_POSTHOG_HOST` and `MARFA_POSTHOG_PROJECT_TOKEN` too, a failed command goes to PostHog error tracking; with none set, it sends nothing. [What the CLI sends](/run/telemetry#what-the-cli-sends) covers it. For apps and devices: @@ -616,7 +616,7 @@ Released October 14, 2026. [v0.0.8 on GitHub](https://github.com/withmarfa/marfa For running an instance: - **The CLI for macOS is signed and notarized by Apple**, so a copy downloaded in a browser runs, as one downloaded in a terminal does. -- **One setting turns on the server's telemetry.** With `OTEL_EXPORTER_OTLP_ENDPOINT` set, the server sends its traces and logs to that endpoint, and `MARFA_OTEL_ENABLED` is gone. [Send traces and logs](/guides/telemetry#send-traces-and-logs) covers it. +- **One setting turns on the server's telemetry.** With `OTEL_EXPORTER_OTLP_ENDPOINT` set, the server sends its traces and logs to that endpoint, and `MARFA_OTEL_ENABLED` is gone. [Send traces and logs](/run/telemetry#send-traces-and-logs) covers it. - **Replacing a connector's revoked or expired key keeps its inbound webhook addresses** and the deliveries waiting for it. For the API and the CLI: @@ -642,7 +642,7 @@ For claiming and access: - **A new instance is claimed once, by its owner.** Claim it with a setup code from the server's log, a single-use link opened in a browser, or `marfa --socket setup claim` on the instance's own machine. [Claim the instance](/quickstart#claim-the-instance) covers it. - **The owner and named permissions replace the operator key.** Managing an instance takes a permission given to a key or an app on purpose, such as `keys.mint`, `audit.read` or `config.manage`. [Permissions](/catalog/permissions) lists them. - **`--socket` gives the CLI the machine's own authority** through a private socket, for `setup`, making keys, restoring an archive and `owner recover`, which resets a lost owner password. [CLI](/cli) covers the socket. -- **A key can expire.** `marfa keys create` and `marfa keys update` set, change or clear an expiry, and an expired key is refused `401`, as a revoked one is. [Make a key expire](/guides/manage-keys#make-a-key-expire) covers it. +- **A key can expire.** `marfa keys create` and `marfa keys update` set, change or clear an expiry, and an expired key is refused `401`, as a revoked one is. [Make a key expire](/run/manage-keys#make-a-key-expire) covers it. - **An app the owner connected stays signed in when the owner's browser session ends.** - **Every app is shown as unverified** on the consent and sign-in pages. - **An extension namespace is reached only through a key's extension map**, never through its label. @@ -653,7 +653,7 @@ For claiming and access: For running an instance: -- **A Railway template creates a personal instance** with persistent storage, HTTPS and daily backups. [Deploy](/guides/deploy) covers it. +- **A Railway template creates a personal instance** with persistent storage, HTTPS and daily backups. [Deploy](/run/deploy) covers it. - **An upload or a restore that would fill the disk is refused** with `507 insufficient_storage`, rather than failing later writes. - **Text extraction from office documents is bounded**, so a large document can't exhaust the server's memory. - **The server's diagnostics never carry values from the database.** @@ -670,7 +670,7 @@ For the API and the CLI: - **The CLI warns once before it sends a credential over plain `http`** to a host other than this machine, and `--allow-http` says that's intended. - **A device client that can't refresh its token is refused `offline_access`**, rather than approved without a refresh token. - **Version history names the sign-in that wrote each version**, and carries a cursor to continue its pages. [History](/versions#history) covers it. -- **`POST /types/normalize` and `marfa types normalize` compare a candidate with its registered declared and resolved definitions** without writing it. [Change the type](/guides/change-a-type#change-the-type) covers it. +- **`POST /types/normalize` and `marfa types normalize` compare a candidate with its registered declared and resolved definitions** without writing it. [Change the type](/build/change-a-type#change-the-type) covers it. - **Unknown type and edge type schema keys are refused.** - **A connector registration shows persistent problems apart from its state and runs.** A connector replaces or clears its problems under its hold. @@ -728,23 +728,23 @@ For devices and folders: - **A search matches the same rows on the server and in a working copy.** - **An app can read and save to its working copy before its first sync**, and declares the types it uses. - **The Swift SDK adds, syncs and watches folders** as the CLI does. -- **A folder's first sync waits for you.** It says what it will write and send, and waits for `marfa folders confirm`, or `--yes` on `marfa folders add`. [Sync a folder](/guides/sync-a-folder) covers it. +- **A folder's first sync waits for you.** It says what it will write and send, and waits for `marfa folders confirm`, or `--yes` on `marfa folders add`. [Sync a folder](/run/sync-a-folder) covers it. ## v0.0.3 Released October 3, 2026. [v0.0.3 on GitHub](https://github.com/withmarfa/marfa/releases/tag/v0.0.3) -For folders, a directory on your machine kept in step with your instance as Markdown and other files ([Sync a folder](/guides/sync-a-folder)): +For folders, a directory on your machine kept in step with your instance as Markdown and other files ([Sync a folder](/run/sync-a-folder)): - **A folder's settings are a `system.folder` item**, shared by every machine bound to it. - **A file's frontmatter carries its item's fields and edges.** - **Moving a file from one folder to another on the same machine is a move**, not a delete. -- **A large removal waits for you to confirm it.** [Confirm or undo a large removal](/guides/folder-safeguards#confirm-or-undo-a-large-removal) covers it. +- **A large removal waits for you to confirm it.** [Confirm or undo a large removal](/run/folder-safeguards#confirm-or-undo-a-large-removal) covers it. For connectors: -- **A connector can have an inbound webhook address** on the instance, where a service such as GitHub posts its changes. [Receive vendor webhooks](/guides/connector-webhooks) covers it. -- **A connector holds its registration while it runs**, and keeps its state on the instance. [Holds and retries](/guides/connector-holds-and-retries) covers it. +- **A connector can have an inbound webhook address** on the instance, where a service such as GitHub posts its changes. [Receive vendor webhooks](/build/connector-webhooks) covers it. +- **A connector holds its registration while it runs**, and keeps its state on the instance. [Holds and retries](/build/connector-holds-and-retries) covers it. - **A type can name a link field**, unique across its items. - **A purge leaves a tombstone**, so a connector doesn't bring a purged record back. - **`POST /items/lookup` finds items and tombstones** by link, natural key or ID. @@ -755,7 +755,7 @@ For the API: - **`DELETE /items/{id}` takes the `version` you read.** - **An item of a type the key can't read answers `404 item_not_found`**, as a missing one does. - **An edge type can declare a reverse name.** -- **`GET /items/stats` counts the items a listing's filters match.** [Count items](/guides/list-filter-search#count-items) covers it. +- **`GET /items/stats` counts the items a listing's filters match.** [Count items](/build/list-filter-search#count-items) covers it. - **An `Idempotency-Key` belongs to the credential that sent it.** - **Every blob downloads as a sandboxed file.** @@ -763,7 +763,7 @@ For running an instance: - **Every setting is checked when the server starts**, and a bad value stops it with a message that names the setting. - **The server refuses a database written by another build**, naming what differs. -- **With telemetry on, unhandled errors can go to PostHog's error tracking.** [Report errors to PostHog](/guides/telemetry#report-errors-to-posthog) covers it. +- **With telemetry on, unhandled errors can go to PostHog's error tracking.** [Report errors to PostHog](/run/telemetry#report-errors-to-posthog) covers it. For the CLI: diff --git a/content/docs/(reference)/cli.mdx b/content/docs/(reference)/cli.mdx index ed851de0..e159b67b 100644 --- a/content/docs/(reference)/cli.mdx +++ b/content/docs/(reference)/cli.mdx @@ -6,9 +6,9 @@ description: Install the marfa program, see how it finds your instance and crede The CLI is the `marfa` program. **Every operation in the API has a command**, such as `marfa items list`, and most AI agents work with Marfa through it. It also does work the API doesn't: - `marfa device` keeps a [working copy](/devices) of part of an instance. -- `marfa server` installs, upgrades and removes a server in Docker on this machine, as [Install with the CLI](/guides/install) shows. -- `marfa folders` keeps part of an instance as files in a directory, as [Sync a folder](/guides/sync-a-folder) shows. -- `marfa types scaffold` prints a starting definition for a type without a server, as [Define a type](/guides/define-a-type) shows. +- `marfa server` installs, upgrades and removes a server in Docker on this machine, as [Install with the CLI](/run/install) shows. +- `marfa folders` keeps part of an instance as files in a directory, as [Sync a folder](/run/sync-a-folder) shows. +- `marfa types scaffold` prints a starting definition for a type without a server, as [Define a type](/build/define-a-type) shows. - `marfa docs` searches these docs from the terminal, as [Docs as Markdown](/docs-as-markdown) shows. ## Install @@ -63,7 +63,7 @@ On either system, the install script picks the archive for your machine, checks curl -fsSL https://raw.githubusercontent.com/withmarfa/marfa/main/scripts/install.sh | sh ``` -With Docker installed, [`marfa server install`](/guides/install) sets up a server on the same machine. +With Docker installed, [`marfa server install`](/run/install) sets up a server on the same machine. ## Connect to a server @@ -112,7 +112,7 @@ The keychain holds one credential for each server, and only what these commands | Command | What it does | | --- | --- | -| `marfa login` | Signs in as an app with the [device grant](/guides/authenticate#connect-a-command-line-app): you approve what it may reach on a page on the server, and it keeps the app's token. It refuses while a key is kept for that server. | +| `marfa login` | Signs in as an app with the [device grant](/build/authenticate#connect-a-command-line-app): you approve what it may reach on a page on the server, and it keeps the app's token. It refuses while a key is kept for that server. | | `marfa logout` | Revokes the token and forgets it. | | `marfa keys keep` | Checks a key you already have with the server, then keeps it. It sends the key, so a key from `MARFA_API_KEY` needs the server named. | | `marfa keys forget` | Forgets a kept key, without revoking it. | @@ -121,7 +121,7 @@ The keychain holds one credential for each server, and only what these commands marfa login --url https://marfa.example.com ``` -[Manage keys](/guides/manage-keys#give-a-key-to-a-script-or-agent) covers `marfa keys keep`, and how to give a script a key from a secrets manager. +[Manage keys](/run/manage-keys#give-a-key-to-a-script-or-agent) covers `marfa keys keep`, and how to give a script a key from a secrets manager. ### Run without the keychain @@ -139,7 +139,7 @@ On macOS, `MARFA_KEYCHAIN` names a keychain file to use instead of your own, for `marfa folders watch` with no directory, and the login agent `marfa folders autostart` sets up, read each folder's credential from the keychain. The login agent starts outside your shell, so it never sees the variables or options above and reads your own keychain. -For a machine where nobody signs in to unlock the keychain, give the watch a key file instead: `marfa folders watch --key-file `, or `marfa folders autostart on --key-file ` for the login agent. [Keep folders running](/guides/keep-folders-running#use-a-key-file-instead-of-the-keychain) covers both. +For a machine where nobody signs in to unlock the keychain, give the watch a key file instead: `marfa folders watch --key-file `, or `marfa folders autostart on --key-file ` for the login agent. [Keep folders running](/run/keep-folders-running#use-a-key-file-instead-of-the-keychain) covers both. ## Color and piped output @@ -186,7 +186,7 @@ a key from MARFA_API_KEY With a profile, the first line ends with `(profile example)`, and the credential line says the credential came from `the profile's key_command` or `the keychain`. -The instance name defaults to Marfa, and [Settings](/guides/settings#change-settings-while-it-runs) changes it. A server built from a clone shows only the commit. Under an app's token, `marfa whoami` also lists what the owner approved, in the words of the approval page: each type as No access, Read only or Read and edit, and each permission by its title. `marfa keys list`, `current`, `create` and `update` print a key's access the same way, and `--json` prints it structured. +The instance name defaults to Marfa, and [Settings](/run/settings#change-settings-while-it-runs) changes it. A server built from a clone shows only the commit. Under an app's token, `marfa whoami` also lists what the owner approved, in the words of the approval page: each type as No access, Read only or Read and edit, and each permission by its title. `marfa keys list`, `current`, `create` and `update` print a key's access the same way, and `--json` prints it structured. ## Use the private socket @@ -213,14 +213,14 @@ That's how the owner makes the first key in the [Quickstart](/quickstart). These | Command | What it does | Guide | | --- | --- | --- | | `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 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 owner recover` | Set a new owner password. | [Sign in as the owner](/run/sign-in-as-the-owner#recover-the-owners-password) | +| `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](/run/sign-in-as-the-owner#change-the-owners-profile) | +| `marfa owner passkeys` | Add, list, rename and remove passkeys. | [Sign in as the owner](/run/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 items trash` | List, move, restore and purge items in the trash. | [Housekeeping](/run/housekeeping#empty-the-bin) | +| `marfa sign-ins` | List, rename and end the sign-ins that can reach the server. | [Sign in as the owner](/run/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 ` and `--clear-avatar`. [Sign in as the owner](/guides/sign-in-as-the-owner#change-the-owners-profile) shows both. +`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 ` and `--clear-avatar`. [Sign in as the owner](/run/sign-in-as-the-owner#change-the-owners-profile) shows both. **The socket carries no key.** `--socket` can't be combined with `--url`, `--key`, `--profile`, `MARFA_API_URL`, `MARFA_API_KEY` or `MARFA_PROFILE`. 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. @@ -228,11 +228,11 @@ That's how the owner makes the first key in the [Quickstart](/quickstart). These `marfa folders change --version --file settings.json` changes the folder's settings on the instance without opening its local directory. Read the version with `marfa items get `. Use `--body` for inline JSON, or name settings with flags such as `--query`. -**Each named setting replaces its value whole.** Omitted declared settings stay, and every accepted change removes stored settings the folder type does not declare. To repair a folder that holds `search`, use a server at v0.0.33 or later and supply the intended `query`, leaving out `search`; the server does not translate it. [Change a folder's settings](/guides/sync-a-folder#change-a-folders-settings) shows the file, version rules and read-back. +**Each named setting replaces its value whole.** Omitted declared settings stay, and every accepted change removes stored settings the folder type does not declare. To repair a folder that holds `search`, use a server at v0.0.33 or later and supply the intended `query`, leaving out `search`; the server does not translate it. [Change a folder's settings](/run/sync-a-folder#change-a-folders-settings) shows the file, version rules and read-back. ## Read a view from a working copy -The `marfa device views` commands read saved views from the database named by `--db`. **They need no server connection.** [Read a view offline](/guides/read-a-view) shows how to fill the copy and read its items and groups. +The `marfa device views` commands read saved views from the database named by `--db`. **They need no server connection.** [Read a view offline](/build/read-a-view) shows how to fill the copy and read its items and groups. | Command | Answer | | --- | --- | diff --git a/content/docs/(reference)/folder-watch-record.mdx b/content/docs/(reference)/folder-watch-record.mdx index 948e0320..e5dd4406 100644 --- a/content/docs/(reference)/folder-watch-record.mdx +++ b/content/docs/(reference)/folder-watch-record.mdx @@ -3,7 +3,7 @@ title: Folder watch record description: The file a running folder watch keeps for other programs on the computer, and how to read whether folders are syncing. --- -While `marfa folders watch` runs, it keeps a record of its folders in `folder-watch.json`. A menu-bar app, a status script or any other program on the computer can read it to show whether folders sync and how each one fares. A reader doesn't open a folder's store, call the server or run a command. [Keep folders running](/guides/keep-folders-running) covers the watch itself. +While `marfa folders watch` runs, it keeps a record of its folders in `folder-watch.json`. A menu-bar app, a status script or any other program on the computer can read it to show whether folders sync and how each one fares. A reader doesn't open a folder's store, call the server or run a command. [Keep folders running](/run/keep-folders-running) covers the watch itself. ## Where it is diff --git a/content/docs/(reference)/swift-sdk.mdx b/content/docs/(reference)/swift-sdk.mdx index c7ea5404..583464ea 100644 --- a/content/docs/(reference)/swift-sdk.mdx +++ b/content/docs/(reference)/swift-sdk.mdx @@ -5,7 +5,7 @@ description: Give an iOS or macOS app a working copy of your instance that reads The Swift SDK gives an app on iPhone, iPad or Mac a **working copy** of an instance: a local database it reads with or without a connection, and a queue that holds its writes until it can send them. **An app built on it is a [device](/devices)**: that page explains the working copy, the queue and verdicts. -Use it for an app that must keep working offline. A script or a server needs no working copy: use the [TypeScript SDK](/typescript-sdk) or the [HTTP API](/api-reference). For the calls only the instance can answer, such as registering a type, the SDK also has a [client](#call-the-server-directly). [Build an app with Swift](/guides/swift-app) builds a SwiftUI app on it. +Use it for an app that must keep working offline. A script or a server needs no working copy: use the [TypeScript SDK](/typescript-sdk) or the [HTTP API](/api-reference). For the calls only the instance can answer, such as registering a type, the SDK also has a [client](#call-the-server-directly). [Build an app with Swift](/build/swift-app) builds a SwiftUI app on it. ## Install @@ -44,7 +44,7 @@ guard let key = try Keychain.system.key(service: service, account: serverURL.abs let copy = try await WorkingCopy.open(store: storeURL, server: Server(url: serverURL, key: key)) ``` -`Keychain.system` also has `holds(service:account:)` and `delete(service:account:)`. [Manage keys](/guides/manage-keys) makes a key that reaches only your app's types. +`Keychain.system` also has `holds(service:account:)` and `delete(service:account:)`. [Manage keys](/run/manage-keys) makes a key that reaches only your app's types. | Call | What it does | | --- | --- | @@ -64,7 +64,7 @@ A store that another build of the core made throws `wrongSchema` from `open`, an - **The files the copy downloaded stay.** The core keeps them by content, beside the store, not in it. - **What only the old store held is gone:** pins, the position in the instance's events, declared types and the file budget's setting. Pass `fileBudget` to set it again; otherwise it is the default. -- **A store that holds an unsent write is left as it is**, and the call throws `wrongSchema` with the count. A store that opens as it is, or a path with no store, throws `invalid`. [Troubleshooting](/guides/troubleshooting#staying-in-step) covers what to do with unsent writes. +- **A store that holds an unsent write is left as it is**, and the call throws `wrongSchema` with the count. A store that opens as it is, or a path with no store, throws `invalid`. [Troubleshooting](/run/troubleshooting#staying-in-step) covers what to do with unsent writes. ## Reach a plain http server @@ -247,7 +247,7 @@ try await copy.declareTypes([ - **It replaces each declared `app.*` type the instance holds in another definition**, if the credential may change it. The server judges whether a definition differs, after normalizing both, so key order and defaults written out or left out never cause a write. The `HydrateReport` lists the replaced types in `replacedTypes`. - **It leaves a type the instance refuses to replace as the instance holds it.** A refusal doesn't stop the hydration. The `HydrateReport` lists the type in `unreplacedTypes` with the server's code and message. A key without `schema.write` can add optional fields and change a label, but the server refuses a change that makes a field required or removes one. -A type outside `app.*`, such as one under `user.*`, is registered when missing and never replaced. [Define a type](/guides/define-a-type) explains what a definition holds. [Troubleshooting](/guides/troubleshooting#staying-in-step) says what to do about a refusal. +A type outside `app.*`, such as one under `user.*`, is registered when missing and never replaced. [Define a type](/build/define-a-type) explains what a definition holds. [Troubleshooting](/run/troubleshooting#staying-in-step) says what to do about a refusal. ## Call the server directly @@ -382,7 +382,7 @@ A `Reading` is `.list(_:sort:)` or `.search(_:filters:limit:)` for `Rows` ## Saved views -A `SavedView` is a [`system.view`](/catalog/view) item: a title, a query, an optional sort and an optional grouping. **A view holds no layout**: how an app draws the items is the app's own. **The working copy answers a view offline**, so every app lists and groups the same items the same way. [Read a view offline](/guides/read-a-view) explains what the copy needs: hydrate `system.view` and every type the view's query names, at the tier the query lists, and the edge type of a `backref` condition or an edge group. +A `SavedView` is a [`system.view`](/catalog/view) item: a title, a query, an optional sort and an optional grouping. **A view holds no layout**: how an app draws the items is the app's own. **The working copy answers a view offline**, so every app lists and groups the same items the same way. [Read a view offline](/build/read-a-view) explains what the copy needs: hydrate `system.view` and every type the view's query names, at the tier the query lists, and the edge type of a `backref` condition or an edge group. ```swift _ = try await copy.hydrate(types: ["system.view", "core.task"], tier: .all) @@ -466,7 +466,7 @@ The rules are in the contract's [views written through a copy](https://github.co ## Folders on a Mac -On macOS, `Folders` keeps a directory of files in step with a folder on the instance, and shares the Mac's list of folders with `marfa folders`. **Nothing is written into the directory or sent until the first sync is confirmed.** [Sync a folder](/guides/sync-a-folder) sets one up, with Swift examples. +On macOS, `Folders` keeps a directory of files in step with a folder on the instance, and shares the Mac's list of folders with `marfa folders`. **Nothing is written into the directory or sent until the first sync is confirmed.** [Sync a folder](/run/sync-a-folder) sets one up, with Swift examples. A folder's settings are a `system.folder` item. `copy.createFolder(_:)`, `changeFolder(_:_:baseVersion:)` and `revokeFolder(_:)` write them at once, never queued, and `copy.items.folder(_:)` reads one. `Folders(server:)` works the directories on this Mac: @@ -511,7 +511,7 @@ Every call on a working copy, `Folders` or `Client` throws a `MarfaError`, whose | `network`, `unnamed` | The server couldn't be reached, or something in front of it answered naming no contract. A queued write stays queued. | | `noConnection` | A `Client` call found the device offline, so nothing was sent. | | `contractMismatch` | The server speaks a contract this build wasn't made for. A write it answered stays queued, and goes again once the app speaks the server's contract. | -| `wrongServer` | The server at the copy's address is another instance than the copy recorded. The copy sends it no credential or data: [Troubleshooting](/guides/troubleshooting) explains. | +| `wrongServer` | The server at the copy's address is another instance than the copy recorded. The copy sends it no credential or data: [Troubleshooting](/run/troubleshooting) explains. | | `copyExpired`, `hydrationIncomplete`, `readingHandle`, `firstSyncWaiting`, `noServer`, `closed` | States covered on this page. | The other cases (`unknownType`, `io`, `decoding`, `store`, `storageFull`, `signedOut`, `noKeychain`, `redirected`, `noCursor`, `noCatalog`, `wrongSchema`, `bytesAbsent`, `streamIncomplete`, `canceled` and `invalid`) each carry a message that says what happened. The [marfa-swift repository](https://github.com/withmarfa/marfa-swift) holds the source. diff --git a/content/docs/(reference)/typescript-sdk.mdx b/content/docs/(reference)/typescript-sdk.mdx index 4b114cf9..6376ef5a 100644 --- a/content/docs/(reference)/typescript-sdk.mdx +++ b/content/docs/(reference)/typescript-sdk.mdx @@ -17,7 +17,7 @@ npm install @withmarfa/client ## Create a client -Pass the URL your instance is served at and a credential: a key ([Manage keys](/guides/manage-keys)) or an app's access token ([Authenticate](/guides/authenticate)). +Pass the URL your instance is served at and a credential: a key ([Manage keys](/run/manage-keys)) or an app's access token ([Authenticate](/build/authenticate)). ```ts title="client.ts" import { createClient } from "@withmarfa/client"; @@ -96,7 +96,7 @@ console.log(blob); -[Handle files](/guides/handle-files) attaches the blob to an item. +[Handle files](/build/handle-files) attaches the blob to an item. ## Handle errors @@ -146,4 +146,4 @@ The fix is the `@withmarfa/client` release that matches your instance. An error ## What the SDK doesn't do -The SDK sends what the API description declares and returns what the server answered. It doesn't retry a request (even one refused with `Retry-After`), poll a bulk job, read the event stream, verify a webhook's signature or resolve a version conflict. [Write safely](/guides/write-safely), [Keep in sync](/guides/keep-in-sync) and [Receive webhooks](/guides/receive-webhooks) show how to do each with the API. +The SDK sends what the API description declares and returns what the server answered. It doesn't retry a request (even one refused with `Retry-After`), poll a bulk job, read the event stream, verify a webhook's signature or resolve a version conflict. [Write safely](/build/write-safely), [Keep in sync](/build/keep-in-sync) and [Receive webhooks](/build/receive-webhooks) show how to do each with the API. diff --git a/content/docs/guides/ai-agents.mdx b/content/docs/build/ai-agents.mdx similarity index 98% rename from content/docs/guides/ai-agents.mdx rename to content/docs/build/ai-agents.mdx index 9c42f229..18e7a8a5 100644 --- a/content/docs/guides/ai-agents.mdx +++ b/content/docs/build/ai-agents.mdx @@ -16,7 +16,7 @@ marfa --socket /srv/marfa/data/control/marfa.sock keys create --label claude-cod --expires-in 30d ``` -The CLI prints the key, which begins `marfa_k1_`, once, on the first line. **The source, `claude-code`, is stamped on every item the agent creates**, so you can always tell its work from yours. [Manage keys](/guides/manage-keys) covers the other things a key can hold. +The CLI prints the key, which begins `marfa_k1_`, once, on the first line. **The source, `claude-code`, is stamped on every item the agent creates**, so you can always tell its work from yours. [Manage keys](/run/manage-keys) covers the other things a key can hold. ## Give it to the agent diff --git a/content/docs/guides/authenticate.mdx b/content/docs/build/authenticate.mdx similarity index 94% rename from content/docs/guides/authenticate.mdx rename to content/docs/build/authenticate.mdx index f2b1fdf9..b5203828 100644 --- a/content/docs/guides/authenticate.mdx +++ b/content/docs/build/authenticate.mdx @@ -5,7 +5,7 @@ description: Send a key with data requests, or connect an app with OAuth so the A program has two ways into Marfa. A script sends a key, which begins `marfa_k1_`. An app connects with OAuth, which is the preferred way for an app: the owner approves it, and it receives an access token, which begins `marfa_at_`. An app may also use a key, which the owner gives it or which it mints with `keys.mint`. -No program ever needs **the owner's password**. [Access](/access) explains what a credential can hold, and [Sign in as the owner](/guides/sign-in-as-the-owner) covers the owner's own sign-in. +No program ever needs **the owner's password**. [Access](/access) explains what a credential can hold, and [Sign in as the owner](/run/sign-in-as-the-owner) covers the owner's own sign-in. ## Send a key @@ -39,7 +39,7 @@ The CLI reads the key from a profile, `--key`, `MARFA_API_KEY` or the keychain, The app registers, sends the owner to approve it, and receives an access token for the scopes the owner approved. The instance's discovery document, at `/.well-known/oauth-authorization-server/auth`, lists every endpoint and the `scopes_supported`. -These steps use the authorization code grant. A web page follows them with a few more constraints, which [Connect a web page](/guides/connect-a-web-page) covers. An app with no browser uses [the device grant](#connect-a-command-line-app). +These steps use the authorization code grant. A web page follows them with a few more constraints, which [Connect a web page](/build/connect-a-web-page) covers. An app with no browser uses [the device grant](#connect-a-command-line-app). @@ -219,7 +219,7 @@ Marfa answers `200`, even for a token it doesn't hold. The owner can narrow or disconnect an app on the **Connected apps** page. Narrowing ends tokens and refresh chains that exceed the remaining access; covered chains keep working. Increasing access requires fresh approval. When disconnecting, the owner can revoke the keys the app made at the same time. A credential holding `grants.manage` lists apps with `GET /auth/grants` and ends one with `DELETE /auth/grants/{id}`. -Narrowing or disconnecting from a browser **needs a sign-in from the last five minutes**, and the page asks the owner to [confirm it's them](/guides/sign-in-as-the-owner#confirm-its-you) if theirs is older. A program that sends the same request gets `403 forbidden`, and the app keeps working. The private local command is not held to it, and nor is a credential holding `grants.manage` when it disconnects. Only the owner or the private local command narrows. +Narrowing or disconnecting from a browser **needs a sign-in from the last five minutes**, and the page asks the owner to [confirm it's them](/run/sign-in-as-the-owner#confirm-its-you) if theirs is older. A program that sends the same request gets `403 forbidden`, and the app keeps working. The private local command is not held to it, and nor is a credential holding `grants.manage` when it disconnects. Only the owner or the private local command narrows. ## What a refusal looks like diff --git a/content/docs/guides/change-a-type.mdx b/content/docs/build/change-a-type.mdx similarity index 80% rename from content/docs/guides/change-a-type.mdx rename to content/docs/build/change-a-type.mdx index 787a672b..874f3a4e 100644 --- a/content/docs/guides/change-a-type.mdx +++ b/content/docs/build/change-a-type.mdx @@ -3,9 +3,9 @@ title: Change or delete a type description: Check a change to a registered type, replace its definition, delete a type nothing uses, and remove a platform type a build no longer ships. --- -A type you registered changes as your app grows. This guide checks a change, replaces the definition, and deletes a type, using the receipt type from [Define a type](/guides/define-a-type). +A type you registered changes as your app grows. This guide checks a change, replaces the definition, and deletes a type, using the receipt type from [Define a type](/build/define-a-type). -Each call sends `receipt.json` as that guide left it, and `definition` in the examples is that file read in, as [Register the type](/guides/define-a-type#register-the-type) shows. The TypeScript examples use a client made as [the TypeScript SDK](/typescript-sdk) shows, and the Swift examples a `Client` made as [the Swift SDK](/swift-sdk#call-the-server-directly) shows, with `receiptURL` the file's location. [What your key needs](/guides/define-a-type#what-your-key-needs) lists the permissions each call takes. +Each call sends `receipt.json` as that guide left it, and `definition` in the examples is that file read in, as [Register the type](/build/define-a-type#register-the-type) shows. The TypeScript examples use a client made as [the TypeScript SDK](/typescript-sdk) shows, and the Swift examples a `Client` made as [the Swift SDK](/swift-sdk#call-the-server-directly) shows, with `receiptURL` the file's location. [What your key needs](/build/define-a-type#what-your-key-needs) lists the permissions each call takes. ## Change the type @@ -70,7 +70,7 @@ A replacement **changes no item you already hold**. An item meets a property you Send the `version` every time: a replacement without one sets it back to 0. You can't replace or delete a type Marfa ships. -**A type's identifier never changes.** To rename a type, register the new one, [move the items](/guides/move-items-to-another-type) to it, then delete the old one. A move also changes the folders, views and webhooks that name the old type. +**A type's identifier never changes.** To rename a type, register the new one, [move the items](/build/move-items-to-another-type) to it, then delete the old one. A move also changes the folders, views and webhooks that name the old type. ## Delete the type @@ -97,13 +97,13 @@ try await api.delete(itemType: "app.example.receipt") Otherwise it refuses with `409 type_in_use` when items hold the type or a setting names it, `409 type_has_subtypes` when a type names it as its parent, and `422 compatible_with_violation` when a type names it in `compatible_with`. -A `type_in_use` refusal counts the items in `details.item_count` and names each folder, view and webhook setting in `details.settings`, without the `id` of a setting your key can't read. **[Move the items to another type](/guides/move-items-to-another-type) first:** the move changes those settings too. `details.grants` lists the keys and apps whose type map names the type, if your key may manage them. They don't stop a delete, so change them through [Manage keys](/guides/manage-keys) when you're ready. +A `type_in_use` refusal counts the items in `details.item_count` and names each folder, view and webhook setting in `details.settings`, without the `id` of a setting your key can't read. **[Move the items to another type](/build/move-items-to-another-type) first:** the move changes those settings too. `details.grants` lists the keys and apps whose type map names the type, if your key may manage them. They don't stop a delete, so change them through [Manage keys](/run/manage-keys) when you're ready. A key can send `force=true` (the CLI's `--force`) to delete a type that items still hold or a setting names. A subtype or a `compatible_with` claim still blocks it. The items and settings stay, but Marfa refuses writes to the items' properties, tier or time until the type is registered again. You can still tag, archive or trash them. -The owner can remove an unused type without a key: with a [sign-in from the last five minutes](/guides/sign-in-as-the-owner#confirm-its-you), select **Remove type** on the type's page under **Items** in the owner's console, or send the deletion through the private socket. Marfa makes the same checks, and refuses `force=true` from the owner with `403 forbidden`. +The owner can remove an unused type without a key: with a [sign-in from the last five minutes](/run/sign-in-as-the-owner#confirm-its-you), select **Remove type** on the type's page under **Items** in the owner's console, or send the deletion through the private socket. Marfa makes the same checks, and refuses `force=true` from the owner with `403 forbidden`. ## Remove a retired platform type diff --git a/content/docs/guides/connect-a-web-page.mdx b/content/docs/build/connect-a-web-page.mdx similarity index 91% rename from content/docs/guides/connect-a-web-page.mdx rename to content/docs/build/connect-a-web-page.mdx index 7606c2e8..641d7d6e 100644 --- a/content/docs/guides/connect-a-web-page.mdx +++ b/content/docs/build/connect-a-web-page.mdx @@ -3,9 +3,9 @@ title: Connect a web page description: Let a page in a browser register itself with an instance, get the owner's approval in a small window, and read the owner's data with the token it receives. --- -A web page connects the way any [app signs in with OAuth](/guides/authenticate), with every step taken by the page's own script. The instance needs **no setup** and the page needs **no key**. The page registers itself, the owner approves it once, and the instance then lets the page's address call it from a browser. +A web page connects the way any [app signs in with OAuth](/build/authenticate), with every step taken by the page's own script. The instance needs **no setup** and the page needs **no key**. The page registers itself, the owner approves it once, and the instance then lets the page's address call it from a browser. -The steps follow the [authorization code grant](/guides/authenticate#connect-an-app-with-oauth) and cover what a browser changes about it. +The steps follow the [authorization code grant](/build/authenticate#connect-an-app-with-oauth) and cover what a browser changes about it. ## What the page needs @@ -60,7 +60,7 @@ const clientId: string = registered.client_id; Marfa answers `201` with the `client_id`. A page keeps no secret, so `"token_endpoint_auth_method": "none"` registers a public client that proves itself with PKCE. -A registration takes at most 32 KiB and 10 redirect addresses. A larger body is refused `413 request_too_large`, and more addresses `400 invalid_redirect_uri`. A registration nobody approves is removed after a day idle, as [Housekeeping](/guides/housekeeping) describes, so register again at each connection until the owner approves, then keep the `client_id`. +A registration takes at most 32 KiB and 10 redirect addresses. A larger body is refused `413 request_too_large`, and more addresses `400 invalid_redirect_uri`. A registration nobody approves is removed after a day idle, as [Housekeeping](/run/housekeeping) describes, so register again at each connection until the owner approves, then keep the `client_id`. @@ -202,7 +202,7 @@ Authorization: Bearer marfa_at_… Don't send `credentials: "include"`: Marfa never allows credentials on a cross-origin answer, so the page works only with the token it holds. [List items](/api-reference/items/listItems) has every parameter. -The token expires after `expires_in` seconds. To get another, open the approval window again and exchange the new code: an owner who is signed in and approved these scopes already goes straight back with a code. To end the approval, [revoke access](/guides/authenticate#revoke-access). +The token expires after `expires_in` seconds. To get another, open the approval window again and exchange the new code: an owner who is signed in and approved these scopes already goes straight back with a code. To end the approval, [revoke access](/build/authenticate#revoke-access). @@ -215,7 +215,7 @@ A page can skip the window and send the browser itself to the authorization addr Approving the page allows the origin (scheme, host and port) of each `https` redirect address it registered to call the instance from a browser, until the last connected app on that origin is disconnected. Before approval, only discovery and registration answer the page. The access token is the only authority: the origin lets the browser deliver answers to the page and gives it nothing else. A form posted from that origin to the sign-in or approval address is refused `403 forbidden`, so the page never acts as the owner. -An operator who trusts an origin in advance names it in `CORS_ORIGINS`, as [Settings](/guides/settings#every-environment-setting) shows. A native app's redirect address, where a program on a device listens, allows no origin. +An operator who trusts an origin in advance names it in `CORS_ORIGINS`, as [Settings](/run/settings#every-environment-setting) shows. A native app's redirect address, where a program on a device listens, allows no origin. ## If a step fails diff --git a/content/docs/guides/connector-follow-changes.mdx b/content/docs/build/connector-follow-changes.mdx similarity index 84% rename from content/docs/guides/connector-follow-changes.mdx rename to content/docs/build/connector-follow-changes.mdx index 1866f293..4c134103 100644 --- a/content/docs/guides/connector-follow-changes.mdx +++ b/content/docs/build/connector-follow-changes.mdx @@ -3,7 +3,7 @@ title: Make a connector follow changes description: Teach the reading list connector to archive entries its service drops, then change the list, run it again, and see a failed run reported. --- -- **You'll build** on the connector from [Build a connector](/guides/connector). It will archive an entry the service drops, and you'll watch it follow a changed list. +- **You'll build** on the connector from [Build a connector](/build/connector). It will archive an entry the service drops, and you'll watch it follow a changed list. - **You need** that connector, built, with its key, and the reading list service still running. Keep the `marfa-connectors` checkout as your working folder. - **You'll have** a connector that creates, updates and archives items to match the service, and a failed run you can read on the instance. @@ -28,7 +28,7 @@ At the end of `run`, after the `upsert`, archive each item whose entry the servi await archive(readlistEntry.id, gone); ``` -`held` answers the items the connector wrote. Reading a missing entry as deleted is safe here because **this service lists every entry it has**; [Pull and push](/guides/connector-pull-and-push#set-aside-what-the-vendor-deleted) covers a vendor that doesn't. +`held` answers the items the connector wrote. Reading a missing entry as deleted is safe here because **this service lists every entry it has**; [Pull and push](/build/connector-pull-and-push#set-aside-what-the-vendor-deleted) covers a vendor that doesn't. Build again: @@ -154,8 +154,8 @@ You have a connector that keeps your instance in step with a vendor and reports This connector only reads. Edit an entry's title in Marfa, and the next run puts the service's title back, with a warning that names the field. To carry changes back to the vendor, or to act on its webhooks, go on to the connector guides: - Run a connector on a schedule, and read its exit codes. - Write what a vendor holds, and carry changes made in Marfa back to it. - Let the vendor tell the connector what changed, as it happens. - Keep one process writing at a time, and recover from a failed request. + Run a connector on a schedule, and read its exit codes. + Write what a vendor holds, and carry changes made in Marfa back to it. + Let the vendor tell the connector what changed, as it happens. + Keep one process writing at a time, and recover from a failed request. diff --git a/content/docs/guides/connector-holds-and-retries.mdx b/content/docs/build/connector-holds-and-retries.mdx similarity index 96% rename from content/docs/guides/connector-holds-and-retries.mdx rename to content/docs/build/connector-holds-and-retries.mdx index f979c7e4..22237b4f 100644 --- a/content/docs/guides/connector-holds-and-retries.mdx +++ b/content/docs/build/connector-holds-and-retries.mdx @@ -129,7 +129,7 @@ The document belongs to the key's source, not to one key, so a new key under the ## Record agreements -An agreement records, for one item, what the connector and the vendor last agreed it holds. [Pull and push](/guides/connector-pull-and-push#watch-for-changes-made-in-marfa) shows how a connector uses them. [Write connector agreements](/api-reference/connector-state/writeConnectorAgreements) sets some and clears others in one request, under the hold: +An agreement records, for one item, what the connector and the vendor last agreed it holds. [Pull and push](/build/connector-pull-and-push#watch-for-changes-made-in-marfa) shows how a connector uses them. [Write connector agreements](/api-reference/connector-state/writeConnectorAgreements) sets some and clears others in one request, under the hold: ```http tab="HTTP" tab-group="client" POST /connectors/01a12313-d219-7e92-8869-1f6546b43dbc/agreements @@ -209,7 +209,7 @@ await marfa.POST("/items", { A write to the vendor is **yours to make safe**. Send the vendor an idempotency key, if it takes one, made from something that doesn't change between attempts, such as the item's ID. Record the vendor's ID for a record you made as soon as you have it, in the type's `link_field`, so a retry updates that record instead of making another. -[Write safely](/guides/write-safely) covers versions and idempotency keys in full. +[Write safely](/build/write-safely) covers versions and idempotency keys in full. ## Report a run diff --git a/content/docs/guides/connector-pull-and-push.mdx b/content/docs/build/connector-pull-and-push.mdx similarity index 96% rename from content/docs/guides/connector-pull-and-push.mdx rename to content/docs/build/connector-pull-and-push.mdx index 62e0e39c..788195bd 100644 --- a/content/docs/guides/connector-pull-and-push.mdx +++ b/content/docs/build/connector-pull-and-push.mdx @@ -5,7 +5,7 @@ description: Write a vendor's records into Marfa as items, notice what people ch A connector's work runs in two directions. It pulls: it reads the vendor and writes what it finds as items. A two-way connector also pushes: it notices what people change in Marfa and carries those changes back to the vendor. -The operations below come in the order a run uses them, for a connector built directly on the API. [The connector kit](/guides/connector) does all of this for you. +The operations below come in the order a run uses them, for a connector built directly on the API. [The connector kit](/build/connector) does all of this for you. The examples follow a task manager connector with a key of its own under the source `tasks`, which writes the type `tasks.task` with `link_field` `task_id`. In the TypeScript examples, `marfa` is a [TypeScript SDK](/typescript-sdk) client made with that key, `connectorId` is the registration's ID, and `processName` is the process's name (`` in HTTP). @@ -120,9 +120,9 @@ Authorization: Bearer Last-Event-ID: 11 ``` -Each change arrives as an event with the item as it now stands. `Last-Event-ID` resumes after the last event you handled, so keep that cursor in the state document. [Keep a copy in sync](/guides/keep-in-sync) explains the stream, including what to do when it can't replay from an old cursor. +Each change arrives as an event with the item as it now stands. `Last-Event-ID` resumes after the last event you handled, so keep that cursor in the state document. [Keep a copy in sync](/build/keep-in-sync) explains the stream, including what to do when it can't replay from an old cursor. -Your own writes **come back on the stream too**. To tell a person's change from your own, keep an agreement for each item: a record of what the connector and the vendor last agreed it holds. After every write you make or carry, [write the agreement](/api-reference/connector-state/writeConnectorAgreements), under the connector's hold, as [Holds and retries](/guides/connector-holds-and-retries) shows: +Your own writes **come back on the stream too**. To tell a person's change from your own, keep an agreement for each item: a record of what the connector and the vendor last agreed it holds. After every write you make or carry, [write the agreement](/api-reference/connector-state/writeConnectorAgreements), under the connector's hold, as [Holds and retries](/build/connector-holds-and-retries) shows: ```http tab="HTTP" tab-group="client" POST /connectors/01a12313-d219-7e92-8869-1f6546b43dbc/agreements diff --git a/content/docs/guides/connector-webhooks.mdx b/content/docs/build/connector-webhooks.mdx similarity index 96% rename from content/docs/guides/connector-webhooks.mdx rename to content/docs/build/connector-webhooks.mdx index 5ba407e2..bb397fd0 100644 --- a/content/docs/guides/connector-webhooks.mdx +++ b/content/docs/build/connector-webhooks.mdx @@ -126,7 +126,7 @@ Each vendor names its own header, prefix and encoding, so follow its documentati Treat a delivery as **a prompt to read the vendor**, not as the vendor's state. Vendors send webhooks out of order, twice, or not at all. -Take from the delivery only what names the thing that changed, such as a task's ID, then fetch it from the vendor and write it as [Pull and push](/guides/connector-pull-and-push) shows. **Keep the scheduled read as well**, for the webhook that never comes. +Take from the delivery only what names the thing that changed, such as a task's ID, then fetch it from the vendor and write it as [Pull and push](/build/connector-pull-and-push) shows. **Keep the scheduled read as well**, for the webhook that never comes. When a delivery repeats the `duplicate_header` value of an earlier one on the same endpoint, Marfa stores both and fills the later one's `duplicate_of` with the earlier one's `id` and `outcome`. If that outcome is `processed`, you've already acted on it. @@ -167,12 +167,12 @@ The instance answers the vendor **before your connector sees anything**, and man | --- | --- | | `202`, with the delivery's `id` | The instance stored the delivery. | | `404`, and nothing is stored | The address isn't a live endpoint: it was retired, never existed, had its connector deleted, or belongs to a connector whose key was revoked or expired. | -| `413 request_too_large`, and nothing is stored | The body is over **25 MiB**, 26,214,400 bytes. `MARFA_INBOUND_MAX_BYTES` changes the limit, as [Settings](/guides/settings#every-environment-setting) lists. | +| `413 request_too_large`, and nothing is stored | The body is over **25 MiB**, 26,214,400 bytes. `MARFA_INBOUND_MAX_BYTES` changes the limit, as [Settings](/run/settings#every-environment-setting) lists. | | `408 request_timeout`, and nothing is stored | The body didn't arrive in time. | | `429 rate_limited`, with `Retry-After` | The endpoint is past its request limit. | | `503 inbound_unavailable`, with `Retry-After` | The backlog is full. | -[Errors](/api-reference/errors) lists every refusal. Marking a delivery handled frees its place among the unhandled ones, but **only age removes it**. [Settings](/guides/settings#every-environment-setting) lists the retention windows and [Housekeeping](/guides/housekeeping) the job that applies them. +[Errors](/api-reference/errors) lists every refusal. Marking a delivery handled frees its place among the unhandled ones, but **only age removes it**. [Settings](/run/settings#every-environment-setting) lists the retention windows and [Housekeeping](/run/housekeeping) the job that applies them. ## Replace an endpoint diff --git a/content/docs/guides/connector.mdx b/content/docs/build/connector.mdx similarity index 92% rename from content/docs/guides/connector.mdx rename to content/docs/build/connector.mdx index 75af41f8..94f5afa4 100644 --- a/content/docs/guides/connector.mdx +++ b/content/docs/build/connector.mdx @@ -125,7 +125,7 @@ A connector writes items of a type of its own. Save its definition beside the co } ``` -Its parent is `core.bookmark`, so an entry already has a `title`, a `url` and an `author`. The type adds only the entry's ID in the service. [Define a type](/guides/define-a-type) explains each part. +Its parent is `core.bookmark`, so an entry already has a `title`, a `url` and an `author`. The type adds only the entry's ID in the service. [Define a type](/build/define-a-type) explains each part. @@ -252,7 +252,7 @@ key 01a15f19-6c20-7c68-a0b3-40bd8a431442 (readlist); the plaintext is shown once Marfa shows it once. If you lose it, make a new one. -The key writes the connector's one type and can register it, and its source matches the connector's `source`. **The connector refuses a key that reaches further**, such as your own. [Manage keys](/guides/manage-keys) explains each flag. +The key writes the connector's one type and can register it, and its source matches the connector's `source`. **The connector refuses a key that reaches further**, such as your own. [Manage keys](/run/manage-keys) explains each flag. Run the connector once, with its key and the service's address and token. **Set them on the command alone**, so they don't replace the key your CLI uses: @@ -273,7 +273,7 @@ node connectors/readlist/dist/main.js --once -The connector registered with your instance, wrote both entries, reported the run and exited. [Run a connector](/guides/run-a-connector) covers running one on a schedule, and what each exit code means. +The connector registered with your instance, wrote both entries, reported the run and exited. [Run a connector](/run/run-a-connector) covers running one on a schedule, and what each exit code means. @@ -334,8 +334,8 @@ The answer has more fields than this. The item waits in the **feed**, where a co You have a connector that reads a vendor and writes what it finds to your instance. Leave the reading list service running if you go on to the next tutorial. - Change the list, run the connector again, and see it archive what the service drops and report a failure. - Run a connector on a schedule, and read its exit codes. - Write what a vendor holds, and carry changes made in Marfa back to it. - Let the vendor tell the connector what changed, as it happens. + Change the list, run the connector again, and see it archive what the service drops and report a failure. + Run a connector on a schedule, and read its exit codes. + Write what a vendor holds, and carry changes made in Marfa back to it. + Let the vendor tell the connector what changed, as it happens. diff --git a/content/docs/guides/define-a-type.mdx b/content/docs/build/define-a-type.mdx similarity index 96% rename from content/docs/guides/define-a-type.mdx rename to content/docs/build/define-a-type.mdx index 39abe57e..cc0b9106 100644 --- a/content/docs/guides/define-a-type.mdx +++ b/content/docs/build/define-a-type.mdx @@ -27,7 +27,7 @@ Keep the definition in a file. Each call on this page sends the file as it stand A property can't share a name with a field every item has, such as `source` or `version`: the Catalog lists the [reserved field names](/catalog/your-own-types#reserved-field-names). Keep the time the thing happened in the item's own `occurred_at`. [Register a type](/api-reference/types/registerType) lists the rest a type can declare, such as its label, display hints and policies. -Marfa keeps a property the type doesn't declare and serves it back. To refuse one, name the type in the `strict_mode` lever, as [Settings](/guides/settings#what-the-enforcement-levers-do) shows. +Marfa keeps a property the type doesn't declare and serves it back. To refuse one, name the type in the `strict_mode` lever, as [Settings](/run/settings#what-the-enforcement-levers-do) shows. To start from a scaffold, `marfa types scaffold` prints a definition. It needs no server or key: @@ -113,7 +113,7 @@ Marfa made the label from the identifier, and `provenance.created_by` names who `marfa types get app.example.receipt` (`GET /types/{id}`) answers the type with the properties it inherits, such as `blob_ref`, `mime_type`, `width` and `height`. An item must hold **every required property along the chain**, so a receipt needs a `blob_ref`, a `mime_type` and a `merchant`. A write that lacks one is refused `400 invalid_properties`, naming it. -Once the type is registered, [change or delete it](/guides/change-a-type) as your app grows. +Once the type is registered, [change or delete it](/build/change-a-type) as your app grows. ## What your key needs @@ -127,6 +127,6 @@ Each operation needs a key whose type map grants write on the type's identifier, A new parent or `compatible_with` target that isn't a platform type also needs write on that type; one the type already names doesn't, so a connector can update its type after its permissions narrow. A missing write is refused `403 type_not_permitted`, and `details.grant` names it. -A replacement that needs `schema.write` from a key without it is refused `403 forbidden`, and `details.changes` lists each part that needs it. Send those parts with a key that holds `schema.write`. [Manage keys](/guides/manage-keys) shows how to give a key its maps and permissions. +A replacement that needs `schema.write` from a key without it is refused `403 forbidden`, and `details.changes` lists each part that needs it. Send those parts with a key that holds `schema.write`. [Manage keys](/run/manage-keys) shows how to give a key its maps and permissions. The rules behind this guide are in the [types](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md) chapter of the contract. diff --git a/content/docs/guides/handle-files.mdx b/content/docs/build/handle-files.mdx similarity index 99% rename from content/docs/guides/handle-files.mdx rename to content/docs/build/handle-files.mdx index b29c9603..20e58f88 100644 --- a/content/docs/guides/handle-files.mdx +++ b/content/docs/build/handle-files.mdx @@ -165,7 +165,7 @@ Marfa checks your read **when it makes the link**, not when someone fetches it. ## Replace a file's bytes -Upload the new bytes, then update the file item's `blob_ref` to the new hash, **naming the version you read**, as [Write safely](/guides/write-safely) shows: +Upload the new bytes, then update the file item's `blob_ref` to the new hash, **naming the version you read**, as [Write safely](/build/write-safely) shows: ```bash tab="CLI" tab-group="client" marfa blobs upload reading-list.txt diff --git a/content/docs/build/index.mdx b/content/docs/build/index.mdx new file mode 100644 index 00000000..30ed6049 --- /dev/null +++ b/content/docs/build/index.mdx @@ -0,0 +1,17 @@ +--- +title: Build on Marfa +description: Build an app, an agent or a connector that reads and writes your data with a key of its own. +--- + +Marfa keeps what you save. Your app, agent or connector reads and writes it through the API with a key, and sees only what that key allows. A tutorial takes you through one complete piece of work, and each guide does one task. + + + An iPhone and Mac app that lists your notes and keeps working offline. + A program that brings records from another service into Marfa. + A command that saves an item to your instance, with a key that reaches only notes. + A key of its own, so an AI agent works with your notes and nothing else. + Register a type for items no shipped type fits. + Follow the event stream so a copy of your data stays current. + + +Looking something up? The [Catalog](/catalog) lists the types, edges, events and permissions Marfa ships, and the [Reference](/reference) covers the HTTP API, the CLI and the SDKs. To run an instance rather than build on one, go to [Run](/run). diff --git a/content/docs/guides/keep-in-sync.mdx b/content/docs/build/keep-in-sync.mdx similarity index 100% rename from content/docs/guides/keep-in-sync.mdx rename to content/docs/build/keep-in-sync.mdx diff --git a/content/docs/guides/list-filter-search.mdx b/content/docs/build/list-filter-search.mdx similarity index 98% rename from content/docs/guides/list-filter-search.mdx rename to content/docs/build/list-filter-search.mdx index 67c23190..27555583 100644 --- a/content/docs/guides/list-filter-search.mdx +++ b/content/docs/build/list-filter-search.mdx @@ -3,7 +3,7 @@ title: List, filter and search description: Read the items you need from an instance, a page at a time, narrowed by type, tags and properties, or found by the words they hold. --- -Most of what an app reads, it reads as a listing: the notes tagged `reading`, the tasks due this week, everything changed since it last looked. Each operation reads only the items your key or token can read. In the TypeScript examples, `marfa` is a client made as [the TypeScript SDK](/typescript-sdk#create-a-client) shows. To keep a listing so every app shows the same one, [save it as a view](/guides/save-a-view). +Most of what an app reads, it reads as a listing: the notes tagged `reading`, the tasks due this week, everything changed since it last looked. Each operation reads only the items your key or token can read. In the TypeScript examples, `marfa` is a client made as [the TypeScript SDK](/typescript-sdk#create-a-client) shows. To keep a listing so every app shows the same one, [save it as a view](/build/save-a-view). ## List a page at a time @@ -139,7 +139,7 @@ const { data } = await marfa.GET("/items", { }); ``` -The bound includes items changed at that instant, so **skip the ones you already hold by `id`**: items written together share an `updated_at`. This listing is always oldest change first, and any other `sort` or `direction` is refused. A listing **can't report a purged item**: to hear of every change, purges included, follow the event stream as [Keep a copy in sync](/guides/keep-in-sync) shows. +The bound includes items changed at that instant, so **skip the ones you already hold by `id`**: items written together share an `updated_at`. This listing is always oldest change first, and any other `sort` or `direction` is refused. A listing **can't report a purged item**: to hear of every change, purges included, follow the event stream as [Keep a copy in sync](/build/keep-in-sync) shows. ## Search diff --git a/content/docs/guides/meta.json b/content/docs/build/meta.json similarity index 52% rename from content/docs/guides/meta.json rename to content/docs/build/meta.json index c1e5028e..ebd759fb 100644 --- a/content/docs/guides/meta.json +++ b/content/docs/build/meta.json @@ -1,13 +1,14 @@ { "root": true, - "title": "Guides", - "icon": "Compass", + "title": "Build", + "icon": "Hammer", "pages": [ + "index", "---Tutorials---", "swift-app", + "raycast-extension", "connector", "connector-follow-changes", - "raycast-extension", "---Build apps and agents---", "authenticate", "ai-agents", @@ -22,29 +23,9 @@ "keep-in-sync", "handle-files", "receive-webhooks", - "sync-a-folder", - "folder-files", - "folder-from-a-mac-app", - "keep-folders-running", - "folder-safeguards", "---Build connectors---", "connector-pull-and-push", "connector-webhooks", - "connector-holds-and-retries", - "---Run Marfa---", - "install", - "deploy", - "sign-in-as-the-owner", - "settings", - "telemetry", - "manage-keys", - "run-a-connector", - "google-project", - "back-up-and-restore", - "export-and-restore", - "upgrade", - "housekeeping", - "audit-log", - "troubleshooting" + "connector-holds-and-retries" ] } diff --git a/content/docs/guides/move-items-to-another-type.mdx b/content/docs/build/move-items-to-another-type.mdx similarity index 94% rename from content/docs/guides/move-items-to-another-type.mdx rename to content/docs/build/move-items-to-another-type.mdx index 3f1e0a88..0bd6cc20 100644 --- a/content/docs/guides/move-items-to-another-type.mdx +++ b/content/docs/build/move-items-to-another-type.mdx @@ -3,7 +3,7 @@ title: Move items to another type description: Move every item of one type to another in one step, and change the folders, views and webhooks that name it. --- -A type's identifier never changes. **To rename a type, or to fold one type into another, register the new type, move the items, then delete the old type.** Each item keeps its ID, edges, tags and files; only its type changes. [Define a type](/guides/define-a-type) shows how to register and delete a type. +A type's identifier never changes. **To rename a type, or to fold one type into another, register the new type, move the items, then delete the old type.** Each item keeps its ID, edges, tags and files; only its type changes. [Define a type](/build/define-a-type) shows how to register and delete a type. The examples move every `acme.deal` item to a registered type, `acme.contract`. @@ -103,7 +103,7 @@ Once every item has moved, **Marfa changes the settings that name the old type** | View | `query.types`, and each `type eq` or `type neq` term in `query.filter` | | Webhook | `type_filter`, when it's exactly the type. A pattern, such as `acme.*`, stays as it is | -While any item of the old type remains, including one your key can't see, Marfa changes no setting, so a folder or view never stops showing items that haven't moved. The job's `result.settings` lists the settings, and `result.settings_rewritten` says whether Marfa changed them. A folder keeps its files; [Sync a folder](/guides/sync-a-folder#change-a-folders-settings) says what changes on disk. +While any item of the old type remains, including one your key can't see, Marfa changes no setting, so a folder or view never stops showing items that haven't moved. The job's `result.settings` lists the settings, and `result.settings_rewritten` says whether Marfa changed them. A folder keeps its files; [Sync a folder](/run/sync-a-folder#change-a-folders-settings) says what changes on disk. **When the type has subtypes,** a folder or view keeps the old type in `query.types` beside the new one, because the subtypes' items still need it, and a folder's `first_placement` keeps the old type's key too. @@ -126,10 +126,10 @@ A move needs a key whose type map grants write on both types. Marfa refuses it ` The key also needs what each setting's own operation needs: write on `system.folder` to change a folder and write on `system.view` to change a view. Only the key or app that made a webhook changes it, and it needs the `webhooks.manage` permission. A setting the key can't change blocks the move with `not_permitted`. When your key can't read the setting either, Marfa names it by `kind` and `field` only, without its `id`, in a move's answer and in a `type_in_use` refusal. -**A move never changes a key's or an app's type map.** The dry run lists in `grants` each key whose type map names the old type, if your key has `keys.manage`, and each app, if it has `grants.manage`. Give them the new type through [Manage keys](/guides/manage-keys) before you delete the old one. +**A move never changes a key's or an app's type map.** The dry run lists in `grants` each key whose type map names the old type, if your key has `keys.manage`, and each app, if it has `grants.manage`. Give them the new type through [Manage keys](/run/manage-keys) before you delete the old one. ## Delete the old type -Once no item and no setting names the old type, delete it as [Change or delete a type](/guides/change-a-type#delete-the-type) shows. +Once no item and no setting names the old type, delete it as [Change or delete a type](/build/change-a-type#delete-the-type) shows. The rules behind this guide are in [Moving a type's items](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#moving-a-types-items) in the contract. diff --git a/content/docs/guides/raycast-extension.mdx b/content/docs/build/raycast-extension.mdx similarity index 95% rename from content/docs/guides/raycast-extension.mdx rename to content/docs/build/raycast-extension.mdx index c86d4546..8ed37803 100644 --- a/content/docs/guides/raycast-extension.mdx +++ b/content/docs/build/raycast-extension.mdx @@ -33,7 +33,7 @@ key 01a17415-4521-7b79-a8e5-39276a8b527d (Raycast); the plaintext is shown once, Marfa shows it once. If you lose it, make a new one. -The **source** is the label Marfa puts on every item the key creates, so you can always tell what the extension saved. The `--type-permission` flag names the one type the key may write. [Manage keys](/guides/manage-keys) explains each flag, and [Access](/access) explains what a key can hold. +The **source** is the label Marfa puts on every item the key creates, so you can always tell what the extension saved. The `--type-permission` flag names the one type the key may write. [Manage keys](/run/manage-keys) explains each flag, and [Access](/access) explains what a key can hold. @@ -304,7 +304,7 @@ No access to type "core.task" The refusal comes from the server, not from the extension. `MarfaError` kept the `code` and the `message`, and `save` showed them. The body of the refusal also names the grant the key lacks, in `details.grant`. [Errors](/api-reference/errors) lists every code and what each one tells a program to do. -To let the extension write tasks, make a new key that names both types. A source belongs to one live key, so revoke the first key before you reuse `raycast`. [Rotate a key](/guides/manage-keys#rotate-a-key) covers this: +To let the extension write tasks, make a new key that names both types. A source belongs to one live key, so revoke the first key before you reuse `raycast`. [Rotate a key](/run/manage-keys#rotate-a-key) covers this: ```bash marfa keys revoke @@ -324,8 +324,8 @@ You now have a Raycast command that saves to your own instance, with a key that Add a type of your own and open the command again. Its form appears with no change to the extension. To save an item of it, give the extension a key that names the type, as in the last step. - Make a type, then save an item of it from this command. - Narrow, expire, rotate and revoke the extension's key. + Make a type, then save an item of it from this command. + Narrow, expire, rotate and revoke the extension's key. Call every operation from TypeScript, and walk a list a page at a time. - Write a program that keeps items in step with another service. + Write a program that keeps items in step with another service. diff --git a/content/docs/guides/read-a-view.mdx b/content/docs/build/read-a-view.mdx similarity index 96% rename from content/docs/guides/read-a-view.mdx rename to content/docs/build/read-a-view.mdx index da63f3b1..9d810de6 100644 --- a/content/docs/guides/read-a-view.mdx +++ b/content/docs/build/read-a-view.mdx @@ -5,7 +5,7 @@ description: Read a saved view's items and groups from a device's working copy. A [view](/catalog/view) saves which items to list, in what order and how they group. **The device core answers a view offline** from a [working copy](/devices), so an app uses the same sorting and grouping as the CLI. How the app shows the items is its own. -Start with a view [saved on the instance](/guides/save-a-view) and a credential that can read `system.view` and every content type its query names. The examples use a view of `core.task` items. +Start with a view [saved on the instance](/build/save-a-view) and a credential that can read `system.view` and every content type its query names. The examples use a view of `core.task` items. ## Fill the working copy diff --git a/content/docs/guides/receive-webhooks.mdx b/content/docs/build/receive-webhooks.mdx similarity index 96% rename from content/docs/guides/receive-webhooks.mdx rename to content/docs/build/receive-webhooks.mdx index 2af858d1..8e5f9712 100644 --- a/content/docs/guides/receive-webhooks.mdx +++ b/content/docs/build/receive-webhooks.mdx @@ -223,7 +223,7 @@ await marfa.POST("/webhooks/{id}/deliveries/{delivery_id}/redeliver", { }); ``` -Only a retained `dead_letter` delivery can be sent again, while its subscription is active and its maker still has the required access. This holds for an owner's retry too, and a retry from the owner's browser needs a [recent sign-in](/guides/sign-in-as-the-owner#confirm-its-you). A `409 conflict` carries the reason in `details.reason`; [Redeliver a webhook delivery](/api-reference/webhooks/redeliverWebhookDelivery) lists the refusals. A delivery's record is kept for as long as the instance keeps its audit log. +Only a retained `dead_letter` delivery can be sent again, while its subscription is active and its maker still has the required access. This holds for an owner's retry too, and a retry from the owner's browser needs a [recent sign-in](/run/sign-in-as-the-owner#confirm-its-you). A `409 conflict` carries the reason in `details.reason`; [Redeliver a webhook delivery](/api-reference/webhooks/redeliverWebhookDelivery) lists the refusals. A delivery's record is kept for as long as the instance keeps its audit log. ## Change what a webhook sends @@ -234,7 +234,7 @@ Only a retained `dead_letter` delivery can be sent again, while its subscription ## Receive deliveries on a private network -By default, Marfa posts only to public addresses. It refuses, with `400 validation_error`, a `url` whose host is a loopback, private, link-local, unique-local or carrier-grade NAT address, such as `localhost` or a Tailscale address. It checks a host written as a name too, so a receiver on a Tailscale or home-network name meets this refusal first. The message names the setting that allows it, `MARFA_WEBHOOK_ALLOW_PRIVATE_ADDRESSES`, which [Settings](/guides/settings#let-webhooks-reach-a-private-network) shows how to set. +By default, Marfa posts only to public addresses. It refuses, with `400 validation_error`, a `url` whose host is a loopback, private, link-local, unique-local or carrier-grade NAT address, such as `localhost` or a Tailscale address. It checks a host written as a name too, so a receiver on a Tailscale or home-network name meets this refusal first. The message names the setting that allows it, `MARFA_WEBHOOK_ALLOW_PRIVATE_ADDRESSES`, which [Settings](/run/settings#let-webhooks-reach-a-private-network) shows how to set. Marfa checks a name again at each delivery. One that now resolves to an address that isn't public is never sent: the delivery is recorded as `dead_letter`, with the error "The receiver's address is not public.". Once the setting allows it or the name is fixed, [redeliver it](#when-a-delivery-fails). diff --git a/content/docs/guides/save-a-view.mdx b/content/docs/build/save-a-view.mdx similarity index 92% rename from content/docs/guides/save-a-view.mdx rename to content/docs/build/save-a-view.mdx index 5ff2da6e..dd8398bb 100644 --- a/content/docs/guides/save-a-view.mdx +++ b/content/docs/build/save-a-view.mdx @@ -42,9 +42,9 @@ const { data } = await marfa.POST("/views", { In the TypeScript example, `marfa` is a client made as [the TypeScript SDK](/typescript-sdk#create-a-client) shows. -The answer is the view as a `system.view` item at `version` 1. The server returns its settings, not the items it lists. **The device core can read its items and groups offline**, as [Read a view offline](/guides/read-a-view) shows. +The answer is the view as a `system.view` item at `version` 1. The server returns its settings, not the items it lists. **The device core can read its items and groups offline**, as [Read a view offline](/build/read-a-view) shows. -A view's `filter` takes any expression that [List, filter and search](/guides/list-filter-search#filter-by-properties) describes, so a view can list one folder's items with `edge[in-folder] eq ""`, or one source's with `source eq ""`. +A view's `filter` takes any expression that [List, filter and search](/build/list-filter-search#filter-by-properties) describes, so a view can list one folder's items with `edge[in-folder] eq ""`, or one source's with `source eq ""`. ## Change, delete or read a view diff --git a/content/docs/guides/swift-app.mdx b/content/docs/build/swift-app.mdx similarity index 97% rename from content/docs/guides/swift-app.mdx rename to content/docs/build/swift-app.mdx index 13b8b027..6699fdbd 100644 --- a/content/docs/guides/swift-app.mdx +++ b/content/docs/build/swift-app.mdx @@ -278,7 +278,7 @@ Add a function inside `Library` that changes a note's title: } ``` -The `Edit` carries the version of the note the app read, so the instance can tell whether someone else changed it since. [Write safely](/guides/write-safely) explains versions from the API's side. +The `Edit` carries the version of the note the app read, so the instance can tell whether someone else changed it since. [Write safely](/build/write-safely) explains versions from the API's side. Add an editable row at the end of `ContentView.swift`, and replace `Text(note.title ?? "Untitled")` in `ContentView` with `NoteRow(library: library, note: note)`: @@ -334,6 +334,6 @@ Press Command-R. The app catches up and sends the rename, and **Changes** shows Keep a key in the keychain, handle each error, and declare your own types. How a working copy stays current, and what each verdict means. - Keep your notes as Markdown files in a folder on your Mac. - How versions and conflicts work from the API's side. + Keep your notes as Markdown files in a folder on your Mac. + How versions and conflicts work from the API's side. diff --git a/content/docs/guides/write-safely.mdx b/content/docs/build/write-safely.mdx similarity index 100% rename from content/docs/guides/write-safely.mdx rename to content/docs/build/write-safely.mdx diff --git a/content/docs/catalog/(types)/(system)/folder.mdx b/content/docs/catalog/(types)/(system)/folder.mdx index 00427018..a94ee5a7 100644 --- a/content/docs/catalog/(types)/(system)/folder.mdx +++ b/content/docs/catalog/(types)/(system)/folder.mdx @@ -4,6 +4,6 @@ description: "A folder's settings, held here and shared by every machine bound t identifier: system.folder --- -[Sync a folder](/guides/sync-a-folder) uses the folder operations that write these settings. No item operation writes a folder: each one refuses a `system.*` type with `403 type_not_permitted` ([`items/system-types`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/items.md#itemssystem-types)). +[Sync a folder](/run/sync-a-folder) uses the folder operations that write these settings. No item operation writes a folder: each one refuses a `system.*` type with `403 type_not_permitted` ([`items/system-types`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/items.md#itemssystem-types)). src/generated/catalog/system.folder.md diff --git a/content/docs/catalog/(types)/(system)/view.mdx b/content/docs/catalog/(types)/(system)/view.mdx index 12a99a93..0756305d 100644 --- a/content/docs/catalog/(types)/(system)/view.mdx +++ b/content/docs/catalog/(types)/(system)/view.mdx @@ -4,6 +4,6 @@ description: "A saved list of items, held here and shared by every app: which it identifier: system.view --- -[Save a list as a view](/guides/save-a-view) uses the view operations that write these settings. No item operation writes a view: each one refuses a `system.*` type with `403 type_not_permitted` ([`items/system-types`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/items.md#itemssystem-types)). +[Save a list as a view](/build/save-a-view) uses the view operations that write these settings. No item operation writes a view: each one refuses a `system.*` type with `403 type_not_permitted` ([`items/system-types`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/items.md#itemssystem-types)). src/generated/catalog/system.view.md diff --git a/content/docs/catalog/(types)/types.mdx b/content/docs/catalog/(types)/types.mdx index 9fc4e6e0..1cb78bd9 100644 --- a/content/docs/catalog/(types)/types.mdx +++ b/content/docs/catalog/(types)/types.mdx @@ -5,6 +5,6 @@ description: The core and system types Marfa ships, and the pages for their rule Marfa ships two families of types: **core types** for what people keep, such as notes and tasks, and **system types** for Marfa's own records, which it writes itself. [Type rules](/catalog/your-own-types) and [Field kinds](/catalog/field-kinds) list what a type you register must meet and what a field can hold. -[Items and types](/items-and-types) explains how types work, and [Define a type](/guides/define-a-type) shows how to add your own. +[Items and types](/items-and-types) explains how types work, and [Define a type](/build/define-a-type) shows how to add your own. src/generated/catalog/types.md diff --git a/content/docs/catalog/(types)/your-own-types.mdx b/content/docs/catalog/(types)/your-own-types.mdx index e6a9b44d..e024d800 100644 --- a/content/docs/catalog/(types)/your-own-types.mdx +++ b/content/docs/catalog/(types)/your-own-types.mdx @@ -3,7 +3,7 @@ title: Type rules description: "The rules a type you register must meet: its identifier, fields, parent, roles, merge policy and retention policy." --- -A type you register must meet the rules on this page, for its identifier, its fields, its parent, its roles and its policies. **Marfa refuses a definition that breaks a rule with `400 invalid_schema`** unless the rule names another answer. [Define a type](/guides/define-a-type) shows how to register one. +A type you register must meet the rules on this page, for its identifier, its fields, its parent, its roles and its policies. **Marfa refuses a definition that breaks a rule with `400 invalid_schema`** unless the rule names another answer. [Define a type](/build/define-a-type) shows how to register one. ## Identifier @@ -75,7 +75,7 @@ A replacement or deletion that breaks another type's claim is refused the same w | `feed_days` | Effect | | --- | --- | -| A whole number from 1 to 36500 | The `feed-expiry` [housekeeping job](/guides/housekeeping) moves the type's feed items to the bin once more than that many days have passed since their `created_at`. A natural-key upsert keeps `created_at`, so a refresh doesn't restart the count. | +| A whole number from 1 to 36500 | The `feed-expiry` [housekeeping job](/run/housekeeping) moves the type's feed items to the bin once more than that many days have passed since their `created_at`. A natural-key upsert keeps `created_at`, so a refresh doesn't restart the count. | | `null` | The type keeps its feed items, even when a parent sets a window. | | Left out | The type takes its parent's window, and keeps its feed items where no type in the chain sets one. | diff --git a/content/docs/catalog/connectors/github.mdx b/content/docs/catalog/connectors/github.mdx index da65c260..d5400de2 100644 --- a/content/docs/catalog/connectors/github.mdx +++ b/content/docs/catalog/connectors/github.mdx @@ -75,4 +75,4 @@ Repositories, pull requests, notifications, files, duplicates and closing links ## Run it -[Run a connector](/guides/run-a-connector) runs any connector. The [README](https://github.com/withmarfa/marfa-connectors/blob/main/connectors/github/README.md) lists the permissions this connector's key needs and walks through registering the GitHub App. +[Run a connector](/run/run-a-connector) runs any connector. The [README](https://github.com/withmarfa/marfa-connectors/blob/main/connectors/github/README.md) lists the permissions this connector's key needs and walks through registering the GitHub App. diff --git a/content/docs/catalog/connectors/google-calendar.mdx b/content/docs/catalog/connectors/google-calendar.mdx index e6daf7d6..22298a99 100644 --- a/content/docs/catalog/connectors/google-calendar.mdx +++ b/content/docs/catalog/connectors/google-calendar.mdx @@ -33,7 +33,7 @@ Every write sets `sendUpdates=none`, and none names guests. ## Settings -The Google settings are those of every Google connector, and [Create a Google Cloud project for Google connectors](/guides/google-project) shows how to get them. +The Google settings are those of every Google connector, and [Create a Google Cloud project for Google connectors](/run/google-project) shows how to get them. | Setting | What it holds | | --- | --- | @@ -54,4 +54,4 @@ The Google settings are those of every Google connector, and [Create a Google Cl ## Run it -[Run a connector](/guides/run-a-connector) runs any connector. This one's key also holds write on the `google.calendar.in-calendar` and `parent-of` connections, and its [README](https://github.com/withmarfa/marfa-connectors/blob/main/connectors/google-calendar/README.md) has the `marfa keys create` command. +[Run a connector](/run/run-a-connector) runs any connector. This one's key also holds write on the `google.calendar.in-calendar` and `parent-of` connections, and its [README](https://github.com/withmarfa/marfa-connectors/blob/main/connectors/google-calendar/README.md) has the `marfa keys create` command. diff --git a/content/docs/catalog/connectors/index.mdx b/content/docs/catalog/connectors/index.mdx index f165d390..930494e8 100644 --- a/content/docs/catalog/connectors/index.mdx +++ b/content/docs/catalog/connectors/index.mdx @@ -3,7 +3,7 @@ title: Connectors description: The six connectors Marfa ships, what each reads from its service, and whether it carries changes back. --- -A connector is a program you run that reads another service and writes what it finds to your instance, under a key made for it alone. Some also carry changes made in Marfa back to the service. [Connectors](/connectors) explains how they work, and [Run a connector](/guides/run-a-connector) runs one. +A connector is a program you run that reads another service and writes what it finds to your instance, under a key made for it alone. Some also carry changes made in Marfa back to the service. [Connectors](/connectors) explains how they work, and [Run a connector](/run/run-a-connector) runs one. Marfa ships six, in the [withmarfa/marfa-connectors](https://github.com/withmarfa/marfa-connectors) repository. You build each from that repository and run it yourself. diff --git a/content/docs/catalog/connectors/readwise-reader.mdx b/content/docs/catalog/connectors/readwise-reader.mdx index 775f55fb..3693aa11 100644 --- a/content/docs/catalog/connectors/readwise-reader.mdx +++ b/content/docs/catalog/connectors/readwise-reader.mdx @@ -53,4 +53,4 @@ Only changed supported fields are sent. Body replacement, passage edits, reading ## Run it -[Run a connector](/guides/run-a-connector) runs any connector. This one's key holds write on `readwise.source`, `readwise.highlight` and `readwise.file`, and on the `references` and `attached-to` connections, and its [README](https://github.com/withmarfa/marfa-connectors/blob/main/connectors/readwise-reader/README.md) lists what the key needs. +[Run a connector](/run/run-a-connector) runs any connector. This one's key holds write on `readwise.source`, `readwise.highlight` and `readwise.file`, and on the `references` and `attached-to` connections, and its [README](https://github.com/withmarfa/marfa-connectors/blob/main/connectors/readwise-reader/README.md) lists what the key needs. diff --git a/content/docs/catalog/connectors/readwise.mdx b/content/docs/catalog/connectors/readwise.mdx index 905e5826..385ba22e 100644 --- a/content/docs/catalog/connectors/readwise.mdx +++ b/content/docs/catalog/connectors/readwise.mdx @@ -43,4 +43,4 @@ Nothing. The connector sends no write to Readwise, and trashing or purging a row ## Run it -[Run a connector](/guides/run-a-connector) runs any connector. This one's key holds write on `readwise.source`, `readwise.highlight` and `readwise.file`, and on the `references` and `attached-to` connections, and its [README](https://github.com/withmarfa/marfa-connectors/blob/main/connectors/readwise/README.md) has the `marfa keys create` command. +[Run a connector](/run/run-a-connector) runs any connector. This one's key holds write on `readwise.source`, `readwise.highlight` and `readwise.file`, and on the `references` and `attached-to` connections, and its [README](https://github.com/withmarfa/marfa-connectors/blob/main/connectors/readwise/README.md) has the `marfa keys create` command. diff --git a/content/docs/catalog/connectors/rss.mdx b/content/docs/catalog/connectors/rss.mdx index b751fd55..a3de3089 100644 --- a/content/docs/catalog/connectors/rss.mdx +++ b/content/docs/catalog/connectors/rss.mdx @@ -47,4 +47,4 @@ An entry in `RSS_FEEDS` is an `http` or `https` address, or `name=address`. A fe ## Run it -[Run a connector](/guides/run-a-connector) uses this connector as its example. Its key holds write on `rss.entry` and the right to register it. +[Run a connector](/run/run-a-connector) uses this connector as its example. Its key holds write on `rss.entry` and the right to register it. diff --git a/content/docs/catalog/connectors/todoist.mdx b/content/docs/catalog/connectors/todoist.mdx index 9f589b6f..c8079b84 100644 --- a/content/docs/catalog/connectors/todoist.mdx +++ b/content/docs/catalog/connectors/todoist.mdx @@ -56,4 +56,4 @@ The parent and the order among siblings do not travel, and archiving a row sends ## Run it -[Run a connector](/guides/run-a-connector) runs any connector. This one's key holds write on `todoist.task` and the right to register that type, and nothing else. +[Run a connector](/run/run-a-connector) runs any connector. This one's key holds write on `todoist.task` and the right to register that type, and nothing else. diff --git a/content/docs/catalog/edges.mdx b/content/docs/catalog/edges.mdx index 00162ede..42d62979 100644 --- a/content/docs/catalog/edges.mdx +++ b/content/docs/catalog/edges.mdx @@ -7,7 +7,7 @@ An edge is a typed, directed link from one item, its source, to another, its tar src/generated/catalog/edge-types.md -Each edge type has two headings. The label names the source's end of an edge and the reverse label the target's end: `parent-of` lists a note's `Children` and, read from the child, its `Parent`. A label is optional when you register an edge type: without one, Marfa makes it from the last segment of the ID, such as `List Member` for `acme.list-member`. A reverse label is optional too. When you change a registered type, a label you leave out stays as it is, and so does the reverse label. To make a type two-way, send a reverse label equal to the label. An edge type with no reverse label names both ends alike, such as `Related to`, and an app shows one list for both directions. Both headings are for people. The reverse name is different: it is the edge's name read from its target, used as a frontmatter key. In a [synced folder](/guides/sync-a-folder), the source item's file writes the edge, unless its section below gives `written_at`: `target` puts it in the target item's file under its reverse name, and `none` in no file. +Each edge type has two headings. The label names the source's end of an edge and the reverse label the target's end: `parent-of` lists a note's `Children` and, read from the child, its `Parent`. A label is optional when you register an edge type: without one, Marfa makes it from the last segment of the ID, such as `List Member` for `acme.list-member`. A reverse label is optional too. When you change a registered type, a label you leave out stays as it is, and so does the reverse label. To make a type two-way, send a reverse label equal to the label. An edge type with no reverse label names both ends alike, such as `Related to`, and an app shows one list for both directions. Both headings are for people. The reverse name is different: it is the edge's name read from its target, used as a frontmatter key. In a [synced folder](/run/sync-a-folder), the source item's file writes the edge, unless its section below gives `written_at`: `target` puts it in the target item's file under its reverse name, and `none` in no file. ## Fields every edge has diff --git a/content/docs/catalog/events.mdx b/content/docs/catalog/events.mdx index d2d1d42e..04374993 100644 --- a/content/docs/catalog/events.mdx +++ b/content/docs/catalog/events.mdx @@ -7,4 +7,4 @@ description: The events that streams and webhooks deliver, and the fields each o src/generated/catalog/event-fields.md -[Receive webhooks](/guides/receive-webhooks) shows how to subscribe to them. +[Receive webhooks](/build/receive-webhooks) shows how to subscribe to them. diff --git a/content/docs/catalog/index.mdx b/content/docs/catalog/index.mdx index 1bbb577b..d785b0b6 100644 --- a/content/docs/catalog/index.mdx +++ b/content/docs/catalog/index.mdx @@ -3,7 +3,7 @@ title: Catalog description: "Every name and shape Marfa ships: the fields of an item, its types, edge types, events and permissions." --- -The Catalog lists exactly what Marfa ships: the name of each field, type, event and permission, and what it holds. To learn how the parts fit together, read [How Marfa works](/how-marfa-works) or the Guides. +The Catalog lists exactly what Marfa ships: the name of each field, type, event and permission, and what it holds. To learn how the parts fit together, read [How Marfa works](/how-marfa-works), then [Build](/build) or [Run](/run). diff --git a/content/docs/catalog/items.mdx b/content/docs/catalog/items.mdx index 4624174e..dba56ac1 100644 --- a/content/docs/catalog/items.mdx +++ b/content/docs/catalog/items.mdx @@ -13,4 +13,4 @@ Search always reads these fields where they hold a `string` ([`search-and-filter src/generated/catalog/searched-field-names.md -It also reads every other `string` field a type declares or inherits, unless the field sets `searchable` to `false`, and an item's tags. [List, filter and search](/guides/list-filter-search#search) shows how to search. +It also reads every other `string` field a type declares or inherits, unless the field sets `searchable` to `false`, and an item's tags. [List, filter and search](/build/list-filter-search#search) shows how to search. diff --git a/content/docs/catalog/permissions.mdx b/content/docs/catalog/permissions.mdx index a92721c5..d6069748 100644 --- a/content/docs/catalog/permissions.mdx +++ b/content/docs/catalog/permissions.mdx @@ -32,4 +32,4 @@ A key's `profile_permissions` names `name`, `email`, `avatar`, or `*`. The wildc 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. +[Access](/access) explains the maps, and [Manage keys](/run/manage-keys) shows how to give a key its access. diff --git a/content/docs/meta.json b/content/docs/meta.json index 02e05713..91f50731 100644 --- a/content/docs/meta.json +++ b/content/docs/meta.json @@ -1,7 +1,8 @@ { "pages": [ "(overview)", - "guides", + "build", + "run", "catalog", "(reference)" ] diff --git a/content/docs/guides/audit-log.mdx b/content/docs/run/audit-log.mdx similarity index 94% rename from content/docs/guides/audit-log.mdx rename to content/docs/run/audit-log.mdx index 9be870a4..944a485c 100644 --- a/content/docs/guides/audit-log.mdx +++ b/content/docs/run/audit-log.mdx @@ -39,7 +39,7 @@ Each entry holds these fields: | `client_ip` | `string \| null` | The address the request came from, or null where there is none. | | `details` | `object` | More about the action, depending on action. | -**Use `actor` to identify who acted.** For authenticated actions, its `kind`, `id` and `name` are captured at the time, so a later rename or removal does not change the entry. Server work has actor `{ "kind": "server", "id": "server", "name": "Marfa" }`. Older entries without a captured actor, and actions before authentication, have `actor: null`; do not infer a person from their `key_id`. Behind a proxy or a tunnel, `client_ip` is the proxy's address unless the server is told which address to believe, as [Deploy](/guides/deploy#the-address) explains. +**Use `actor` to identify who acted.** For authenticated actions, its `kind`, `id` and `name` are captured at the time, so a later rename or removal does not change the entry. Server work has actor `{ "kind": "server", "id": "server", "name": "Marfa" }`. Older entries without a captured actor, and actions before authentication, have `actor: null`; do not infer a person from their `key_id`. Behind a proxy or a tunnel, `client_ip` is the proxy's address unless the server is told which address to believe, as [Deploy](/run/deploy#the-address) explains. ## Read the log @@ -126,8 +126,8 @@ A page holds 50 entries unless `limit` asks for up to 200. When more match, the ## How long entries are kept -Marfa keeps entries for **90 days** by default. The `audit-cleanup` [housekeeping job](/guides/housekeeping) runs once a day and deletes older ones, along with the history of outbound webhook deliveries past the same age. +Marfa keeps entries for **90 days** by default. The `audit-cleanup` [housekeeping job](/run/housekeeping) runs once a day and deletes older ones, along with the history of outbound webhook deliveries past the same age. -`AUDIT_RETENTION_DAYS` sets the retention when the server starts, and `audit_retention_days` in the [configuration](/guides/settings#change-settings-while-it-runs) changes it while the server runs. At `0`, no entry expires. +`AUDIT_RETENTION_DAYS` sets the retention when the server starts, and `audit_retention_days` in the [configuration](/run/settings#change-settings-while-it-runs) changes it while the server runs. At `0`, no entry expires. The rules behind this page are in [the audit log](https://github.com/withmarfa/marfa/blob/main/conformance/spec/events.md#the-audit-log) section of the events chapter of the contract, including the [kinds](https://github.com/withmarfa/marfa/blob/main/conformance/spec/events.md#eventsaudit-kind) and the [text search](https://github.com/withmarfa/marfa/blob/main/conformance/spec/events.md#eventsaudit-search). diff --git a/content/docs/guides/back-up-and-restore.mdx b/content/docs/run/back-up-and-restore.mdx similarity index 91% rename from content/docs/guides/back-up-and-restore.mdx rename to content/docs/run/back-up-and-restore.mdx index da2ef20c..336b5f5b 100644 --- a/content/docs/guides/back-up-and-restore.mdx +++ b/content/docs/run/back-up-and-restore.mdx @@ -3,13 +3,13 @@ title: Back up and restore description: Copy an instance's data directory and secrets so you can bring it back after a failed disk or a lost machine. --- -Keep a copy of your instance somewhere the instance isn't. A copy of the data directory brings the same instance back with everything in it: items, keys, configuration and sign-ins. To carry your data into another instance instead, [export an archive](/guides/export-and-restore). A different build may refuse the archive; [Upgrade](/guides/upgrade) explains how to test it before switching. +Keep a copy of your instance somewhere the instance isn't. A copy of the data directory brings the same instance back with everything in it: items, keys, configuration and sign-ins. To carry your data into another instance instead, [export an archive](/run/export-and-restore). A different build may refuse the archive; [Upgrade](/run/upgrade) explains how to test it before switching. ## What to copy Copy two things: -- **The data directory.** It holds the database, the `marfa.db-wal` log beside it and the blob folder, as [Deploy](/guides/deploy#the-data-directory) shows. In the container, it's the volume at `/data`, which with a bucket also holds the hidden `.marfa.db-litestream` folder and `marfa.db-replica` file. The blob folder holds a hidden `.marfa-store` file that names its store. Copy all of them. +- **The data directory.** It holds the database, the `marfa.db-wal` log beside it and the blob folder, as [Deploy](/run/deploy#the-data-directory) shows. In the container, it's the volume at `/data`, which with a bucket also holds the hidden `.marfa.db-litestream` folder and `marfa.db-replica` file. The blob folder holds a hidden `.marfa-store` file that names its store. Copy all of them. - **The two secrets**, `API_KEY_SALT` and `MARFA_AUTH_SECRET`. They aren't in the data directory. Without the original `API_KEY_SALT`, a restored instance holds every key and **accepts none**. Without `MARFA_AUTH_SECRET`, every signed-in session ends. ## Copy the directory safely @@ -39,7 +39,7 @@ Stop the server and delete the `marfa.db-wal` and `marfa.db-shm` files in the da When `S3_BUCKET` is set, **the container copies for you**. It streams each committed database write to the bucket within a second, keeps a day of hourly snapshots, and the `blob-replicate` housekeeping job copies each blob; a blob whose store hasn't answered is skipped and copied on a later run. A new volume started with the same bucket, prefix and secrets restores the database as it starts and brings the blobs back as replication runs. -Everything the instance keeps in the bucket is under its `S3_PREFIX`, so **give each instance on a shared bucket its own prefix**. [Settings](/guides/settings#every-environment-setting) says what a prefix may hold. +Everything the instance keeps in the bucket is under its `S3_PREFIX`, so **give each instance on a shared bucket its own prefix**. [Settings](/run/settings#every-environment-setting) says what a prefix may hold. **The container refuses to replicate a database that may be older than the bucket's copy.** Replicating it would make it the bucket's newest copy, and the writes made since would stay only in history that expires. When the volume already holds a database, the container compares it with the bucket's replica before it starts the server. If the database may be older, it exits with status `1`, names the reason in its log, and leaves the bucket unchanged. A copy taken before the bucket's newest write, a copy restored without its `.marfa.db-litestream` folder, a database that lost writes since it last replicated, and a point-in-time restore onto the same prefix all count. If the bucket can't be listed, the container can't compare and exits the same way, so a restart tries again. diff --git a/content/docs/guides/deploy.mdx b/content/docs/run/deploy.mdx similarity index 88% rename from content/docs/guides/deploy.mdx rename to content/docs/run/deploy.mdx index 67927509..ca8d9403 100644 --- a/content/docs/guides/deploy.mdx +++ b/content/docs/run/deploy.mdx @@ -9,16 +9,16 @@ An instance is one server process and one SQLite database, so there's no databas | Where | Good for | How | | --- | --- | --- | -| A Mac or Linux machine with Docker | A Mac or a home server that stays on. | [Install with the CLI](/guides/install), one command. | +| A Mac or Linux machine with Docker | A Mac or a home server that stays on. | [Install with the CLI](/run/install), one command. | | A container you run yourself | A server where you choose the layout. | [Run it in a container](#run-it-in-a-container), with one volume. | | A computer you own, without Docker | A machine where you can't run containers. | [Run it without a container](#run-it-without-a-container), under a supervisor. | | Railway | The quickest way to an instance on the internet, billed to you by Railway. | The [Marfa template](https://railway.com/new/template/5z9-Ja) creates a server that runs the published release image, a volume and a backup bucket, and sets the secrets and the address, as [the Railway steps](https://github.com/withmarfa/marfa/blob/main/deploy/railway.md) say. | -The server runs [housekeeping jobs](/guides/housekeeping) and backups in the background, so turn off any host feature that stops an idle service, such as Railway's Serverless mode. +The server runs [housekeeping jobs](/run/housekeeping) and backups in the background, so turn off any host feature that stops an idle service, such as Railway's Serverless mode. ## Run it in a container -[`marfa server install`](/guides/install) does these steps for you. Do them by hand to choose your own layout. +[`marfa server install`](/run/install) does these steps for you. Do them by hand to choose your own layout. Every release publishes the server image to `ghcr.io/withmarfa/marfa-server` for `linux/amd64` and `linux/arm64`, tagged with its release number and `latest`. The image sets `NODE_ENV=production`, the port and the paths, with the database, the blobs and the private socket under `/data`. @@ -53,20 +53,20 @@ MARFA_AUTH_SECRET= MARFA_AUTH_BASE_URL=https://marfa.example.com ``` -In production, the server refuses to start without these three, or with a weak secret. [Settings](/guides/settings#every-environment-setting) defines every setting. +In production, the server refuses to start without these three, or with a weak secret. [Settings](/run/settings#every-environment-setting) defines every setting. ### Start the container -Pull a release by its tag, not `latest`, so a restart never changes the build and an [upgrade](/guides/upgrade) can name both builds. Start it with one volume at `/data`: +Pull a release by its tag, not `latest`, so a restart never changes the build and an [upgrade](/run/upgrade) can name both builds. Start it with one volume at `/data`: src/generated/server-image.md `-p 127.0.0.1:8600:8600` publishes the port on the loopback address only, for a proxy or tunnel on the same machine. A bare `-p 8600:8600` opens it on every network interface, on Linux past the host's firewall. -Without `S3_BUCKET`, no copy leaves the machine, so [back up](/guides/back-up-and-restore) the volume yourself. To run code that no release holds, [build the image yourself](https://github.com/withmarfa/marfa/blob/main/deploy/README.md#building-the-image-yourself). +Without `S3_BUCKET`, no copy leaves the machine, so [back up](/run/back-up-and-restore) the volume yourself. To run code that no release holds, [build the image yourself](https://github.com/withmarfa/marfa/blob/main/deploy/README.md#building-the-image-yourself). @@ -105,7 +105,7 @@ MARFA_AUTH_SECRET= MARFA_AUTH_BASE_URL=https://marfa.example.com ``` -Start it from `packages/server` under a supervisor, such as launchd or systemd, with these settings in its environment: the server doesn't read `marfa.env` itself. The `--import` loads [telemetry](/guides/telemetry). +Start it from `packages/server` under a supervisor, such as launchd or systemd, with these settings in its environment: the server doesn't read `marfa.env` itself. The `--import` loads [telemetry](/run/telemetry). ```bash cd packages/server @@ -120,14 +120,14 @@ Then claim the instance as the account that runs the server, with `marfa --socke Devices reach the instance on your local network, from anywhere over Tailscale, which these docs suggest, or through a Cloudflare Tunnel at an address on your own domain. -**Behind a proxy or tunnel, tell the server which caller address to believe.** Otherwise every request seems to come from the proxy, and the limit of ten password attempts from one address in fifteen minutes lets anyone's wrong guesses lock the owner out. [`marfa server install`](/guides/install#choose-how-its-reached) sets the right one for you. By hand, set one of these: +**Behind a proxy or tunnel, tell the server which caller address to believe.** Otherwise every request seems to come from the proxy, and the limit of ten password attempts from one address in fifteen minutes lets anyone's wrong guesses lock the owner out. [`marfa server install`](/run/install#choose-how-its-reached) sets the right one for you. By hand, set one of these: - **`TRUSTED_PROXY_CIDRS`** names the proxy's address ranges. The server believes `X-Forwarded-For` from any address in them, so they must hold only your proxy. A proxy beside a container, such as Tailscale Serve, reaches it from Docker's network, whose subnet `docker network inspect bridge --format '{{json .IPAM.Config}}'` shows. Every container there is then trusted, so run nothing there you don't control. - **`TRUSTED_PROXY_HEADER`** names a header the platform's edge overwrites with the caller's address, for a host whose edge addresses you can't list. Set it only when nothing reaches the server except through that edge, or any client can choose its address. ## An object store -Set `S3_BUCKET` and the other `S3_` settings, and the server keeps a second copy of every blob in an S3-compatible bucket. The container also [streams the database there](/guides/back-up-and-restore#with-a-bucket). +Set `S3_BUCKET` and the other `S3_` settings, and the server keeps a second copy of every blob in an S3-compatible bucket. The container also [streams the database there](/run/back-up-and-restore#with-a-bucket). - **Give each instance that shares a bucket its own `S3_PREFIX`**, such as `trial/blobs`. An instance keeps its blobs and its database replica under its prefix. Two instances with one prefix write one replica, and a new volume of either starts on whichever database was replicated last. - **Share a bucket only between instances of one owner.** A prefix keeps replicas apart, not access: each instance holds credentials for the whole bucket. @@ -154,11 +154,11 @@ With an [object store](#an-object-store) in the image, the directory also holds **A blob folder that has gone missing is a refused start, not an empty one.** When the database records saved files in the folder and either the folder or its `.marfa-store` file is gone, such as a drive that didn't mount, the server names the folder and stops with status `1` without creating anything there. Mount the drive, or restore the folder with its `.marfa-store` file. To start with an empty folder on purpose, point `BLOB_PATH` at another folder, which detaches the old store. With a bucket, the server starts on an empty folder and `blob-replicate` fills it ([the rule](https://github.com/withmarfa/marfa/blob/main/conformance/spec/stores.md#storesmissing-folder-refused)). -[Back up and restore](/guides/back-up-and-restore) says which files a copy needs. +[Back up and restore](/run/back-up-and-restore) says which files a copy needs. ### The private socket -`control/marfa.sock` lets a command run on the same machine, as the server's account, reach the instance with the owner's authority. You use it to claim the instance, make the first key and [recover the owner's password](/guides/sign-in-as-the-owner#recover-the-owners-password). +`control/marfa.sock` lets a command run on the same machine, as the server's account, reach the instance with the owner's authority. You use it to claim the instance, make the first key and [recover the owner's password](/run/sign-in-as-the-owner#recover-the-owners-password). - The socket carries no key, so the server refuses item properties, edges, tags, search, counts, events and exports through it with `401 unauthorized`. Run those with a key. Item summaries, the trash, attachments, the status of bulk jobs and removing an unused type you registered work through the socket. - The CLI refuses `--socket` while `MARFA_API_URL`, `MARFA_API_KEY` or `MARFA_PROFILE` is set, so unset them in that shell first. @@ -174,8 +174,8 @@ Point your supervisor or monitor at `GET /health`, which needs no credential. It | Status | Meaning | Your supervisor | | --- | --- | --- | | `0` | A clean stop. | Starts it again if you want it running. | -| `1` | A failed start or stop, such as a port in use, a setting outside its rules, a blob folder that isn't mounted or, in the container, a database that may be older than the bucket's copy. | Starts it again. The last two repeat until you act: see [Troubleshooting](/guides/troubleshooting#starting-and-claiming). | -| `78` | The database was written by another build. The server changed nothing. | Doesn't start it again. See [Upgrade](/guides/upgrade). | +| `1` | A failed start or stop, such as a port in use, a setting outside its rules, a blob folder that isn't mounted or, in the container, a database that may be older than the bucket's copy. | Starts it again. The last two repeat until you act: see [Troubleshooting](/run/troubleshooting#starting-and-claiming). | +| `78` | The database was written by another build. The server changed nothing. | Doesn't start it again. See [Upgrade](/run/upgrade). | The container doesn't exit on `78`. It stays up and unhealthy with the reason in its log, so a restart policy can't loop on it. diff --git a/content/docs/guides/export-and-restore.mdx b/content/docs/run/export-and-restore.mdx similarity index 95% rename from content/docs/guides/export-and-restore.mdx rename to content/docs/run/export-and-restore.mdx index 15d58d9b..09742408 100644 --- a/content/docs/guides/export-and-restore.mdx +++ b/content/docs/run/export-and-restore.mdx @@ -3,9 +3,9 @@ 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, 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 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](/run/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. +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](/run/back-up-and-restore) instead. ## Export an archive @@ -128,7 +128,7 @@ A store that fails is not a lost file. Unless another store holds the blob whole - **It ends when its credential does.** If the key or app that started it is revoked, expires, is disconnected or loses access to what is left to send, the response ends with an error and the file doesn't unpack. Export again. - **It's checked against a restore.** Before it sends a byte, Marfa checks that the archive restores into an empty instance of the same build, and reads the selection again if a row fails. A row that fails twice refuses the export with `409 export_rows_unrestorable` and is named in `details.row`: correct the row or its type, then export again. If writes changed the row between the reads, the answer is `503 export_unrestorable`: export again once the writes stop. - **It needs room.** The server builds the archive in the blob folder first, so leave that disk room for about the archive's uncompressed text. An export that would take the disk below `MARFA_DISK_RESERVE_BYTES` is refused `507 insufficient_storage`. -- **A source filter hides items.** A [source filter](/guides/settings#what-the-enforcement-levers-do) on the instance or the key leaves items out of an export, the owner's too. +- **A source filter hides items.** A [source filter](/run/settings#what-the-enforcement-levers-do) on the instance or the key leaves items out of an export, the owner's too. [Export an archive](/api-reference/export-and-restore/exportArchive) lists every answer. @@ -171,7 +171,7 @@ The same archive restored again answers `"imported": 0`, `"duplicates": 6` and ` A restore writes in one transaction. If it's refused, fails or is interrupted, it leaves nothing behind, so you can run it again. Other writes wait while it runs, so let it answer before you start your apps. -A restore refuses the whole archive when the file isn't a valid archive, lacks one of its four files, names another format version, or conflicts with what the instance registers. [Restore from an archive](/api-reference/export-and-restore/restoreArchive) lists each refusal. If a newer build refuses your archive, [keep running the old build](/guides/upgrade#if-the-new-build-refuses-the-archive). +A restore refuses the whole archive when the file isn't a valid archive, lacks one of its four files, names another format version, or conflicts with what the instance registers. [Restore from an archive](/api-reference/export-and-restore/restoreArchive) lists each refusal. If a newer build refuses your archive, [keep running the old build](/run/upgrade#if-the-new-build-refuses-the-archive). ## What a restore leaves for you diff --git a/content/docs/guides/folder-files.mdx b/content/docs/run/folder-files.mdx similarity index 88% rename from content/docs/guides/folder-files.mdx rename to content/docs/run/folder-files.mdx index 14664bb8..8eff5d16 100644 --- a/content/docs/guides/folder-files.mdx +++ b/content/docs/run/folder-files.mdx @@ -3,7 +3,7 @@ title: Files in a folder description: See what a file holds when a folder writes an item as a file, where attached files go, and how an edit on either side reaches the other. --- -A folder writes each item as a file and reads a changed file back as an edit. The examples follow the `Notes` folder made in [Sync a folder](/guides/sync-a-folder). +A folder writes each item as a file and reads a changed file back as an edit. The examples follow the `Notes` folder made in [Sync a folder](/run/sync-a-folder). ## How items appear as files @@ -30,7 +30,7 @@ The lines between the `---` fences are the frontmatter. The text after them is t A new Markdown file needs no frontmatter. It takes its type and tier from the folder's `defaults` or its query, and its title from `defaults` or, without one, its file name. With `query.tier: all`, a new file without an explicit tier uses `defaults.tier` or, without one, `library`. Once the instance creates the item, Marfa writes the frontmatter and `marfa_id` into the file. -A `.txt` file is all body. Any other file the query admits, such as a photo, becomes a file item with its bytes uploaded as a blob, as [Handle files](/guides/handle-files) describes. A file item stays a file item at a `.md`, `.markdown` or `.txt` name, such as after a pull places it there or you rename it: its bytes go to the item as they are, and Marfa doesn't read them as a note. +A `.txt` file is all body. Any other file the query admits, such as a photo, becomes a file item with its bytes uploaded as a blob, as [Handle files](/build/handle-files) describes. A file item stays a file item at a `.md`, `.markdown` or `.txt` name, such as after a pull places it there or you rename it: its bytes go to the item as they are, and Marfa doesn't read them as a note. ## Files attached to a note @@ -52,10 +52,10 @@ Pack light. - **It follows the note.** Move the note to another directory and the next sync moves the file beside it. A file you move away from its note goes back beside the note; to keep it somewhere else, embed it by its path. - **One file, one place.** A file attached to notes in two directories is written once, beside the first note in path order, and the sync reports the other note. Two attached files with the same name are told apart by a number. - **Another folder keeps its own copy.** If another folder on the same Mac holds the file through its own query, each folder keeps a copy, and neither takes the other's. When a note leaves one folder's query for another's, its attached files move with it. -- **It leaves with the note.** When every note it's attached to goes to the bin or leaves the states the query holds, the file goes too, under the same [safeguards](/guides/folder-safeguards) as any other. If the query holds the file itself, it stays. +- **It leaves with the note.** When every note it's attached to goes to the bin or leaves the states the query holds, the file goes too, under the same [safeguards](/run/folder-safeguards) as any other. If the query holds the file itself, it stays. - **Remove the line, keep the file.** Take a file out of `has-attachment` and the next sync removes the `attached-to` edge. The file stays in the folder as one it no longer holds. -The folder reads attached files with its key, so the key needs read on their types, such as `core.file.*` in [the key you made for the folder](/guides/sync-a-folder#make-a-key-for-the-folder). A file whose bytes the folder can't fetch is reported, and the sync goes on. +The folder reads attached files with its key, so the key needs read on their types, such as `core.file.*` in [the key you made for the folder](/run/sync-a-folder#make-a-key-for-the-folder). A file whose bytes the folder can't fetch is reported, and the sync goes on. ## Edit on either side diff --git a/content/docs/guides/folder-from-a-mac-app.mdx b/content/docs/run/folder-from-a-mac-app.mdx similarity index 94% rename from content/docs/guides/folder-from-a-mac-app.mdx rename to content/docs/run/folder-from-a-mac-app.mdx index 367c4761..201a77eb 100644 --- a/content/docs/guides/folder-from-a-mac-app.mdx +++ b/content/docs/run/folder-from-a-mac-app.mdx @@ -3,7 +3,7 @@ title: Use a folder from a Mac app description: Create a folder, bind a directory to it and confirm its first sync from a Mac app, with the Swift SDK's Folders. --- -A Mac app can do what `marfa folders` does. **The Swift SDK's `Folders` shares the Mac's list of folders with `marfa folders`.** [Sync a folder](/guides/sync-a-folder) explains what a folder is and the CLI's steps. +A Mac app can do what `marfa folders` does. **The Swift SDK's `Folders` shares the Mac's list of folders with `marfa folders`.** [Sync a folder](/run/sync-a-folder) explains what a folder is and the CLI's steps. This code creates the folder, binds a directory and confirms the first sync, with `serverURL`, `key` and `storeURL` as in [Open a working copy](/swift-sdk#open-a-working-copy). Until the first sync is confirmed, `sync(_:)` returns `.awaitingConfirmation` with the plan: diff --git a/content/docs/guides/folder-safeguards.mdx b/content/docs/run/folder-safeguards.mdx similarity index 96% rename from content/docs/guides/folder-safeguards.mdx rename to content/docs/run/folder-safeguards.mdx index 4ad72357..eb708df8 100644 --- a/content/docs/guides/folder-safeguards.mdx +++ b/content/docs/run/folder-safeguards.mdx @@ -3,7 +3,7 @@ title: Folder safeguards description: What removes a file or an item in a folder, what a folder refuses to touch, and how to tell when a push has stopped. --- -A folder deletes in both directions: a file you remove can send its item to the bin, and an item that leaves the folder can remove its file. [Sync a folder](/guides/sync-a-folder) sets a folder up. +A folder deletes in both directions: a file you remove can send its item to the bin, and an item that leaves the folder can remove its file. [Sync a folder](/run/sync-a-folder) sets a folder up. ## What removes a file or an item @@ -12,7 +12,7 @@ A folder deletes in both directions: a file you remove can send its item to the | **You delete a file** | Moves its item to the bin after a five-second grace, so a save that replaces a file isn't taken for a delete. A sync already running doesn't write the file back unchanged. If another device changes what the file showed, the query still holds the item and no other registered folder holds the move, the file comes back with that change, even after the grace. | | **You move or rename a file in the folder** | Keeps its item, even when the move lands while a sync is running, and doesn't write the file back where it was. | | **An item is moved to the bin, purged, or leaves the states the query holds** | Removes its file at the next sync, and a file attached to it once every item it's attached to has gone the same way, unless the query holds the file itself. A file you changed since Marfa wrote it stays. | -| **An item stops matching the query** | Keeps its file, and edits to the file still reach the item. [`marfa folders unmatched`](/guides/keep-folders-running#remove-files-a-folder-keeps-but-no-longer-holds) lists these files. | +| **An item stops matching the query** | Keeps its file, and edits to the file still reach the item. [`marfa folders unmatched`](/run/keep-folders-running#remove-files-a-folder-keeps-but-no-longer-holds) lists these files. | | **You move a file to another folder on the same Mac** | Keeps one item and writes nothing in the folder it left, even when another device changes it during the grace. The change reaches the moved file once the receiving folder binds it. A new Markdown note moved before its `marfa_id` line is written becomes a second item in the destination and keeps every edit ([`folders/moved-arrival-markdown-new`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/folders.md#foldersmoved-arrival-markdown-new)). | A tier change **never deletes or moves an item to the bin**. If it takes an item outside the query's reach, the folder keeps its file unless another folder takes it. @@ -42,7 +42,7 @@ the server answered 0 write(s) A removal pauses when it takes **more than 10 files and more than a quarter of the folder's files**, which the `removal_threshold` setting changes. It also holds files whose items left the folder elsewhere, such as notes another app moved to the bin. -Cancel the removal to write the files back, or confirm it to move unchanged items to the bin at the next sync. Files attached to a note come back with the note, whether or not the query holds them. Confirmation checks for moves and remote edits too: a missing file found nowhere comes back if another device changed content it showed while the query still holds it. In Swift, `folders` and `directory` are those of [Use a folder from a Mac app](/guides/folder-from-a-mac-app): +Cancel the removal to write the files back, or confirm it to move unchanged items to the bin at the next sync. Files attached to a note come back with the note, whether or not the query holds them. Confirmation checks for moves and remote edits too: a missing file found nowhere comes back if another device changed content it showed while the query still holds it. In Swift, `folders` and `directory` are those of [Use a folder from a Mac app](/run/folder-from-a-mac-app): ```bash tab="CLI" tab-group="client" marfa folders restore Notes diff --git a/content/docs/guides/google-project.mdx b/content/docs/run/google-project.mdx similarity index 96% rename from content/docs/guides/google-project.mdx rename to content/docs/run/google-project.mdx index 473dbca6..58b27637 100644 --- a/content/docs/guides/google-project.mdx +++ b/content/docs/run/google-project.mdx @@ -5,7 +5,7 @@ description: Create a Google Cloud project of your own, publish it, and sign in A Google connector signs in through a **Google Cloud project that you own**, not one that Marfa publishes. You create the project once, give it a Desktop app client, and sign in once. Every Google connector you run then shares that one grant. Nothing depends on a client that someone else maintains, and Google never has to review it. -You need a Google account, a connector from [withmarfa/marfa-connectors](https://github.com/withmarfa/marfa-connectors) that signs in to Google, and its environment from [Run a connector](/guides/run-a-connector). An instance supports **one Google account** for its Google connectors: each connector keeps the account it first ran with and refuses a grant for another. To use another account, use another instance. +You need a Google account, a connector from [withmarfa/marfa-connectors](https://github.com/withmarfa/marfa-connectors) that signs in to Google, and its environment from [Run a connector](/run/run-a-connector). An instance supports **one Google account** for its Google connectors: each connector keeps the account it first ran with and refuses a grant for another. To use another account, use another instance. @@ -157,4 +157,4 @@ Google deletes an OAuth client that has gone unused for six months, and its toke -[Connectors](/connectors) explains what a connector is, and [Run a connector](/guides/run-a-connector) runs one. +[Connectors](/connectors) explains what a connector is, and [Run a connector](/run/run-a-connector) runs one. diff --git a/content/docs/guides/housekeeping.mdx b/content/docs/run/housekeeping.mdx similarity index 94% rename from content/docs/guides/housekeeping.mdx rename to content/docs/run/housekeeping.mdx index b5b1bb7d..b9724a8e 100644 --- a/content/docs/guides/housekeeping.mdx +++ b/content/docs/run/housekeeping.mdx @@ -47,7 +47,7 @@ A job whose `last_outcome` stays `error`, or whose `next_run_at` is long past, i ## The housekeeping jobs -[Settings](/guides/settings#every-environment-setting) lists the environment settings that change intervals and retention windows. +[Settings](/run/settings#every-environment-setting) lists the environment settings that change intervals and retention windows. src/generated/housekeeping.md @@ -63,7 +63,7 @@ Each run reports `moved` and `held` in `last_result`. `moved` includes items a c ### Switch a job off -**Jobs controlled by live configuration stay listed when switched off.** You can still run one on demand; it answers `200` without doing the disabled work. This includes `enrichment-sweep` when `enrichment_enabled` is false and `grant-inactivity-retirement` when its retention is `0`. Enabling either in [configuration](/guides/settings#change-settings-while-it-runs) starts its work without a restart. A retention of `0` likewise keeps a cleanup job registered but stops age-based expiry. +**Jobs controlled by live configuration stay listed when switched off.** You can still run one on demand; it answers `200` without doing the disabled work. This includes `enrichment-sweep` when `enrichment_enabled` is false and `grant-inactivity-retirement` when its retention is `0`. Enabling either in [configuration](/run/settings#change-settings-while-it-runs) starts its work without a restart. A retention of `0` likewise keeps a cleanup job registered but stops age-based expiry. `heartbeat` is registered when a receiver is configured at startup. Its live `heartbeat_enabled` switch controls whether a run sends a ping. **A machine-only setting can prevent a job from being registered at all**, as `MARFA_BLOB_CLEANUP_INTERVAL_MS=0` does for `blob-orphans`. Such a job is absent from the listing and a run request for it answers `404 housekeeping_job_not_found`. Check the listing for the jobs this instance has. @@ -101,7 +101,7 @@ The server refuses `409 housekeeping_job_running` while a run of the same housek ## Empty the bin -An item stays in the bin until it is purged. Left alone, `trash-purge` purges each item once it has been in the bin longer than `TRASH_RETENTION_DAYS`, which is 60 days unless you change it. `trash_retention_days` in the [configuration](/guides/settings#change-settings-while-it-runs) overrides that while the server runs, and a retention of `0` keeps every item. A run removes a limited number of items, and while items past the retention remain after a run that removed some, the job runs again at once. +An item stays in the bin until it is purged. Left alone, `trash-purge` purges each item once it has been in the bin longer than `TRASH_RETENTION_DAYS`, which is 60 days unless you change it. `trash_retention_days` in the [configuration](/run/settings#change-settings-while-it-runs) overrides that while the server runs, and a retention of `0` keeps every item. A run removes a limited number of items, and while items past the retention remain after a run that removed some, the job runs again at once. It deletes the items and their history. Emptying the trash has no dry run, so read the trash first. diff --git a/content/docs/run/index.mdx b/content/docs/run/index.mdx new file mode 100644 index 00000000..8e3a511a --- /dev/null +++ b/content/docs/run/index.mdx @@ -0,0 +1,19 @@ +--- +title: Run Marfa +description: Start an instance, keep it safe and look after it, without writing any code. +--- + +An instance is yours to run: you start it, claim it, give your apps and scripts their keys, and keep a copy of it. These pages cover that work, from the first install to an upgrade, and they need no code. To write code against an instance, go to [Build](/build). + + + Take a Mac or Linux machine with Docker to an instance ready to claim. + Run an instance you keep, with storage that survives a restart and an address your devices reach. + Sign in to the owner's pages with a password or a passkey. + Give a script a key that reaches only what it needs, then narrow, rotate or revoke it. + Copy the data directory and secrets, and bring them back. + Try a new build against your database while keeping a way back. + Keep a directory on your computer in step with your items, as Markdown files. + The cause and the fix for the problems people meet first. + + +Looking something up? The [Catalog](/catalog) lists what Marfa ships, and the [Reference](/reference) covers the CLI and the HTTP API. diff --git a/content/docs/guides/install.mdx b/content/docs/run/install.mdx similarity index 92% rename from content/docs/guides/install.mdx rename to content/docs/run/install.mdx index 458e6688..10e2998e 100644 --- a/content/docs/guides/install.mdx +++ b/content/docs/run/install.mdx @@ -57,7 +57,7 @@ Open this link to choose the owner's email and password. It works once, for five 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`. +To give an AI agent access to this server, run `marfa docs build/ai-agents`. ``` @@ -76,7 +76,7 @@ Open the link in a browser and choose the owner's email and password. **Keep a c ## Choose how it's reached -The way you reach the instance decides which caller address the server believes, so the command asks, and sets [the address](/guides/deploy#the-address) and the proxy setting to match. +The way you reach the instance decides which caller address the server believes, so the command asks, and sets [the address](/run/deploy#the-address) and the proxy setting to match. | `--access` | Reached | Address | Proxy setting | | --- | --- | --- | --- | @@ -103,7 +103,7 @@ The command keeps everything in one folder, `marfa-server` in your home director - **`compose.yaml`** is the same for every install. It runs the release image with `restart: unless-stopped` and one volume. - **`.env`** holds what Docker Compose reads and what the command remembers between runs: the install's name, the release, the port, how you reach the instance and whether it starts at login. It holds no secret. -- **`marfa.env`** holds the server's [settings](/guides/settings), including the two secrets. Only you can read it. The command makes the secrets once and keeps them on every later run. If the data volume exists and `marfa.env` doesn't, it stops, because new secrets would make every key stop working. +- **`marfa.env`** holds the server's [settings](/run/settings), including the two secrets. Only you can read it. The command makes the secrets once and keeps them on every later run. If the data volume exists and `marfa.env` doesn't, it stops, because new secrets would make every key stop working. The folder works without the CLI: `docker network create marfa-net` once, then `docker compose up -d` in the folder. @@ -114,7 +114,7 @@ Run `marfa server install` again with a flag to change a setting, such as `--acc | Command | What it does | | --- | --- | | `marfa server status` | Shows whether the container runs and answers, the release, the address and whether the instance has an owner. | -| `marfa server upgrade` | Moves to the release of your CLI, or to `--release`. It refuses an older release than the one installed, because a newer one may have changed the database. Read [Upgrade](/guides/upgrade) first. | +| `marfa server upgrade` | Moves to the release of your CLI, or to `--release`. It refuses an older release than the one installed, because a newer one may have changed the database. Read [Upgrade](/run/upgrade) first. | | `marfa server start` | Starts Docker's engine on a Mac if it's down, then the container. | | `marfa server uninstall` | Removes the container and keeps the data. `--delete-data` removes the volume, the network and the settings too, after you type the install's name (`--yes` skips the question). | @@ -128,4 +128,4 @@ If a secrets manager writes `marfa.env`, pass `--external-settings`. The command ## Keep a copy in a bucket -Answer yes to the bucket question, or pass `--s3-bucket` and `--s3-access-key-id`. The secret key comes from `S3_SECRET_ACCESS_KEY` in the environment or a hidden prompt, never from an option. The command writes the [`S3_` settings](/guides/deploy#an-object-store), with the install's name as `S3_PREFIX` unless you pass `--s3-prefix`. +Answer yes to the bucket question, or pass `--s3-bucket` and `--s3-access-key-id`. The secret key comes from `S3_SECRET_ACCESS_KEY` in the environment or a hidden prompt, never from an option. The command writes the [`S3_` settings](/run/deploy#an-object-store), with the install's name as `S3_PREFIX` unless you pass `--s3-prefix`. diff --git a/content/docs/guides/keep-folders-running.mdx b/content/docs/run/keep-folders-running.mdx similarity index 95% rename from content/docs/guides/keep-folders-running.mdx rename to content/docs/run/keep-folders-running.mdx index 80ed3069..6f368a0e 100644 --- a/content/docs/guides/keep-folders-running.mdx +++ b/content/docs/run/keep-folders-running.mdx @@ -3,7 +3,7 @@ title: Keep folders running description: Keep every folder on your computer in step with one watch, start it at login, and work on a folder's files while it runs. --- -A folder you [set up](/guides/sync-a-folder) stays in step only while something watches it. **One `marfa folders watch` keeps every folder on the computer in step**, and `marfa folders autostart` starts it at login, so a folder needs no terminal left open. +A folder you [set up](/run/sync-a-folder) stays in step only while something watches it. **One `marfa folders watch` keeps every folder on the computer in step**, and `marfa folders autostart` starts it at login, so a folder needs no terminal left open. The watch needs no `--url` or `--key`: it reads each folder's credential from the keychain, so run `marfa keys keep` for a folder's key first, as [the CLI](/cli#connect-to-a-server) shows. On a computer where nobody signs in to unlock the keychain, [use a key file instead](#use-a-key-file-instead-of-the-keychain). @@ -43,7 +43,7 @@ To watch one folder, name its directory: `marfa folders watch Notes`. - **Each folder keeps its own server and credential.** Folders on different servers share one watch, and no key is sent to a server it wasn't made for. - **A profile works for a watch you run yourself.** `marfa folders watch --profile ` uses the profile's key for folders on the profile's server, and the keychain for the rest. A `key_command` runs once, when the watch starts, so restart the watch after you change the key. `marfa folders autostart on` refuses `--profile`, and the watch it starts doesn't read `MARFA_PROFILE`. - **One folder that can't be watched doesn't stop the others.** The watch reports it once and tries again. A folder fails to open while its first sync waits for `marfa folders confirm`, while another process works it, or when the keychain holds no credential for its server. -- **A folder whose directory can't be found stays listed,** such as one on a disk that isn't connected. The watch reports these in one line, and `marfa folders remove` takes one off. [Folder safeguards](/guides/folder-safeguards#a-missing-or-unreadable-directory) covers a path that now holds a different directory. +- **A folder whose directory can't be found stays listed,** such as one on a disk that isn't connected. The watch reports these in one line, and `marfa folders remove` takes one off. [Folder safeguards](/run/folder-safeguards#a-missing-or-unreadable-directory) covers a path that now holds a different directory. ## Sync from login @@ -82,7 +82,7 @@ Another program can show the same state from [the folder watch record](/folder-w A login agent starts outside your shell and reads your own keychain. On a machine that no one signs in to, such as a server or an automated Mac, the keychain stays locked and would raise a prompt that nobody answers. **A key file replaces the keychain for the watch**, and the agent holds the file's path and never what is in it. -Make a key with only the permissions the folders need, as [Manage keys](/guides/manage-keys) shows, and write the file with two lines: +Make a key with only the permissions the folders need, as [Manage keys](/run/manage-keys) shows, and write the file with two lines: ```bash MARFA_API_URL=https://marfa.example.com diff --git a/content/docs/guides/manage-keys.mdx b/content/docs/run/manage-keys.mdx similarity index 89% rename from content/docs/guides/manage-keys.mdx rename to content/docs/run/manage-keys.mdx index 4d15d0cd..c999381d 100644 --- a/content/docs/guides/manage-keys.mdx +++ b/content/docs/run/manage-keys.mdx @@ -3,17 +3,17 @@ title: Manage keys description: Give a script or service a key that reaches only what it needs, then narrow, expire, rotate or revoke it. --- -A key is the credential for a script or service you run yourself. It reaches only what it was made with: a notes app gets notes and nothing else. An app that people sign in to should connect with OAuth instead, as [Authenticate](/guides/authenticate) shows, though it may also hold a key. [Access](/access) explains the maps and permissions a key holds. +A key is the credential for a script or service you run yourself. It reaches only what it was made with: a notes app gets notes and nothing else. An app that people sign in to should connect with OAuth instead, as [Authenticate](/build/authenticate) shows, though it may also hold a key. [Access](/access) explains the maps and permissions a key holds. -The owner makes keys in a browser or through the private socket. A key holding `keys.mint` makes keys within its own access. The examples use the socket, so run them on the instance's machine as the account that runs the server, with `MARFA_API_URL`, `MARFA_API_KEY` and `MARFA_PROFILE` unset. The socket path is the one in [Deploy](/guides/deploy#the-private-socket). +The owner makes keys in a browser or through the private socket. A key holding `keys.mint` makes keys within its own access. The examples use the socket, so run them on the instance's machine as the account that runs the server, with `MARFA_API_URL`, `MARFA_API_KEY` and `MARFA_PROFILE` unset. The socket path is the one in [Deploy](/run/deploy#the-private-socket). ## Use the API keys page -Open **API keys** from [Home](/guides/sign-in-as-the-owner#start-from-home) and choose **Create key**. **Details** holds the name and when the key ends. **Access starts with everything off**, so turn on only what the key needs. **Advanced** holds its sources and where its items land. +Open **API keys** from [Home](/run/sign-in-as-the-owner#start-from-home) and choose **Create key**. **Details** holds the name and when the key ends. **Access starts with everything off**, so turn on only what the key needs. **Advanced** holds its sources and where its items land. Marfa shows it once, under **Copy your key**. If you lose it, make a new one. -A saved key can only get less access or an earlier end. **Creating a key, saving a change to it and revoking it need a sign-in from the last five minutes**: the page asks the owner to [confirm it's them](/guides/sign-in-as-the-owner#confirm-its-you) when theirs is older. The socket, and a key with the permission for the operation, aren't held to it. +A saved key can only get less access or an earlier end. **Creating a key, saving a change to it and revoking it need a sign-in from the last five minutes**: the page asks the owner to [confirm it's them](/run/sign-in-as-the-owner#confirm-its-you) when theirs is older. The socket, and a key with the permission for the operation, aren't held to it. ## Make a key @@ -47,7 +47,7 @@ These flags work on `keys create` and `keys update` unless noted, and `--help` l | `--edge-permission`, `--extension-permission`, `--metadata-permission`, `--profile-permission` | Entries in the other four maps, as `name=level`. `email` is `read` only. | | `--permission ` | One [permission](/catalog/permissions), such as `audit.read`. | | `--claim ` | Another source the key may write as. | -| `--default-tier `, `--enforcement-override ` | Where its items land when a write names none (`library` or `feed`), and [levers](/guides/settings#what-the-enforcement-levers-do) for this key alone, looser or stricter than the instance's. Levers need `config.manage`. | +| `--default-tier `, `--enforcement-override ` | Where its items land when a write names none (`library` or `feed`), and [levers](/run/settings#what-the-enforcement-levers-do) for this key alone, looser or stricter than the instance's. Levers need `config.manage`. | | `--expires-in `, `--expires-at @@ -119,4 +119,4 @@ A failed run loses nothing, and the next run reads the vendor again. - **`--every 15m`** keeps one process running every 15 minutes. Runs never overlap, and a failed run lengthens the wait before the next. - **`--once` from a scheduler**, such as cron, launchd or a scheduled container. -The connector keeps its place at the vendor on the instance, not in a file, so a new process carries on from the last run. **Run one process for each connector**: a second under the same key waits under `--every` and exits under `--once`, as [Holds and retries](/guides/connector-holds-and-retries) explains. The [template README](https://github.com/withmarfa/marfa-connectors/blob/main/template/README.md#run-it) has a launchd file and a container image. +The connector keeps its place at the vendor on the instance, not in a file, so a new process carries on from the last run. **Run one process for each connector**: a second under the same key waits under `--every` and exits under `--once`, as [Holds and retries](/build/connector-holds-and-retries) explains. The [template README](https://github.com/withmarfa/marfa-connectors/blob/main/template/README.md#run-it) has a launchd file and a container image. diff --git a/content/docs/guides/settings.mdx b/content/docs/run/settings.mdx similarity index 89% rename from content/docs/guides/settings.mdx rename to content/docs/run/settings.mdx index 7fec1cc8..2b28d085 100644 --- a/content/docs/guides/settings.mdx +++ b/content/docs/run/settings.mdx @@ -35,20 +35,20 @@ Most defaults suit any instance. Check these first: | Setting | Check it when | Read more | | --- | --- | --- | -| `NODE_ENV` | Always. `production` refuses a start without `API_KEY_SALT`, `MARFA_AUTH_SECRET` and `MARFA_AUTH_BASE_URL`. | [Deploy](/guides/deploy#run-it-without-a-container) | -| `SQLITE_PATH`, `BLOB_PATH` | Always. They say where your data lives. | [Back up and restore](/guides/back-up-and-restore) | -| `TRUSTED_PROXY_CIDRS`, `TRUSTED_PROXY_HEADER` | A proxy or tunnel sits in front of the server. | [Deploy](/guides/deploy#the-address) | -| `S3_PREFIX` | Instances share a bucket. Each needs its own. | [Deploy](/guides/deploy#an-object-store) | +| `NODE_ENV` | Always. `production` refuses a start without `API_KEY_SALT`, `MARFA_AUTH_SECRET` and `MARFA_AUTH_BASE_URL`. | [Deploy](/run/deploy#run-it-without-a-container) | +| `SQLITE_PATH`, `BLOB_PATH` | Always. They say where your data lives. | [Back up and restore](/run/back-up-and-restore) | +| `TRUSTED_PROXY_CIDRS`, `TRUSTED_PROXY_HEADER` | A proxy or tunnel sits in front of the server. | [Deploy](/run/deploy#the-address) | +| `S3_PREFIX` | Instances share a bucket. Each needs its own. | [Deploy](/run/deploy#an-object-store) | | `MARFA_WEBHOOK_ALLOW_PRIVATE_ADDRESSES` | A webhook receiver is on a Tailscale or home network. | [Let webhooks reach a private network](#let-webhooks-reach-a-private-network) | -| `OTEL_RESOURCE_ATTRIBUTES`, `OTEL_SERVICE_NAME` | You run more than one instance and send telemetry. | [Send telemetry](/guides/telemetry#tell-instances-apart) | -| `CORS_ORIGINS` | A web page on another origin calls the API. | [Connect a web page](/guides/connect-a-web-page) | +| `OTEL_RESOURCE_ATTRIBUTES`, `OTEL_SERVICE_NAME` | You run more than one instance and send telemetry. | [Send telemetry](/run/telemetry#tell-instances-apart) | +| `CORS_ORIGINS` | A web page on another origin calls the API. | [Connect a web page](/build/connect-a-web-page) | | `PORT`, `MARFA_LISTEN_HOST` | The default port `8600` or address doesn't suit. `0` lets the operating system assign a port. | | -The container's entrypoint reads one more name, `MARFA_REPLACE_NEWER_REPLICA`, which the server doesn't read and the list below leaves out. Set it to `true` for one start to replicate a database that may be older than the bucket's copy. [With a bucket](/guides/back-up-and-restore#with-a-bucket) says when to use it. +The container's entrypoint reads one more name, `MARFA_REPLACE_NEWER_REPLICA`, which the server doesn't read and the list below leaves out. Set it to `true` for one start to replicate a database that may be older than the bucket's copy. [With a bucket](/run/back-up-and-restore#with-a-bucket) says when to use it. ## Every environment setting -The list includes the intervals and retention windows of the [housekeeping jobs](/guides/housekeeping). The `MARFA_TEST_` settings are for the conformance harness. **Never set them on an instance that holds real data**: the server refuses `MARFA_TEST_HOOKS` when `NODE_ENV` is `production`, which the container sets. [Housekeeping](/guides/housekeeping#try-feed-expiry-with-a-moved-clock) shows one use. +The list includes the intervals and retention windows of the [housekeeping jobs](/run/housekeeping). The `MARFA_TEST_` settings are for the conformance harness. **Never set them on an instance that holds real data**: the server refuses `MARFA_TEST_HOOKS` when `NODE_ENV` is `production`, which the container sets. [Housekeeping](/run/housekeeping#try-feed-expiry-with-a-moved-clock) shows one use. The owner reads each setting's current value on the **All settings** page at `/auth/owner/settings`. A secret shows only whether it is set. @@ -64,7 +64,7 @@ Marfa posts webhooks only to public addresses. A `url` whose host is a loopback, To reach a receiver on a Tailscale or home network, set `MARFA_WEBHOOK_ALLOW_PRIVATE_ADDRESSES=true` and restart the server. To try a receiver on your own machine, do that on a development instance and register `http://localhost:3000/marfa`. **Leave it off** on any other instance. -Marfa accepts a name that doesn't resolve or answer in time, and checks it again at each delivery. [Receive webhooks](/guides/receive-webhooks#receive-deliveries-on-a-private-network) shows what a delivery to a non-public address records. +Marfa accepts a name that doesn't resolve or answer in time, and checks it again at each delivery. [Receive webhooks](/build/receive-webhooks#receive-deliveries-on-a-private-network) shows what a delivery to a non-public address records. ## Change settings while it runs @@ -137,7 +137,7 @@ Content-Type: application/json `enforcement` is one setting: an update that names it replaces all three levers, and `null` removes them all. [Update the configuration](/api-reference/instance/updateConfig) has every refusal. -The server records each accepted replace or update in the [audit log](/guides/audit-log) as `config.update`. +The server records each accepted replace or update in the [audit log](/run/audit-log) as `config.update`. ## What the enforcement levers do @@ -153,6 +153,6 @@ A lever holds chosen types to a stricter rule. Each names `types`, and `source_a **Reading an item by its ID isn't filtered.** An item with no source counts as one whose source is `unknown`. -A key can carry levers of its own in `enforcement_override`. A lever it names replaces the instance's for that key, looser or stricter. Setting one needs `config.manage`, or the owner's own authority. A key made by another key doesn't take its maker's levers: unless it names its own, it follows the instance's. [Manage keys](/guides/manage-keys) shows the `--enforcement-override` flag. +A key can carry levers of its own in `enforcement_override`. A lever it names replaces the instance's for that key, looser or stricter. Setting one needs `config.manage`, or the owner's own authority. A key made by another key doesn't take its maker's levers: unless it names its own, it follows the instance's. [Manage keys](/run/manage-keys) shows the `--enforcement-override` flag. The rules behind this page are in the contract's [instance](https://github.com/withmarfa/marfa/blob/main/conformance/spec/instance.md#settings), [types](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#enforcement-levers), [housekeeping](https://github.com/withmarfa/marfa/blob/main/conformance/spec/housekeeping.md#retention), [events](https://github.com/withmarfa/marfa/blob/main/conformance/spec/events.md) and [keys and sign-in](https://github.com/withmarfa/marfa/blob/main/conformance/spec/keys-and-oauth.md#a-keys-own-levers) chapters. diff --git a/content/docs/guides/sign-in-as-the-owner.mdx b/content/docs/run/sign-in-as-the-owner.mdx similarity index 88% rename from content/docs/guides/sign-in-as-the-owner.mdx rename to content/docs/run/sign-in-as-the-owner.mdx index 5c2b530b..b9a8a710 100644 --- a/content/docs/guides/sign-in-as-the-owner.mdx +++ b/content/docs/run/sign-in-as-the-owner.mdx @@ -5,7 +5,7 @@ description: Sign in to the owner's pages with a password or a passkey, confirm The owner signs in in a browser at `/auth/sign-in`, with the password chosen when claiming the instance or a registered passkey. A valid passkey works even when password attempts are locked. No key or app ever gets **the owner's authority**. -Commands that start `marfa --socket` run on the instance's machine, as the server's account, through [the private socket](/guides/deploy#the-private-socket). +Commands that start `marfa --socket` run on the instance's machine, as the server's account, through [the private socket](/run/deploy#the-private-socket). ## Manage passkeys and sign-ins @@ -51,18 +51,18 @@ Open the instance's address in a browser signed in as the owner, and Home lists | Group | Page | Address | | --- | --- | --- | -| Access | [API keys](/guides/manage-keys#use-the-api-keys-page) | `/auth/owner/keys` | -| Access | [Connected apps](/guides/authenticate#revoke-access) | `/auth/owner/apps` | +| Access | [API keys](/run/manage-keys#use-the-api-keys-page) | `/auth/owner/keys` | +| Access | [Connected apps](/build/authenticate#revoke-access) | `/auth/owner/apps` | | Access | [Browsers](#manage-passkeys-and-sign-ins) | `/auth/owner/browsers` | | Your data | [Items](/access) | `/auth/owner/items` | -| Your data | [Trash](/guides/housekeeping#empty-the-bin) | `/auth/owner/trash` | -| Your data | [Export and restore](/guides/export-and-restore) | `/auth/owner/export` | +| Your data | [Trash](/run/housekeeping#empty-the-bin) | `/auth/owner/trash` | +| Your data | [Export and restore](/run/export-and-restore) | `/auth/owner/export` | | Connections | [Connectors](/connectors) | `/auth/owner/connectors` | | Connections | [Folders](/devices#folders) | `/auth/owner/folders` | -| This Marfa | [General](/guides/settings#change-settings-while-it-runs) | `/auth/owner/general` | -| This Marfa | [Health](/guides/housekeeping) | `/auth/owner/health` | -| This Marfa | [Activity](/guides/audit-log) | `/auth/owner/activity` | -| This Marfa | [All settings](/guides/settings#every-environment-setting) | `/auth/owner/settings` | +| This Marfa | [General](/run/settings#change-settings-while-it-runs) | `/auth/owner/general` | +| This Marfa | [Health](/run/housekeeping) | `/auth/owner/health` | +| This Marfa | [Activity](/run/audit-log) | `/auth/owner/activity` | +| This Marfa | [All settings](/run/settings#every-environment-setting) | `/auth/owner/settings` | | You | [Profile](#change-the-owners-profile) | `/auth/owner/profile` | | You | [Password and passkeys](#manage-passkeys-and-sign-ins) | `/auth/owner/passkeys` | @@ -100,7 +100,7 @@ Set a lost password through the socket: marfa --socket /srv/marfa/data/control/marfa.sock owner recover ``` -The CLI asks for the new password without showing it, and `--stdin` reads `{"password": "…"}` instead. **Recovery signs out every browser**, and keys and apps keep working. It also lifts a password lock: **ten attempts from one address in 15 minutes** answer `429` with `Retry-After`. [Behind a proxy](/guides/deploy#the-address), every client shares one address unless you set `TRUSTED_PROXY_CIDRS` or `TRUSTED_PROXY_HEADER`. +The CLI asks for the new password without showing it, and `--stdin` reads `{"password": "…"}` instead. **Recovery signs out every browser**, and keys and apps keep working. It also lifts a password lock: **ten attempts from one address in 15 minutes** answer `429` with `Retry-After`. [Behind a proxy](/run/deploy#the-address), every client shares one address unless you set `TRUSTED_PROXY_CIDRS` or `TRUSTED_PROXY_HEADER`. In a container, run `docker exec -it marfa marfa --socket /data/control/marfa.sock owner recover`. If the server can't start in full, such as when its port is taken, **start it with `MARFA_CONTROL_ONLY=true`** to open the socket alone. diff --git a/content/docs/guides/sync-a-folder.mdx b/content/docs/run/sync-a-folder.mdx similarity index 87% rename from content/docs/guides/sync-a-folder.mdx rename to content/docs/run/sync-a-folder.mdx index e5c45bde..f882c5be 100644 --- a/content/docs/guides/sync-a-folder.mdx +++ b/content/docs/run/sync-a-folder.mdx @@ -7,9 +7,9 @@ description: Keep a directory on your computer and a set of items in step, so ea - **You need** the `marfa` CLI and an instance you own, as the [Quickstart](/quickstart) sets them up. You make the first key on the instance's own computer. - **You'll have** a directory with a file for each note, where a change on either side reaches the other. -A folder is a [device](/devices) whose working copy is a directory. **Its settings live on the instance** as a `system.folder` item, so every computer that follows the folder shares them, and each keeps its own copy and queue of writes. If you stop following the folder, the files stay behind as plain files. [Folder safeguards](/guides/folder-safeguards) says when a folder removes a file or an item. +A folder is a [device](/devices) whose working copy is a directory. **Its settings live on the instance** as a `system.folder` item, so every computer that follows the folder shares them, and each keeps its own copy and queue of writes. If you stop following the folder, the files stay behind as plain files. [Folder safeguards](/run/folder-safeguards) says when a folder removes a file or an item. -The steps use the CLI. A Mac app does the same with the Swift SDK, as [Use a folder from a Mac app](/guides/folder-from-a-mac-app) shows. +The steps use the CLI. A Mac app does the same with the Swift SDK, as [Use a folder from a Mac app](/run/folder-from-a-mac-app) shows. @@ -31,7 +31,7 @@ marfa --socket /srv/marfa/data/control/marfa.sock keys create \ - **The types the folder holds:** write on each type the query admits, here `core.note`. `core.file.*` covers `core.file` and every type under it. - **`in-folder`:** write on this edge, which records where each file sits. -`folders add` is refused without read on `system.folder`, naming `system.folder:read`, and without write on `in-folder`, naming `edge.in-folder:write`. [Manage keys](/guides/manage-keys) explains each flag. +`folders add` is refused without read on `system.folder`, naming `system.folder:read`, and without write on `in-folder`, naming `edge.in-folder:write`. [Manage keys](/run/manage-keys) explains each flag. @@ -88,7 +88,7 @@ Keep the directory out of iCloud Drive, other cloud-synced folders and network s Check the plan first. **The query decides every item the folder takes in.** A query that names only a type holds every active and archived item of that type in the library tier, and the first sync writes all of them to disk. With no `types`, it holds every type the key reads except `system.*`. -Set `tier: all` to write matching items from both the library and feed tiers as files. To narrow the query, use `filter`, `tier`, `state` or `beneath`, as [list, filter and search](/guides/list-filter-search) describes. +Set `tier: all` to write matching items from both the library and feed tiers as files. To narrow the query, use `filter`, `tier`, `state` or `beneath`, as [list, filter and search](/build/list-filter-search) describes. Then confirm, and sync once: @@ -97,7 +97,7 @@ marfa folders confirm Notes marfa folders push Notes ``` -`marfa folders push` sends what changed in the directory, catches up with the instance, and writes out what changed there. To sync without running it by hand, [keep folders running](/guides/keep-folders-running). +`marfa folders push` sends what changed in the directory, catches up with the instance, and writes out what changed there. To sync without running it by hand, [keep folders running](/run/keep-folders-running). @@ -115,7 +115,7 @@ The directory now holds a file for each note, and `Ideas.md` is an item: -[Files in a folder](/guides/folder-files) says what each file holds, where attached files go, and how an edit on either side reaches the other. +[Files in a folder](/run/folder-files) says what each file holds, where attached files go, and how an edit on either side reaches the other. ## Change a folder's settings @@ -166,17 +166,17 @@ Content-Type: application/json Use the version you read in place of `` or `1`. The CLI also takes the JSON directly through `--body`. **To repair a folder that still stores `search`, use a server at v0.0.33 or later** and send the intended `query`, leaving out `search`. Copy every part of the old selection you want to keep, because the server never translates `search` into `query`. Editing `.marfa/folder.yaml` alone cannot remove it. Read the item again to check the accepted settings, then sync the folder. -**Moving a type's items changes the folder too.** When you [move items to another type](/guides/move-items-to-another-type), Marfa changes `query.types`, `defaults.type`, `first_placement` and the `type eq` and `type neq` terms in `query.filter` in every folder that names the old type. Each item keeps its file, and the next sync writes the new type into the file's frontmatter. +**Moving a type's items changes the folder too.** When you [move items to another type](/build/move-items-to-another-type), Marfa changes `query.types`, `defaults.type`, `first_placement` and the `type eq` and `type neq` terms in `query.filter` in every folder that names the old type. Each item keeps its file, and the next sync writes the new type into the file's frontmatter. ## Replace a folder's key -A new key with the same `--source` is refused `409 conflict` while the old key is live. [Revoke the old key](/guides/manage-keys#revoke-a-key), run the `keys create` command from the first step again, then run `marfa keys keep` so the folder reads the new key from the keychain. +A new key with the same `--source` is refused `409 conflict` while the old key is live. [Revoke the old key](/run/manage-keys#revoke-a-key), run the `keys create` command from the first step again, then run `marfa keys keep` so the folder reads the new key from the keychain. Then run `marfa folders push Notes`. If the key's reach changed, the push rebuilds the working copy once and sends the waiting writes in the same run, keeping earlier verdicts. If the reach changes again during that run, it stops and keeps the remaining writes queued: try again once the access is settled. ## Move to a newer CLI -Each folder keeps its working copy, and its record of which file is which item, in a store that one build of the core made. When a newer CLI uses a different shape, the folder's next push, pull, scan or watch **replaces the store itself, as long as the old store holds no write still to be sent and no answer the folder has yet to record**. It says so on standard error, keeps the folder's record of its files and its first sync, and changes no file. If writes are waiting, push the folder with the earlier CLI first. [Troubleshooting](/guides/troubleshooting#staying-in-step) covers a write the server refused or gave up on. +Each folder keeps its working copy, and its record of which file is which item, in a store that one build of the core made. When a newer CLI uses a different shape, the folder's next push, pull, scan or watch **replaces the store itself, as long as the old store holds no write still to be sent and no answer the folder has yet to record**. It says so on standard error, keeps the folder's record of its files and its first sync, and changes no file. If writes are waiting, push the folder with the earlier CLI first. [Troubleshooting](/run/troubleshooting#staying-in-step) covers a write the server refused or gave up on. ## Stop following a folder diff --git a/content/docs/guides/telemetry.mdx b/content/docs/run/telemetry.mdx similarity index 92% rename from content/docs/guides/telemetry.mdx rename to content/docs/run/telemetry.mdx index 814699ff..813c0b21 100644 --- a/content/docs/guides/telemetry.mdx +++ b/content/docs/run/telemetry.mdx @@ -3,7 +3,7 @@ title: Send telemetry description: Send an instance's traces and logs to an OpenTelemetry endpoint, its errors to PostHog, and a notice or a heartbeat to a URL you choose. --- -An instance reports on itself in four ways, and **each is off until you set its variable**: an instance that sets none of them sends nothing anywhere. Set the variables in the server's environment, as [Settings](/guides/settings#set-an-environment-setting) shows, and restart the server. +An instance reports on itself in four ways, and **each is off until you set its variable**: an instance that sets none of them sends nothing anywhere. Set the variables in the server's environment, as [Settings](/run/settings#set-an-environment-setting) shows, and restart the server. | Variable | What it does | Default | | --- | --- | --- | @@ -20,7 +20,7 @@ An instance reports on itself in four ways, and **each is off until you set its | `MARFA_HEARTBEAT_URL` | An address the server requests on a schedule, so something else can notice when the requests stop. | None | | `MARFA_HEARTBEAT_INTERVAL_MS` | How often it does. | `60000` | -[Every environment setting](/guides/settings#every-environment-setting) has the limits. +[Every environment setting](/run/settings#every-environment-setting) has the limits. ## Send traces and logs @@ -72,7 +72,7 @@ Set `ERROR_WEBHOOK_URL` and the server posts a JSON notice to it when a request ## Send a heartbeat -An instance can't report its own death. Set `MARFA_HEARTBEAT_URL` and the `heartbeat` [housekeeping job](/guides/housekeeping) requests it with a `GET`, no body and no identity, every `MARFA_HEARTBEAT_INTERVAL_MS`. Give the address to an uptime monitor that raises an alarm when the requests stop. A failed request is logged as a warning. +An instance can't report its own death. Set `MARFA_HEARTBEAT_URL` and the `heartbeat` [housekeeping job](/run/housekeeping) requests it with a `GET`, no body and no identity, every `MARFA_HEARTBEAT_INTERVAL_MS`. Give the address to an uptime monitor that raises an alarm when the requests stop. A failed request is logged as a warning. ## What the CLI sends diff --git a/content/docs/guides/troubleshooting.mdx b/content/docs/run/troubleshooting.mdx similarity index 93% rename from content/docs/guides/troubleshooting.mdx rename to content/docs/run/troubleshooting.mdx index 2a6cf633..2ccbba9a 100644 --- a/content/docs/guides/troubleshooting.mdx +++ b/content/docs/run/troubleshooting.mdx @@ -17,14 +17,14 @@ The server cannot start; fix these settings: RATE_LIMIT_REQUESTS must be a whole number, 1 or more (got "0") ``` -Correct the values it names. [Settings](/guides/settings) lists every setting with the values it takes. +Correct the values it names. [Settings](/run/settings) lists every setting with the values it takes. With `NODE_ENV=production`, the server needs two secrets of its own and its public address: `API_KEY_SALT`, `MARFA_AUTH_SECRET` and `MARFA_AUTH_BASE_URL`. It refuses a secret that is missing, short or a placeholder. Outside production the server fills in what's missing, but still refuses a `MARFA_AUTH_SECRET` under 32 characters. -Generate each secret with `openssl rand -hex 32`, and set `MARFA_AUTH_BASE_URL` to the address clients reach the server at. [Deploy](/guides/deploy) shows a complete production setup. +Generate each secret with `openssl rand -hex 32`, and set `MARFA_AUTH_BASE_URL` to the address clients reach the server at. [Deploy](/run/deploy) shows a complete production setup. @@ -45,14 +45,14 @@ Give the server a path with no links in it, such as `/private/tmp/marfa/marfa.db The server found no blob folder, or no `.marfa-store` file in it, where the database records saved files, and no bucket is set. A drive may not have mounted, or the folder wasn't restored with its files. The server exits with status `1`, creates nothing and names the folder. -Mount or restore the folder with its `.marfa-store` file, then start the server again. To start with an empty folder on purpose, point `BLOB_PATH` at another folder. [Restore the copy](/guides/back-up-and-restore#restore-the-copy) explains both. +Mount or restore the folder with its `.marfa-store` file, then start the server again. To start with an empty folder on purpose, point `BLOB_PATH` at another folder. [Restore the copy](/run/back-up-and-restore#restore-the-copy) explains both. The container found a database on the volume that may be older than the bucket's copy, so it didn't start the server or replicate, and the bucket is unchanged. The message names the reason: a copy taken before the bucket's newest write, a database restored without Litestream's record, one that lost writes since it last replicated, or one that last replicated to another prefix or bucket. If the message says it could not list or compare the replica instead, the bucket couldn't be reached: start again once it can be. -Run the copy under a new `S3_PREFIX`, restore the bucket's newest state by deleting `marfa.db`, `marfa.db-wal` and `marfa.db-shm`, or replace the bucket's copy with `MARFA_REPLACE_NEWER_REPLICA=true` for one start. [With a bucket](/guides/back-up-and-restore#with-a-bucket) compares the three. +Run the copy under a new `S3_PREFIX`, restore the bucket's newest state by deleting `marfa.db`, `marfa.db-wal` and `marfa.db-shm`, or replace the bucket's copy with `MARFA_REPLACE_NEWER_REPLICA=true` for one start. [With a bucket](/run/back-up-and-restore#with-a-bucket) compares the three. @@ -66,12 +66,12 @@ Stop the other process, or set `PORT` to a free port. The database was written by a different build of Marfa. A build doesn't upgrade a database in place: it refuses the file and changes nothing in it, so the build that wrote it can still open it. -To test moving your data, export it with the old build and try restoring the archive into fresh storage with the new one. Keep the old build and database until you have checked the result; a different build may refuse the archive. [Upgrade](/guides/upgrade) shows each step. +To test moving your data, export it with the old build and try restoring the archive into fresh storage with the new one. Keep the old build and database until you have checked the result; a different build may refuse the archive. [Upgrade](/run/upgrade) shows each step. -A restore writes nothing when it refuses any part of the archive, so you can run it again. [Restore from an archive](/api-reference/export-and-restore/restoreArchive) lists each refusal, and [Upgrade](/guides/upgrade#if-the-new-build-refuses-the-archive) says what to do when a newer build can't read an archive. +A restore writes nothing when it refuses any part of the archive, so you can run it again. [Restore from an archive](/api-reference/export-and-restore/restoreArchive) lists each refusal, and [Upgrade](/run/upgrade#if-the-new-build-refuses-the-archive) says what to do when a newer build can't read an archive. @@ -89,7 +89,7 @@ To get a new code, restart the server, which prints one, or run `marfa --socket The instance already has an owner, and an instance is claimed only once. -If you are the owner and have lost the password, [recover the owner's password](/guides/sign-in-as-the-owner#recover-the-owners-password) from the instance's own machine. +If you are the owner and have lost the password, [recover the owner's password](/run/sign-in-as-the-owner#recover-the-owners-password) from the instance's own machine. @@ -101,7 +101,7 @@ If you are the owner and have lost the password, [recover the owner's password]( Marfa didn't accept the credential: it's missing, mistyped, revoked or expired. The CLI exits with `5`. -Send it as `Authorization: Bearer `, in full. A key sent correctly and still refused has been revoked or has expired: make a new one. [Manage keys](/guides/manage-keys) shows how. +Send it as `Authorization: Bearer `, in full. A key sent correctly and still refused has been revoked or has expired: make a new one. [Manage keys](/run/manage-keys) shows how. @@ -125,26 +125,26 @@ Your credential can read the item's type but not write it. `details.grant` names } ``` -Give the key that grant, or use a key that holds it. [Manage keys](/guides/manage-keys) shows how. +Give the key that grant, or use a key that holds it. [Manage keys](/run/manage-keys) shows how. The owner's browser signed in more than five minutes ago, and the operation, such as making, changing or revoking a key or approving a permission, needs a recent sign-in. Marfa changes nothing. The owner's console asks you to confirm it's you, so you see this message only from a program, or after a refused confirmation. -Sign in again in a browser, or use the private socket. A key can't confirm. [Confirm it's you](/guides/sign-in-as-the-owner#confirm-its-you) lists the operations that need a recent sign-in. +Sign in again in a browser, or use the private socket. A key can't confirm. [Confirm it's you](/run/sign-in-as-the-owner#confirm-its-you) lists the operations that need a recent sign-in. The sign-in page, or a `429` with `Retry-After`, says the owner's password was tried too often. The page says how many minutes to wait, rounded up, and that a passkey signs in during the lock. A device code page names its wait the same way. -[Recover the owner's password](/guides/sign-in-as-the-owner#recover-the-owners-password) has the limit and how to lift the lock. Behind a proxy, every client can share one address, as [Deploy](/guides/deploy#the-address) explains. +[Recover the owner's password](/run/sign-in-as-the-owner#recover-the-owners-password) has the limit and how to lift the lock. Behind a proxy, every client can share one address, as [Deploy](/run/deploy#the-address) explains. -`MARFA_AUTH_BASE_URL` is a plain `http` address that isn't `localhost`, so the server refuses the owner's pages and passkeys with `403 forbidden`. Serve the instance over `https` and set `MARFA_AUTH_BASE_URL` to that address. A passkey belongs to its host name, so use the same address each time you sign in. [Deploy](/guides/deploy#the-address) covers the address. +`MARFA_AUTH_BASE_URL` is a plain `http` address that isn't `localhost`, so the server refuses the owner's pages and passkeys with `403 forbidden`. Serve the instance over `https` and set `MARFA_AUTH_BASE_URL` to that address. A passkey belongs to its host name, so use the same address each time you sign in. [Deploy](/run/deploy#the-address) covers the address. @@ -154,12 +154,12 @@ A code from `marfa login` works once and for 10 minutes, and the page can word a -The credential sent more requests in the current window than the instance allows. Wait the seconds `Retry-After` names, then send the request again. The CLI exits with `3`, which means a retry can succeed. [Settings](/guides/settings) lists the `RATE_LIMIT_*` limits. +The credential sent more requests in the current window than the instance allows. Wait the seconds `Retry-After` names, then send the request again. The CLI exits with `3`, which means a retry can succeed. [Settings](/run/settings) lists the `RATE_LIMIT_*` limits. -No bytes of the restore or blob upload arrived for `MARFA_UPLOAD_IDLE_TIMEOUT_MS`, 60 seconds unless the instance sets it, so the server ended it and wrote or stored nothing. Send the request again over a connection that stays up. [Settings](/guides/settings#every-environment-setting) lists the setting. +No bytes of the restore or blob upload arrived for `MARFA_UPLOAD_IDLE_TIMEOUT_MS`, 60 seconds unless the instance sets it, so the server ended it and wrote or stored nothing. Send the request again over a connection that stays up. [Settings](/run/settings#every-environment-setting) lists the setting. @@ -171,7 +171,7 @@ No bytes of the restore or blob upload arrived for `MARFA_UPLOAD_IDLE_TIMEOUT_MS Your write changed a field that someone else also changed since the version you read. Marfa refuses your write rather than overwrite theirs, and sends the item as it is now in `current`, beside the version you read in `ancestor`. -Combine your change with `current`, and send it again naming `current.version`. Or send the write with `conflict=auto` and let the type's rules settle it. [Write safely](/guides/write-safely) shows both. +Combine your change with `current`, and send it again naming `current.version`. Or send the write with `conflict=auto` and let the type's rules settle it. [Write safely](/build/write-safely) shows both. @@ -237,14 +237,14 @@ A key without `schema.write` can add optional fields and change a label, but the The folder's `system.folder` item still holds `search`, which its type does not declare. **Repair the settings on the instance**, using a server at v0.0.33 or later. Read the item and its version with `marfa items get `, then run `marfa folders change --version --file settings.json` with the intended `query` and no `search` in the file. -Each supplied setting replaces its value whole, omitted declared settings stay, and the accepted change removes stored undeclared settings. There is no translation from `search` to `query`. [Change a folder's settings](/guides/sync-a-folder#change-a-folders-settings) shows the file and how to check the result before syncing again; editing local YAML alone cannot repair the stored item. +Each supplied setting replaces its value whole, omitted declared settings stay, and the accepted change removes stored undeclared settings. There is no translation from `search` to `query`. [Change a folder's settings](/run/sync-a-folder#change-a-folders-settings) shows the file and how to check the result before syncing again; editing local YAML alone cannot repair the stored item. You asked to resume from a cursor older than anything the instance's event log still holds, so the stream can't fill the gap. The frame names the oldest event the log holds in `min_retained_id` and your cursor in `requested`, then the stream closes. -Start over as you did the first time: read your data again from the API, and follow the stream from its head. The log keeps events for at least 30 days unless the instance is set otherwise. [Keep a copy in sync](/guides/keep-in-sync) shows the whole loop. +Start over as you did the first time: read your data again from the API, and follow the stream from its head. The log keeps events for at least 30 days unless the instance is set otherwise. [Keep a copy in sync](/build/keep-in-sync) shows the whole loop. @@ -262,7 +262,7 @@ Run `marfa device hydrate` again. After `catchup_too_old`, `marfa device status` An app built on the Swift SDK that holds a `changes()` stream refills a stale copy by itself. [Refill a stale copy](/swift-sdk#refill-a-stale-copy) says how. -For a folder, run `marfa folders push` again instead: it builds the copy again itself. [Sync a folder](/guides/sync-a-folder#replace-a-folders-key) covers a key you replaced. +For a folder, run `marfa folders push` again instead: it builds the copy again itself. [Sync a folder](/run/sync-a-folder#replace-a-folders-key) covers a key you replaced. @@ -289,7 +289,7 @@ The message says how many writes the file holds that the server has not taken: w The command also leaves the store as it is in two more cases. If the store keeps its record of the files in a shape the newer build can't carry, remove the folder with the build that made the store, and add it again with the newer build. If the store names no folder, add the folder again with `marfa folders add`, which replaces the store. -`marfa folders status` only reads, so it reports the refusal with the same advice and changes nothing. [Sync a folder](/guides/sync-a-folder) explains a folder's state. +`marfa folders status` only reads, so it reports the refusal with the same advice and changes nothing. [Sync a folder](/run/sync-a-folder) explains a folder's state. 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`. @@ -402,7 +402,7 @@ The profile's `key_command` gave the CLI no key. The command ends with exit code - **`the server says this is not a key`, exit `1`:** it's an app's access token, which ends within the hour. `marfa login` keeps a sign-in that renews it. - **`no key to keep`:** the CLI found no key. Set `MARFA_API_KEY`, write the key on standard input, or pass `--key`. -[Manage keys](/guides/manage-keys) shows how to make a key and read what it holds. +[Manage keys](/run/manage-keys) shows how to make a key and read what it holds. diff --git a/content/docs/guides/upgrade.mdx b/content/docs/run/upgrade.mdx similarity index 87% rename from content/docs/guides/upgrade.mdx rename to content/docs/run/upgrade.mdx index 91c71109..870dfaeb 100644 --- a/content/docs/guides/upgrade.mdx +++ b/content/docs/run/upgrade.mdx @@ -5,7 +5,7 @@ description: Check a new build against your database while keeping a way back to A build **never upgrades a database in place**. It refuses a database whose schema differs from its own or that holds retired tables or settings. If a new build refuses your database, keep the previous build and its data. An archive from it may be refused by a different build, so check a restore on separate storage before deciding to switch. -[`marfa server upgrade`](/guides/install#change-upgrade-or-remove-it) moves a server that `marfa server install` set up to a release, and does not move data. When the [changelog](/changelog) says a release changes the database's shape, follow the steps below instead. +[`marfa server upgrade`](/run/install#change-upgrade-or-remove-it) moves a server that `marfa server install` set up to a release, and does not move data. When the [changelog](/changelog) says a release changes the database's shape, follow the steps below instead. ## What a new build does with your database @@ -26,7 +26,7 @@ Use these steps to test a move when the new build refuses your database. A build ### Export with the running build -Follow [Export an archive](/guides/export-and-restore#export-an-archive). **Stop your apps first**, so the archive holds every write, and check that the file unpacks. +Follow [Export an archive](/run/export-and-restore#export-an-archive). **Stop your apps first**, so the archive holds every write, and check that the file unpacks. Note which build this is. `GET /` answers its `release` beside `version`, which is the commit, and `marfa status` shows both. **The image of that release, `ghcr.io/withmarfa/marfa-server:`, stays available under that tag**, so you can start the old build again at any time, even on a machine that has pulled a newer one. A build made from a clone has no `release`, so note its `version` and keep that clone. @@ -57,14 +57,14 @@ Try the restore as the owner, through the private socket of the new instance. ** marfa --socket /srv/marfa/data/control/marfa.sock restore marfa-archive.tar.gz ``` -[Restore an archive](/guides/export-and-restore#restore-an-archive) explains the answer. +[Restore an archive](/run/export-and-restore#restore-an-archive) explains the answer. ### Recreate what an archive doesn't hold -Make your keys again, register your webhooks again, set the configuration again and upload the owner's picture again: **an archive carries none of them**. Sign your apps in again too. Restored app connections are revoked historical records, and their items carry `restored:` sources rather than the sources of the apps you reconnect. [Manage keys](/guides/manage-keys), [Settings](/guides/settings) and [Change the owner's profile](/guides/sign-in-as-the-owner#change-the-owners-profile) cover them. +Make your keys again, register your webhooks again, set the configuration again and upload the owner's picture again: **an archive carries none of them**. Sign your apps in again too. Restored app connections are revoked historical records, and their items carry `restored:` sources rather than the sources of the apps you reconnect. [Manage keys](/run/manage-keys), [Settings](/run/settings) and [Change the owner's profile](/run/sign-in-as-the-owner#change-the-owners-profile) cover them. diff --git a/src/components/section-nav.test.ts b/src/components/section-nav.test.ts index a0329005..41bb879b 100644 --- a/src/components/section-nav.test.ts +++ b/src/components/section-nav.test.ts @@ -6,7 +6,10 @@ describe('sectionFor', () => { expect(sectionFor('/overview')).toBe('Overview'); expect(sectionFor('/files')).toBe('Overview'); expect(sectionFor('/glossary')).toBe('Overview'); - expect(sectionFor('/guides/deploy')).toBe('Guides'); + expect(sectionFor('/build')).toBe('Build'); + expect(sectionFor('/build/swift-app')).toBe('Build'); + expect(sectionFor('/run')).toBe('Run'); + expect(sectionFor('/run/deploy')).toBe('Run'); expect(sectionFor('/catalog')).toBe('Catalog'); expect(sectionFor('/catalog/media')).toBe('Catalog'); expect(sectionFor('/docs-mcp-server')).toBe('Overview'); @@ -21,7 +24,8 @@ describe('sectionFor', () => { it('leaves a path that only starts like a Reference page in the Overview', () => { expect(sectionFor('/client')).toBe('Overview'); expect(sectionFor('/clix')).toBe('Overview'); - expect(sectionFor('/guidesx')).toBe('Overview'); + expect(sectionFor('/buildx')).toBe('Overview'); + expect(sectionFor('/running')).toBe('Overview'); expect(sectionFor('/catalogx')).toBe('Overview'); }); }); diff --git a/src/components/section-nav.tsx b/src/components/section-nav.tsx index d8ef9b77..d4dd7774 100644 --- a/src/components/section-nav.tsx +++ b/src/components/section-nav.tsx @@ -26,7 +26,8 @@ function isUnder(pathname: string, prefix: string) { // the sidebar by URL; these pills are the visible switcher. const SECTIONS: { label: string; href: string; matches: ((pathname: string) => boolean) | null }[] = [ { label: 'Overview', href: '/overview', matches: null }, - { label: 'Guides', href: '/guides/swift-app', matches: (pathname) => isUnder(pathname, '/guides') }, + { label: 'Build', href: '/build', matches: (pathname) => isUnder(pathname, '/build') }, + { label: 'Run', href: '/run', matches: (pathname) => isUnder(pathname, '/run') }, { label: 'Catalog', href: '/catalog', matches: (pathname) => isUnder(pathname, '/catalog') }, { label: 'Reference', diff --git a/src/lib/catalog.test.ts b/src/lib/catalog.test.ts index 9b93f29e..75b87859 100644 --- a/src/lib/catalog.test.ts +++ b/src/lib/catalog.test.ts @@ -648,7 +648,7 @@ describe('the links', () => { if (!file.includes('/catalog/') && !target.startsWith('/catalog')) continue; const page = target.startsWith('/catalog') ? PAGES.find((candidate) => candidate.slug === (target.split('/').at(-1) === 'catalog' ? 'index' : target.split('/').at(-1)))?.file ?? target - : `content/docs${target.startsWith('/guides') ? target : `/(overview)${target}`}.mdx`; + : `content/docs${/^\/(build|run)\//.test(target) ? target : `/(overview)${target}`}.mdx`; const ids = headingIds(page); checked += 1; if (!ids?.has(id)) broken.push(`${file}: ${target}#${id}`); diff --git a/src/lib/command-check-allowlist.json b/src/lib/command-check-allowlist.json index 0fc59b09..97132963 100644 --- a/src/lib/command-check-allowlist.json +++ b/src/lib/command-check-allowlist.json @@ -40,27 +40,27 @@ "reason": "The changelog names an option that a release removed." }, { - "page": "guides/deploy.mdx", + "page": "run/deploy.mdx", "text": "--import", "reason": "A Node option, for the command that starts the server." }, { - "page": "guides/list-filter-search.mdx", + "page": "build/list-filter-search.mdx", "text": "--data-urlencode", "reason": "A curl option, in the HTTP tab's request." }, { - "page": "guides/run-a-connector.mdx", + "page": "run/run-a-connector.mdx", "text": "--once", "reason": "An option of the connector program, which is not the CLI." }, { - "page": "guides/run-a-connector.mdx", + "page": "run/run-a-connector.mdx", "text": "--every", "reason": "An option of the connector program, which is not the CLI." }, { - "page": "guides/google-project.mdx", + "page": "run/google-project.mdx", "text": "--setup", "reason": "An option of the connector program, which is not the CLI." } diff --git a/src/lib/content-lint.test.ts b/src/lib/content-lint.test.ts index be212f07..ac295886 100644 --- a/src/lib/content-lint.test.ts +++ b/src/lib/content-lint.test.ts @@ -667,7 +667,7 @@ describe('content lint: each rule fires on a bad sample', () => { }); it('coming-soon', () => { - expect(rules(checkComingSoon('guides/x.mdx', 'This is Coming Soon.'))).toEqual(['coming-soon']); + expect(rules(checkComingSoon('build/x.mdx', 'This is Coming Soon.'))).toEqual(['coming-soon']); expect(checkComingSoon('(overview)/introduction.mdx', 'Coming soon.')).toEqual([]); expect(rules(checkComingSoon('(overview)/run-an-instance.mdx', 'Coming soon.'))).toEqual(['coming-soon']); }); @@ -721,7 +721,7 @@ describe('content lint: each rule fires on a bad sample', () => { }); it('fires on a relative link', () => { - for (const dest of ['files', './files', '../guides/deploy', 'files.md']) { + for (const dest of ['files', './files', '../run/deploy', 'files.md']) { const found = checkLinks('x.mdx', `Ok.\nSee [it](${dest}).`, urls); expect(rules(found)).toEqual(['link']); expect(found[0].line).toBe(2); @@ -756,7 +756,7 @@ describe('content lint: each rule fires on a bad sample', () => { const cwd = '/repo'; const files = new Map([ ['/repo/src/generated/rules.md', '# Rules\n\nIntro.\n\n## One\nFirst — dash.\n### Two\nSecond.\n## One\nThird.\n'], - ['/repo/content/docs/guides/local.md', 'Local — text.\n'], + ['/repo/content/docs/build/local.md', 'Local — text.\n'], ['/repo/src/generated/fenced.md', '## A\n```\n## B\n```\nIn A.\n## C\nIn C.\n'], ['/repo/src/generated/custom.md', '## Words [#custom-id]\nBody.\n## `Code` and [a link](/x)\nMore.\n'], ['/repo/src/generated/front.md', '---\ntitle: T — U\n---\nBody.\n'], @@ -766,14 +766,14 @@ describe('content lint: each rule fires on a bad sample', () => { ['/repo/src/generated/code.ts', 'export const x = 1; // —\n'], ]); const read = (absolutePath: string) => files.get(absolutePath); - const resolve = (text: string, page = 'guides/page.mdx') => + const resolve = (text: string, page = 'build/page.mdx') => resolveIncludes(page, `/repo/content/docs/${page}`, text, cwd, read); const lines = (included: Included[]) => included.map((i) => i.text.split('\n').map((l) => l.trim())); it('resolves a path from the site root with cwd and from the page without it', () => { const root = resolve('src/generated/rules.md'); expect(root.violations).toEqual([]); - expect(root.included.map((i) => i.file)).toEqual(['src/generated/rules.md (included by guides/page.mdx)']); + expect(root.included.map((i) => i.file)).toEqual(['src/generated/rules.md (included by build/page.mdx)']); const local = resolve('./local.md'); expect(local.violations).toEqual([]); expect(local.included[0].text).toBe('Local — text.\n'); @@ -811,8 +811,8 @@ describe('content lint: each rule fires on a bad sample', () => { const { included, violations } = resolve('src/generated/outer.md'); expect(violations).toEqual([]); expect(included.map((i) => i.file)).toEqual([ - 'src/generated/outer.md (included by guides/page.mdx)', - 'src/generated/inner.md (included by src/generated/outer.md (included by guides/page.mdx))', + 'src/generated/outer.md (included by build/page.mdx)', + 'src/generated/inner.md (included by src/generated/outer.md (included by build/page.mdx))', ]); }); @@ -837,7 +837,7 @@ describe('content lint: each rule fires on a bad sample', () => { const { included } = resolve('src/generated/rules.md'); const found = checkIncluded(included[0].file, included[0].text, urls); expect(rules(found)).toEqual(['em-dash']); - expect(found[0].file).toBe('src/generated/rules.md (included by guides/page.mdx)'); + expect(found[0].file).toBe('src/generated/rules.md (included by build/page.mdx)'); expect(found[0].line).toBe(6); }); @@ -867,7 +867,7 @@ describe('content lint: each rule fires on a bad sample', () => { }); it('failure messages name the file, the line and the rule', () => { - const [violation] = checkEmDashes('guides/x.mdx', 'a\nb — c'); - expect(format(violation)).toMatch(/^guides\/x\.mdx:2 \[em-dash\] /); + const [violation] = checkEmDashes('build/x.mdx', 'a\nb — c'); + expect(format(violation)).toMatch(/^build\/x\.mdx:2 \[em-dash\] /); }); }); diff --git a/src/lib/docs-api.test.ts b/src/lib/docs-api.test.ts index 4e1614a5..364845e0 100644 --- a/src/lib/docs-api.test.ts +++ b/src/lib/docs-api.test.ts @@ -13,7 +13,7 @@ describe('GET /api/docs/search', () => { expect(res.status).toBe(200); expect(res.headers.get('content-type')).toContain('application/json'); const body = (await res.json()) as { hits: { title: string; url: string; snippets: string[] }[] }; - expect(body.hits.map((h) => h.url)).toContain('/guides/housekeeping'); + expect(body.hits.map((h) => h.url)).toContain('/run/housekeeping'); }); it('honors limit', async () => { diff --git a/src/lib/docs-map.test.ts b/src/lib/docs-map.test.ts index ed4b797e..71fd7d43 100644 --- a/src/lib/docs-map.test.ts +++ b/src/lib/docs-map.test.ts @@ -21,22 +21,24 @@ describe('buildDocsMap', () => { const map = buildDocsMap(); const lines = map.split('\n'); - it('lists the four tabs in order, as H2 headings', () => { - expect(lines.filter((line) => line.startsWith('## '))).toEqual(['## Overview', '## Guides', '## Catalog', '## Reference']); + it('lists the five tabs in order, as H2 headings', () => { + expect(lines.filter((line) => line.startsWith('## '))).toEqual(['## Overview', '## Build', '## Run', '## Catalog', '## Reference']); }); it('groups each tab as the sidebar does', () => { - const overview = lines.slice(lines.indexOf('## Overview'), lines.indexOf('## Guides')); + const overview = lines.slice(lines.indexOf('## Overview'), lines.indexOf('## Build')); expect(overview.filter((line) => line.startsWith('### '))).toEqual(['### Get started', '### Concepts', '### AI tools']); expect(overview.some((line) => line.startsWith('- [Docs MCP server](/docs-mcp-server)'))).toBe(true); expect(overview.some((line) => line.startsWith('- [Overview](/overview)'))).toBe(true); expect(overview.some((line) => line.startsWith('- [Glossary](/glossary)'))).toBe(true); - const guides = lines.slice(lines.indexOf('## Guides'), lines.indexOf('## Catalog')); - expect(guides).toContain('### Build apps and agents'); - expect(guides).not.toContain('### AI agents'); - expect(guides.some((line) => line.startsWith('- [Use Marfa with AI agents](/guides/ai-agents)'))).toBe(true); - expect(guides.some((line) => line.startsWith('- [Troubleshooting](/guides/troubleshooting)'))).toBe(true); + const build = lines.slice(lines.indexOf('## Build'), lines.indexOf('## Run')); + expect(build.filter((line) => line.startsWith('### '))).toEqual(['### Tutorials', '### Build apps and agents', '### Build connectors']); + expect(build.some((line) => line.startsWith('- [Use Marfa with AI agents](/build/ai-agents)'))).toBe(true); + + const run = lines.slice(lines.indexOf('## Run'), lines.indexOf('## Catalog')); + expect(run.filter((line) => line.startsWith('### '))).toEqual(['### Set up', '### Keys and access', '### Folders', '### Connectors', '### Look after it']); + expect(run.some((line) => line.startsWith('- [Troubleshooting](/run/troubleshooting)'))).toBe(true); // The Catalog has no group headings: the Types and Connectors pages are folders, so their pages nest under them. const catalog = lines.slice(lines.indexOf('## Catalog'), lines.indexOf('## Reference')); diff --git a/src/lib/docs-search.test.ts b/src/lib/docs-search.test.ts index dba01912..81eea697 100644 --- a/src/lib/docs-search.test.ts +++ b/src/lib/docs-search.test.ts @@ -25,54 +25,54 @@ const EXPECTED: [query: string, pages: string[]][] = [ // Errors, headers and writes. ['409', ['/api-reference/errors']], - ['version_conflict', ['/api-reference/errors', '/guides/write-safely']], + ['version_conflict', ['/api-reference/errors', '/build/write-safely']], ['type_not_permitted', ['/api-reference/errors']], ['unauthorized', ['/api-reference/errors']], - ['idempotency key', ['/guides/write-safely']], - ['retry a write safely', ['/guides/write-safely']], + ['idempotency key', ['/build/write-safely']], + ['retry a write safely', ['/build/write-safely']], [ 'rate limit 429', - ['/guides/troubleshooting', '/guides/folder-safeguards', '/guides/connector-holds-and-retries', '/guides/settings'], + ['/run/troubleshooting', '/run/folder-safeguards', '/build/connector-holds-and-retries', '/run/settings'], ], - ['retry-after', ['/guides/connector-webhooks', '/guides/connector-holds-and-retries', '/guides/keep-in-sync']], - ['CORS', ['/guides/settings']], + ['retry-after', ['/build/connector-webhooks', '/build/connector-holds-and-retries', '/build/keep-in-sync']], + ['CORS', ['/run/settings']], // Reading and listing. - ['pagination cursor', ['/api-reference', '/guides/list-filter-search']], - ['next_cursor', ['/guides/list-filter-search', '/api-reference']], - ['list items by tag', ['/guides/list-filter-search']], - ['filter items by property', ['/guides/list-filter-search']], + ['pagination cursor', ['/api-reference', '/build/list-filter-search']], + ['next_cursor', ['/build/list-filter-search', '/api-reference']], + ['list items by tag', ['/build/list-filter-search']], + ['filter items by property', ['/build/list-filter-search']], // Webhooks and connectors. - ['webhook signature', ['/guides/connector-webhooks', '/guides/receive-webhooks']], - ['hmac', ['/guides/receive-webhooks']], + ['webhook signature', ['/build/connector-webhooks', '/build/receive-webhooks']], + ['hmac', ['/build/receive-webhooks']], // Sign-in, keys and the command line. - ['oauth device flow', ['/guides/authenticate']], + ['oauth device flow', ['/build/authenticate']], ['marfa login', ['/cli']], - ['create type scaffold', ['/cli', '/guides/define-a-type']], - ['MARFA_API_KEY', ['/cli', '/guides/manage-keys']], - ['rotate a key', ['/guides/manage-keys']], - ['passkey', ['/guides/sign-in-as-the-owner']], + ['create type scaffold', ['/cli', '/build/define-a-type']], + ['MARFA_API_KEY', ['/cli', '/run/manage-keys']], + ['rotate a key', ['/run/manage-keys']], + ['passkey', ['/run/sign-in-as-the-owner']], ['json output', ['/cli']], ['exit code 5', ['/cli']], ['print-token', ['/cli']], ['--print-token', ['/cli']], // Running an instance. - ['environment variables', ['/guides/settings']], - ['docker compose', ['/guides/install', '/guides/deploy']], - ['litestream', ['/guides/deploy', '/guides/back-up-and-restore']], - ['s3 bucket', ['/guides/settings', '/guides/deploy', '/guides/install', '/guides/back-up-and-restore']], - ['upgrade server', ['/guides/upgrade']], - ['change instance name', ['/guides/settings']], - ['install marfa on linux', ['/cli', '/guides/install', '/quickstart']], - ['how do I back up my data', ['/guides/back-up-and-restore', '/guides/export-and-restore']], - ['restore', ['/guides/back-up-and-restore', '/guides/export-and-restore']], + ['environment variables', ['/run/settings']], + ['docker compose', ['/run/install', '/run/deploy']], + ['litestream', ['/run/deploy', '/run/back-up-and-restore']], + ['s3 bucket', ['/run/settings', '/run/deploy', '/run/install', '/run/back-up-and-restore']], + ['upgrade server', ['/run/upgrade']], + ['change instance name', ['/run/settings']], + ['install marfa on linux', ['/cli', '/run/install', '/quickstart']], + ['how do I back up my data', ['/run/back-up-and-restore', '/run/export-and-restore']], + ['restore', ['/run/back-up-and-restore', '/run/export-and-restore']], // Files. - ['blob upload', ['/guides/handle-files', '/api-reference/blobs/uploadBlob']], - ['attach a file to a note', ['/guides/handle-files']], + ['blob upload', ['/build/handle-files', '/api-reference/blobs/uploadBlob']], + ['attach a file to a note', ['/build/handle-files']], // A dotted type key, and a word that is the end of one. ['core.task', ['/catalog/task']], @@ -80,13 +80,13 @@ const EXPECTED: [query: string, pages: string[]][] = [ ['core.note', ['/catalog/note']], // A flag that is also an ordinary word, and a word that starts a permission scope (`webhooks.manage`). - ['--force', ['/guides/change-a-type']], + ['--force', ['/build/change-a-type']], ['--scope', ['/docs-mcp-server']], - ['webhooks', ['/events', '/guides/receive-webhooks']], - ['audit', ['/guides/audit-log']], + ['webhooks', ['/events', '/build/receive-webhooks']], + ['audit', ['/run/audit-log']], // An apostrophe is part of a word, not a break in it. - ["can't overwrite", ['/guides/ai-agents', '/guides/export-and-restore']], + ["can't overwrite", ['/build/ai-agents', '/run/export-and-restore']], ]; /** @@ -289,7 +289,7 @@ describe('snippets', () => { it('say something about the match, in a short passage', async () => { const { hits } = await searchDocs('rotate a key'); - const keys = hits.find((h) => h.url === '/guides/manage-keys')!; + const keys = hits.find((h) => h.url === '/run/manage-keys')!; expect(keys.snippets[0]).not.toBe('Rotate a key'); expect(keys.snippets[0].length).toBeGreaterThan(30); for (const hit of hits) for (const snippet of hit.snippets) expect(snippet.length).toBeLessThanOrEqual(400); diff --git a/src/lib/docs-structure.test.ts b/src/lib/docs-structure.test.ts index a065c16f..488cafec 100644 --- a/src/lib/docs-structure.test.ts +++ b/src/lib/docs-structure.test.ts @@ -69,7 +69,7 @@ describe('the server image commands', () => { const including = ALL_PAGES.filter(({ text }) => text.includes('src/generated/server-image.md'), ).map(({ file }) => file); - expect(including).toEqual([path.join('guides', 'deploy.mdx')]); + expect(including).toEqual([path.join('run', 'deploy.mdx')]); }); it('name the latest release and publish the port on loopback only', () => { diff --git a/src/lib/docs-tools.test.ts b/src/lib/docs-tools.test.ts index afb6cf94..3fd9feec 100644 --- a/src/lib/docs-tools.test.ts +++ b/src/lib/docs-tools.test.ts @@ -19,13 +19,13 @@ describe('searchDocs', () => { const urls = hits.map((h) => h.url); expect(new Set(urls).size).toBe(urls.length); - expect(urls.filter((u) => u === '/guides/housekeeping')).toHaveLength(1); + expect(urls.filter((u) => u === '/run/housekeeping')).toHaveLength(1); for (const hit of hits) { expect(hit.url).not.toContain('#'); expect(hit.snippets.length).toBeLessThanOrEqual(MAX_SNIPPETS_PER_PAGE); } - const page = hits.find((h) => h.url === '/guides/housekeeping')!; + const page = hits.find((h) => h.url === '/run/housekeeping')!; expect(page.title).toBe('Housekeeping'); expect(page.snippets.length).toBeGreaterThan(0); }); @@ -86,12 +86,12 @@ describe('pathToSlugs', () => { describe('fetchDocsPage', () => { it('returns the Markdown of a page, whichever way the path is written', async () => { - const page = await fetchDocsPage('/guides/housekeeping'); + const page = await fetchDocsPage('/run/housekeeping'); expect(page).not.toBeNull(); - expect(page).toMatch(/^# Housekeeping \(\/guides\/housekeeping\)\n/); + expect(page).toMatch(/^# Housekeeping \(\/run\/housekeeping\)\n/); expect(page).toContain('housekeeping jobs'); - for (const form of ['guides/housekeeping', '/guides/housekeeping.md', '/guides/housekeeping/', '/docs/guides/housekeeping']) { + for (const form of ['run/housekeeping', '/run/housekeeping.md', '/run/housekeeping/', '/docs/run/housekeeping']) { expect(await fetchDocsPage(form), form).toBe(page); } }); @@ -110,12 +110,12 @@ describe('fetchDocsPage', () => { const quickstart = await fetchDocsPage('/quickstart'); expect(quickstart).toContain('marfa items create --type core.note'); expect(quickstart).toContain('Output:'); - expect(await fetchDocsPage('/guides/write-safely')).toContain('**TypeScript**'); + expect(await fetchDocsPage('/build/write-safely')).toContain('**TypeScript**'); expect(await fetchDocsPage('/cli-reference')).toBeNull(); expect(await fetchDocsPage('/docs-for-agents')).toBeNull(); expect(await fetchDocsPage('/cli/items')).toBeNull(); expect(await fetchDocsPage('/typescript-client')).toBeNull(); - expect(await fetchDocsPage('/guides/docs-mcp-server')).toBeNull(); + expect(await fetchDocsPage('/build/docs-mcp-server')).toBeNull(); expect(await fetchDocsPage('/get-started/files')).toBeNull(); expect(await fetchDocsPage('/tools/marfa-command')).toBeNull(); }); @@ -134,12 +134,12 @@ describe('listTopics', () => { it('lists every page with its title, url, description and breadcrumbs', () => { const { pages } = listTopics(); expect(pages.length).toBeGreaterThan(0); - const housekeeping = pages.find((p) => p.url === '/guides/housekeeping'); + const housekeeping = pages.find((p) => p.url === '/run/housekeeping'); expect(housekeeping).toEqual({ title: 'Housekeeping', - url: '/guides/housekeeping', + url: '/run/housekeeping', description: expect.any(String), - breadcrumbs: ['guides', 'housekeeping'], + breadcrumbs: ['run', 'housekeeping'], }); }); }); diff --git a/src/lib/page-growth.test.ts b/src/lib/page-growth.test.ts index 603413aa..e3bcab8f 100644 --- a/src/lib/page-growth.test.ts +++ b/src/lib/page-growth.test.ts @@ -20,8 +20,8 @@ describe('grewTooMuch', () => { describe('growthWarning', () => { it('is a workflow annotation on the file, pointing to STYLE.md', () => { - expect(growthWarning('content/docs/guides/files.mdx', 4000, 6000)).toBe( - '::warning file=content/docs/guides/files.mdx::This page grew from 4000 to 6000 characters (50 percent longer). ' + + expect(growthWarning('content/docs/build/files.mdx', 4000, 6000)).toBe( + '::warning file=content/docs/build/files.mdx::This page grew from 4000 to 6000 characters (50 percent longer). ' + 'Check whether the new text belongs in the API reference or the contract instead, ' + 'or replaces something already here: STYLE.md, "Keeping pages current".', ); @@ -38,14 +38,14 @@ describe('parseChangedPages', () => { 'M\tcontent/docs/a.mdx', 'A\tcontent/docs/added.mdx', 'D\tcontent/docs/deleted.mdx', - 'R087\tcontent/docs/old.mdx\tcontent/docs/guides/new.mdx', + 'R087\tcontent/docs/old.mdx\tcontent/docs/build/new.mdx', 'M\tcontent/docs/meta.json', 'M\tREADME.md', '', ].join('\n'); expect(parseChangedPages(status)).toEqual([ { path: 'content/docs/a.mdx', basePath: 'content/docs/a.mdx' }, - { path: 'content/docs/guides/new.mdx', basePath: 'content/docs/old.mdx' }, + { path: 'content/docs/build/new.mdx', basePath: 'content/docs/old.mdx' }, ]); }); }); diff --git a/src/lib/staleness-report.test.ts b/src/lib/staleness-report.test.ts index 32b668b7..93bcd487 100644 --- a/src/lib/staleness-report.test.ts +++ b/src/lib/staleness-report.test.ts @@ -179,18 +179,18 @@ describe('findStale', () => { })()), }; const pages = { - 'content/docs/guides/files.mdx': [ + 'content/docs/build/files.mdx': [ `[a](${SPEC}/blobs.md#blobslend-earlier-version) [b](${SPEC}/blobs.md#blobsgone)`, `[c](${SPEC}/blobs.md#blobsupload-hash) [d](/api-reference/items/listItems) [e](/api-reference/types/listTypes)`, ].join('\n'), - 'content/docs/guides/chapter.mdx': `[blobs](${SPEC}/blobs.md) [items](${SPEC}/items.md) [x](${SPEC}/blobs.md#uploading)`, - 'content/docs/guides/quiet.mdx': `[c](${SPEC}/blobs.md#blobsupload-hash) [items](${SPEC}/items.md) [op](/api-reference/items/createItem)`, + 'content/docs/build/chapter.mdx': `[blobs](${SPEC}/blobs.md) [items](${SPEC}/items.md) [x](${SPEC}/blobs.md#uploading)`, + 'content/docs/build/quiet.mdx': `[c](${SPEC}/blobs.md#blobsupload-hash) [items](${SPEC}/items.md) [op](/api-reference/items/createItem)`, }; it('lists a page under each rule or operation it cites that changed or went away', () => { expect(findStale(pages, comparison).pages).toEqual([ { - path: 'content/docs/guides/files.mdx', + path: 'content/docs/build/files.mdx', rulesChanged: ['blobs/lend-earlier-version'], rulesRemoved: ['blobs/gone'], operationsChanged: ['listItems'], @@ -202,13 +202,13 @@ describe('findStale', () => { it('lists a page that cites a chapter, or a heading in it that is no rule, only when a rule in it changed', () => { const { chapterPages } = findStale(pages, comparison); expect(chapterPages.map((page) => [page.path, page.chapters.map((c) => c.chapter)])).toEqual([ - ['content/docs/guides/chapter.mdx', ['blobs']], + ['content/docs/build/chapter.mdx', ['blobs']], ]); }); it('does not list a page that cites only what did not change', () => { const { pages: listed, chapterPages } = findStale( - { 'content/docs/quiet.mdx': pages['content/docs/guides/quiet.mdx'] }, + { 'content/docs/quiet.mdx': pages['content/docs/build/quiet.mdx'] }, comparison, ); expect(listed).toEqual([]); From a9c1fe96fa5405ebf01698f45fb87ab3659647e1 Mon Sep 17 00:00:00 2001 From: August Cayzer Date: Sun, 25 Oct 2026 18:22:23 +0000 Subject: [PATCH 2/2] docs(build): point Build to keys and tidy the landing pages --- content/docs/build/authenticate.mdx | 2 +- content/docs/build/index.mdx | 4 ++-- content/docs/run/index.mdx | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/content/docs/build/authenticate.mdx b/content/docs/build/authenticate.mdx index b5203828..a2cf827c 100644 --- a/content/docs/build/authenticate.mdx +++ b/content/docs/build/authenticate.mdx @@ -9,7 +9,7 @@ No program ever needs **the owner's password**. [Access](/access) explains what ## Send a key -Send the key in the `Authorization` header as a bearer token. [Get the current API key](/api-reference/api-keys/getCurrentKey) checks one by answering what it holds. +The owner makes a key as [Manage keys](/run/manage-keys) shows. Send it in the `Authorization` header as a bearer token. [Get the current API key](/api-reference/api-keys/getCurrentKey) checks one by answering what it holds. ```bash tab="CLI" tab-group="client" export MARFA_API_URL=http://localhost:8600 diff --git a/content/docs/build/index.mdx b/content/docs/build/index.mdx index 30ed6049..cd4b1ca0 100644 --- a/content/docs/build/index.mdx +++ b/content/docs/build/index.mdx @@ -3,13 +3,13 @@ title: Build on Marfa description: Build an app, an agent or a connector that reads and writes your data with a key of its own. --- -Marfa keeps what you save. Your app, agent or connector reads and writes it through the API with a key, and sees only what that key allows. A tutorial takes you through one complete piece of work, and each guide does one task. +Your app, agent or connector reads and writes your data through the API with a key, and sees only what that key allows. A tutorial takes you through one complete piece of work, and each guide does one task. You need an instance and a key: the [Quickstart](/quickstart) starts one, and [Manage keys](/run/manage-keys) makes more. An iPhone and Mac app that lists your notes and keeps working offline. A program that brings records from another service into Marfa. A command that saves an item to your instance, with a key that reaches only notes. - A key of its own, so an AI agent works with your notes and nothing else. + A key of its own, so an AI agent works with your notes and nothing else. Register a type for items no shipped type fits. Follow the event stream so a copy of your data stays current. diff --git a/content/docs/run/index.mdx b/content/docs/run/index.mdx index 8e3a511a..990952b5 100644 --- a/content/docs/run/index.mdx +++ b/content/docs/run/index.mdx @@ -1,12 +1,12 @@ --- title: Run Marfa -description: Start an instance, keep it safe and look after it, without writing any code. +description: Install an instance, claim it, give your apps their keys, and keep a copy of it. --- -An instance is yours to run: you start it, claim it, give your apps and scripts their keys, and keep a copy of it. These pages cover that work, from the first install to an upgrade, and they need no code. To write code against an instance, go to [Build](/build). +An instance is yours to run, so installing, upgrading and backing it up fall to you. These pages cover that work, from the first install to an upgrade, mostly with the CLI and a browser. To write code against an instance, go to [Build](/build). - Take a Mac or Linux machine with Docker to an instance ready to claim. + Take a Mac or Linux machine with Docker to an instance ready to claim. Run an instance you keep, with storage that survives a restart and an address your devices reach. Sign in to the owner's pages with a password or a passkey. Give a script a key that reaches only what it needs, then narrow, rotate or revoke it.