diff --git a/content/docs/(reference)/changelog.mdx b/content/docs/(reference)/changelog.mdx
index e5aa6af6..ffd95783 100644
--- a/content/docs/(reference)/changelog.mdx
+++ b/content/docs/(reference)/changelog.mdx
@@ -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)
diff --git a/content/docs/catalog/your-own-types.mdx b/content/docs/catalog/your-own-types.mdx
index ee52447a..ab07e4cb 100644
--- a/content/docs/catalog/your-own-types.mdx
+++ b/content/docs/catalog/your-own-types.mdx
@@ -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
diff --git a/content/docs/guides/back-up-and-restore.mdx b/content/docs/guides/back-up-and-restore.mdx
index 532d78d5..09c769f1 100644
--- a/content/docs/guides/back-up-and-restore.mdx
+++ b/content/docs/guides/back-up-and-restore.mdx
@@ -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.
diff --git a/content/docs/guides/define-a-type.mdx b/content/docs/guides/define-a-type.mdx
index 37a3b30d..bbce562b 100644
--- a/content/docs/guides/define-a-type.mdx
+++ b/content/docs/guides/define-a-type.mdx
@@ -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:
@@ -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
@@ -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.
-`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.
## 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:
diff --git a/openapi.json b/openapi.json
index b449402e..28950636 100644
--- a/openapi.json
+++ b/openapi.json
@@ -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": [
@@ -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": [
@@ -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": [
@@ -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": {
@@ -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": {
@@ -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": {
@@ -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": {
@@ -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"
},
@@ -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": {
@@ -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": {
diff --git a/src/generated/cli-download.md b/src/generated/cli-download.md
index 64615024..7f607a24 100644
--- a/src/generated/cli-download.md
+++ b/src/generated/cli-download.md
@@ -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
diff --git a/src/generated/contract-index.json b/src/generated/contract-index.json
index 08d18775..fb8f5128 100644
--- a/src/generated/contract-index.json
+++ b/src/generated/contract-index.json
@@ -1281,9 +1281,10 @@
"errors/contention-declared": "a2f3c16a9fc5a0bc",
"errors/contention-budget": "964de57f56b59d21",
"errors/contention-wait": "e835fa727e3e2904",
- "errors/contention-read": "22d63ba4c716f80d",
- "errors/contention-stamp-retry": "e11cd6c5d69d6782",
- "errors/contention-stamp-once": "20d13a44fc3184a6",
+ "errors/contention-read": "07102d7f56d5038d",
+ "errors/contention-stamp-retry": "92e74ffe359a4c4f",
+ "errors/contention-stamp-once": "c719b3579dcfae19",
+ "errors/contention-session-use": "bc43a53f1aa25f7c",
"errors/contention-housekeeping": "9ee74ae3a480b4b5",
"errors/contention-registration": "8d8c799de60cd10d",
"errors/contention-bulk-page": "382a533f25873d94",
@@ -2084,7 +2085,7 @@
"folders/placement-end-departure": "a2dc7fe79d989816",
"folders/placement-end-transfer": "6bdce71a386b7b1c",
"folders/placement-end-delete": "f1dad8e00ab469ba",
- "folders/placement-last-binding": "42d170c20ec1c56d",
+ "folders/placement-last-binding": "69c49b911bffc613",
"folders/placement-end-retry": "b0983773b46ba5c4",
"folders/placement-purge-no-delete": "8bf7adbb13dee4b6",
"folders/placement-held-transfer": "13745e82fb10f2b4",
@@ -2860,6 +2861,7 @@
"instance-claim/profile-email-write-scope": "2c8d39d2b5553841"
},
"instance": {
+ "instance/background-request-authority": "6b301c17e7b3e56e",
"instance/id-at-root": "bc89ddd4415e9dfa",
"instance/id-at-config": "fb9e2f911b39cb7f",
"instance/id-in-archive": "13c5ec8c03c8fb79",
@@ -2997,7 +2999,10 @@
"instance/test-hooks-production-refused": "56d7519b98e6c855",
"instance/test-clock-offset-needs-hooks": "3c00a72bda96d930",
"instance/test-clock-offset": "54d28a72bc621295",
+ "instance/test-clock-sign-in": "01397e3ca72e5578",
"instance/test-clock-forward": "c207432168d82d57",
+ "instance/test-contract-version-needs-hooks": "adc2fefadbc0f87a",
+ "instance/test-contract-version": "9abf49b28dff9d7f",
"instance/test-faults-cleared-at-start": "ebf3cdbd390b3e35",
"instance/telemetry-instance-id": "74c9710ddade37ea",
"instance/telemetry-exception-id": "49c3f87daa82ad6c",
@@ -3018,6 +3023,7 @@
"instance/settings-cross-field-last": "9091bc69f86b4947"
},
"items": {
+ "items/read-authorized-state": "65088fe42a188b8f",
"items/refusal-writes-nothing": "2a2c1664e8494de7",
"items/create": "673d6e04323545e2",
"items/create-defaults": "e6a85d81f383bc90",
@@ -3367,7 +3373,7 @@
"keys-and-oauth/revoke-immediate": "82be5ab25c5e5159",
"keys-and-oauth/revoke-twice": "144cbcd1a99b0221",
"keys-and-oauth/last-used-new": "f8705cb69ee8790a",
- "keys-and-oauth/last-used-set": "07c3cd68d346094f",
+ "keys-and-oauth/last-used-set": "f8d96d459a14de12",
"keys-and-oauth/command-mint-nothing": "ad6dce3535d8cada",
"keys-and-oauth/command-update-nothing": "33e872b13a530e04",
"keys-and-oauth/command-update-one-map": "df3e17a5497d053d",
@@ -3603,7 +3609,7 @@
"keys-and-oauth/sign-ins-key-name": "5d74c4066bc37568",
"keys-and-oauth/sign-ins-unprintable-name": "abc9dbce980ffb76",
"keys-and-oauth/sign-ins-minted-by": "8210a1eeb2d2511c",
- "keys-and-oauth/sign-ins-last-used": "8950aa866d10f609",
+ "keys-and-oauth/sign-ins-last-used": "cbcb1f6217a66958",
"keys-and-oauth/sign-ins-current": "f35e94e14b42bf13",
"keys-and-oauth/sign-ins-no-token": "95307667ebaa2349",
"keys-and-oauth/sign-ins-no-credential": "b7c8f19ce4feebe9",
@@ -3633,14 +3639,14 @@
"keys-and-oauth/sign-in-rename-missing": "71c27ce8d35986a9",
"keys-and-oauth/sign-in-rename-unknown": "c978b1a9b135a422",
"keys-and-oauth/sign-in-rename-recent": "4942b4f5f8a66215",
- "keys-and-oauth/session-use-recorded": "8e399354b1cddc0f",
+ "keys-and-oauth/session-use-recorded": "f6158a9178d80dfd",
"keys-and-oauth/session-use-expiry": "779b5866c22d6c13",
"keys-and-oauth/session-sign-in-expiry": "1cb3a0d5832135df",
"keys-and-oauth/session-cookie-lifetime": "35b8d8fc9045030a",
"keys-and-oauth/session-cookie-forgotten": "4b0568f90ba8c521",
"keys-and-oauth/session-use-cookie": "ef9706c924611bd9",
"keys-and-oauth/session-use-unwritten": "fc2f88a7cb902a66",
- "keys-and-oauth/session-idle-kept": "3eb2f33409eaff42",
+ "keys-and-oauth/session-idle-kept": "4d31fd2a36dfabdc",
"keys-and-oauth/session-idle-ends": "d2b3e446935a8a51",
"keys-and-oauth/session-end-restart": "13409ec2954ad560",
"keys-and-oauth/session-use-restart": "08a2e00268031994",
@@ -3710,9 +3716,9 @@
"occurrences/gap-skipped": "467c80c3c8eeea01",
"occurrences/gap-repeated": "497d08e300a1b887",
"occurrences/series-days": "cd4cffe583f43e66",
- "occurrences/moved-replaces": "ed2a9cfbe73af8ce",
- "occurrences/moved-ids": "8462c232f648f2a2",
- "occurrences/moved-ids-hidden": "53e3138dfabf9181",
+ "occurrences/moved-replaces": "de5b69c34f894796",
+ "occurrences/moved-ids": "4269fd07ed5f0e89",
+ "occurrences/moved-ids-hidden": "db1d9462bb16401e",
"occurrences/moved-own-times": "98b678ac066ee27d",
"occurrences/moved-slot-empty": "389b6b653335edd5",
"occurrences/moved-once": "cb8bd4553cd61137",
@@ -3922,7 +3928,7 @@
"queue-and-verdicts/withdraw-refuses-held": "fabf78c907ff2c9e",
"queue-and-verdicts/withdrawn-never-released": "5cabf3332fc26418",
"queue-and-verdicts/withdraw-keeps-caught-up": "811d3e1f9b857911",
- "queue-and-verdicts/drain-keeps-refusal": "2d7438239184959d",
+ "queue-and-verdicts/drain-keeps-refusal": "a33e508152d6d0f1",
"queue-and-verdicts/clear-answered": "a8e70db165ba9237",
"queue-and-verdicts/clear-keeps-content": "8cef747227d53fd6",
"queue-and-verdicts/clear-keeps-releasable": "dbd96a208d0e6195",
@@ -4348,12 +4354,14 @@
"search-and-filters/cli-export-lacking": "c42c855be7a1fe2e",
"search-and-filters/cli-restore-lacking": "e263a4ac77220af7",
"search-and-filters/cli-export-cut": "44a507870f0529a3",
- "search-and-filters/restore-edge-skips": "2a1314cde93b14f2",
+ "search-and-filters/restore-edge-skips": "d7040dd2f7ca1e54",
"search-and-filters/restore-items-kept": "da3e8ce050fe34ec",
"search-and-filters/restore-edges-kept": "84dce9e9ac2720ba",
+ "search-and-filters/restore-edges-bin": "52876fb5aeea05c7",
"search-and-filters/restore-duplicate": "dd7fff2c26d2baed",
"search-and-filters/restore-duplicate-kept": "41bb44c904809d1d",
"search-and-filters/restore-link-held": "29f5864a8482d1c7",
+ "search-and-filters/restore-compatible-types": "d695b945a596ba34",
"search-and-filters/restore-type-conflict": "af0ea2b603b8f9c4",
"search-and-filters/restore-core-edge-type": "e963fd4e927c7a66",
"search-and-filters/restore-type-unknown-key": "991655b9dfe38349",
@@ -4538,7 +4546,8 @@
"types/child-shape": "672029f0cd861917",
"types/listing-subtree": "09d1c1404b519887",
"types/compatible-with": "c4d6065929e29fa1",
- "types/compatible-with-refused": "93355dc65541079d",
+ "types/compatible-with-refused": "5e13ad050091e8ff",
+ "types/compatible-with-preserved": "aad48a3c912965b4",
"types/custom-lifecycle": "7d7376591002eb2b",
"types/merge-policy": "25831575b00f0059",
"types/merge-policy-refused": "e4417faff5ba9518",
@@ -4574,18 +4583,19 @@
"types/field-removed": "1c5b8a9f7d89b3b7",
"types/field-removed-writable": "53c3023548f2f28e",
"types/subtype-members": "7ab7cc92e5cc02fa",
- "types/evolve-free": "9836a673ee8b2e91",
+ "types/evolve-free": "061195ad726fda74",
"types/evolve-held-name": "d959cf9f392b0a01",
"types/evolve-schema-change": "a6477105ff9ae0b1",
"types/evolve-refusal-details": "9281ce52046616bb",
"types/evolve-schema-write": "74e4556f39a87cc8",
- "types/parent-reach": "45fc9c64720a453d",
- "types/parent-kept": "6a9fb1ea628bf121",
+ "types/parent-reach": "e3a217697e2b99f4",
+ "types/compatible-with-reach": "cad41147f000f5b3",
+ "types/parent-kept": "db62751ee91dc787",
"types/parent-shipped": "7a64f6e71e3e90a2",
- "types/delete": "9e4aabe35a5cdd83",
+ "types/delete": "c7ab4ee4aea4401b",
"types/delete-in-use": "73b27f0c8375442d",
"types/delete-subtypes": "d84630c66be17221",
- "types/delete-force": "0cc40fc95484730a",
+ "types/delete-force": "d0088d290fd8de2c",
"types/delete-force-value": "e4ab96a0308bdfae",
"types/delete-permission": "d046f81f4dddbb0f",
"types/orphan-write": "a99804511e3c78c1",
@@ -4616,9 +4626,9 @@
"types/entity": "699d3bbbab2cdc19",
"types/connection": "f244e49517636a4f",
"types/platform-drift": "c3206752327937e7",
- "types/platform-remove": "63dc9904e2990c56",
+ "types/platform-remove": "5593c577555cf988",
"types/platform-remove-audit": "4af05bd070fc0587",
- "types/platform-remove-refused": "a45ebb36dfa5378b",
+ "types/platform-remove-refused": "b2730e92ba20e97e",
"types/platform-remove-missing": "04e8cd32852b7725",
"types/platform-operator-only": "f120677ebe910d01",
"types/normalize-authenticated": "ca834dd1005ac669",
diff --git a/src/generated/error-codes.md b/src/generated/error-codes.md
index e84a4fcd..31497524 100644
--- a/src/generated/error-codes.md
+++ b/src/generated/error-codes.md
@@ -60,7 +60,7 @@
| `version_conflict` | 409 | The write names a version that is no longer the current one. The answer carries the current state. |
| `request_too_large` | 413 | The request body is over the cap for its operation. |
| `range_not_satisfiable` | 416 | A `Range` request asks for bytes the blob does not have. `Content-Range` names its size. |
-| `compatible_with_violation` | 422 | A `compatible_with` declaration names a target that does not exist, or leaves out a field the target requires. |
+| `compatible_with_violation` | 422 | A `compatible_with` claim is invalid, or a change breaks another type's claim. `details.dependent_type_ids` names dependents whose claims the change would break. |
| `idempotency_key_reused` | 422 | An `Idempotency-Key` is sent with a different request than the one it first named. |
| `idempotency_result_not_retained` | 422 | An `Idempotency-Key` repeats a request whose first answer was too large to keep. `details.original_status` is the status it carried. |
| `rate_limited` | 429 | The credential, or an inbound endpoint, is past its request cap for the current window. |
diff --git a/src/generated/marfa-release.json b/src/generated/marfa-release.json
index 09ae870c..b2850b8d 100644
--- a/src/generated/marfa-release.json
+++ b/src/generated/marfa-release.json
@@ -1 +1 @@
-{"version":"v0.0.14"}
+{"version":"v0.0.15"}
diff --git a/src/generated/server-image.md b/src/generated/server-image.md
index 61169ba5..3d7cbb55 100644
--- a/src/generated/server-image.md
+++ b/src/generated/server-image.md
@@ -1,5 +1,5 @@
```bash
-docker pull ghcr.io/withmarfa/marfa-server:0.0.14
+docker pull ghcr.io/withmarfa/marfa-server:0.0.15
docker run --name marfa -d --restart unless-stopped -p 127.0.0.1:8600:8600 \
- -v marfa-data:/data --env-file marfa.env ghcr.io/withmarfa/marfa-server:0.0.14
+ -v marfa-data:/data --env-file marfa.env ghcr.io/withmarfa/marfa-server:0.0.15
```
diff --git a/src/generated/settings.md b/src/generated/settings.md
index c4afa8f5..99437838 100644
--- a/src/generated/settings.md
+++ b/src/generated/settings.md
@@ -1,112 +1,113 @@
-| Setting | Default | Allowed values | What it sets |
-| ------------------------------------------- | ----------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
-| `NODE_ENV` | `development` | `production`, `development` or `test` | The environment the server runs in. Production makes `API_KEY_SALT`, `MARFA_AUTH_SECRET` and `MARFA_AUTH_BASE_URL` mandatory, and the two secrets strong. |
-| `PORT` | `8600` | Whole number, 0 to 65535 | The port the server listens on. Zero lets the operating system assign a port. |
-| `MARFA_LISTEN_HOST` | None | Any text | The IP address or hostname the public HTTP listener binds to. Unset uses the runtime's unspecified address: IPv6 when available, otherwise IPv4. |
-| `SQLITE_PATH` | `./data/marfa.db` | Any text | The path of the database file. A value starting with `file:` is passed to the database as written, and `:memory:` opens a database held in memory. |
-| `MARFA_CONTROL_SOCKET` | None | Any text | Absolute private Unix socket path. Defaults to control/marfa.sock beside the database. Its directory must belong to the server account with mode 0700. Linux requires getfacl. |
-| `MARFA_CONTROL_ONLY` | `false` | `true`, `1`, `yes`, `on`, `false`, `0`, `no` or `off`, in any case | Start only the private control listener, without public HTTP or background jobs, for local recovery. |
-| `MARFA_TEST_HOOKS` | `false` | `true`, `1`, `yes`, `on`, `false`, `0`, `no` or `off`, in any case | Serve the clock and fault routes a conformance harness drives on the private control socket. Never on the public listener, and refused when `NODE_ENV` is production. Leave off on every instance that holds real data. |
-| `MARFA_TEST_CLOCK_OFFSET_MS` | `0` ms | Whole number of milliseconds, 0 to 3153600000000 | How far ahead of the system clock the server's clock starts. Needs `MARFA_TEST_HOOKS`, so a harness that moved the clock can restart the server without the time going back. |
-| `SQLITE_BUSY_BUDGET_MS` | `5000` ms | Whole number of milliseconds, 0 to 2147483647 | How long a write that meets a locked database is retried before the server answers `503 write_contention`. At 0 the first refusal is answered. |
-| `BLOB_PATH` | `./data/blobs` | Any text | The folder of the disk store, where every upload lands. |
-| `MARFA_DISK_RESERVE_BYTES` | `134217728` bytes | Whole number of bytes, 0 or more | The free space on the disk store's volume that an upload or a restore must leave. A body that would take the volume below it is refused `507 insufficient_storage`. At 0 nothing is held back. |
-| `MARFA_MAX_REQUEST_BYTES` | `1048576` bytes | Whole number of bytes, 1 or more | The largest ordinary JSON request body. A larger body is answered `413 request_too_large`. Bulk writes, connector state, client registration, blob uploads and inbound webhook deliveries have separate limits. |
-| `MARFA_MAX_BULK_REQUEST_BYTES` | `16777216` bytes | Whole number of bytes, 1 or more | The largest body the bulk operations accept. |
-| `S3_BUCKET` | None | Any text | The bucket of an S3-compatible object store. The store is attached when this is set. |
-| `S3_REGION` | `us-east-1` | Any text | The region of the object store. |
-| `S3_ENDPOINT` | None | Absolute `http` or `https` URL | The endpoint of the object store. When blank, the store's provider chooses it. |
-| `S3_ACCESS_KEY_ID` | None | Any text | The access key ID for the object store. |
-| `S3_SECRET_ACCESS_KEY` | None | Text with no whitespace around it | The secret access key for the object store. |
-| `S3_FORCE_PATH_STYLE` | `true` | `true`, `1`, `yes`, `on`, `false`, `0`, `no` or `off`, in any case | Whether the object store is addressed by path rather than by virtual host. Read only when `S3_ENDPOINT` is set. |
-| `S3_PREFIX` | `blobs` | `/`-joined segments of letters, digits, `.`, `_` and `-` | The key prefix the instance keeps everything in the bucket under: its blobs and, in the image, its database replica under `db/`. Each instance on a bucket needs its own. A `/` at either end is dropped; a `.` or `..` segment is refused. |
-| `MARFA_BLOB_MIN_COPIES` | `1` | Whole number, 1 or more | The fewest live copies a blob keeps. A drop that would leave fewer is refused. |
-| `MARFA_BLOB_READ_IDLE_TIMEOUT_MS` | `30000` ms | Whole number of milliseconds, 1 to 2147483647 | How long a blob read may make no progress before its stream and file or socket are closed. Slow consumers must read within this interval. |
-| `MARFA_BLOB_REPLICATE_INTERVAL_MS` | `60000` ms | Whole number of milliseconds, 1 to 3153600000000 | How often the `blob-replicate` job gives every attached store its copies. The job has no off switch. |
-| `MARFA_BLOB_REPLICATE_BATCH` | `100` | Whole number, 1 or more | The most blobs one replication run copies. |
-| `MARFA_BLOB_REPLICATE_BATCH_BYTES` | `1073741824` bytes | Whole number of bytes, 1 or more | The most bytes one replication run copies. |
-| `MARFA_BLOB_INTEGRITY_INTERVAL_MS` | `3600000` ms | Whole number of milliseconds, 1 to 3153600000000 | How often the `blob-integrity` job checks the copies it holds. The job has no off switch. |
-| `MARFA_BLOB_INTEGRITY_BATCH` | `500` | Whole number, 1 or more | The most copies one integrity run checks. |
-| `MARFA_BLOB_INTEGRITY_BATCH_BYTES` | `1073741824` bytes | Whole number of bytes, 1 or more | The most bytes one integrity run checks. |
-| `MARFA_BLOB_CLEANUP_INTERVAL_MS` | `86400000` ms | Whole number of milliseconds, 0 to 3153600000000 | How often the `blob-orphans` job sweeps for unreferenced blobs. At 0 the sweep is off. |
-| `MARFA_BLOB_CLEANUP_GRACE_MS` | `86400000` ms | Whole number of milliseconds, 0 to 3153600000000 | How long an unreferenced blob stands in the orphan report before a later run purges it. At 0 the next run purges it. |
-| `MARFA_ENRICHMENT_ENABLED` | `true` | `true`, `1`, `yes`, `on`, `false`, `0`, `no` or `off`, in any case | Whether the server extracts text from uploaded files. |
-| `MARFA_ENRICHMENT_OCR_ENABLED` | `true` | `true`, `1`, `yes`, `on`, `false`, `0`, `no` or `off`, in any case | Whether the server reads text from images with OCR. When off, images are recorded as unsupported. |
-| `MARFA_ENRICHMENT_TESSDATA_DIR` | None | Any text | The folder where the OCR language model is cached. When unset, it is a `tessdata` folder beside the database file. |
-| `MARFA_ENRICHMENT_INTERVAL_MS` | `30000` ms | Whole number of milliseconds, 1 to 3153600000000 | How often the enrichment sweep runs. |
-| `MARFA_ENRICHMENT_BATCH_SIZE` | `8` | Whole number, 1 or more | The most items one enrichment sweep extracts. |
-| `MARFA_ENRICHMENT_ITEM_TIMEOUT_MS` | `60000` ms | Whole number of milliseconds, 1 to 2147483647 | How long the extraction of one item may take. |
-| `MARFA_ENRICHMENT_MAX_BLOB_BYTES` | `20971520` bytes | Whole number of bytes, 1 or more | The largest blob the server extracts text from. A larger blob is skipped unread. |
-| `MARFA_ENRICHMENT_MAX_TEXT_CHARS` | `100000` | Whole number, 1 or more | The most characters of extracted text the server keeps. Longer text is truncated. |
-| `MARFA_ENRICHMENT_MAX_INFLATED_BYTES` | `67108864` bytes | Whole number of bytes, 1 or more | The most a document may inflate to when the server extracts its text, all of its parts together. A document that inflates past it is skipped. |
-| `MARFA_ENRICHMENT_MAX_MEMORY_BYTES` | `268435456` bytes | Whole number of bytes, 1 or more | The most the JavaScript heap of the process that reads one document's text may take. That process also holds the document and what it inflates to, outside the heap, so allow this and the two limits above together. A document that needs more is skipped. |
-| `MARFA_ENRICHMENT_MAX_ATTEMPTS` | `3` | Whole number, 1 or more | How many times the server tries to extract text from a failing item before it stops. |
-| `TRUSTED_PROXY_CIDRS` | None | Comma-separated CIDR ranges, IPv4 or IPv6 | The address ranges of the proxies whose `X-Forwarded-For` header the server believes. When empty, the header is ignored. |
-| `TRUSTED_PROXY_HEADER` | None | An HTTP header name | The name of a header that the platform's edge overwrites with the client address. It takes precedence over `TRUSTED_PROXY_CIDRS`. |
-| `API_KEY_SALT` | None. Required in production. | Text with no whitespace around it | The salt for hashing API keys and app tokens. Changing it invalidates every existing key and token. Outside production, an unset salt falls back to a built-in one. |
-| `MARFA_AUTH_SECRET` | None. Required in production. | Text of at least 32 characters, with no whitespace around it | The secret that signs sign-in cookies, authorize queries, read-view tokens and credential-free blob links. It must be at least 32 characters. Outside production, an unset secret falls back to a random one for each process. |
-| `MARFA_AUTH_BASE_URL` | None. Required in production. | Absolute `http` or `https` URL | The public URL clients reach the server at. It is the OAuth issuer, the cookie domain and the origin of links the server mints. Outside production, an unset URL uses `http://localhost:` with the actual listening port. |
-| `CORS_ORIGINS` | None | Comma-separated origins, each `scheme://host[:port]` with no path | The browser origins allowed to call the API across origins, beside the web pages the owner approves, and trusted by the sign-in forms. Each is an exact scheme and host with no trailing slash. |
-| `MARFA_PERMISSION_BUNDLES` | None | JSON array of permission bundles | The permission bundles the consent screen offers, replacing the built-in ones. Each entry has a string `id`, an array of `scopes` and a boolean `default_on`. |
-| `ENABLE_HSTS` | `false` | `true`, `1`, `yes`, `on`, `false`, `0`, `no` or `off`, in any case | Whether the server sends the `Strict-Transport-Security` header. |
-| `RATE_LIMIT_ENABLED` | `true` | `true`, `1`, `yes`, `on`, `false`, `0`, `no` or `off`, in any case | Whether the rate limiter is on. |
-| `RATE_LIMIT_REQUESTS` | `1000` | Whole number, 1 or more | The requests one credential may make to one group of operations in each window. A `GET` may make twice as many, except in a group that another setting caps. |
-| `RATE_LIMIT_WINDOW_MS` | `60000` ms | Whole number of milliseconds, 1 to 3153600000000 | The length of a rate-limit window. |
-| `RATE_LIMIT_KEYS_REQUESTS` | `200` | Whole number, 1 or more | The requests one credential may make in each window to the operations under `/keys`. |
-| `RATE_LIMIT_AGGREGATE_MULTIPLIER` | `4` | Whole number, 0 or more | The multiplier of `RATE_LIMIT_REQUESTS` that caps a credential across every operation in a window. At 0 there is no such cap. |
-| `MARFA_RATE_LIMIT_CLEANUP_INTERVAL_MS` | `3600000` ms | Whole number of milliseconds, 1 to 3153600000000 | How often the `rate-limit-cleanup` job drops expired rate-limit windows. |
-| `MARFA_CONNECTOR_HOLD_MS` | `180000` ms | Whole number of milliseconds, 1000 to 3600000 | How long a connector's hold on its registration lasts after its last take or renewal. |
-| `MARFA_INBOUND_MAX_BYTES` | `26214400` bytes | Whole number of bytes, 1 or more | The largest inbound webhook delivery the server stores. |
-| `RATE_LIMIT_INBOUND_REQUESTS` | `600` | Whole number, 1 or more | The deliveries one inbound endpoint accepts in each rate-limit window. |
-| `MARFA_INBOUND_BACKLOG_DELIVERIES` | `10000` | Whole number, 1 or more | The unhandled deliveries one registration may hold before the server refuses more deliveries for it. |
-| `MARFA_INBOUND_BACKLOG_BYTES` | `1073741824` bytes | Whole number of bytes, 1 or more | The bytes of unhandled deliveries one registration may hold before the server refuses more deliveries for it. |
-| `MARFA_INBOUND_RETAINED_DELIVERIES` | `10000` | Whole number, 1 or more | The most deliveries the server retains across live and retired endpoints. |
-| `MARFA_INBOUND_RETAINED_BYTES` | `1073741824` bytes | Whole number of bytes, 1 or more | The most bytes of deliveries the server retains across live and retired endpoints. |
-| `MARFA_INBOUND_CLEANUP_INTERVAL_MS` | `60000` ms | Whole number of milliseconds, 1 to 3153600000000 | How often the `inbound-delivery-cleanup` job runs. |
-| `MARFA_INBOUND_IN_FLIGHT_BYTES` | `104857600` bytes | Whole number of bytes, 1 or more | The most bytes of inbound webhook deliveries the server holds in memory at once. |
-| `MARFA_INBOUND_READ_TIMEOUT_MS` | `30000` ms | Whole number of milliseconds, 1 to 2147483647 | How long a delivery's body has to arrive whole. |
-| `MARFA_INBOUND_HANDLED_RETENTION_DAYS` | `7` days | Whole number of days, 0 to 36500 | How long the server keeps a handled delivery. `PUT /config` overrides it. At 0 age does not expire a delivery. |
-| `MARFA_INBOUND_PENDING_RETENTION_DAYS` | `30` days | Whole number of days, 0 to 36500 | How long the server keeps an unhandled delivery. `PUT /config` overrides it. At 0 age does not expire a delivery. |
-| `MARFA_HOUSEKEEPING_POLL_INTERVAL_MS` | `1000` ms | Whole number of milliseconds, 1 to 2147483647 | How often the housekeeping scheduler asks its table which jobs are due. |
-| `AUDIT_RETENTION_DAYS` | `90` days | Whole number of days, 0 to 36500 | How long the server keeps audit entries and webhook delivery history. At 0 no entry expires by age. `PUT /config` overrides it. |
-| `AUDIT_CLEANUP_INTERVAL_MS` | `86400000` ms | Whole number of milliseconds, 1 to 3153600000000 | How often the `audit-cleanup` job runs. |
-| `MARFA_REVOKED_GRANT_RETENTION_DAYS` | `90` days | Whole number of days, 0 to 36500 | How long a revoked application grant is kept. At 0 the job that purges them is off. `PUT /config` does not override it. |
-| `MARFA_GRANT_INACTIVITY_DAYS` | `365` days | Whole number of days, 0 to 36500 | How long an application grant may go unused before the server retires it. At 0 grants are never retired. `PUT /config` does not override it. |
-| `MARFA_EVENT_LOG_RETENTION_HOURS` | `168` hours | Whole number of hours, 0 to 876000 | The least time an event stays in the event log, which bounds how far back a stream can replay. At 0 no event expires by age. `PUT /config` overrides it. |
-| `MARFA_EVENT_LOG_CLEANUP_INTERVAL_MS` | `3600000` ms | Whole number of milliseconds, 1 to 3153600000000 | How often the `event-log-cleanup` job runs. |
-| `VERSION_THINNING_INTERVAL_MS` | `3600000` ms | Whole number of milliseconds, 1 to 3153600000000 | How often the `version-thinning` job runs. |
-| `FEED_EXPIRY_INTERVAL_MS` | `3600000` ms | Whole number of milliseconds, 1 to 3153600000000 | How often the `feed-expiry` job runs. |
-| `VERSION_RECENT_DAYS` | `30` days | Whole number of days, 0 to 36500 | The window in which the version policy keeps every version of an item. |
-| `VERSION_DAILY_SNAPSHOT_DAYS` | `90` days | Whole number of days, 0 to 36500 | The window in which the version policy keeps one version a day. It must be at least `VERSION_RECENT_DAYS`. |
-| `VERSION_WEEKLY_SNAPSHOT_DAYS` | `365` days | Whole number of days, 0 to 36500 | The window in which the version policy keeps one version a week. It must be at least `VERSION_DAILY_SNAPSHOT_DAYS`. |
-| `VERSION_MAX_VERSIONS` | `500` | Whole number, 1 or more | The most versions the version policy keeps for one item. |
-| `TRASH_RETENTION_DAYS` | `60` days | Whole number of days, 0 to 36500 | How long an item stays in the trash before the server deletes it. At 0 nothing in the trash expires by age. `PUT /config` overrides it. |
-| `TRASH_PURGE_INTERVAL_MS` | `86400000` ms | Whole number of milliseconds, 1 to 3153600000000 | How often the `trash-purge` job runs. |
-| `AUTH_SESSION_CLEANUP_INTERVAL_MS` | `3600000` ms | Whole number of milliseconds, 1 to 3153600000000 | How often the `auth-session-cleanup` job drops expired sign-in sessions. |
-| `MARFA_DCR_CLIENT_RETENTION_DAYS` | `1` day | Whole number of days, 0 to 36500 | How long a dynamically registered client with no grants is kept, counted from its registration or the last time an approval page offered it. At 0 the job that removes them is off. |
-| `MARFA_DCR_CLIENT_CLEANUP_INTERVAL_MS` | `3600000` ms | Whole number of milliseconds, 1 to 3153600000000 | How often the job that removes dynamically registered clients with no grants runs. |
-| `MARFA_BULK_ACTION_JOB_RETENTION_MS` | `604800000` ms | Whole number of milliseconds, 0 to 3153600000000 | How long a finished bulk-action job is kept. At 0 the sweep that removes them is off. |
-| `MARFA_BULK_ACTION_JOB_GC_INTERVAL_MS` | `3600000` ms | Whole number of milliseconds, 1 to 3153600000000 | How often the sweep that removes finished bulk-action jobs runs. |
-| `MARFA_BULK_ACTION_POLL_INTERVAL_MS` | `500` ms | Whole number of milliseconds, 1 to 2147483647 | The base interval at which the bulk-action worker looks for work. |
-| `MARFA_BULK_ACTION_POLL_MAX_INTERVAL_MS` | `60000` ms | Whole number of milliseconds, 1 to 2147483647 | The longest the bulk-action worker waits between looks for work when it is idle. It must be at least `MARFA_BULK_ACTION_POLL_INTERVAL_MS`. |
-| `MARFA_BULK_ACTION_POLL_BACKOFF_MULTIPLIER` | `2` | Decimal number, 1 to 100 | The factor by which the bulk-action worker's wait grows after each look that finds no work. |
-| `MARFA_SSE_MAX_VIEWERS` | `0` | Whole number, 0 or more | The most event-stream viewers one server instance serves at once. At 0 there is no limit. |
-| `MARFA_WEBHOOK_ALLOW_PRIVATE_ADDRESSES` | `false` | `true`, `1`, `yes`, `on`, `false`, `0`, `no` or `off`, in any case | Whether outbound webhooks may reach loopback, private and other non-public addresses. |
-| `ERROR_WEBHOOK_URL` | None | Absolute `http` or `https` URL | The URL the server posts a notice to when a request fails with a 500 error. When blank, no notice is sent. |
-| `MARFA_ERROR_WEBHOOK_TIMEOUT_MS` | `5000` ms | Whole number of milliseconds, 1 to 2147483647 | How long one post to `ERROR_WEBHOOK_URL` may take. |
-| `MARFA_HEARTBEAT_URL` | None | Absolute `http` or `https` URL | The URL the server requests on a schedule, so that something else can notice when the requests stop. When blank, there is no heartbeat. |
-| `MARFA_HEARTBEAT_INTERVAL_MS` | `60000` ms | Whole number of milliseconds, 1 to 3153600000000 | How often the server requests `MARFA_HEARTBEAT_URL`. |
-| `MARFA_PLACEMENT_REGION` | None | Any text | Free text naming the region the server runs in, reported at `/health` as `placement.region`. |
-| `MARFA_PLACEMENT_LOCATION` | None | Any text | Free text naming the location the server runs in, reported at `/health` as `placement.location`. |
-| `MARFA_PLACEMENT_COUNTRY` | None | Any text | Free text naming the country the server runs in, reported at `/health` as `placement.country`. |
-| `MARFA_OTEL_SAMPLE_RATIO` | `0.05` | Decimal number, 0 to 1 | The share of traces exported. A trace that contains an error is exported whatever this says: the span that failed, the spans held for the trace (at most 500) and every span after it. |
-| `OTEL_SERVICE_NAME` | `marfa-server` | Any text | The service name on exported telemetry. It takes precedence over a `service.name` in `OTEL_RESOURCE_ATTRIBUTES`. |
-| `OTEL_RESOURCE_ATTRIBUTES` | None | Comma-separated `key=value` pairs, values percent-encoded | Attributes added to everything exported, as OpenTelemetry defines them. One named here replaces the server's own of the same name, such as `deployment.environment`, which an error notification names too. |
-| `OTEL_EXPORTER_OTLP_ENDPOINT` | None | Absolute `http` or `https` URL | The base URL of the OTLP receiver. Setting it turns telemetry on: the server exports traces to `v1/traces` and logs to `v1/logs` under it. Unset, nothing is exported and the telemetry SDK is not loaded. |
-| `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | None | Absolute `http` or `https` URL | The URL traces post to, used as written. It takes precedence over `OTEL_EXPORTER_OTLP_ENDPOINT`, which must also be set. |
-| `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` | None | Absolute `http` or `https` URL | The URL logs post to, used as written. It takes precedence over `OTEL_EXPORTER_OTLP_ENDPOINT`, which must also be set. |
-| `OTEL_EXPORTER_OTLP_HEADERS` | None | Comma-separated `key=value` pairs, values percent-encoded | The headers sent with every OTLP export, as `key=value` pairs. |
-| `OTEL_EXPORTER_OTLP_TRACES_HEADERS` | None | Comma-separated `key=value` pairs, values percent-encoded | The headers sent with trace exports, as `key=value` pairs. They override `OTEL_EXPORTER_OTLP_HEADERS`. |
-| `OTEL_EXPORTER_OTLP_LOGS_HEADERS` | None | Comma-separated `key=value` pairs, values percent-encoded | The headers sent with log exports, as `key=value` pairs. They override `OTEL_EXPORTER_OTLP_HEADERS`. |
-| `MARFA_POSTHOG_HOST` | None | Absolute `http` or `https` URL | The PostHog host that receives exception reports, which OTLP cannot carry into error tracking. It needs `OTEL_EXPORTER_OTLP_ENDPOINT` and `MARFA_POSTHOG_PROJECT_TOKEN` set too. |
-| `MARFA_POSTHOG_PROJECT_TOKEN` | None | Text with no whitespace around it | The PostHog project token for exception reports. It needs `OTEL_EXPORTER_OTLP_ENDPOINT` and `MARFA_POSTHOG_HOST` set too. |
+| Setting | Default | Allowed values | What it sets |
+| ------------------------------------------- | ----------------------------- | ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `NODE_ENV` | `development` | `production`, `development` or `test` | The environment the server runs in. Production makes `API_KEY_SALT`, `MARFA_AUTH_SECRET` and `MARFA_AUTH_BASE_URL` mandatory, and the two secrets strong. |
+| `PORT` | `8600` | Whole number, 0 to 65535 | The port the server listens on. Zero lets the operating system assign a port. |
+| `MARFA_LISTEN_HOST` | None | Any text | The IP address or hostname the public HTTP listener binds to. Unset uses the runtime's unspecified address: IPv6 when available, otherwise IPv4. |
+| `SQLITE_PATH` | `./data/marfa.db` | Any text | The path of the database file. A value starting with `file:` is passed to the database as written, and `:memory:` opens a database held in memory. |
+| `MARFA_CONTROL_SOCKET` | None | Any text | Absolute private Unix socket path. Defaults to control/marfa.sock beside the database. Its directory must belong to the server account with mode 0700. Linux requires getfacl. |
+| `MARFA_CONTROL_ONLY` | `false` | `true`, `1`, `yes`, `on`, `false`, `0`, `no` or `off`, in any case | Start only the private control listener, without public HTTP or background jobs, for local recovery. |
+| `MARFA_TEST_HOOKS` | `false` | `true`, `1`, `yes`, `on`, `false`, `0`, `no` or `off`, in any case | Serve the clock and fault routes a conformance harness drives on the private control socket. Never on the public listener, and refused when `NODE_ENV` is production. Leave off on every instance that holds real data. |
+| `MARFA_TEST_CLOCK_OFFSET_MS` | `0` ms | Whole number of milliseconds, 0 to 3153600000000 | How far ahead of the system clock the server's clock starts. Needs `MARFA_TEST_HOOKS`, so a harness that moved the clock can restart the server without the time going back. |
+| `MARFA_TEST_CONTRACT_VERSION` | The build's contract version | Whole number, 0 to 2147483647 | The contract version the server speaks instead of the build's, in the root's `contract`, the `X-Marfa-Contract` header and the document's `info.version`. Needs `MARFA_TEST_HOOKS`, so a harness can show what a change of contract does to what the server kept. |
+| `SQLITE_BUSY_BUDGET_MS` | `5000` ms | Whole number of milliseconds, 0 to 2147483647 | How long a write that meets a locked database is retried before the server answers `503 write_contention`. At 0 the first refusal is answered. |
+| `BLOB_PATH` | `./data/blobs` | Any text | The folder of the disk store, where every upload lands. |
+| `MARFA_DISK_RESERVE_BYTES` | `134217728` bytes | Whole number of bytes, 0 or more | The free space on the disk store's volume that an upload or a restore must leave. A body that would take the volume below it is refused `507 insufficient_storage`. At 0 nothing is held back. |
+| `MARFA_MAX_REQUEST_BYTES` | `1048576` bytes | Whole number of bytes, 1 or more | The largest ordinary JSON request body. A larger body is answered `413 request_too_large`. Bulk writes, connector state, client registration, blob uploads and inbound webhook deliveries have separate limits. |
+| `MARFA_MAX_BULK_REQUEST_BYTES` | `16777216` bytes | Whole number of bytes, 1 or more | The largest body the bulk operations accept. |
+| `S3_BUCKET` | None | Any text | The bucket of an S3-compatible object store. The store is attached when this is set. |
+| `S3_REGION` | `us-east-1` | Any text | The region of the object store. |
+| `S3_ENDPOINT` | None | Absolute `http` or `https` URL | The endpoint of the object store. When blank, the store's provider chooses it. |
+| `S3_ACCESS_KEY_ID` | None | Any text | The access key ID for the object store. |
+| `S3_SECRET_ACCESS_KEY` | None | Text with no whitespace around it | The secret access key for the object store. |
+| `S3_FORCE_PATH_STYLE` | `true` | `true`, `1`, `yes`, `on`, `false`, `0`, `no` or `off`, in any case | Whether the object store is addressed by path rather than by virtual host. Read only when `S3_ENDPOINT` is set. |
+| `S3_PREFIX` | `blobs` | `/`-joined segments of letters, digits, `.`, `_` and `-` | The key prefix the instance keeps everything in the bucket under: its blobs and, in the image, its database replica under `db/`. Each instance on a bucket needs its own. A `/` at either end is dropped; a `.` or `..` segment is refused. |
+| `MARFA_BLOB_MIN_COPIES` | `1` | Whole number, 1 or more | The fewest live copies a blob keeps. A drop that would leave fewer is refused. |
+| `MARFA_BLOB_READ_IDLE_TIMEOUT_MS` | `30000` ms | Whole number of milliseconds, 1 to 2147483647 | How long a blob read may make no progress before its stream and file or socket are closed. Slow consumers must read within this interval. |
+| `MARFA_BLOB_REPLICATE_INTERVAL_MS` | `60000` ms | Whole number of milliseconds, 1 to 3153600000000 | How often the `blob-replicate` job gives every attached store its copies. The job has no off switch. |
+| `MARFA_BLOB_REPLICATE_BATCH` | `100` | Whole number, 1 or more | The most blobs one replication run copies. |
+| `MARFA_BLOB_REPLICATE_BATCH_BYTES` | `1073741824` bytes | Whole number of bytes, 1 or more | The most bytes one replication run copies. |
+| `MARFA_BLOB_INTEGRITY_INTERVAL_MS` | `3600000` ms | Whole number of milliseconds, 1 to 3153600000000 | How often the `blob-integrity` job checks the copies it holds. The job has no off switch. |
+| `MARFA_BLOB_INTEGRITY_BATCH` | `500` | Whole number, 1 or more | The most copies one integrity run checks. |
+| `MARFA_BLOB_INTEGRITY_BATCH_BYTES` | `1073741824` bytes | Whole number of bytes, 1 or more | The most bytes one integrity run checks. |
+| `MARFA_BLOB_CLEANUP_INTERVAL_MS` | `86400000` ms | Whole number of milliseconds, 0 to 3153600000000 | How often the `blob-orphans` job sweeps for unreferenced blobs. At 0 the sweep is off. |
+| `MARFA_BLOB_CLEANUP_GRACE_MS` | `86400000` ms | Whole number of milliseconds, 0 to 3153600000000 | How long an unreferenced blob stands in the orphan report before a later run purges it. At 0 the next run purges it. |
+| `MARFA_ENRICHMENT_ENABLED` | `true` | `true`, `1`, `yes`, `on`, `false`, `0`, `no` or `off`, in any case | Whether the server extracts text from uploaded files. |
+| `MARFA_ENRICHMENT_OCR_ENABLED` | `true` | `true`, `1`, `yes`, `on`, `false`, `0`, `no` or `off`, in any case | Whether the server reads text from images with OCR. When off, images are recorded as unsupported. |
+| `MARFA_ENRICHMENT_TESSDATA_DIR` | None | Any text | The folder where the OCR language model is cached. When unset, it is a `tessdata` folder beside the database file. |
+| `MARFA_ENRICHMENT_INTERVAL_MS` | `30000` ms | Whole number of milliseconds, 1 to 3153600000000 | How often the enrichment sweep runs. |
+| `MARFA_ENRICHMENT_BATCH_SIZE` | `8` | Whole number, 1 or more | The most items one enrichment sweep extracts. |
+| `MARFA_ENRICHMENT_ITEM_TIMEOUT_MS` | `60000` ms | Whole number of milliseconds, 1 to 2147483647 | How long the extraction of one item may take. |
+| `MARFA_ENRICHMENT_MAX_BLOB_BYTES` | `20971520` bytes | Whole number of bytes, 1 or more | The largest blob the server extracts text from. A larger blob is skipped unread. |
+| `MARFA_ENRICHMENT_MAX_TEXT_CHARS` | `100000` | Whole number, 1 or more | The most characters of extracted text the server keeps. Longer text is truncated. |
+| `MARFA_ENRICHMENT_MAX_INFLATED_BYTES` | `67108864` bytes | Whole number of bytes, 1 or more | The most a document may inflate to when the server extracts its text, all of its parts together. A document that inflates past it is skipped. |
+| `MARFA_ENRICHMENT_MAX_MEMORY_BYTES` | `268435456` bytes | Whole number of bytes, 1 or more | The most the JavaScript heap of the process that reads one document's text may take. That process also holds the document and what it inflates to, outside the heap, so allow this and the two limits above together. A document that needs more is skipped. |
+| `MARFA_ENRICHMENT_MAX_ATTEMPTS` | `3` | Whole number, 1 or more | How many times the server tries to extract text from a failing item before it stops. |
+| `TRUSTED_PROXY_CIDRS` | None | Comma-separated CIDR ranges, IPv4 or IPv6 | The address ranges of the proxies whose `X-Forwarded-For` header the server believes. When empty, the header is ignored. |
+| `TRUSTED_PROXY_HEADER` | None | An HTTP header name | The name of a header that the platform's edge overwrites with the client address. It takes precedence over `TRUSTED_PROXY_CIDRS`. |
+| `API_KEY_SALT` | None. Required in production. | Text with no whitespace around it | The salt for hashing API keys and app tokens. Changing it invalidates every existing key and token. Outside production, an unset salt falls back to a built-in one. |
+| `MARFA_AUTH_SECRET` | None. Required in production. | Text of at least 32 characters, with no whitespace around it | The secret that signs sign-in cookies, authorize queries, read-view tokens and credential-free blob links. It must be at least 32 characters. Outside production, an unset secret falls back to a random one for each process. |
+| `MARFA_AUTH_BASE_URL` | None. Required in production. | Absolute `http` or `https` URL | The public URL for OAuth, cookies, and links. Outside production, an unset URL uses the assigned port with `http://127.0.0.1` when `MARFA_LISTEN_HOST` is `127.0.0.1`, otherwise `http://localhost`. |
+| `CORS_ORIGINS` | None | Comma-separated origins, each `scheme://host[:port]` with no path | The browser origins allowed to call the API across origins, beside the web pages the owner approves, and trusted by the sign-in forms. Each is an exact scheme and host with no trailing slash. |
+| `MARFA_PERMISSION_BUNDLES` | None | JSON array of permission bundles | The permission bundles the consent screen offers, replacing the built-in ones. Each entry has a string `id`, an array of `scopes` and a boolean `default_on`. |
+| `ENABLE_HSTS` | `false` | `true`, `1`, `yes`, `on`, `false`, `0`, `no` or `off`, in any case | Whether the server sends the `Strict-Transport-Security` header. |
+| `RATE_LIMIT_ENABLED` | `true` | `true`, `1`, `yes`, `on`, `false`, `0`, `no` or `off`, in any case | Whether the rate limiter is on. |
+| `RATE_LIMIT_REQUESTS` | `1000` | Whole number, 1 or more | The requests one credential may make to one group of operations in each window. A `GET` may make twice as many, except in a group that another setting caps. |
+| `RATE_LIMIT_WINDOW_MS` | `60000` ms | Whole number of milliseconds, 1 to 3153600000000 | The length of a rate-limit window. |
+| `RATE_LIMIT_KEYS_REQUESTS` | `200` | Whole number, 1 or more | The requests one credential may make in each window to the operations under `/keys`. |
+| `RATE_LIMIT_AGGREGATE_MULTIPLIER` | `4` | Whole number, 0 or more | The multiplier of `RATE_LIMIT_REQUESTS` that caps a credential across every operation in a window. At 0 there is no such cap. |
+| `MARFA_RATE_LIMIT_CLEANUP_INTERVAL_MS` | `3600000` ms | Whole number of milliseconds, 1 to 3153600000000 | How often the `rate-limit-cleanup` job drops expired rate-limit windows. |
+| `MARFA_CONNECTOR_HOLD_MS` | `180000` ms | Whole number of milliseconds, 1000 to 3600000 | How long a connector's hold on its registration lasts after its last take or renewal. |
+| `MARFA_INBOUND_MAX_BYTES` | `26214400` bytes | Whole number of bytes, 1 or more | The largest inbound webhook delivery the server stores. |
+| `RATE_LIMIT_INBOUND_REQUESTS` | `600` | Whole number, 1 or more | The deliveries one inbound endpoint accepts in each rate-limit window. |
+| `MARFA_INBOUND_BACKLOG_DELIVERIES` | `10000` | Whole number, 1 or more | The unhandled deliveries one registration may hold before the server refuses more deliveries for it. |
+| `MARFA_INBOUND_BACKLOG_BYTES` | `1073741824` bytes | Whole number of bytes, 1 or more | The bytes of unhandled deliveries one registration may hold before the server refuses more deliveries for it. |
+| `MARFA_INBOUND_RETAINED_DELIVERIES` | `10000` | Whole number, 1 or more | The most deliveries the server retains across live and retired endpoints. |
+| `MARFA_INBOUND_RETAINED_BYTES` | `1073741824` bytes | Whole number of bytes, 1 or more | The most bytes of deliveries the server retains across live and retired endpoints. |
+| `MARFA_INBOUND_CLEANUP_INTERVAL_MS` | `60000` ms | Whole number of milliseconds, 1 to 3153600000000 | How often the `inbound-delivery-cleanup` job runs. |
+| `MARFA_INBOUND_IN_FLIGHT_BYTES` | `104857600` bytes | Whole number of bytes, 1 or more | The most bytes of inbound webhook deliveries the server holds in memory at once. |
+| `MARFA_INBOUND_READ_TIMEOUT_MS` | `30000` ms | Whole number of milliseconds, 1 to 2147483647 | How long a delivery's body has to arrive whole. |
+| `MARFA_INBOUND_HANDLED_RETENTION_DAYS` | `7` days | Whole number of days, 0 to 36500 | How long the server keeps a handled delivery. `PUT /config` overrides it. At 0 age does not expire a delivery. |
+| `MARFA_INBOUND_PENDING_RETENTION_DAYS` | `30` days | Whole number of days, 0 to 36500 | How long the server keeps an unhandled delivery. `PUT /config` overrides it. At 0 age does not expire a delivery. |
+| `MARFA_HOUSEKEEPING_POLL_INTERVAL_MS` | `1000` ms | Whole number of milliseconds, 1 to 2147483647 | How often the housekeeping scheduler asks its table which jobs are due. |
+| `AUDIT_RETENTION_DAYS` | `90` days | Whole number of days, 0 to 36500 | How long the server keeps audit entries and webhook delivery history. At 0 no entry expires by age. `PUT /config` overrides it. |
+| `AUDIT_CLEANUP_INTERVAL_MS` | `86400000` ms | Whole number of milliseconds, 1 to 3153600000000 | How often the `audit-cleanup` job runs. |
+| `MARFA_REVOKED_GRANT_RETENTION_DAYS` | `90` days | Whole number of days, 0 to 36500 | How long a revoked application grant is kept. At 0 the job that purges them is off. `PUT /config` does not override it. |
+| `MARFA_GRANT_INACTIVITY_DAYS` | `365` days | Whole number of days, 0 to 36500 | How long an application grant may go unused before the server retires it. At 0 grants are never retired. `PUT /config` does not override it. |
+| `MARFA_EVENT_LOG_RETENTION_HOURS` | `168` hours | Whole number of hours, 0 to 876000 | The least time an event stays in the event log, which bounds how far back a stream can replay. At 0 no event expires by age. `PUT /config` overrides it. |
+| `MARFA_EVENT_LOG_CLEANUP_INTERVAL_MS` | `3600000` ms | Whole number of milliseconds, 1 to 3153600000000 | How often the `event-log-cleanup` job runs. |
+| `VERSION_THINNING_INTERVAL_MS` | `3600000` ms | Whole number of milliseconds, 1 to 3153600000000 | How often the `version-thinning` job runs. |
+| `FEED_EXPIRY_INTERVAL_MS` | `3600000` ms | Whole number of milliseconds, 1 to 3153600000000 | How often the `feed-expiry` job runs. |
+| `VERSION_RECENT_DAYS` | `30` days | Whole number of days, 0 to 36500 | The window in which the version policy keeps every version of an item. |
+| `VERSION_DAILY_SNAPSHOT_DAYS` | `90` days | Whole number of days, 0 to 36500 | The window in which the version policy keeps one version a day. It must be at least `VERSION_RECENT_DAYS`. |
+| `VERSION_WEEKLY_SNAPSHOT_DAYS` | `365` days | Whole number of days, 0 to 36500 | The window in which the version policy keeps one version a week. It must be at least `VERSION_DAILY_SNAPSHOT_DAYS`. |
+| `VERSION_MAX_VERSIONS` | `500` | Whole number, 1 or more | The most versions the version policy keeps for one item. |
+| `TRASH_RETENTION_DAYS` | `60` days | Whole number of days, 0 to 36500 | How long an item stays in the trash before the server deletes it. At 0 nothing in the trash expires by age. `PUT /config` overrides it. |
+| `TRASH_PURGE_INTERVAL_MS` | `86400000` ms | Whole number of milliseconds, 1 to 3153600000000 | How often the `trash-purge` job runs. |
+| `AUTH_SESSION_CLEANUP_INTERVAL_MS` | `3600000` ms | Whole number of milliseconds, 1 to 3153600000000 | How often the `auth-session-cleanup` job drops expired sign-in sessions. |
+| `MARFA_DCR_CLIENT_RETENTION_DAYS` | `1` day | Whole number of days, 0 to 36500 | How long a dynamically registered client with no grants is kept, counted from its registration or the last time an approval page offered it. At 0 the job that removes them is off. |
+| `MARFA_DCR_CLIENT_CLEANUP_INTERVAL_MS` | `3600000` ms | Whole number of milliseconds, 1 to 3153600000000 | How often the job that removes dynamically registered clients with no grants runs. |
+| `MARFA_BULK_ACTION_JOB_RETENTION_MS` | `604800000` ms | Whole number of milliseconds, 0 to 3153600000000 | How long a finished bulk-action job is kept. At 0 the sweep that removes them is off. |
+| `MARFA_BULK_ACTION_JOB_GC_INTERVAL_MS` | `3600000` ms | Whole number of milliseconds, 1 to 3153600000000 | How often the sweep that removes finished bulk-action jobs runs. |
+| `MARFA_BULK_ACTION_POLL_INTERVAL_MS` | `500` ms | Whole number of milliseconds, 1 to 2147483647 | The base interval at which the bulk-action worker looks for work. |
+| `MARFA_BULK_ACTION_POLL_MAX_INTERVAL_MS` | `60000` ms | Whole number of milliseconds, 1 to 2147483647 | The longest the bulk-action worker waits between looks for work when it is idle. It must be at least `MARFA_BULK_ACTION_POLL_INTERVAL_MS`. |
+| `MARFA_BULK_ACTION_POLL_BACKOFF_MULTIPLIER` | `2` | Decimal number, 1 to 100 | The factor by which the bulk-action worker's wait grows after each look that finds no work. |
+| `MARFA_SSE_MAX_VIEWERS` | `0` | Whole number, 0 or more | The most event-stream viewers one server instance serves at once. At 0 there is no limit. |
+| `MARFA_WEBHOOK_ALLOW_PRIVATE_ADDRESSES` | `false` | `true`, `1`, `yes`, `on`, `false`, `0`, `no` or `off`, in any case | Whether outbound webhooks may reach loopback, private and other non-public addresses. |
+| `ERROR_WEBHOOK_URL` | None | Absolute `http` or `https` URL | The URL the server posts a notice to when a request fails with a 500 error. When blank, no notice is sent. |
+| `MARFA_ERROR_WEBHOOK_TIMEOUT_MS` | `5000` ms | Whole number of milliseconds, 1 to 2147483647 | How long one post to `ERROR_WEBHOOK_URL` may take. |
+| `MARFA_HEARTBEAT_URL` | None | Absolute `http` or `https` URL | The URL the server requests on a schedule, so that something else can notice when the requests stop. When blank, there is no heartbeat. |
+| `MARFA_HEARTBEAT_INTERVAL_MS` | `60000` ms | Whole number of milliseconds, 1 to 3153600000000 | How often the server requests `MARFA_HEARTBEAT_URL`. |
+| `MARFA_PLACEMENT_REGION` | None | Any text | Free text naming the region the server runs in, reported at `/health` as `placement.region`. |
+| `MARFA_PLACEMENT_LOCATION` | None | Any text | Free text naming the location the server runs in, reported at `/health` as `placement.location`. |
+| `MARFA_PLACEMENT_COUNTRY` | None | Any text | Free text naming the country the server runs in, reported at `/health` as `placement.country`. |
+| `MARFA_OTEL_SAMPLE_RATIO` | `0.05` | Decimal number, 0 to 1 | The share of traces exported. A trace that contains an error is exported whatever this says: the span that failed, the spans held for the trace (at most 500) and every span after it. |
+| `OTEL_SERVICE_NAME` | `marfa-server` | Any text | The service name on exported telemetry. It takes precedence over a `service.name` in `OTEL_RESOURCE_ATTRIBUTES`. |
+| `OTEL_RESOURCE_ATTRIBUTES` | None | Comma-separated `key=value` pairs, values percent-encoded | Attributes added to everything exported, as OpenTelemetry defines them. One named here replaces the server's own of the same name, such as `deployment.environment`, which an error notification names too. |
+| `OTEL_EXPORTER_OTLP_ENDPOINT` | None | Absolute `http` or `https` URL | The base URL of the OTLP receiver. Setting it turns telemetry on: the server exports traces to `v1/traces` and logs to `v1/logs` under it. Unset, nothing is exported and the telemetry SDK is not loaded. |
+| `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | None | Absolute `http` or `https` URL | The URL traces post to, used as written. It takes precedence over `OTEL_EXPORTER_OTLP_ENDPOINT`, which must also be set. |
+| `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` | None | Absolute `http` or `https` URL | The URL logs post to, used as written. It takes precedence over `OTEL_EXPORTER_OTLP_ENDPOINT`, which must also be set. |
+| `OTEL_EXPORTER_OTLP_HEADERS` | None | Comma-separated `key=value` pairs, values percent-encoded | The headers sent with every OTLP export, as `key=value` pairs. |
+| `OTEL_EXPORTER_OTLP_TRACES_HEADERS` | None | Comma-separated `key=value` pairs, values percent-encoded | The headers sent with trace exports, as `key=value` pairs. They override `OTEL_EXPORTER_OTLP_HEADERS`. |
+| `OTEL_EXPORTER_OTLP_LOGS_HEADERS` | None | Comma-separated `key=value` pairs, values percent-encoded | The headers sent with log exports, as `key=value` pairs. They override `OTEL_EXPORTER_OTLP_HEADERS`. |
+| `MARFA_POSTHOG_HOST` | None | Absolute `http` or `https` URL | The PostHog host that receives exception reports, which OTLP cannot carry into error tracking. It needs `OTEL_EXPORTER_OTLP_ENDPOINT` and `MARFA_POSTHOG_PROJECT_TOKEN` set too. |
+| `MARFA_POSTHOG_PROJECT_TOKEN` | None | Text with no whitespace around it | The PostHog project token for exception reports. It needs `OTEL_EXPORTER_OTLP_ENDPOINT` and `MARFA_POSTHOG_HOST` set too. |