Write
On a Postgres-backed namespace, []string, []int, []uint, []float and
[]bool attributes are stored as native Postgres arrays, in element order, and
filterable ones are indexed for array filters.
An undeclared array attribute takes its type from its elements, so declare an
attribute whose first write is an empty array. []uuid and []datetime return
422 UnsupportedByStore with
feature: "schema.type".
Contains / ContainsAny filters and their Not forms: supported
On a Postgres-backed namespace, patches and filtered writes run in the write’s
single transaction, in turbopuffer’s order: delete_by_filter, then
patch_by_filter, then upserts, patch_rows / patch_columns, and deletes.
- A patch writes only the attributes it names;
nullclears one. A patch to an id that does not exist is ignored and creates no row. - A patched attribute not yet in the schema is declared by the
schemain the same write or inferred from the value, as on an upsert. - Unlike turbopuffer, a client vector attribute can be patched; it is validated like an upserted vector.
- An attribute Layer embeds on write, and its vector, cannot be patched: Layer
cannot recompute the embedding from a patch, so the write is rejected with a
validation_error. Upsert the full row instead. delete_by_filterandpatch_by_filtertake the query filter syntax. Past 5,000,000 matching rows for a delete or 50,000 for a patch, the write is a400unlessdelete_by_filter_allow_partial/patch_by_filter_allow_partialis set, in which case the first rows are written androws_remainingistrue.rows_patchedandrows_deletedcount the rows written;rows_affectedis their sum withrows_upserted.- A write any part of which is rejected, including a
schemachange beside it, has no effects.
For a nested payload that is only returned with a row, declare an explicit
{"type":"json","filterable":false} attribute. For example:
{
"schema": {"links": {"type": "json", "filterable": false}},
"upsert_rows": [{
"id": "job-52", "kind": "job", "project_record_id": "project-52",
"links": [
{"label": "Job folder", "system": "SharePoint", "url": "https://example.test/jobs/52"},
{"label": "Accounting", "system": "QuickBooks", "url": "https://example.test/accounts/52"}
]
}]
}Query, search and collapse preserve that JSON value when it is included in
the returned attributes, including MCP structured results. JSON attributes
cannot be filtered, sorted or ranked; the declaration must set filterable
to false. Types are not inferred from nested objects. This is a Postgres
extension; turbopuffer’s schema does not accept nested JSON attributes.
Keep separately searchable links as rows with kind: "link" and query them
with both project_record_id and kind equality filters.
On a Postgres-backed namespace, conditions use the query filter syntax and are evaluated against each targeted row’s stored value in the write’s transaction:
-
upsert_condition: an existing row is replaced only if it passes. A new id is always inserted. A$ref_newvalue reads the row being written, where a missing attribute isnull.["id", "Eq", null]inserts without overwriting, and this condition keeps only newer versions:["version", "Lt", {"$ref_new": "version"}] -
patch_condition: applies topatch_rowsandpatch_columns. A row is patched only if it passes;$ref_newreads the row as patched. -
delete_condition: a row is deleted only if it passes.$ref_newisnullfor every attribute, and a missing id is skipped. -
rows_affectedcounts only the rows whose condition passed. -
A condition on an undeclared attribute, or a
$ref_newwhose type differs from the compared attribute, is a400validation error. The whole write is rejected with no effects.
Layer stamps every row-producing write with _hevlayer_upserted_at for supported stores. The stamp is what holds the
read watermark; the full set of reserved attributes Layer
manages on a row lives in the document model.
To branch or copy a namespace, see Branching.
Upsert and delete
POST /v2/namespaces/{namespace} accepts a JSON body. The first schema write
creates the namespace. With the common client:
await client.write_namespace("preview_docs", {
"distance_metric": "cosine_distance",
"schema": {
"vector": {"type": "[3]f32"},
"body": {"type": "string", "full_text_search": True},
"category": {"type": "string"},
"year": {"type": "int"},
},
"upsert_rows": [{"id": "atlas", "vector": [1, 0, 0],
"body": "moon geology", "category": "space", "year": 2026}],
})_, err := client.WriteNamespace(ctx, "preview_docs", hevlayer.TurbopufferWriteRequest{
"distance_metric": "cosine_distance",
"schema": map[string]any{
"vector": map[string]any{"type": "[3]f32"},
"body": map[string]any{"type": "string", "full_text_search": true},
"category": map[string]any{"type": "string"},
"year": map[string]any{"type": "int"},
},
"upsert_rows": []map[string]any{{"id": "atlas", "vector": []float64{1, 0, 0},
"body": "moon geology", "category": "space", "year": 2026}},
})await client.writeNamespace("preview_docs", {
distance_metric: "cosine_distance",
schema: {
vector: { type: "[3]f32" },
body: { type: "string", full_text_search: true },
category: { type: "string" }, year: { type: "int" },
},
upsert_rows: [{ id: "atlas", vector: [1, 0, 0],
body: "moon geology", category: "space", year: 2026 }],
});curl --fail-with-body "$LAYER_GATEWAY_URL/v2/namespaces/preview_docs" \
-H "Authorization: Bearer $LAYER_GATEWAY_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"distance_metric":"cosine_distance","schema":{"vector":{"type":"[3]f32"},"body":{"type":"string","full_text_search":true},"category":{"type":"string"},"year":{"type":"int"}},"upsert_rows":[{"id":"atlas","vector":[1,0,0],"body":"moon geology","category":"space","year":2026}]}' Delete by string ID using the same route:
await client.write_namespace("preview_docs", {"deletes": ["atlas"]})await client.writeNamespace("preview_docs", { deletes: ["atlas"] });curl --fail-with-body "$LAYER_GATEWAY_URL/v2/namespaces/preview_docs" \
-H "Authorization: Bearer $LAYER_GATEWAY_API_KEY" \
-H 'Content-Type: application/json' -d '{"deletes":["atlas"]}' Patches, conditions and column writes
On other stores, write-shape support follows the backend notes above. Use
patch_rows to modify named fields without replacing a complete document only
when the selected store supports patches.
Status
Layer validates the body before forwarding and can fail independently of the backing store, so the write path carries a few statuses a plain proxy wouldn’t:
- 200 OK — applied to the selected store. Gateway bookkeeping depends on the backend.
- 422 Unprocessable Entity — Layer rejected the body before forwarding:
no recognized native write operation, a reserved
_hevlayer_*attribute name, a removed custom-write key, or a schema type the configured store cannot represent. The body is a Layer error ({ "error": "validation_error", … }or{ "error": "UnsupportedByStore", … }), not a store one. - Upstream passthrough — any non-2xx the backing store returns is relayed verbatim.
- 502 Bad Gateway — Layer could not reach the backing store
(
{ "error": "upstream_error", … }); the write did not apply.
The passthrough case includes a failed conditional write (upsert_condition,
patch_condition, delete_condition) — turbopuffer’s error body comes back
untouched.