SurrealDB 3.1.0 lands as the first minor on top of the 3.0 line, and it is deliberately a stability release. The themes are a thorough security pass against the engine, a long list of correctness fixes, measurable performance work on the read and scan paths, and a unified observability surface that is finally one pipeline instead of three.
On top of that, the release adds enough new surface to be worth the version bump on its own - a first-party MCP server for AI agents, a DiskANN approximate-nearest-neighbour index alongside HNSW, async functions in Surrealism plugins, full ALTER coverage for every DEFINE statement, distributed trace-context propagation on every endpoint, a GraphQL overhaul (Apollo naming, cursor pagination, GRAPHQL_ALIAS, and expanded filters), value::expect for assertions in SurrealQL chains, and - for SurrealDB Enterprise - a durable audit logging and slow-query pipeline with hash-chained, optionally fsync'd file sinks.
The 3.0 → 3.1 catalog and KV on-disk layouts are unchanged, so an existing 3.0.x deployment can roll forward in place. Internally, Value / Object / Array / Set adopt an optimised revision 0.23.0 encoding on the planner hot path (indexed prologues for O(1) field access without a migration step). A handful of operational defaults have been tightened (the most notable being RocksDB readahead, which is now 256 KiB on every deployment); see the Improvements section for the full list and how to override.
This entry covers everything that landed across v3.1.0-beta.1, v3.1.0-beta.2, v3.1.0-beta.3, and the final stretch into stable. Items marked 🆕 first shipped between the final beta and 3.1.0 stable.
v3.1.0 lands 10 highlight features, 46 broader improvements, 108 bug fixes, and 22 security fixes across the 3.1 series.
🚀 Highlights
🤖 First-party Model Context Protocol (MCP) server
A typed, structured tool surface for AI agents, exposed as a surreal mcp stdio subcommand for local IDE integrations and as an HTTP /mcp endpoint guarded by the existing authentication middleware.
Twelve typed tools:
query,select,create,insert,upsert,update,delete,relate,info,list,use, andrun.read_only_hint/destructive_hint/idempotent_hintannotations so MCP clients can prompt the user before mutating operations.Self-describing schema resources at
surrealdb://schema/ns/{ns}/db/{db}[/table/{table}].New environment variables:
SURREAL_HTTP_MAX_MCP_BODY_SIZE(default 4 MiB),SURREAL_MCP_QUERY_TIMEOUT_SECS(default 60 s),SURREAL_MCP_MAX_RESULT_BYTES(default 256 KiB),SURREAL_MCP_RUN_MAX_ARGS(default 64), andSURREAL_MCP_PARAMS_MAX_KEYS(default 256).
📊 Unified observability and monitoring pipeline
The observability and monitoring layer is now one OpenTelemetry pipeline. The previous community Prometheus path and the parallel OtelObserver SemConv pipeline have been collapsed onto one SdkMeterProvider and one SdkLoggerProvider.
Every signal is recorded once and routed to many exporters: Prometheus text on
/metrics, OTLP metrics push, OTLP logs push.Metric families are reorganised under the
surrealdb.*scope (statement, query, transaction, RPC, auth, session, network, HTTP, live query, process, storage).The
/metricsendpoint enforces a render-timePUBLIC_METRICSallow-list: anonymous scrapers see only aggregate process gauges, while root-authenticated operators see the full surface.Tenant context (
namespace/database/user) is carried on every labelled family but filtered out of the public view by family name.New
surrealdb.transaction.retries/surrealdb.transaction.conflictscounters fire from the commit path when the storage engine returns a retryable error.MCP tool dispatch now emits per-tool request metrics alongside GraphQL, HTTP, and WebSocket surfaces.
New environment variable:
SURREAL_PROCESS_METRICS_REFRESH_INTERVAL(default 5 s). Retired:SURREAL_TELEMETRY_NAMESPACE,SURREAL_TELEMETRY_RPC_LIVE_ID. Repurposed:SURREAL_METRICS_ENABLED.This is a breaking change for existing dashboards; see the observability documentation for the migration table and the full new surface.
🆕 🧭 Distributed trace-context propagation
Inbound traceparent / tracestate headers are now honoured on every transport, so an OTEL-aware client tracing db.query("…") sees the trace tree extend continuously through the server.
HTTP:
traceparentextracted from request headers and parented onto the existingrequestspan. Applies to/rpc,/sql,/key/*,/graphql,/health, and every other route.WebSocket: per-message trace context lives in the RPC envelope (
trace_contextfield, matching the W3C name), so a long-lived connection serves many independent SDK operations under their own traces rather than bucketing them all under the first one.When
SURREAL_LOG_OTEL_LEVELis unset, the four user-facing spans (request, rpc/call, rpc.execute, executor) surface out of the box; operators wanting deep nested core instrumentation setSURREAL_LOG_OTEL_LEVEL=trace.Behaviour is unchanged for SDKs that don't yet emit propagation context - requests without a
traceparentcontinue to produce fresh root spans.
🧭 DiskANN approximate-nearest-neighbour index
A new vector index type sits alongside HNSW under the same KNN query operator, with an overhauled warm-lookup path that benefits both implementations.
New index syntax:
<|K, EF|>queries DiskANN through the same operator used for HNSW.Both DiskANN and HNSW gain
TYPE F16,TYPE U8, andTYPE I8element types, plusDISTANCE INNER_PRODUCTandDISTANCE COSINE_NORMALIZEDmetrics. AddHASHED_VECTORto the index definition if vector-to-document mappings should be stored through vector hashes instead of full vectors.The RocksDB-backed ANN lookup path was reworked end-to-end. The dominant cost on warm-lookup profiles - scanning empty pending-update ranges - is gone; in its place sit process-local caches for graph payloads, vector-to-document doc sets, and doc-id-to-record-id mappings. KV stays the source of truth: cache misses fall back to existing reads, write paths evict, and uncommitted writes never poison the positive doc-id cache.
DEFINE INDEXon an ANN index now drains pending vectors before returning, so a freshly-defined index is immediately usable.Concurrent builds and kNN at release. DiskANN compactions coordinate the process-local cache and graph lock through commit so concurrent
DEFINE INDEX … CONCURRENTLYand KNN queries do not race; HNSW steady-state kNN avoids serialising on graph reload when the in-memory graph already matches KV. If you used DiskANN on a 3.1 beta and saw intermittentDiskANN KNN search failed, that class of failure is addressed (#7318).
⚡ Predicate prefilter and scan-path performance work
A series of changes lift the new executor's scan throughput well above 3.0, both on cold and warm data.
Predicate prefilter on every KV scan. The new executor's KV scan operator evaluates supported
WHEREpredicates against the raw RocksDB key/value pair before the record is decoded. Rows whose predicate is provably false from raw bytes are rejected before revision decode, schema enforcement, projection, and downstream operators run. Supported predicate shapes: root-level equality,IN/NOT IN, byte-levelCONTAINS/CONTAINSANY/CONTAINSALL, simple unaryNOT, conjunctions/disjunctions, and shared-prefix nested accesses.EXPLAINreports whether the prefilter applied and which predicates were pushed down.K-way merge for
WHERE col IN [...] ORDER BY other LIMIT N.UnionIndexScannow merges per-IN-list branches by the composite-index suffix column, so an outerLIMITcan cancel after ~LIMITrows globally instead of materialising every matching row through a heap. On thecrud-benchcombined-workload scan against 5M rows this turns ~2M intermediate rows into ~LIMITrows and removes the OOM that drove this work.🆕 Composite array-element indexes for
CONTAINS/INSIDEandCONTAINSANY/ANYINSIDE. On indexes such asDEFINE INDEX tag_age ON t FIELDS tags.*, age,WHERE tags CONTAINS 'x' ORDER BY age DESC LIMIT N(and theCONTAINSANY/ANYINSIDEmulti-value forms) now plan as boundedIndexScan/UnionIndexScanwalks withLIMITpushed into the scan instead of a full-range index read plus heap sort.CONTAINSALL/ALLINSIDEstill keep a residual filter above the union (intersection semantics) until a follow-up lands.🆕 Single-scan
->edge->vertexgraph traversals. Vertex-side adjacency keys now embed the target(table, id)afterRELATE, so plainSELECT ->likes->person FROM person:…can be served by one range scan instead of scanning edges then chasing each edge's adjacency. Legacy keys remain readable; re-RELATElazily upgrades format. Edge tables with non-FULLSELECTpermissions stay on the two-scan path.Lower default RocksDB readahead for better NVMe scan throughput.
SURREAL_ROCKSDB_MAX_AUTO_READAHEAD_SIZEis now 256 KiB on every deployment, replacing the previous RAM-tiered defaults that reached 4 MiB. Most NVMe block layers cap a single device IO around/sys/block/<device>/queue/max_sectors_kb(typically 128–512 KiB), so larger readahead was being split into multiple IOs with per-IO overhead and over-prefetching. On a representative NVMe seek-random benchmark the per-machine optimum was 128 KiB at 578 ops/sec / 640 MB/s; 4 MiB readahead was ~60% slower.Per-row clone elimination on the projection hot path. The streaming executor now moves row payloads through
Computeand projection instead of deep-cloning them per row, cutting allocator pressure on read-heavy workloads.Hash-keyed
GROUP BYaggregation. The aggregate state map is now hash-keyed and probes buckets in place - for1M rows × 100 groupsthat is roughly~100group-key allocations instead of~1M, while still preserving the previous key-sorted output ordering.
💾 Memory optimisations on the value layer
A round of memory work on the value layer including a 24-byte small-string-optimised Strand type, a new surrealdb-collections crate with VecMap/VecSet, and a per-transaction overhead reduction on the hot path.
Strandis a 24-byte small-string-optimised immutable string type, routed through every core hot-path string:Value::String,Objectkeys,TableName, andRecordIdKey::String.Strandpacks into the same 24 bytes asStringvia arepr(C)SmolStr-style tagged union, with an inline variant storing strings up to 23 bytes with zero heap allocation.A heap variant uses
Arc<str>so clones are a refcount bump rather than a copy.Object entries and
Value::Stringshrink by 8 bytes each, which compounds at document scale.The wire format is unchanged:
Strandserialises identically toStringthroughserde,Revisioned, andstorekey.A companion
Strand::new_staticconstructor lets compile-time-known strings in core skip allocation entirely.
⚙️ Async functions in Surrealism plugins
Plugin authors can mark functions exported through #[surrealism] as async fn and .await host imports directly (storage, network, queries).
Modules can now use async crates like
reqwestandsqlxthat internally depend ontokio::spawnfor connection pooling and timers.The host runtime is initialised once per module instance and persists across invocations on pooled controllers, so there's no per-call setup cost.
The accompanying
surrealism::democrate gains anasync fn fetch_pokemonexample that exercises WASI socket access underallow_net = ["127.0.0.1"].
The broader Surrealism overhaul (WASI Preview 2, WIT-typed host/guest contract, FlatBuffers boundary, attached read-only filesystem, two-engine epoch model) is included in 3.1; see the original beta.1 entry for the full surface change.
🛠 Expanded ALTER coverage
Every DEFINE statement now has a matching ALTER counterpart.
Added
ALTERsupport forEVENT,PARAM,BUCKET,ANALYZER,FUNCTION,USER,ACCESS,CONFIG, andAPI.Each follows the same property-by-property pattern as the existing
ALTER TABLE/FIELD/INDEXstatements.Individual properties on a definition can be modified without re-specifying the whole definition via
DEFINE … OVERWRITE.ALTERresource names and table targets accept bound parameters, matching theDEFINE/REMOVEparameterisation story.ALTER PARAMdoes not supportDROP VALUE(a param without a value is meaningless).ALTER EVENTusesDROP ASYNCto revert to synchronous mode.ALTER ACCESSdoes not allow changing the access type itself.
📦 Durable distributed index build coordination
Concurrently built indexes now keep their build state in the catalog rather than in process memory, and every node coordinates through it.
On multi-node deployments sharing a storage engine, all nodes now agree on which node owns the build, which writes the in-progress index admits, when replay is complete, and when the index becomes query-eligible.
INFO FOR INDEXreflects the durable state.The same change closes a class of issues where COUNT indexes could overcount when the initial build ran on multiple nodes concurrently.
No SurrealQL surface change.
🔐 Audit logging and slow-query telemetry (Enterprise)
A new audit-logging and slow-query pipeline ships in SurrealDB Enterprise, alongside the unified observability surface.
A queued, worker-backed
audit_logmodule records authenticated operations to a configurable sink, with a redaction layer that strips sensitive fields before serialisation.Slow-query detection runs alongside the audit pipeline and emits structured records for queries that exceed the configured threshold.
Both pipelines share a size-rotated, SHA-256 hash-chained, optionally fsync'd file sink in append-only
0600mode. Slow-query is essentially the audit pipeline with a duration gate at the front of the observer.Configuration via 13
SURREAL_AUDIT_*and matchingSURREAL_SLOW_QUERY_*environment variables - sink kind, file path, rotation, fsync cadence, hash chaining, SQL inclusion, redaction (tables / regex / literals), queue capacity, overflow strategy, OTLP export.See the observability documentation for the full operator surface.
✨ Improvements
RocksDB and storage engine
Upgraded RocksDB to 11.0.0 and tuned the engine. Enabled prefix extractor for keyspace-aware bloom filters; aligned the compaction readahead size to
max_sector_kbon disk (currently 256 KiB on Cloud); enabled blob-file separation by default for large-value workloads. Scaled defaults for constrained environments (the SurrealDB Cloud free tier sees disproportionate memory and disk pressure on the previous mid-sized defaults). Large deployments (≥16 GiB memory) keep the previous defaults via tiered scaling; small deployments get appropriately sized buffers, caches, and compaction settings out of the box.Tuned RocksDB scans and grouped-commit. Read-only transactions now bypass the
BaseDeltaIteratormerge layer incount,keys,keysr,scan, andscanr, which removes overhead on every step. The offload heuristic for moving large scans to the storage threadpool now uses byte-based sizing (including skip work) and handlesBytesOrCountcorrectly. The grouped-commit coordinators (RocksDB and SurrealKV) had shutdown and lost-wakeup edge cases that could stall threads or callers; both are fixed.🆕 Full-keyspace compaction to bottommost on
ALTER SYSTEM COMPACT.ALTER SYSTEM COMPACTand the per-resourceALTER {DATABASE,NAMESPACE,TABLE} COMPACTverbs now land outputs at L6 with bottommost-level Zstd compaction forced, instead of leaving compactions at L1/L2 with bottommost SSTs skipped. Tightened auto-compaction trigger defaults (L0 file-trigger 4 → 2; newlevel0_slowdown_writes_triggerdefault 8 andlevel0_stop_writes_triggerdefault 12; deletion-factory window 1000 → 500 and count 50 → 25). NewSURREAL_ROCKSDB_PERIODIC_COMPACTION_SECONDS(default 3600, 0 disables) bounds version overhang and tombstone accumulation on cold ranges. Universal compaction is now a first-class option (still off by default) withuniversal_size_ratio/min_merge_width/max_merge_width/max_size_amplification_percent/compression_size_percent/stop_styleknobs.🆕 Cleaner RocksDB shutdown sequence. Optional full-keyspace compaction (
SURREAL_ROCKSDB_COMPACT_ON_SHUTDOWN, defaultfalse), boundedwait_for_compact(SURREAL_ROCKSDB_SHUTDOWN_WAIT_FOR_COMPACT_SECONDS, default 60 s), and deterministic background-thread teardown viacancel_all_background_work. Errors in the shutdown path are logged-and-continued rather than propagated - a partial shutdown beats panicking on the way out.Optional RocksDB scan checksum verification. New
SURREAL_ROCKSDB_SCAN_VERIFY_CHECKSUMSflag, defaulttrue(existing behaviour). Setting it tofalseskips CRC32C verification on first-read blocks during scans and counts; cached blocks were never re-checksummed either way. Trades a small amount of integrity checking for cold-scan throughput on trusted storage.RocksDB user-defined timestamps for versioned reads.
SELECT … VERSION d'…',INFO FOR DB / TABLE VERSION …, graph and reference traversals, theFETCHclause, and inherited inner subqueries are all isolated from the LRU transaction cache so historical reads cannot pollute current-time reads. RocksDB gains a custom comparator that stores 8-byte LE timestamps as key suffixes (newer versions ordered before older), with HLC commit timestamps assigned at commit time andReadOptions::set_timestampfor point-in-time reads.INFO FOR ROOTandINFO FOR NSnow also acceptVERSION.SurrealKV updated to 0.21.2, picking up the upstream fixes from surrealdb/surrealdb#7303; raised the maximum memtable size for large transactions, allowing larger batches to be inserted without flushing mid-transaction.
🆕 Memory backend versioning parity.
memory://?versioned=true(andmem://) is parsed correctly at startup;SELECT … VERSIONon a non-versioned memory datastore returns the same error as RocksDB and SurrealKV. Defaultsurreal startno longer enables versioning implicitly, and the spuriousCould not parse configuration value for key DATASTORE_PERSISTwarning on plain in-memory start is gone.
Performance and executor
Reduced per-transaction overhead on the hot path.
FunctionRegistry::with_builtinsis now built once per datastore and shared across transactions instead of being rebuilt on every query (was ~22% of CPU on a CREATE-heavy profile). The change-feedWriteris allocated lazily so transactions that touch no changefeed-enabled tables no longer pay for aDashMap<ChangeKey, TableMutations>and its drop. The per-transactionquick_cachenow uses a single shard sized for transaction-scope rather than theavailable_parallelism() * 4default, eliminating the 256CacheShardallocations that used to happen on everyTransaction::newon a 64-core host.Capped the scanner's initial batch size using the query
LIMITso that small-LIMITqueries don't fetch more rows than they will ever return.Added a sync fast-path for
PhysicalExprevaluation in scan filters, avoiding the async overhead for the common case where no IO is required.Avoided cloning the document on
SELECT *projection where the source row can be passed through unchanged, reducing allocator pressure on read-heavy workloads.Replaced the global HTTP client cache (which keyed on a hash of capabilities) with a single
HttpClientinstance perDatastore, exposed viaContext::http_client. Removes process-wide global state and simplifies test setup.🆕 Configurable scan batch size. Promoted the scan
BATCH_SIZEconstant toSURREAL_SCAN_BATCH_SIZE(default 1000). Memory-constrained deployments can lower it; throughput-focused workloads stay on the default.In-memory performance improvements via SurrealMX (SurrealDB's in-memory engine) switching to ferntree, a concurrent B+tree with optimistic lock coupling.
🆕 Optimised internal
Valuewire encoding (revision0.23.0).Value,Object,Array, andSetuse indexed collection prologues and tagged variants so planner walks (pre-decode filter, projection, field lookup) can skip or seek without fully deserialising large records. User-visible serialisation through RPC and export is unchanged; this is a hot-path storage-encoding improvement, not a database migration.
Observability and operations
OTLP exporters now build with TLS (
tls+tls-rootsfeatures), using the platform's native root store viarustls-native-certs.rpc.request_idis emitted as a string identifier rather than the previous binary form, matching the OTelrpc.*conventions. The OTLP runtime is driven by an async runtime so exporters no longer block on a dedicated thread.Made datastore initialisation retry resilient to hung attempts. Each retry attempt is wrapped in its own per-attempt
tokio::time::timeoutthat increases linearly (10 s, 20 s, 30 s, …). A single hung attempt against a slow or unresponsive storage backend can no longer silently consume the entire retry budget. Per-attempt timeouts and back-off sleeps are capped to the remaining global budget. The global retry budget is raised from 60 s to 120 s. Failures are now logged with a per-task label so the failed operation is identifiable. Exhaustion returns a descriptiveInternalerror instead of re-raising the lastTransactionConflict.Bounded shutdown background work to mirror the bounded-startup retry behaviour. Shutdown of long-running background tasks now uses
tokio::time::timeout_atagainst the global deadline, so a stuck task can no longer indefinitely delay datastore shutdown.MCP request instrumentation added to the community OpenTelemetry pipeline, with error classification distinguishing client errors from server errors without parsing message strings.
Auto-detect TTY for the console log output and respect the
NO_COLORenvironment variable. ANSI escape codes are now only emitted when both stdout and stderr are TTYs, fixing garbled output in Docker containers, Kubernetes pods, and log shippers like Kibana and GKE Log Explorer.🆕 Pluggable live-query broker for clustered deployments.
Datastoreaccepts an optionalMessageBrokerso a composer can forward live-query notifications to the node that owns each subscription when writes commit on a different node than the subscriber's WebSocket. The defaultLocalMessageBrokerpreserves single-node behaviour when unset.
Functions and SQL surface
🆕 Added
value::expectmethod. Assert a predicate on a value via a closure and pass the original value through on success - useful in method chains alongsidevalue::chain. On failure, throws with an optional message.Added
REMOVE CONFIG [IF EXISTS] GRAPHQL | API | DEFAULTto mirrorDEFINE CONFIG, providing a way to deleteGRAPHQL,API, andDEFAULTconfigurations.Extended GraphQL schema generation to support fields whose type is a literal (record / object / array literal types), so they now appear in the generated GraphQL schema rather than being silently omitted. Added GraphQL Subscriptions and root-level field comments.
Added
encoding::json::encodeandencoding::json::decodefor serialising SurrealDB values to / from JSON strings, and added support for UTF-16 surrogate-pair escape sequences (\uD800–\uDFFF) in the JSON parser. The surrogate-pair support is gated on ajson_string_escapesparser setting so SurrealQL string parsing is unaffected.Use
serde_jsonto parse external JSON inhttp::*functions so valid JSON responses containing escaped forward slashes (\/) - produced by defaultjson_encode()in PHP and other systems - are no longer rejected by SurrealQL's own JSON parser.Added
--tables-excludeflag tosurreal exportfor excluding specific tables from an export.Allow
surreal validateto read input from stdin, so SurrealQL can be piped in for syntax checking without writing to a file first.Configurable CORS allow list for the HTTP server.
🆕 Reject
REMOVE ANALYZERwhile a full-text index uses it, with a clear error naming the offending index and table - previously the analyzer would be dropped unconditionally, leaving the index pointing at nothing.🆕
REMOVE MODELnow deletes the model file from the configured object store rather than leaving the.surmlartifact orphaned. The delete is idempotent and invalidates the in-process model cache.🆕 Tightened numeric input handling on standard library functions.
rand::id,rand::string,array::repeat,string::repeat,string::semver::set_{major,minor,patch},duration::from_{nanos,micros,millis,secs,mins,hours,days,weeks}, and the<bytes> […]cast /type::bytesnow reject negative or out-of-range inputs with a clearArithmeticNegativeOverflowerror rather than silently wrappingi64tousize/u64. The previous behaviour produced confusing outputs likestring::semver::set::major("1.1.3", -1) → "18446744073709551615.1.3"and could trigger multi-exabyte allocations inrand::*.🆕 Strict array-index conversion. Array indexing (
[1,2,3][i]and the equivalent idiom paths) now treats negatives, NaN, infinities, fractional values, and out-of-range magnitudes as out-of-bounds and returnsNONE, rather than wrapping or saturating. Integer-valued floats and decimals (1.0,1dec) remain valid indices.
SDK and types
Expanded the
surrealdb-typesSDK and derive surface. Added#[surreal(rename_all = "...")]on enums and structs and honoured per-variant#[surreal(rename = "...")](previously parsed only for fields). Added#[surreal(wrap)]for transparent wrapper types. Added moreSurrealValueimplementations for standard-library types and aNaiveDateimplementation. Generalised theCowand&strSurrealValueimplementations. Added serde interop on theValuetype. Restored query chaining on theQuerybuilder. Added several missing crate exports.Added a builder pattern to
Datastorefor properties that cannot be changed after construction, replacing the previous post-construction setters. Makes invalid configurations harder to express. The language-test harness has been overhauled in the same change.Added
surrealdb-collectionsworkspace crate withVecMapandVecSet- vec-backed, ordered, insertion-order-preserving map and set types tuned for the small-collection workloads that dominate SurrealDB's value layer.ObjectandSetinsurrealdb-corenow use these types. User-facing semantics are unchanged.Removed several process-wide configuration globals in
surrealdb-coreso configuration is now carried throughDatastore/Contextrather than read from static state, simplifying embedding and testing.🆕 SDK tolerates servers denying the
versionRPC during connect (--deny-rpc version). Operators who explicitly opt out of exposing the version no longer break new SDK connections.🆕 Improved error communication. Type errors in arithmetic / extend operators now report only the type name rather than the raw operand, and propagated anyhow source chains are no longer copied onto wire errors.
GraphQL
A consolidated GraphQL pass standardises the auto-generated schema on Apollo conventions, adds Relay-style cursor pagination, and closes a batch of schema-generation and filter bugs. See GraphQL overview and DEFINE CONFIG GRAPHQL.
Schema naming (Apollo-only)
There is no longer a NAMING DEFAULT|APOLLO switch - Apollo-style names are the only generated shape:
Fetch one record:
person(id: ID!)(was_get_person(id:)).List records:
people(filter, where, order, limit, start, version)- pluralised table name (wasperson(...)on the list query).Aggregate:
people_aggregate(filter, groupBy, …)(wasperson_aggregate).Mutations:
createPerson/updatePerson/upsertPerson/deletePerson, with bulk formscreatePeople/updatePeople/ etc. (wascreateManyPersonand similar).Object and input type names keep the SurrealQL source name so
record<table>references stay stable; field names mirror SurrealQL idioms (first_namestaysfirst_name) unless overridden.Tables whose names are already plural (
likes,follows) keep the legacy_get_<name>/<verb>Many<Cap>shape so singular and bulk mutation names stay distinct.Two tables that collapse to the same query name abort schema generation with a clear error suggesting
GRAPHQL_ALIAS.
GRAPHQL_ALIAS and GRAPHQL_DEPRECATED
New optional clauses on DEFINE FIELD, DEFINE TABLE, and DEFINE FUNCTION:
Aliases must be valid GraphQL Name tokens; invalid aliases fall back to the auto-derived name. INFO FOR DB and export round-trip the clauses. Deprecation reasons surface in field descriptions as [Deprecated: …] until the schema builder can emit the @deprecated directive on dynamic fields.
Cursor pagination and batched HTTP
Each table gains a Relay-style connection field, for example peopleConnection(first, after, last, before, filter, where, order, version), returning { edges { cursor node { … } }, pageInfo, totalCount }. Cursors are URL-safe base64 of the record id; first / last default to 20 and cap at 1000. totalCount runs only when the client selects it. Offset pagination via limit / start on the list query remains available.
Posting a JSON array of { query, variables, … } envelopes to the GraphQL HTTP endpoint returns a parallel array of responses (batched requests).
Filters and aggregations
Vector similarity and KNN filters on numeric array fields (for example
array<float>embeddings). Per-field filter inputs acceptsimilarity(maps tovector::similarity::*/vector::distance::*with a comparison threshold) andnearest(maps to SurrealQL<|k, distance|>). Distance metrics include cosine, Euclidean, Manhattan, Hamming, Jaccard, Chebyshev, and Pearson. Closes the vector-similarity portion of #7312.Full-text search filter via
matcheson string fields, translating to SurrealQL@@(requiresDEFINE INDEX … SEARCH ANALYZER …). Example:articles(filter: { title: { matches: { query: "fox" } } }) { id title }.Table aggregation via
<plural>_aggregate(filter, groupBy), exposingcount, per-numeric-field{field}_min/{field}_max/{field}_sum/{field}_avg, and optionalgroupBy. Example:products_aggregate(groupBy: [category]) { category count price_avg }.Function-call filters via
callon every field filter input (string::len,vector::*, userfn::…). List queries still acceptorderalongside these predicates.idfilters:id: { in: […] }(expanded toORfor SurrealQLINsemantics),gt/gte/lt/lte, andrange: { from, to, inclusive }. Closes #4555.Relation
countinWHERE: relation fields exposecounton the table filter - e.g.people(filter: { sent: { count: { gt: 5 } } })compiles tocount(->sent) > 5. Closes #4554.
Other GraphQL fixes
Schema cache now keys on a content hash of catalog entries (tables, fields, functions, accesses), so DDL changes appear on the next GraphQL request. Closes #6942.
TYPE { bar: record<foo> }object literals generate nested GraphQL objects instead of failing at schema build. Closes #7034.array<record<T>>filters and mutation inputs use valid GraphQL names (e.g._filter_list_child); nested-object fields no longer leak dots; mutation inputs for record arrays map to[ID!]. Closes #4999.record<T>mutation inputs areIDeverywhere - output object types no longer leak into input position.COMPUTEDandREADONLYfields are omitted fromCreate*/Update*/Upsert*inputs; pre-computedDEFINE TABLE … AS SELECT …views no longer expose create/update/delete mutations.Regex string filters regression-tested (#5078).
🐛 Bug fixes
surrealdb-core compilation time
The issue causing much longer compile times for surrealdb-core since SurrealDB 3.0 has been identified. 3.1 includes a number of fixes that have vastly reduced compile times when compiling surrealdb-core or adding it as a dependency. More fixes to further reduce this time are already planned out and will continue throughout upcoming 3.1.x releases.
Query Engine
NONE.*,NULL.*, scalar.*, and geometry.*now returnNONEin the new streaming planner - matching the legacy executor - instead of wrapping the value in a single-element array. Fixes the Go SDKcannot decode array into <StructType>error that appeared when anoption<record<…>>field wasNONEand the query expanded it with.*. Mixed arrays also now pass non-record elements through unchanged, so[1, record:r, "x"].*returns[1, {fetched}, "x"]rather than[[1], {fetched}, ["x"]](fixes surrealdb/surrealdb#7143).Fixed
$parentresolving incorrectly inside graphWHEREclauses in nested subqueries;$parentnow refers to the currentSELECT's row rather than an outer subquery's parent.Fixed
FROM ONLYnot being propagated through fused graph lookup chains, which previously produced incorrect results from constrained graph traversals.Fixed
ORDER BY(andWHERE) being silently ignored in subqueries with graph traversal sources likeFROM $parent->edge->target. The streaming source operator now resolvesRecordIdvalues to full documents so downstreamFilter,Sort,Group, andSplitoperators have the fields they need.Fixed bare field paths in a subquery's
FROMclause (e.g.FROM data.fileswheredatais a record link on the outer document) evaluating toNONEbecause the source operator had no current value in correlated-subquery context.Fixed
$parentnot being bound duringiterator.prepare, which caused correlated subqueries usingFROM $parent->…(including nestedCREATE,UPDATE, andDELETE) to see$parentunset while select targets were being planned.Fixed
$parentresolution in materialised view definitions on the streaming path so view materialisation now sees the same parent context as the legacy executor.Fixed
LETbindings in planned bodies not being visible toIF/FORblocks that fall back to the legacy compute path; the legacyFrozenContextis now refreshed after a plannedLETbody runs.Fixed
$authand$sessionreturningNONEin queries executed inside a client-side transaction; the session is now correctly attached to the per-statement context.Fixed duplicate constant values being omitted in the streaming executor (e.g.
false AS isLiked, false AS isLockedcollapsing to a single column). The expression registry's de-duplication key now includes the alias.Fixed nested
.*destructure returningNONEfor record links;DestructureField::Allnow fetches the linked record before expanding.Fixed
SELECTalias shadowing the source field of the same name on the projection fast path.Preserved alias idiom structure in streaming-executor projections so that
AS foo.barcontinues to nest into{ foo: { bar: _ } }whileASfoo.barstays as a flat key - both the executor projection andGROUP BYalias paths previously stringified the alias and lost the backtick distinction.Fixed
??(null coalescing) and?:(ternary condition) operator precedence binding tighter than arithmetic.$ctx.limit ?? 2 ** 31 - 1now parses as$ctx.limit ?? (2 ** 31 - 1), matching JavaScript / C# / Kotlin / Swift / PHP semantics.Fixed
value::diffandvalue::patchreturningFunction 'value::…' requires async executionwhen invoked via method syntax ({a: 1}.diff({a: 2})). The async-vs-sync check now matches the behaviour of the full-path call.Fixed view materialisation crashing when the source
SELECTaliased theidfield; initialisation now falls back to scanning for anyRecordIdwhen the"id"key is absent.Fixed coercion error messages omitting the position of the offending element, which made
expected array<int>/expected tupleerrors unactionable on large inputs. Coerce and cast paths now thread the element index through the error.🆕 Improved error message when a
LETstatement appears in a non-block expression position (e.g. inside an array literal or function argument). The previousunreachableerror read like an internal panic; the new message clearly states the syntactic constraint.
Query Planner
Fixed
WHEREfilters on indexed fields silently omittingCOMPUTEDfields from evaluation.IndexScan,FullTextScan, andKnnScannow resolve field state and process computed fields through their pipelines, matchingTableScanandUnionIndexScan.Fixed records with
NONE/NULLfields being silently excluded fromORDER BYresults when the sort key was on a unique compound index; unique indexes now storeNONE/NULLtuples in the non-unique key format so they remain visible to scans (REBUILD INDEXis required to fix existing indexes).Fixed
SELECT VALUEwithORDER BYignoring the sort order when querying from an array source; theVALUEprojection is now applied after sorting and pagination instead of before.ORDER BYcan also reference aSELECT VALUEalias.Fixed
ORDER BY DESCwith a variableLIMITinside nestedIF/LETblocks inside a function producing wrong results.Fixed
ORDER BYon edge-table queries with record-link source fields likein.creationDatereturning unsorted output. The sort path now routes resolved alias expressions through theComputeoperator (which can fetch linked records) and restricts the synchronousFieldPathshortcut to single-part paths where record-link traversal is impossible.Fixed
KNN <|K, DISTANCE|>queries bypassing the HNSW index;eval_hnsw_knnnow matches theK(k, d)variant when the distance matches the index configuration.🆕 New-executor index analyzer: detects contradictory range bounds (
a > 10 AND a < 5) and short-circuits to a zero-row scan; merges same-side bounds (a > 5 AND a > 10); short-circuitsWHERE field IN []instead of running a full scan with a filter; recognises single-elementfield IN [v]as compound-prefix-joinable; cleanly pushes single-columnWHERE field != NULL/!= NONEto an exclusive-lower-bound range scan; caps the compound-prefix bonus so a 12-column prefix no longer outscores a unique equality; warns when aWITH INDEXhint matches no candidate instead of silently falling back.🆕 Indexed
count()/IndexCountScanand legacy COUNT-index fast paths now refuse plans when theWHEREclause touches a field governed by non-FullSELECT permissions, so record users cannot infer cardinality of values they cannot read (fixes the indexed-path leak;WITH NOINDEXalready behaved correctly).🆕 Composite
CONTAINSANY/ANYINSIDEUnionIndexScanplans no longer strip predicates on fields with restricted SELECT permissions; the residual filter re-evaluates after permission reduction so hidden array values cannot be inferred from index membership.
Indexes
Made full-text, COUNT and HNSW compaction SSI-safe so concurrent index activity no longer drops work that was committed after the compaction snapshot. Compaction is now split-phase and generation-guarded, with conditional writes that advance an internal generation key and delete only the exact delta keys captured in the read phase. Full-text and COUNT compaction plans are bounded to
COUNT_BATCH_SIZE(50,000) delta keys per batch, so a long-stalled delta backlog is processed in fixed-size batches instead of being held entirely in memory.Fixed concurrent index conflicts and reduced overhead during full-text concurrent builds. The initial-build write transaction no longer reads queue handoff records (those are now read through a separate read-only transaction, so the initial-build write transaction stays out of active queue contention). The append-phase commit retries on transient transaction conflicts instead of treating them as fatal. Full-text concurrent builds now reuse
FullTextIndexacross documents in a batch instead of recreating it per document.Fixed cascade-delete index bugs that caused COUNT-index drift, ghost UNIQUE-index entries on the relation table after deleting a vertex with edges, and phantom UNIQUE-index entries when
IN/OUTrecords were deleted before the relation.🆕 Graph and versioned-read permission enforcement. Vertex-delete cascades now run edge deletes with the caller's permissions (edge-table
DELETEis no longer bypassed). The->edge->vertexsingle-scan fast path is declined whenever aVERSIONclause is present or the edge table'sSELECTis notFULL, so row-level edge filters are not skipped on point-in-time reads. VersionedSELECTnow applies historical field permissions and params (processor, planner, and scan operators that previously used the current catalog). Graph-edge traversal also no longer hardcodes the current version whenSELECT … WITH VERSIONtraverses edges.Fixed transient transaction conflicts at commit time dropping in-flight background index work. The background index runner now distinguishes recoverable conflicts from fatal errors and retries against fresh state.
Catalog ID sequences now seed safely on the first boot after upgrade. The new per-node batch-based ID generator initialised at
0when its state key was missing, so the first namespace, database, table, or index created after an upgrade could reuse an existing ID and collide with the live catalog entry. The sequence loader now scans the existing catalog and starts frommax(existing_id) + 1whenever the state key isn't present.🆕 Fixed a memory leak where
DEFINE INDEXwould pin the entireDatastore(and its underlying RocksDB / SurrealKV file descriptors) until process exit. The backgroundIndexBuilderwas forming anArccycle through the build context; dropping the user'sSurreal/Datastorehandle now reliably closes the storage handles (fixes surrealdb/surrealdb#7304).
View
Grouped view writes now emit changefeed deltas and trigger LIVE-query notifications. Previously, writes to a
DEFINE TABLE … AS SELECT … GROUP BY …view skipped the changefeed-emit and live-query-notify hooks because the aggregate metadata is written directly throughtx.set_record- soSHOW CHANGESand live-query subscribers missed updates to grouped views. The view-write lifecycle now matches the regular document pipeline.
Live Queries
Terminate live queries on session invalidation and TTL expiry.
invalidate()now callscleanup_lqs(), mirroringreset(), so all live-query registrations for the session are removed from the notification engine. The dispatcher also compares the session expiry timestamp against the current time before sending each notification and silently skips expired ones, closing a class of bug where revoked sessions continued receiving CREATE / UPDATE / DELETE events indefinitely.🆕 Remove live-query registrations when the authenticated principal changes on a session (see GHSA-4m82-p8cx-f94j under Security).
🆕
REMOVE TABLEnow bumps the datastore live-query cache version (matchingLIVE/KILL), so subscriptions cannot survive a drop andDEFINE TABLEwith the same name; previously staleLvsentries could notify old live-query ids against the recreated table.
Functions
value::difffollowed byvalue::patchnow round-trips correctly on top-level scalars.value::diffof two scalars produced a JSON Patch with the empty pointer""as its path; the patch parser was treating that as a single-empty-segment path rather than the document root, sovalue::patchsilently no-op'd and returned the original value (fixes surrealdb/surrealdb#7239).Empty-group sentinels for aggregate functions are now consistent with the corresponding scalar functions:
time::min/time::maxreturnNONEfor empty groups (previously surfaced placeholders liked'+262142-12-31T23:59:59.999999999Z');math::variance/math::stddevreturnNaNfor empty groups (previously returned0.0, which conflated "no data" with "all values identical"). A single-element group still returns0.0. (Fixes surrealdb/surrealdb#7301.)Materialised aggregate views now compute
math::powagainst thesumaccumulator (previously usedcount, returning the wrong magnitude) andtime::minvia the correctDatetimeMinaggregate (previously usedDatetimeMax, returning the maximum).Fixed
infinityoutput and added a non-abbreviated constant path so that the constant is preserved on round-trip.Fixed user-duration structure so that durations defined and stored on user definitions round-trip correctly.
Restored the table-membership check on
type::record(rid, table)when arg1 is aRecordIdand arg2 is aString. A previous semantic change had repurposed arg2 from a table constraint to a record key, sotype::record($id, "user")silently accepted any$idregardless of its table. The newertype::record(table, key)form is unaffected.
Types / SDK
Fixed stack overflow on recursive
SurrealValuetypes so deeply nested or self-referential type definitions no longer crash on encode / decode.Fixed
SurrealValuederive macro emittingr#typeinstead oftypefor raw-identifier struct fields. Fields liker#type: Stringnow serialise as"type"without needing an explicit#[surreal(rename = "type")]attribute.Fixed a copy-paste bug in
prepare_arraythat made the lookup-optimisation branch dead code, so edge traversals inside arrayFROMclauses (SELECT * FROM [a:1->edge->table]) now reach the optimised path.Fixed JSON-patch
removeoperation on lists.Fixed
CREATEfailing for fields typed asset<…>.🆕 Fixed negative
FETCHarray indices wrapping to a hugeusizeinstead of explicitly short-circuiting;arr[-1]in a FETCH path now correctly returnsNONE.Local engine
db.runnow validates the function name as an identifier inside theIntoFntrait. The embedded engine had been constructing SurrealQL viaformat!("{name}({args})")and passing the result toDatastore::execute, so non-identifier names were forwarded verbatim.
Storage
🆕 Memory
?versioned=enforcement.VERSION/ point-in-time reads onmemory://without?versioned=truenow returnThe underlying datastore does not support versioned queries, matching RocksDB and SurrealKV. Plainsurreal startno longer treats the memory backend as versioned by default.Fixed long server non-responsiveness when a client cancels
surreal exportmid-stream. The HTTP export handler now exits the proxy task when the body stream channel fails (indicating client disconnect), which causes the next channel send to fail and aborts the export task - releasing the read transaction promptly. On large databases this previously blocked endpoints like/healthwhile the cancelled export ran to completion.🆕 Filesystem bucket keys are now preserved exactly as supplied. The
lowercase_pathsURL option for filesystem buckets defaulted totrue, silently folding everyfile::putkey to lowercase when writing to disk while reads kept the user-supplied case - producing a "split brain" between SurrealQL and external tooling. Default is nowfalse; explicit opt-in (?lowercase_paths=true) is preserved for callers that genuinely want case-folding (fixes surrealdb/surrealdb#7309).🆕
file::list({ limit: -1 })now rejects negative limits withExpected "non-negative int"instead of silently treating-1asusize::MAXand returning an unbounded listing.Tightened the filesystem bucket allowlist (experimental files feature).
is_path_allowednow requires UTF-8 validity and a component-boundary prefix match - previously a bytewise prefix match admitted/srv/data-evilagainst an allowlist of/srv/data. Object keys containing..segments are now rejected at the bucket entry points.
RPC / WebSocket
Respond with a failure when an in-flight WebSocket request is cancelled rather than silently dropping it, so clients always observe an explicit response.
🆕 Propagate CBOR encode errors instead of panicking on the RPC response path, hardening the connection task against future ciborium changes.
/keyPOST/PUT/PATCHhandlers now parse the request body as a single SurrealQL value and bind it to$datainstead of running it throughDatastore::executeahead of the fixed CRUD statement. Behaviour change for callers that relied on body evaluation (e.g. embeddingtime::now()in the body).🆕
/keybodies must be an inert value expression (literals,$param, constants such asmath::PIonly). A single executable form - bareCREATE …,fn::…(), or a parenthesised statement - is rejected even when it is only one statement, closing arbitrary SurrealQL execution on deployments that expose only/key.
Transactions
Surface unified
Cannot COMMIT: …errors when an explicitCOMMITafter a previous statement abort fails (e.g. unique constraint), instead of returning only the first statement error.txn.commit()failures and the post-abortCOMMITfast-forward now share the same error wording so clients no longer treat a missing row as success.
Auth
Fixed
idresolving toNONEinside a table'sFOR select WHERE … permissionclause when a record was created and selected in the same statement (SELECT * FROM ONLY (CREATE ONLY a)), which previously caused the record to be filtered out by its ownSELECTpermission.Fixed user-defined function
PERMISSIONSclauses being evaluated for system-level users (root, namespace, database).PERMISSIONSare intended only for record-level users; system-level users withAction::Viewnow correctly bypass the check and can invoke functions defined withPERMISSIONS NONE. Also removed a hardcodedfn::prefix from theFunctionPermissionserror message that produced double-prefixed names likefn::fn::my_function.🆕 Live-query computed-field permissions are now applied after the computed fields are populated. A computed field marked
PERMISSIONS FOR select NONE(or a conditional permission that returns false) was being shipped to subscribers on the LIVE delivery path because permission reduction ran before computed-field population; both the livedocand theLIVE … DIFFinitial state are now filtered correctly.ALTER APInow recomputes the caller's IAM envelope from current privileges. Legacy API records written byv3.0.0-beta.1previously carried a permissive storedauth_limitthat the check trusted.GraphQL
fn_*resolvers now read the session from the per-request GraphQL context instead of closing over the schema-generation-time session. The cached resolvers capturedsessionat schema build time, so subsequentfn_*calls ran under that captured session rather than the caller's. Brings them in line with the other resolvers in the crate.🆕
USE NS/USE DBno longer auto-creates namespaces or databases without the same authorization asDEFINE NAMESPACE/DEFINE DATABASE. Anonymous guests and under-privileged sessions can no longer materialise new namespaces throughUSEalone; a stale namespace-level token can no longer recreate a dropped parent namespace as a side effect ofUSE NS dropped_db ….
Events
Fixed events defined on a vertex table incorrectly firing for related edge-table records during cascade deletion. When deleting a vertex with graph edges (via
RELATE), the edge deletes now run with the edge table's document context so the vertex table's events, views, live queries, changefeeds, and field validation are no longer applied to edge-record mutations.
Logging
Fixed JSON log format being unused even when configured.
Fixed ANSI escape codes leaking into JSON log output (which previously produced corrupt records like
"level": "\x1b[32mINFO\x1b[0m") and added per-stream TTY detection so mixed-redirect scenarios likesurreal start > app.logno longer strip ANSI from the stream that is still an interactive terminal.
CLI
Prevent panic on systems with restricted
/procaccess (e.g. NixOS withProcSubset=pidhardening) wheresysinfocannot read/proc/meminfoandSystem::total_memory()returns 0. The previouscgroup_limits()assertion put the systemd service into an infinite restart loop on both RocksDB and SurrealKV backends.🆕
--client-ip=forwarded. Parses the RFC 7239Forwardedheader and uses thefor=identifier from the first forwarded-element as the request client IP (complements the existingX-Forwarded-Formode).FIPS build option (Enterprise).
surrealdb-servergains afipsCargo feature that routes TLS through the AWS-LC FIPS module (rustls/fips) and installs the FIPS crypto provider at startup viasurrealdb_server::tls::install_default_crypto_provider().
Imports / Exports
Fixed re-importing an exported database failing with "field already exists" errors. Export now emits
DEFINE FIELD OVERWRITEso a clean re-import succeeds without manual intervention.
GraphQL
Reserved-word and backtick-quoted field names no longer leak into the generated schema. Fields such as
valueortypeare exposed as valid GraphQL names (value,type, …), which fixes introspection failures in strict clients (Postman, GraphiQL) and restores runtime field resolution for table fetch, filter, and aggregate lookups.Nested array types deeper than three levels now collapse to the
JSONscalar in the generated schema instead of building a float list chain that exceeds the standard seven-hop introspection limit. Runtime values are unchanged; only the declared GraphQL type differs (for examplearray<array<array<array<float>>>>is typed asJSON!rather than[[[[Float!]!]!]!]!).Pre-execution HTTP errors (missing
surreal-ns/surreal-db, GraphQL not configured, schema generation failures) now return a spec-compliant JSON body ({"data": null, "errors": [{"message": "…"}]}) withContent-Type: application/json, instead of a plain-text 400 that clients reported as “Received an invalid GraphQL response”.
🔒 Security
This release closes a substantial batch of issues surfaced by SurrealDB's internal security review process and external reviewers. Most are reachable only by an authenticated caller and require existing privileges; a couple are pre-auth DoS or capability-bypass conditions. The fix for each of these issues ships in v3.1.0 and is enabled by default - no configuration is required.
HTTP
/rpcsessionsmethod leaks attached session UUIDs [High] - GHSA-5qfp-32cf-69jhHTTP
/rpcsession race condition allows privilege escalation [High] - GHSA-4vgr-h27g-cf9pDenial of Service in JSON parser due to nested objects [High] - GHSA-q729-696q-g9pq
Unauthenticated remote DoS via malformed RPC
usecall [High] - GHSA-wjjj-24cx-f28gDenial of Service via nested type annotations [Moderate] - GHSA-q8qp-67f9-wr3f
Scraping a table with no available permissions to the current auth level [Moderate] - GHSA-98fx-66cf-fc7c
Graph traversal bypasses table SELECT permissions [Moderate] - GHSA-vjjx-rfw4-rmfc
Authorization bypass via composite record-id paths [Moderate] - GHSA-6vg3-hgrw-p5gf
Malicious LIVE query can block writes on the watched table [Moderate] - GHSA-4v76-cw68-4vc9
Authorization bypass in
KILLstatement allows termination of other users' live queries [Moderate] - GHSA-gcwr-5mrf-fvchPre-auth memory amplification via unbounded
/sqlWebSocket frames [Moderate] - GHSA-65rj-r9fh-jp2vLIVE query subscriptions survive session state changes [Moderate] - GHSA-4m82-p8cx-f94j
Field-level SELECT permissions leaked via arithmetic error messages [Moderate] - GHSA-6g9v-7gq3-p2c6
Field-level SELECT permissions bypassed via JSON Patch
copy/move[Moderate] - GHSA-fpxg-5xmv-922mRELATEoverwrites existing edge records withoutUPDATEpermission [Moderate] - GHSA-f82j-v89j-mf86LIVE subscribers can read hidden records via captured
$value/$before/$after/$event[Moderate] - GHSA-6wqw-vhfr-9999Port-specific
--deny-netrules bypassed on HTTP redirect [Moderate] - GHSA-97vg-427p-8hx5Implicit namespace/database creation via
USEbypasses DEFINE auth [Moderate] - GHSA-wp87-mgvq-5j93Field-level SELECT permissions bypassed via indexed COUNT fast paths [Moderate] - GHSA-c8jx-96c9-8xrp
Edge
PERMISSIONS FOR deletebypassed when a connected node is deleted [Moderate] - GHSA-whwg-vh4f-pmmfDEFINE ACCESS … ALGORITHM ES512silently downgraded to ES384 [Moderate] - GHSA-fwg2-gr34-q3w8
Breaking changes
Catalog and KV on-disk layouts are unchanged from 3.0.x, so you can upgrade in place. The items below are behaviour or default changes that can affect clients, dashboards, or SurrealQL that relied on previous (often undocumented) semantics.
HTTP /key request bodies
POST, PUT, and PATCH on /key/:table and /key/:table/:id no longer treat the request body as SurrealQL to execute. The body is parsed as a single SurrealQL value and bound to $data only (the server runs fixed statements such as CREATE type::table($table) CONTENT $data or … MERGE $data). There is no $body parameter on /key - if you reference $body in custom logic expecting the HTTP payload, use $data instead. The value must be inert: literals, $param references, and constants only. Function calls, idioms, statements, and parenthesised executable forms (for example a bare CREATE … or fn::evil()) are rejected even when they are a single top-level statement.
If you previously sent bodies such as time::now(), fn::…(), or multi-statement SurrealQL through /key expecting server-side evaluation, move that logic to POST /sql or RPC instead, or compute values in the client and send JSON literals.
This aligns /key with its documented contract of accepting a single inert value rather than arbitrary SurrealQL.
Unified observability metrics
The community metrics pipeline was reorganised under the surrealdb.* scope with a new /metrics allow-list. Existing Prometheus dashboards and alerts that scrape metric names from 3.0.x need updating. See the observability documentation for the migration table (also summarised under Unified observability and monitoring pipeline below).
RocksDB readahead default
SURREAL_ROCKSDB_MAX_AUTO_READAHEAD_SIZE now defaults to 256 KiB on every deployment instead of RAM-tiered values up to 4 MiB. Override the variable if you tuned scan behaviour around the old defaults (see Predicate prefilter and scan-path performance below).
Filesystem bucket key casing
For filesystem-backed buckets, the lowercase_paths URL option now defaults to false. Keys are stored with the casing you supply. Set ?lowercase_paths=true explicitly if you depend on the previous default of folding paths to lowercase on write.
GraphQL Apollo schema naming
The auto-generated GraphQL schema now follows a single Apollo-style naming convention (there is no NAMING config). Existing clients must update query and mutation names:
| Before (3.0.x) | After (3.1.0) |
|---|---|
_get_person(id: …) | person(id: …) |
person(filter: …) (list) | people(filter: …) (pluralised) |
person_aggregate(…) | people_aggregate(…) |
createManyPerson(…) | createPeople(…) |
Regenerate introspection, client stubs, and saved queries after upgrade. Use GRAPHQL_ALIAS on DEFINE TABLE, DEFINE FIELD, or DEFINE FUNCTION when the default pluralisation collides or when camelCase names are required. See GraphQL under Improvements for the full surface (cursor pagination, batched HTTP, new filters).
GraphQL object and input types
GeoJSON output and input geometry types now expose coordinates as the JSON scalar instead of deeply nested Float list types (for example [[[Float!]!]!]!). The wire shape at query time is still nested arrays of numbers; only the introspected GraphQL type changes. Clients that generated queries or validators from the old nested-float schema (strict introspection tools, codegen) must be regenerated. This applies to GeometryPoint, GeometryLineString, GeometryPolygon, and the other variant types, plus their *Input counterparts.
--allow-net now also gates resolved private and special-use IPs
When a hostname permitted by --allow-net resolves to a private or special-use IP, the resolved IP must also be covered by an --allow-net entry. Public IPs are unaffected and the check is skipped under --allow-net all. To allow internal traffic by hostname - e.g. --allow-net internal.example.com resolving to 10.0.5.42 - add the range: --allow-net internal.example.com,10.0.5.0/24.
Ranges checked: IPv4 loopback (127.0.0.0/8), RFC 1918 (10/8, 172.16/12, 192.168/16), link-local (169.254.0.0/16), shared (100.64.0.0/10), broadcast, unspecified; IPv6 loopback (::1), unspecified (::), unique-local (fc00::/7), link-local (fe80::/10). IPv4-mapped IPv6 is canonicalised first
Upgrade or install
Get SurrealDB v3.1.0
Pick your platform to copy a command that installs or upgrades SurrealDB to v3.1.0. Already have the CLI? Use surreal upgrade to swap in place.
Install via Homebrew, then upgrade in place to v3.1.0.
Our newsletter
Get tutorials, AI agent recipes, webinars, and early product updates in your inbox every two weeks
No newer release.
v3.1.0-beta.3
Released May 14, 2026