Operations
Store capability matrix
Layer serves one API across its configured stores. Each cell below comes from the gateway’s backend capability declaration. Wire protocol matching explains the contract, and how we validate it is below the matrix. Pages with backend-specific content offer a picker when more than one documented backend applies.
Postgres configuration
Postgres uses VectorStore spec.kind: pgvector with a PostgreSQL connection
URI in spec.endpoint.url. The standalone configuration
covers standalone configuration, database authentication and extension versions,
namespace-to-table mapping, and schema-driven HNSW, BM25, and scalar indexes.
The quickstart runs the bundled database and gateway locally.
Read the Postgres column below for supported wire features and gaps. The write and query and fetch references describe Postgres limitations alongside the affected features. Text ranking follows the shared BM25-class scoring caveat.
A namespace may declare any number of full_text_search string attributes
(see schema limits); a BM25 rank_by names one of them
and scores that attribute only. Postgres keeps every full-text attribute on
one pg_search BM25 index. Declaring a further full_text_search attribute
on an existing namespace rebuilds that index inside the same write
transaction, so the write takes time proportional to the namespace’s row
count and the new attribute is rankable as soon as the write returns.
Removing full_text_search from a declared attribute returns 400.
Matrix
| Wire feature | turbopuffer | Postgres |
|---|---|---|
| Namespace/schema CRUD and listing | supported | supported |
| Row upserts | supported | supported |
| Column upserts | supported | supported |
| Delete by ID | supported | supported |
| Single/batch fetch | supported | supported |
| Dense top-k (ANN) | supported | supported |
| Cosine / squared-Euclidean distance | supported | supported |
| BM25 text rank [1] | supported | supported |
| Explicit native Postgres text fallback | unsupported | unsupported |
| HybridText rank operator (gateway dense + text RRF) | supported | approximateHybridText with fuzziness: 0 only (BM25 + dense legs, gateway RRF); auto/1/2 fuzziness and cursor/temporal_filter return 422 |
| Attribute projection | supported | supported |
| Eq / NotEq / Gt / Gte / Lt / Lte / In; And / Or | supported | supported |
| Not / NotIn filters | supported | supported |
| Contains / ContainsAny filters | supported | unsupported |
| Regex filters | supported | unsupported |
| Other filter operators | supported | unsupported |
| Fuzzy text filters | supported | unsupported |
| Advanced text rank expressions | supported | unsupported |
| Multi-vector ANN | supported | unsupported |
| Sparse rank | unsupported | unsupported |
| Multiple vector/text fields | supported | approximateAny number of full_text_search fields, ranked one at a time by BM25; a second vector field returns 422 |
| Multi-query queries body / rerank_by (client-composed hybrid) | supported | unsupported422 for a queries or rerank_by body; hybrid retrieval is the HybridText rank operator |
| Ranked cursor / searchAfter | supported | unsupported |
| Ordered scans | supported | supported |
| Facets | approximateNative wire request only; the optional portable adapter primitive is unavailable. | unsupported |
| Aggregates / group_by | supported | unsupported |
| Delete by filter | approximateNative wire request only; the optional portable adapter primitive is unavailable. | unsupported |
| Row patches | supported | unsupported |
| Column patches | supported | unsupported |
| Conditional writes | supported | approximateupsert_condition and delete_condition, including $ref_new; patch_condition returns 422 because row and column patches are unsupported |
| Copy namespace | supported | unsupported |
| Branch namespace | supported | unsupportedNo native namespace branching; the gateway does not emulate one. |
| Vendor encryption controls | supported | unsupported |
| Arrow IPC import | unsupported | unsupported |
| Export formats | supported | unsupported |
| Backend warm hints | supported | unsupported |
| Backend stability / watermark signals | supported | unsupported |
| Backend snapshot integration | supported | unsupported |
| UDF discovery / writeback primitives | supported | unsupported |
| Arbitrary vendor administrative passthrough | supported | unsupported |
| Embedding expressions / schema | supported | approximateGateway-resolved embedding only; one embedded attribute per namespace; chunked embedding returns 422. |
| Query by stored vector ID | supported | unsupported |
| as_of / between filters | supported | unsupported |
| Fused leg provenance | supported | unsupported |
| Auto query routing | supported | unsupported |
| Query scatter/gather | supported | unsupported |
| Wire vector encoding | supported | unsupported |
[1] BM25-class scoring; tokenization differs; fused order may differ across backends
Schema limits
The schema_limits block of the capabilities report: whether a schema attribute may declare embed, and how many attributes of each indexed shape one namespace may declare. A blank count is no store-imposed limit.
| Limit | turbopuffer | Postgres |
|---|---|---|
embed — schema attribute may declare embed | supported | approximateGateway-resolved embedding only; one embedded attribute per namespace; chunked embedding returns 422. |
max_gateway_embed_attributes — attributes with a gateway-served embed | 1 | |
max_full_text_search_fields — full_text_search fields | ||
max_vector_fields — vector fields ([N]f32) | 1 |
Blobs
Where blob bytes live. A store with native bytes holds blobs up to max_value_bytes (blank is the gateway's 10 MiB cap); larger blobs, and every blob on a store without native bytes, go to S3.
| Blobs | turbopuffer | Postgres |
|---|---|---|
native — the store holds blob bytes | yes | yes |
max_value_bytes — largest blob in the store | 6291456 |
Reading the cells
Supported means the declared feature is available. Approximate means
it is served with the limits described in the cell. Unsupported means the
store cannot serve that feature and returns 422 UnsupportedByStore.
Capabilities describe individual features, not every combination of options.
The schema limits table beneath the matrix gives the numeric
bounds behind the multiple-fields row.
Two request shapes express hybrid retrieval, and each has its own row. A store accepts Supported routes and Approximate routes within their stated limits. Unsupported routes reject.
| Route | Request | Row |
|---|---|---|
HybridText | rank_by: [field, "HybridText", input] — the gateway runs one ranked query per leg and fuses with RRF | HybridText rank operator |
| Multi-query | a queries body — independent legs, fused by the store when rerank_by is set | Multi-query |
On Postgres, HybridText is approximate — served only with the shape its
cell states (fuzziness: 0, no cursor or temporal filter) — and a queries
or rerank_by body returns 422 UnsupportedByStore naming multi_query.
How we validate it
Matching the wire does not promise identical index internals, latency, scores, or ranking across stores. In particular, full-text ranking is backend-specific. Layer’s additional request fields, routes, and response metadata are documented as gateway enhancements.
- API and client contracts. The SDK harness compares the gateway OpenAPI operations and generated Python client with the upstream API, and checks captured HTTP requests against documented examples using a mock server. These checks catch route, field, and serialization drift; they do not prove that a real backend returns the right results.
- Backend acceptance. Store-specific suites send requests through a real gateway and backend using generated clients. They check supported operations and explicit rejection of unsupported requests. The Postgres suite runs against the Compose database.
- Documented examples. A committed selection of upstream examples runs against a real gateway and store. Each request is classified as ok, unsupported, fail, or blocked by a prerequisite. A baseline change fails the check for review; matching a baseline can still preserve known failures. This is a selected test corpus, not proof that every upstream request or combination works. The observed results are at the end of this page.
The matrix is generated from backend declarations and checked for source drift. It states the contract; acceptance results are evidence of behavior. Both are needed to assess compatibility.
The fail-fast contract
Unsupported requests fail with 422 UnsupportedByStore. The gateway does not
silently drop unsupported predicates or substitute a different backend. The
body names the store, the route, and the rejected feature:
{
"error": "UnsupportedByStore",
"store": "pgvector",
"route": "/v2/namespaces/traces",
"feature": "patch_rows",
"message": "UnsupportedByStore: pgvector: patch_rows"
}
feature is a stable identifier. Match on it; do not parse message.
- A wire-feature id from the matrix when one owns the request:
multi_queryfor aqueriesorrerank_bybody,patch_rows,search_afterforcursororsearchAfter,conditional_writesfor apatch_conditionon Postgres. The same string appears in the matrix and the 422. - Otherwise the rejected wire key, dotted when nested: a second embedded
attribute on Postgres is
schema.embedand a chunked one isembed.chunk; a filter operator is its name, such asContains. - Schema-count limits use the limit name, such as
max_vector_fieldsfor a second vector attribute on Postgres.
message stays human-readable and starts with
UnsupportedByStore: {store}: {feature}; any detail follows after a colon.
feature is additive, so existing clients that read error and message
are unaffected, and clients ignore values they do not know. A rejection that
names no single feature carries no feature field.
from hevlayer import AsyncHevlayer, HevlayerError
try:
await client.write_namespace("traces", body)
except HevlayerError as e:
if e.error == "UnsupportedByStore" and e.feature == "multi_query":
... # choose the HybridText route instead
Observed against the turbopuffer.com docs examples
The matrix above is what each backend declares. The table below is what
happened when the committed selection of documented requests from
turbopuffer.com/docs was run against
a store through the gateway. Ok means the configured response checks passed (including the explicitly
case-insensitive delete-status check). Unsupported means the store returned 422 UnsupportedByStore naming
the feature. Fail means some other status, or a result that contradicts the
docs; those are open findings, not declared gaps. Blocked means a prerequisite did not pass. The “Matrix row” column links
each example to the declared cell it exercises. Regenerate with
python3 tools/tpuf-docs-e2e/run.py --docs-json site/src/generated/tpuf-docs-e2e.json after a store change.
40 documented requests checked against a
pgvector store on gateway 0.7.0-dev
(2026-09-27):
13ok20unsupported2fail5blocked.
31 fixture requests (seed data the docs assume, and checks that a rejected write left no state) are listed separately.
| Docs page | Documented request | Matrix row | Observed |
|---|---|---|---|
| write | documented upsert_rows + patch_rows + deleteswrite-upsert-row | patch_rows | unsupportedUnsupportedByStore: pgvector: patch_rows |
| metadata | documented GET metadatawrite-upsert-row | namespace_crud | failapprox_num_rows missing from response; schema.vector.ann.distance_metric missing from response |
| namespaces | documented GET /v1/namespaces?page_size=50write-upsert-row | namespace_crud | ok |
| delete-namespace | documented DELETE namespacewrite-upsert-row | namespace_crud | oknote: status field is 'OK'; docs show 'ok' |
| write | documented upsert_columns + patch_columns + deleteswrite-upsert-columns | patch_columns | unsupportedUnsupportedByStore: pgvector: patch_columns |
| write | documented uuid id, FTS text, []uuid permissionswrite-schema | upsert_rows | unsupportedUnsupportedByStore: pgvector: schema.id |
| query | permissions ContainsAny (documented on docs/query, python only)write-schema | array_filters | blockeddepends on step that ended 'unsupported': documented uuid id, FTS text, []uuid permissions |
| write | documented two ANN vector columns ([3]f32, [2]f16)write-multivec | multiple_fields | unsupportedUnsupportedByStore: pgvector: schema.ann |
| write | documented seed upsert (drafts v2, v5)write-conditional | upsert_rows | ok |
| write | documented upsert_condition version Lt $ref_newwrite-conditional | conditional_writes | ok |
| write | documented seed upsert (views 10, 2500)write-delete-by-filter | upsert_rows | ok |
| write | documented delete_by_filter (IGlob + Lte)write-delete-by-filter | delete_by_filter | unsupportedUnsupportedByStore: pgvector: delete_by_filter |
| write | documented seed upsert (three published posts)write-patch-by-filter | upsert_rows | ok |
| write | documented patch_by_filter (status -> archived)write-patch-by-filter | patch_rows | unsupportedUnsupportedByStore: pgvector: patch_by_filter |
| write | documented copy_from_namespacewrite-copy-from | copy_from_namespace | unsupportedUnsupportedByStore: pgvector: copy_from_namespace |
| branching | documented branch_from_namespacewrite-copy-from | branch_from_namespace | unsupportedUnsupportedByStore: pgvector: branch_from_namespace |
| api-overview | documented async copy (Prefer: respond-async)write-copy-from | copy_from_namespace | unsupportedUnsupportedByStore: pgvector: copy_from_namespace |
| query | documented ANN queryquery-vector | dense | ok |
| query | documented ANN with And[timestamp Gte, public Eq] filtersquery-filters | scalar_filters | ok |
| query | documented BM25 with And[timestamp Gte, public Eq] filtersquery-fts-filtered | fts | ok |
| query | documented BM25 'quick fox' with include_attributesquery-fts-basic | fts | ok |
| query | documented In / Eq / NotGlob / Or[Glob] filters with exclude_attributesquery-complex-filter | advanced_filters | unsupportedUnsupportedByStore: pgvector: exclude_attributes |
| query | documented exact kNN with category filterquery-knn | dense | unsupportedUnsupportedByStore: pgvector: kNN |
| query | documented SparseKNN queryquery-sparse-vector | sparse | unsupportedUnsupportedByStore: pgvector: SparseKNN |
| query | documented attribute ordering (timestamp desc, Lt filter, limit 1000)query-ordering | ordered_scan | ok |
| query | documented Count aggregate with filterquery-aggregates | aggregate_by | unsupportedUnsupportedByStore: pgvector: aggregate_by |
| query | documented Sum aggregate with filterquery-aggregates | aggregate_by | unsupportedUnsupportedByStore: pgvector: aggregate_by |
| query | documented Count grouped by color, sizequery-group-by | aggregate_by | unsupportedUnsupportedByStore: pgvector: aggregate_by |
| query | documented Count grouped by ForEachUnique tagsquery-group-by | aggregate_by | blockeddepends on step that ended 'unsupported': fixture rows with []string tags for ForEachUnique (classifies the []string type) |
| query | documented multi-query (queries array)query-multi | multi_query | unsupportedUnsupportedByStore: pgvector: multi_query: wire key: queries |
| query | documented multi-query with rerank_by RRF weightsquery-multi | multi_query | unsupportedUnsupportedByStore: pgvector: multi_query: wire key: queries |
| query | documented multi-vector (late interaction) ANNquery-late-interaction | multivector | blockeddepends on step that ended 'unsupported': fixture multi-vector column tokens: [][2]f32, ann (docs use [][128]f32) |
| query | documented per-attribute limit ({per, total})query-limit-per | dense | failHTTP 400: 'top_k must be between 1 and 10000' |
| fts | documented FTS-only write (no vector column)fts-basic | upsert_rows | ok |
| fts | documented BM25 'turbopuffer' (docs: ranking is [3, 1, 2])fts-basic | fts | ok |
| fts | documented ContainsAllTokens 'search engine' (docs: [1, 2])fts-basic | advanced_filters | unsupportedUnsupportedByStore: pgvector: ContainsAllTokens |
| fts | documented write with FTS options on three fields (language, stemming, stopwords, case, []string)fts-advanced | multiple_fields | unsupportedUnsupportedByStore: pgvector: schema.full_text_search: configuration object; only true/false is accepted |
| fts | documented Sum/Product weighted BM25 across fields with filtersfts-advanced | advanced_text | blockeddepends on step that ended 'unsupported': documented write with FTS options on three fields (language, stemming, stopwords, case, []string) |
| fts | documented write with fuzzy + glob schema flagsfts-fuzzy | fuzzy | unsupportedUnsupportedByStore: pgvector: schema.fuzzy |
| fts | documented Sum[Product Glob, Fuzzy] rankfts-fuzzy | fuzzy | blockeddepends on step that ended 'unsupported': documented write with fuzzy + glob schema flags |
Fixture requests (31)
| Scenario | Fixture | Matrix row | Observed |
|---|---|---|---|
| write-upsert-row | upsert_rows only (fixture: documented rows without the patch/delete legs) | upsert_rows | unsupportedUnsupportedByStore: pgvector: schema.type: inferred from attribute my-string-array |
| write-upsert-row | documented rows minus the []string attribute (fixture) | upsert_rows | ok |
| write-upsert-row | list with prefix (fixture: documented `prefix` parameter) | namespace_crud | ok |
| write-upsert-row | metadata after delete is 404 (fixture) | namespace_crud | ok |
| write-upsert-columns | upsert_columns only (fixture) | upsert_columns | unsupportedUnsupportedByStore: pgvector: schema.type: inferred from attribute my-string-array |
| write-upsert-columns | null column value filters as missing (fixture) | scalar_filters | blockeddepends on step that ended 'unsupported': upsert_columns only (fixture) |
| write-schema | BM25 over the documented text (fixture) | fts | blockeddepends on step that ended 'unsupported': documented uuid id, FTS text, []uuid permissions |
| write-multivec | ANN on the named title_embedding column (fixture) | multiple_fields | blockeddepends on step that ended 'unsupported': documented two ANN vector columns ([3]f32, [2]f16) |
| write-conditional | row count reflects the conditional outcome (fixture: 3 if applied, 2 if rejected) | conditional_writes | ok |
| write-delete-by-filter | row count reflects the delete outcome (fixture: 1 if applied, 2 if rejected) | delete_by_filter | ok |
| write-patch-by-filter | archived rows reflect the patch outcome (fixture: 2 if applied, 0 if rejected) | patch_rows | ok |
| write-copy-from | source namespace (fixture) | upsert_rows | ok |
| query-vector | fixture rows matching the documented response ($dist 0.0 and 2.0) | upsert_rows | ok |
| query-filters | fixture rows with datetime timestamp, public flag, title (classifies the documented datetime schema type) | upsert_rows | unsupportedUnsupportedByStore: pgvector: schema.type: datetime |
| query-filters | fixture rows with timestamp as plain string; datetime type is classified separately, public flag, title | upsert_rows | ok |
| query-fts-filtered | fixture rows with FTS content, datetime timestamp, public flag (classifies the documented datetime schema type) | upsert_rows | unsupportedUnsupportedByStore: pgvector: schema.type: datetime |
| query-fts-filtered | fixture rows with FTS content, timestamp as plain string; datetime type is classified separately, public flag | upsert_rows | ok |
| query-fts-basic | fixture rows with FTS content and title | upsert_rows | ok |
| query-complex-filter | fixture rows with key1 and filename | upsert_rows | ok |
| query-knn | fixture rows with category | upsert_rows | ok |
| query-sparse-vector | plain fixture namespace (sparse schema not fixtured; classifies the query path only) | upsert_rows | ok |
| query-ordering | fixture rows with datetime timestamp (no vector) (classifies the documented datetime schema type) | upsert_rows | unsupportedUnsupportedByStore: pgvector: schema.type: datetime |
| query-ordering | fixture rows with timestamp as plain string; datetime type is classified separately (no vector) | upsert_rows | ok |
| query-aggregates | fixture rows with cool_score | upsert_rows | ok |
| query-group-by | fixture rows matching the documented groups (blue/XL x1, red/L x2) | upsert_rows | ok |
| query-group-by | fixture rows with []string tags for ForEachUnique (classifies the []string type) | upsert_rows | unsupportedUnsupportedByStore: pgvector: schema.type: inferred from attribute tags |
| query-multi | fixture rows with vector and FTS attr1 | upsert_rows | ok |
| query-multi | gateway hybrid over the same legs with the documented `limit` alias (fixture) | hybrid | unsupportedUnsupportedByStore: pgvector: limit |
| query-multi | gateway hybrid over the same legs with top_k (fixture: Layer's fused route, RFC 0114 phase one) | hybrid | ok |
| query-late-interaction | fixture multi-vector column tokens: [][2]f32, ann (docs use [][128]f32) | multivector | unsupportedUnsupportedByStore: pgvector: schema.ann |
| query-limit-per | fixture rows across two categories | upsert_rows | ok |