diff --git a/openapi.json b/openapi.json index 96b57f94..d4450e9f 100644 --- a/openapi.json +++ b/openapi.json @@ -452,6 +452,10 @@ "type": "string", "description": "The item's identifier at its source. With `source`, it is the item's natural key. Absent if the writer set none." }, + "read_only": { + "type": "boolean", + "description": "`true` if every connector with a live write key for the item's type that owns or claims its `source` lists the type in `read_only_types`, so it puts back an edit made in Marfa on its next run. Marfa doesn't refuse the edit." + }, "capture_latitude": { "type": "number", "description": "The latitude where the item was captured. Absent if the writer set none." @@ -511,6 +515,7 @@ "version", "schema_version", "source", + "read_only", "occurred_at", "created_at", "updated_at", @@ -4258,6 +4263,13 @@ } ] }, + "conflicts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TypeConflict" + }, + "description": "The fields this type declares in another shape than a type above it. Present only while there are some, and it clears once the type declares the shape above. Items of the type stay readable." + }, "own_roles": { "type": "array", "items": { @@ -4481,6 +4493,39 @@ "additionalProperties": {}, "description": "One member of the entries of an `array` field." }, + "TypeConflict": { + "type": "object", + "properties": { + "field": { + "type": "string", + "description": "The name of the field the type declares." + }, + "attribute": { + "type": "string", + "description": "The first part of the field's shape that differs, such as `type`." + }, + "inherited_from": { + "type": "string", + "description": "The ID of the type above this one that declares the field." + }, + "inherited": { + "type": "string", + "description": "What the types above give the field, such as `type \"string\"`." + }, + "declared": { + "type": "string", + "description": "What this type declares, such as `type \"number\"`." + } + }, + "required": [ + "field", + "attribute", + "inherited_from", + "inherited", + "declared" + ], + "description": "A field this type declares in a shape a type above it declares differently." + }, "TypeDefinition": { "type": "object", "properties": { @@ -6687,7 +6732,7 @@ }, "updated_at": { "type": "string", - "description": "When the connector's name or description was last set." + "description": "When the connector's name, description or `read_only_types` was last set." }, "last_heartbeat_at": { "type": [ @@ -6696,6 +6741,14 @@ ], "description": "When the connector last sent a heartbeat; `null` if never." }, + "read_only_types": { + "type": "array", + "items": { + "type": "string", + "example": "app.github.issue" + }, + "description": "The types whose items the connector holds read only, putting back any edit made in Marfa. `[]` if it declared none." + }, "last_run": { "anyOf": [ { @@ -6734,6 +6787,7 @@ "registered_at", "updated_at", "last_heartbeat_at", + "read_only_types", "last_run", "problems", "hold_expires_at" @@ -42037,7 +42091,7 @@ "Connectors" ], "summary": "Register a connector", - "description": "Registers your key as a connector and returns it. Repeating registration updates its name and description and keeps its ID. A successor key inherits its own source's inbound endpoints and retained deliveries.", + "description": "Registers your key as a connector and returns it. Repeating registration replaces its name, description and `read_only_types` and keeps its ID. A successor key inherits its own source's inbound endpoints and retained deliveries.", "security": [ { "bearerAuth": [] @@ -42060,6 +42114,15 @@ "type": "string", "maxLength": 2000, "description": "What the connector does. Leave it out for none: registering again without it clears the old one." + }, + "read_only_types": { + "type": "array", + "items": { + "type": "string", + "example": "app.github.issue" + }, + "maxItems": 1000, + "description": "The types whose items the connector holds read only, putting back any edit made in Marfa. An item reads `read_only` as `true` when every connector for its source whose live key may write its type lists the type. The types needn't be registered. Leaving it out clears the list." } }, "required": [ @@ -42072,7 +42135,7 @@ }, "responses": { "200": { - "description": "Returns the existing connector with its name and description updated. Its `id` and `registered_at` don't change.", + "description": "Returns the existing connector with its name, description and `read_only_types` replaced. Its `id` and `registered_at` don't change.", "content": { "application/json": { "schema": { @@ -42126,7 +42189,7 @@ } }, "400": { - "description": "- `missing_required_field`: `name` is missing.\n- `validation_error`: `name` isn't 1 to 200 characters, `description` is over 2,000 characters, or the body has a top-level field this endpoint doesn't take.", + "description": "- `missing_required_field`: `name` is missing.\n- `validation_error`: `name` isn't 1 to 200 characters, `description` is over 2,000 characters, `read_only_types` holds a malformed type identifier or over 1,000 entries, or the body has a top-level field this endpoint doesn't take.", "content": { "application/json": { "schema": { diff --git a/scripts/sync-from-marfa.sh b/scripts/sync-from-marfa.sh index ac5fa3b8..4b1e8171 100755 --- a/scripts/sync-from-marfa.sh +++ b/scripts/sync-from-marfa.sh @@ -943,7 +943,7 @@ ITEM_GROUPS = ( ("Identity", ("id", "type", "tier")), ("Content", ("properties", "edges", "extensions")), ("Origin", ("source", "source_id", "capture_latitude", "capture_longitude")), - ("State", ("state", "trashed_at", "trashed_by_cascade", "trashed_with")), + ("State", ("state", "read_only", "trashed_at", "trashed_by_cascade", "trashed_with")), ("Version", ("version", "schema_version")), ("Time", ("occurred_at", "created_at", "updated_at")), ) diff --git a/src/generated/catalog/item-fields.md b/src/generated/catalog/item-fields.md index 4c25c024..49329555 100644 --- a/src/generated/catalog/item-fields.md +++ b/src/generated/catalog/item-fields.md @@ -28,6 +28,7 @@ | Field | Kind | Description | | -------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `state` | `enum` | The item's lifecycle state. Values: `active`, `archived`, `trashed`, `revoked`. | +| `read_only` | `boolean` | `true` if every connector with a live write key for the item's type that owns or claims its `source` lists the type in `read_only_types`, so it puts back an edit made in Marfa on its next run. Marfa doesn't refuse the edit. | | `trashed_at` | `string` or `null` | When the item went to the trash, in UTC, or when a `system.*` item was revoked. A later write to the item leaves it as it is, and a restore clears it. `null` for an item outside the trash, and for one an archive restored to the trash without a time. | | `trashed_by_cascade` | `boolean` | Always `true` where present: the item went to the trash with another item, through a cascading edge such as `parent-of`. Present while it stays in the trash. Absent on an item trashed on its own. | | `trashed_with` | `string` | The ID of the item whose trash took this one into the trash, beside `trashed_by_cascade`, whatever became of that item since. Present only if you can read that item's type. | diff --git a/src/generated/cli-download.md b/src/generated/cli-download.md index 6f419e1f..624fa0a5 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.53/marfa-0.0.53-darwin-arm64.tar.gz | tar -xz +curl -fsSL https://github.com/withmarfa/marfa/releases/download/v0.0.54/marfa-0.0.54-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 b10d458b..67c8598d 100644 --- a/src/generated/contract-index.json +++ b/src/generated/contract-index.json @@ -170,7 +170,7 @@ "blobs/file-size-stale-merges": "c37426d88dbbe3ff" }, "connectors": { - "connectors/register-created": "ba618cc8e54e96b6", + "connectors/register-created": "258e0a50d90b2c50", "connectors/register-key-and-source": "7d2909342595c103", "connectors/register-fresh-nulls": "bd12c3bbe22c1420", "connectors/register-name-bounds": "eabf90995cee557e", @@ -183,6 +183,12 @@ "connectors/register-repeat": "b00578633899b3a2", "connectors/register-repeat-sets": "117e7d545249c9cc", "connectors/register-repeat-no-description": "092de9e197f29461", + "connectors/read-only-types-stored": "84d55d45af86a01b", + "connectors/read-only-types-unregistered": "be080777e57cf6d5", + "connectors/read-only-types-malformed": "68cd30d01bd4bb3d", + "connectors/read-only-types-bounds": "3eca21d856fc0b67", + "connectors/read-only-types-repeat": "b4e23460d512f16f", + "connectors/read-only-types-cleared": "bf9351b933d79639", "connectors/register-repeat-registered-at": "c61983ecb87d5920", "connectors/register-repeat-updated-at": "7e3d0142a868b988", "connectors/register-concurrent": "89cc0faeca95d28a", @@ -1736,7 +1742,8 @@ "errors/bin-unreadable": "85d70e71d99c23a2", "errors/bin-read": "97919e6b48ca0bef", "errors/item-missing-bare": "86f57c491885dd44", - "errors/internal-error": "7c94f359aa098c88", + "errors/internal-error": "540061a5baf8b22d", + "errors/client-closed-request": "0d9f8838ff09afab", "errors/internal-error-body": "371c885f3048a548", "errors/internal-error-entry": "acb21fc45530648f", "errors/internal-error-atomic": "e8b7f89a5fd029ae", @@ -3616,7 +3623,7 @@ "instance/stop-within-eight": "41a6282b962dd665", "instance/stop-exit-1-open-request": "c3195e3308645e64", "instance/stop-slow-run-exit-0": "2b7bbfc0d1cc2a91", - "instance/long-job-health": "062c9b345f2495a1", + "instance/long-job-health": "5b684625c3144f02", "instance/root-page": "c1c32057467c83fa", "instance/root-description": "1a23cea416e66416", "instance/root-vary": "83d7f57713e419b5", @@ -3683,10 +3690,16 @@ "instance/test-contract-version": "9abf49b28dff9d7f", "instance/test-migration-needs-hooks": "6c7d014c12578bba", "instance/test-faults-cleared-at-start": "ebf3cdbd390b3e35", - "instance/telemetry-instance-id": "74c9710ddade37ea", - "instance/telemetry-exception-id": "49c3f87daa82ad6c", - "instance/telemetry-attributes": "60305bf4a7a14b60", - "instance/telemetry-service-name": "d2e0b09a92842b4c", + "instance/telemetry-exception-report": "70e8e4387f4ce705", + "instance/telemetry-exception-reporting-off": "3fe6efa0f6fadfb8", + "instance/telemetry-disabled": "c49d91a63150c421", + "instance/telemetry-exception-host-unusable": "93e850a271c7a46b", + "instance/telemetry-exception-host-unusable-traces": "55b6236b7bea2056", + "instance/telemetry-host-unusable-warning": "8a1348c4f2199217", + "instance/telemetry-instance-id": "104d18b36988ac9b", + "instance/telemetry-exception-id": "490572ef2bb816ed", + "instance/telemetry-attributes": "aea0c04105e182f1", + "instance/telemetry-service-name": "438ce1f7198badaf", "instance/telemetry-error-environment": "d31ebd162b0f35b7", "instance/settings-edge-accepted": "91747bce5932bd24", "instance/settings-refused-exit": "98cf782ae12ae06b", @@ -3706,6 +3719,14 @@ }, "items": { "items/read-authorized-state": "65088fe42a188b8f", + "items/read-only-answered": "4b8173274f299f1e", + "items/read-only-true": "ce7dd7a72a2b2179", + "items/read-only-false": "884c143399738c59", + "items/read-only-key-gone": "e334c727cbc33dd2", + "items/read-only-key-may-write": "f92912663ee072cb", + "items/read-only-current": "82dabe34cf6ee11f", + "items/read-only-replay": "b6fb8fbbc27c435c", + "items/read-only-writable": "85162be94f427fd0", "items/refusal-writes-nothing": "2a2c1664e8494de7", "items/create": "673d6e04323545e2", "items/create-defaults": "e6a85d81f383bc90", @@ -5645,6 +5666,10 @@ "types/compatible-with-refused": "5e13ad050091e8ff", "types/compatible-with-preserved": "aad48a3c912965b4", "types/custom-lifecycle": "7d7376591002eb2b", + "types/conflict-listed": "1e31ad6b6d93690e", + "types/conflict-absent": "2ede08ccb9c24555", + "types/conflict-items-served": "8d7eb4c86d66b5a9", + "types/conflict-clears": "773cfb8fd1328e51", "types/merge-policy": "25831575b00f0059", "types/merge-policy-refused": "e4417faff5ba9518", "types/field-grammar": "44363bba20ec8365", diff --git a/src/generated/marfa-release.json b/src/generated/marfa-release.json index c7d40e07..422348fc 100644 --- a/src/generated/marfa-release.json +++ b/src/generated/marfa-release.json @@ -1 +1 @@ -{"version":"v0.0.53"} +{"version":"v0.0.54"} diff --git a/src/generated/server-image.md b/src/generated/server-image.md index b3967efe..50a45c4c 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.53 +docker pull ghcr.io/withmarfa/marfa-server:0.0.54 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.53 + -v marfa-data:/data --env-file marfa.env ghcr.io/withmarfa/marfa-server:0.0.54 ``` diff --git a/src/generated/settings.md b/src/generated/settings.md index bf9292e4..a9fa078f 100644 --- a/src/generated/settings.md +++ b/src/generated/settings.md @@ -1,121 +1,122 @@ -| 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. | -| `MARFA_TEST_MIGRATION_FAULT` | None | `hold`, `lose-row` or `change-shape` | A failure the update of an earlier schema version meets before it commits: `hold` waits there until the process is stopped, `lose-row` deletes a row the update must keep, and `change-shape` adds a column this build does not declare. Needs `MARFA_TEST_HOOKS`, so a harness can show that a failed or interrupted update changes nothing. | -| `MARFA_TEST_FREE_DISK_BYTES` | What the volume reports | Whole number of bytes, 0 or more | The free space, in bytes, the update of an earlier schema version takes the database's volume to have. Needs `MARFA_TEST_HOOKS`, so a harness can show the refusal of an update there is no room for. | -| `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 uploads, restores, and archive exports must leave. An export's temporary files are included. A request that would take the volume below it is refused `507 insufficient_storage`. At 0 nothing is held back. | -| `MARFA_UPLOAD_IDLE_TIMEOUT_MS` | `60000` ms | Whole number of milliseconds, 1 to 2147483647 | How long a restore or a blob upload may receive no bytes before the server ends it `408 request_timeout`. An upload has no other time limit while bytes keep arriving. | -| `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 UTF-16 code units of extracted text the server keeps. Longer text is cut short, never through a character. | -| `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, reach proofs 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. It has no user name, password or query string. 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. `/config` overrides it. | -| `MARFA_EVENT_LOG_RETENTION_HOURS` | `720` 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. | -| `MARFA_MCP_FETCH_ENABLED` | `true` | `true`, `1`, `yes`, `on`, `false`, `0`, `no` or `off`, in any case | Whether MCP tools fetch a file from a web address, or a file a chat attached, to save it. When off, a tool asked to fetch one is refused. | -| `MARFA_MCP_FETCH_MAX_BYTES` | `52428800` bytes | Whole number of bytes, 1 or more | The largest file an MCP tool fetches from a web address to save. A larger file is refused and nothing is stored. | -| `MARFA_MCP_FETCH_TIMEOUT_MS` | `60000` ms | Whole number of milliseconds, 1 to 2147483647 | How long an MCP tool's fetch of a file from a web address may take, from connecting to the last byte. | -| `MARFA_ERROR_REPORTING_ENABLED` | `true` | `true`, `1`, `yes`, `on`, `false`, `0`, `no` or `off`, in any case | Send error alerts and exception reports to configured receivers. `/config` overrides it; receiver addresses and credentials stay machine settings. | -| `MARFA_HEARTBEAT_ENABLED` | `true` | `true`, `1`, `yes`, `on`, `false`, `0`, `no` or `off`, in any case | Send scheduled requests to the configured heartbeat receiver. `/config` overrides it; its address stays a machine setting. | -| `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. | +| `MARFA_TEST_MIGRATION_FAULT` | None | `hold`, `lose-row` or `change-shape` | A failure the update of an earlier schema version meets before it commits: `hold` waits there until the process is stopped, `lose-row` deletes a row the update must keep, and `change-shape` adds a column this build does not declare. Needs `MARFA_TEST_HOOKS`, so a harness can show that a failed or interrupted update changes nothing. | +| `MARFA_TEST_FREE_DISK_BYTES` | What the volume reports | Whole number of bytes, 0 or more | The free space, in bytes, the update of an earlier schema version takes the database's volume to have. Needs `MARFA_TEST_HOOKS`, so a harness can show the refusal of an update there is no room for. | +| `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 uploads, restores, and archive exports must leave. An export's temporary files are included. A request that would take the volume below it is refused `507 insufficient_storage`. At 0 nothing is held back. | +| `MARFA_UPLOAD_IDLE_TIMEOUT_MS` | `60000` ms | Whole number of milliseconds, 1 to 2147483647 | How long a restore or a blob upload may receive no bytes before the server ends it `408 request_timeout`. An upload has no other time limit while bytes keep arriving. | +| `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 UTF-16 code units of extracted text the server keeps. Longer text is cut short, never through a character. | +| `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, reach proofs 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. It has no user name, password or query string. 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. `/config` overrides it. | +| `MARFA_EVENT_LOG_RETENTION_HOURS` | `720` 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. | +| `MARFA_MCP_FETCH_ENABLED` | `true` | `true`, `1`, `yes`, `on`, `false`, `0`, `no` or `off`, in any case | Whether MCP tools fetch a file from a web address, or a file a chat attached, to save it. When off, a tool asked to fetch one is refused. | +| `MARFA_MCP_FETCH_MAX_BYTES` | `52428800` bytes | Whole number of bytes, 1 or more | The largest file an MCP tool fetches from a web address to save. A larger file is refused and nothing is stored. | +| `MARFA_MCP_FETCH_TIMEOUT_MS` | `60000` ms | Whole number of milliseconds, 1 to 2147483647 | How long an MCP tool's fetch of a file from a web address may take, from connecting to the last byte. | +| `MARFA_ERROR_REPORTING_ENABLED` | `true` | `true`, `1`, `yes`, `on`, `false`, `0`, `no` or `off`, in any case | Send error alerts and exception reports to configured receivers. `/config` overrides it; receiver addresses and credentials stay machine settings. | +| `MARFA_HEARTBEAT_ENABLED` | `true` | `true`, `1`, `yes`, `on`, `false`, `0`, `no` or `off`, in any case | Send scheduled requests to the configured heartbeat receiver. `/config` overrides it; its address stays a machine setting. | +| `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_SDK_DISABLED` | `false` | `true`, `1`, `yes`, `on`, `false`, `0`, `no` or `off`, in any case | Setting this to `true` turns off traces, logs and exception reports together, whatever the other telemetry settings say. Any other value leaves them as they are. | +| `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 traces and logs on: the server exports traces to `v1/traces` and logs to `v1/logs` under it. Unset, no trace or log is exported; exception reports have settings of their own (`MARFA_POSTHOG_HOST`). | +| `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. With `MARFA_POSTHOG_PROJECT_TOKEN` it turns exception reports on; no OTLP setting is needed. It must be an `https` address, or an `http` address on this machine, with no user name or password; any other value turns off exception reports alone and the boot log says so. | +| `MARFA_POSTHOG_PROJECT_TOKEN` | None | Text with no whitespace around it | The PostHog project token for exception reports. It needs `MARFA_POSTHOG_HOST` set too; no OTLP setting is needed. |