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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions content/docs/(reference)/changelog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,23 @@ Each release of Marfa ships the server and its container image, the CLI for macO

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

## v0.0.15

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

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`. `edges_skipped_reasons` now names only `already_present`, `endpoint_missing`, `malformed`, or the error code `POST /edges` would answer for that edge. [Back up and restore](/guides/back-up-and-restore#restore-an-archive) covers it.
- **A backup restores when its types refer to each other.** Types that name each other in `compatible_with`, and a type whose display hints, links or merge fields name fields it inherits, restore in any order. Marfa also refuses a change to a type that would break another type's `compatible_with` claim, so an archive Marfa wrote stays restorable.
- **A first request with a key no longer fails with 503 during a long restore or bulk job.** A read with a key that was new to the process, or the first after a restart, waited behind the job and could answer `503 write_contention`. Marfa now records a key's last use in the background, so `last_used_at` can lag behind, or stay `null` after a busy moment.
- **A bulk job ends when the key that started it is revoked.** It ends `failed` and writes nothing more, where it used to stay `in_progress`. A text-extraction run or a webhook scheduling run started by hand no longer fails for the same reason.

For apps and devices:

- **A read answers from the data it checked.** When an item changed type during a read, the read could return metadata, such as a private tag, to a key that could no longer read the item. The same applied to the link between an occurrence and its series. A read now checks permission against the data it returns.
- **Replacing or deleting a type that another type depends on can be refused.** A replacement that breaks another type's `compatible_with` claim, and a deletion of a type that other types name in `compatible_with`, are refused with `422 compatible_with_violation`, and `details.dependent_type_ids` names the dependents. `force=true` does not bypass it. [Define a type](/guides/define-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 does not, so a connector can still update its type after its permissions narrow. [Define a type](/guides/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)
Expand Down
2 changes: 1 addition & 1 deletion content/docs/catalog/your-own-types.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ A type can't declare a field with one of these names, because every item already
- **Parent:** `parent` names a registered type. Types can't form a loop, and no type has more than ten ancestors ([`types/parent-unknown`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typesparent-unknown), [`types/parent-chain`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typesparent-chain)).
- **What a subtype can do:** it takes every field of its parent. It can add fields, redeclare an inherited field unchanged or with a new `description`, and make an inherited field required ([`types/child-adds`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typeschild-adds)).
- **What it can't do:** change an inherited field's `type`, `format`, `items_type`, `searchable`, `maxLength`, `maxItems` or `enum_values`, or make a required field optional. Marfa answers `400 inheritance_violation` ([`types/child-shape`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typeschild-shape)).
- **Without inheriting:** `compatible_with` names a type that yours is readable as. Yours must require every field that type requires. Each field of that type that yours declares or inherits must be readable as that type's: the same kind, or `url`, `email`, `datetime`, `date`, `thumbnail` or `enum` where that type's is a `string`, or `integer` where it is a `number`; for an `array`, the same `items_type`; for an `enum`, values drawn from that type's; and, where both name a `format`, the same one. Marfa refuses a claim that doesn't hold with `422 compatible_with_violation` ([`types/compatible-with`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typescompatible-with), [`types/compatible-with-refused`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typescompatible-with-refused)).
- **Without inheriting:** `compatible_with` names a type that yours is readable as. Yours must require every field that type requires. Each field of that type that yours declares or inherits must be readable as that type's: the same kind, or `url`, `email`, `datetime`, `date`, `thumbnail` or `enum` where that type's is a `string`, or `integer` where it is a `number`; for an `array`, the same `items_type`; for an `enum`, values drawn from that type's; and, where both name a `format`, the same one. Marfa refuses a claim that doesn't hold with `422 compatible_with_violation` ([`types/compatible-with`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typescompatible-with), [`types/compatible-with-refused`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typescompatible-with-refused)). It also refuses a replacement or deletion that would break another type's claim, with the same code and `details.dependent_type_ids` ([`types/compatible-with-preserved`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typescompatible-with-preserved)). Adding a parent or a claim needs write on that type, and keeping one doesn't ([`types/parent-reach`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typesparent-reach), [`types/compatible-with-reach`](https://github.com/withmarfa/marfa/blob/main/conformance/spec/types.md#typescompatible-with-reach)).

## Roles

Expand Down
2 changes: 2 additions & 0 deletions content/docs/guides/back-up-and-restore.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@ Items keep their IDs, versions, dates and states, with every earlier version and

A restored `system.connection` item for an app has `status: revoked`. It records past access rather than reconnecting the app. Sign the app in again if you want it to resume work.

**An edge to an item in the bin is restored**, as it was exported. A restore skips an edge only for one of four reasons, which `edges_skipped_reasons` counts: `already_present`, `endpoint_missing` (the instance holds no item under an endpoint's ID, in the bin or out of it), `malformed`, or the error code that `POST /edges` would answer for the edge, such as `edge_cycle`. An archive's types restore in any order, including types that name each other in `compatible_with`.

An item the instance already holds is counted in `duplicates` and left as it is, so replaying an archive never overwrites work done since. Restoring the same archive again into the same instance answers `"imported": 0` and `"duplicates": 5`.

`blobs_missing` lists the hashes the archive names but doesn't carry bytes for, that no store of the instance holds either. The restore still writes the items, and **writes no record for those blobs**, so a file that names one is a file with no bytes until you upload them again with `marfa blobs upload`. `marfa restore` prints the answer and exits with code 1 and `blobs_missing` when the list isn't empty, naming up to ten of the hashes. The same check covers an archive whose blob entry is missing or doesn't hash to its name.
Expand Down
14 changes: 9 additions & 5 deletions content/docs/guides/define-a-type.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,8 @@ Marfa checks the claim when you register or replace the type, by the rule in [Ty

A claim that doesn't hold is refused with `422 compatible_with_violation`, naming the property and what it should be. **A compatible type inherits nothing**, and a listing of `core.note` leaves recipes out.

**Marfa keeps the claim true afterward.** Marfa refuses a replacement or deletion of a type that you registered if it would break another type's claim, such as a change to a target's properties that leaves `user.recipe` unreadable as it. A platform type such as `core.note` can't be replaced or deleted at all. [Change the type](#change-the-type) and [Delete the type](#delete-the-type) cover the refusals.

## Change the type

`PUT /types/{id}` **replaces the whole definition**, so send everything you want to keep. This adds an optional `category` and sets the type's `version` to 1:
Expand Down Expand Up @@ -290,9 +292,11 @@ await marfa.PUT("/types/{id}", {

**Name the `version` on every replacement:** one that leaves it out sets it back to 0.

**A replacement can't break another type's `compatible_with` claim.** If a change would leave a type that names this one unable to keep its claim, such as by removing a property the claimant relies on, Marfa refuses it with `422 compatible_with_violation`. `details.dependent_type_ids` lists the claimants. Change or remove their claims first.

## Delete the type

`DELETE /types/{id}` removes a type **that no item holds and no other type names as its parent**:
`DELETE /types/{id}` removes a type **that no item holds and no other type names as its parent or in `compatible_with`**:

```bash tab="CLI" tab-group="client"
marfa types delete app.example.receipt
Expand All @@ -309,18 +313,18 @@ await marfa.DELETE("/types/{id}", {
});
```

Marfa refuses to delete a type that another type names as its parent, with `409 type_has_subtypes`, and one that items still hold, in any state and the bin included, with `409 type_in_use`.
Marfa refuses to delete a type that another type names as its parent, with `409 type_has_subtypes`, and one that items still hold, in any state and the bin included, with `409 type_in_use`. A type that other types name in `compatible_with` is refused with `422 compatible_with_violation`, and `details.dependent_type_ids` lists them. Remove their claims first.

<Callout type="warn">
`force=true` (the CLI's `--force`) deletes a type that items still hold. The items stay, but until the type is registered again, Marfa refuses any write to their properties, tier or time, so they can't be edited. You can still tag, archive or move them to the bin.
`force=true` (the CLI's `--force`) deletes a type that items still hold. It doesn't override a subtype or a `compatible_with` claim. The items stay, but until the type is registered again, Marfa refuses any write to their properties, tier or time, so they can't be edited. You can still tag, archive or move them to the bin.
</Callout>

## What your key needs

Registering, replacing and deleting a type each need a key whose type map **grants write on the type's identifier**. Each also needs a grant or a permission of its own:

- **Registering** needs `types` at `write` in the key's metadata map, written `metadata.types:write`. Naming a parent that Marfa doesn't ship also needs write on the parent.
- **Replacing** needs `metadata.types:write` when it only adds optional properties no item holds a value under, or changes the label, a description, the display hints or the version. Any other change, such as removing a property or making one required, needs the `schema.write` permission.
- **Registering** needs `types` at `write` in the key's metadata map, written `metadata.types:write`. Naming a parent or a `compatible_with` target that Marfa doesn't ship also needs write on it.
- **Replacing** needs `metadata.types:write` when it only adds optional properties no item holds a value under, or changes the label, a description, the display hints or the version. Any other change, such as removing a property or making one required, needs the `schema.write` permission. A replacement that adds a parent or a `compatible_with` target Marfa doesn't ship also needs write on that target, and one that keeps a parent or target the type already names doesn't, so a connector can update its type after its permissions narrow. Marfa answers `403 type_not_permitted` when the key's type map lacks that write, and `details.grant` names the target.
- **Deleting** needs `schema.write`.

When a replacement needs `schema.write` and the key doesn't hold it, Marfa answers `403 forbidden`, and `details.changes` lists each part of the replacement that needs it:
Expand Down
50 changes: 40 additions & 10 deletions openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -6144,7 +6144,7 @@
"string",
"null"
],
"description": "When the key was last used, in UTC, or `null` if never. Marfa updates it at most once an hour."
"description": "When the key's use was last recorded, in UTC, or `null` if none is recorded. Marfa records usage asynchronously at most once an hour; a busy or unavailable store can delay or drop a record."
}
},
"required": [
Expand Down Expand Up @@ -6324,7 +6324,7 @@
"string",
"null"
],
"description": "When the key was last used, in UTC, or `null` if never. Marfa updates it at most once an hour."
"description": "When the key's use was last recorded, in UTC, or `null` if none is recorded. Marfa records usage asynchronously at most once an hour; a busy or unavailable store can delay or drop a record."
}
},
"required": [
Expand Down Expand Up @@ -6653,7 +6653,7 @@
},
"removable": {
"type": "boolean",
"description": "`true` if `DELETE /platform-types/{id}` would remove the type: no item carries it and no type inherits from it."
"description": "`true` if `DELETE /platform-types/{id}` would remove the type: no item carries it and no type inherits from it or names it in `compatible_with`."
}
},
"required": [
Expand Down Expand Up @@ -6823,7 +6823,7 @@
"string",
"null"
],
"description": "When the sign-in last made a request, in UTC, or `null` if it hasn't. Marfa records a browser's use at most once a minute and an app's or key's at most once an hour.",
"description": "When a recorded use occurred, in UTC, or `null` if none is recorded. Uses are recorded at most once a minute for browsers and once an hour for apps or keys. Failed recording leaves the previous value.",
"example": "2026-10-05T14:12:08.000Z"
},
"expires_at": {
Expand Down Expand Up @@ -24065,7 +24065,7 @@
}
},
"403": {
"description": "- `forbidden`: you don't have `metadata.types:write`, or the identifier is under `core.*`, `system.*` or `marfa.*`, which no credential can register.\n- `type_not_permitted`: your type map doesn't grant write on the identifier, or on `parent` (`details.grant` names it).",
"description": "- `forbidden`: you don't have `metadata.types:write`, or the identifier is under `core.*`, `system.*` or `marfa.*`, which no credential can register.\n- `type_not_permitted`: your type map lacks write on the identifier or a non-platform `parent` or `compatible_with` target (`details.grant` names it).",
"content": {
"application/json": {
"schema": {
Expand Down Expand Up @@ -24751,7 +24751,7 @@
}
},
"403": {
"description": "- `forbidden`: you have neither `schema.write` nor `metadata.types:write`, or the change needs `schema.write`, such as adding a field some item already holds a value under. `details.changes` lists what needs it.\n- `core_type_immutable`: Marfa ships this type.\n- `type_not_permitted`: your type map doesn't grant write on the type or a new `parent` (`details.grant` names it).",
"description": "- `forbidden`: you lack `schema.write` and `metadata.types:write`, or `details.changes` names changes needing `schema.write`.\n- `core_type_immutable`: Marfa ships this type.\n- `type_not_permitted`: your type map lacks write on the type or a new non-platform `parent` or `compatible_with` target (`details.grant` names it).",
"content": {
"application/json": {
"schema": {
Expand Down Expand Up @@ -24869,7 +24869,7 @@
}
},
"422": {
"description": "`compatible_with_violation`: `compatible_with` names a type this instance doesn't hold, or one the type doesn't satisfy.",
"description": "`compatible_with_violation`: `compatible_with` names a type this instance doesn't hold, or one the type doesn't satisfy, or the change breaks another type's claim (`details.dependent_type_ids`).",
"content": {
"application/json": {
"schema": {
Expand Down Expand Up @@ -25053,10 +25053,10 @@
"true",
"false"
],
"description": "Delete the type even if items of it exist, in any state. It doesn't cover a type that has subtypes."
"description": "Delete the type even if items of it exist, in any state. It doesn't cover subtypes or compatibility dependents."
},
"required": false,
"description": "Delete the type even if items of it exist, in any state. It doesn't cover a type that has subtypes.",
"description": "Delete the type even if items of it exist, in any state. It doesn't cover subtypes or compatibility dependents.",
"name": "force",
"in": "query"
},
Expand Down Expand Up @@ -25277,6 +25277,36 @@
}
}
},
"422": {
"description": "`compatible_with_violation`: other types name this type in `compatible_with`. `details.dependent_type_ids` lists them. Remove their claims before deleting the target; `force=true` does not bypass this check.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CompatibleWithViolationRefusal"
}
}
},
"headers": {
"X-Marfa-Contract": {
"$ref": "#/components/headers/X-Marfa-Contract"
},
"X-Request-ID": {
"$ref": "#/components/headers/X-Request-ID"
},
"X-RateLimit-Limit": {
"$ref": "#/components/headers/X-RateLimit-Limit"
},
"X-RateLimit-Remaining": {
"$ref": "#/components/headers/X-RateLimit-Remaining"
},
"X-RateLimit-Reset": {
"$ref": "#/components/headers/X-RateLimit-Reset"
},
"X-Error-Code": {
"$ref": "#/components/headers/X-Error-Code"
}
}
},
"429": {
"description": "`rate_limited`: you sent too many requests. Wait for the number of seconds in `Retry-After`, then try again.",
"content": {
Expand Down Expand Up @@ -42447,7 +42477,7 @@
}
},
"409": {
"description": "`conflict`: the type isn't one this build stopped shipping, items still carry it, or another type inherits from it (`details.child_types` lists them).",
"description": "`conflict`: the type isn't one this build stopped shipping, items still carry it, another type inherits from it (`details.child_types`), or another type declares compatibility with it (`details.dependent_type_ids`).",
"content": {
"application/json": {
"schema": {
Expand Down
2 changes: 1 addition & 1 deletion src/generated/cli-download.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
```bash
curl -fsSL https://github.com/withmarfa/marfa/releases/download/v0.0.14/marfa-0.0.14-darwin-arm64.tar.gz | tar -xz
curl -fsSL https://github.com/withmarfa/marfa/releases/download/v0.0.15/marfa-0.0.15-darwin-arm64.tar.gz | tar -xz
sudo mkdir -p /usr/local/bin
sudo mv marfa /usr/local/bin/
marfa --version
Expand Down
Loading
Loading