Docs / EventSourcingDB / Changelog / Changelog
Changelog
All notable changes to EventSourcingDB will be documented in this file.
The format is based on Keep a Changelog, and EventSourcingDB adheres to Semantic Versioning.
1.3.0 (November 11th, 2026)Added in 1.3#
Before upgrading, read Upgrading EventSourcingDB. This release needs attention where:
- the Docker image runs on a data directory that an earlier version wrote, since the image now runs as another user
- an entry of the data directory, such as
eventsorwal, is a link or lies on another file system, or the data directory lies on a file system that cannot lock, since the server then refuses to start - a rollout starts the new server before the old one has stopped, such as a Kubernetes Deployment with the strategy
RollingUpdate, since two servers must never run on one data directory - a reverse proxy or a pool of connections keeps idle connections open longer than EventSourcingDB does, see Limits, since it would then send requests over connections that the server has closed
- the API token is shorter than 32 characters, holds anything but printable ASCII, or has whitespace at its start or end, since the server then refuses to start
- the server is started with
--ai-modelor--ai-option, since this release removes them and the server then refuses to start - clients verify event hashes, since they need an SDK that knows version 2 of the hash before the server is upgraded
- the
sourceof events is not a URI as RFC 3986 defines it, since such writes are now refused - event schemas refer to anything outside themselves, refer back to a part of themselves, as one that describes a tree does, use
$dynamicRefor$recursiveRef, or reach too many subschemas, since writes of their event types are refused afterwards - subjects have more than 32 segments or 1,024 characters, since events can no longer be written or read by such a subject
- EventQL queries or preconditions rely on behavior that this release changes, as marked below
At its first start, this version rebuilds its indexes and refuses some requests until it is done, which can take several minutes. A data directory that it has used cannot be opened by an earlier version again; to go back, restore a backup.
Added#
- Added a v2 HTTP API under
/api/v2, next to the v1 API and on the same data, which reports the outcome of every request it accepts in its body and names each error by a code of its own. - Added importing and exporting events to the v2 API, as one plain CloudEvent per line with its ID and time, so that an export imports without conversion. An import fills a database without events, checking them against the schemas registered beforehand, and computes the hashes itself, so that removing personal data no longer needs them computed by hand.
- Added incremental backups to the v2 API, which hold only the events written since a previous backup; a restore takes a full backup followed by its increments.
- Added recursive preconditions to the v2 API:
isSubjectPristine,isSubjectPopulated, andisSubjectOnEventIdtake the optionrecursive, which applies them to a subject and all subjects below it. - Added an API catalog at
/.well-known/api-catalog, as RFC 9727 describes, which links to the OpenAPI documents of both APIs, that of the v2 API at/api/v2/system/openapiwith the error codes of every route. - Added OpenMetrics to the health endpoint of the v2 API, for a request that accepts
application/openmetrics-text. Every value without a standard name carries the prefixeventsourcingdb_, and the process reports its start time and CPU time under their standard names rather than its uptime and CPU usage, as on the v1 API. - Added to the health endpoint of the v2 API the resident memory and the file descriptors of the process, counters of writes that tell rejected writes from failed ones, and the check
store:clockLag, which warns, also in the management UI, once the clock of the server falls more than a minute behind the time of the latest event. - Added the
--require-event-schemasflag to enforce known event types: writes and restores of events whose type has no registered schema are rejected. It is off by default. - Added rate limiting, which protects the database from too many requests. It is enabled by default, scales with the size of the machine, and can be tuned with
--rate-limit-requests-per-second, where0switches off the limit per client. - Added several keys to
ORDER BYin EventQL queries, each with a direction of its own, as inORDER BY e.type, e.time DESC. - Added documentation for the MCP server extension, the Claude Code Plugin, and the Elixir and Java client SDKs.
- Documented the validation rules for events: what subjects and event types may hold, that
datamust be a JSON object, and the extension attributeshash,predecessorhash, andsignature.
Changed#
- Changed the hash of events: events written from now on carry a hash of version 2, which also covers
traceparentandtracestateand names its version, as inesdb:hash:v2:..., while events written before keep version 1. Update client SDKs that verify hashes before upgrading, since one that knows only version 1 reports every new event as tampered with; a backup with such events cannot be restored in an earlier version. See verifying event signatures. - Changed the error messages of writing, reading, and observing events, of listing subjects and event types, and of registering event schemas, which now say what is expected and name an event or a precondition by its place in the request, such as
/events/3. A refused write says why, such as read-only mode or a full disk. Status codes are unchanged. - Changed the OpenAPI document of the v1 API, which now declares OpenAPI 3.2 and describes each line of its streams under
itemSchema, so that a tool that reads only OpenAPI 3.1 can no longer read it. - Changed how EventSourcingDB shuts down: on
SIGTERMandSIGQUIT, as on Ctrl+C, it now ends long-running requests, gives every other request up to five seconds, and writes what is not yet in the data files, and a server whose port cannot be opened exits with code 1. SetterminationGracePeriodSecondsand a startup probe as the Kubernetes guide now does, since a start after a crash can take longer than the liveness probe waits. - Changed the
diagnosticssubcommand, which now asks the server through the v2 API, shows more of its configuration, and sorts its output. For a server of an earlier version, use the CLI of that version. - Changed the user of the Docker image to
nonroot(UID and GID 65532) of the hardened base image, rather thanesdb(UID 100, GID 101), so that this version cannot open a data directory that an earlier image wrote. Before upgrading, give the data directory to UID 65532, or start this version with--user 100:101. The image now gives its user by these IDs, so that a Kubernetes Pod withrunAsNonRoot: trueand norunAsUser, which did not start before, starts. - Changed
SKIPandTOPin EventQL queries, which now limit the results after the projection,DISTINCT, and aggregates, asOFFSETandLIMITdo in SQL, and take only integers,TOPfrom1andSKIPfrom0. This changes the results of existing queries that combine them with aggregates orDISTINCTwithoutGROUP BY:TOP 2 PROJECT INTO COUNT()now counts all events rather than two, andTOP 0, which set no limit, is now refused; leave outTOPfor no limit. - Changed
NOTin EventQL queries andisEventQlQueryTruepreconditions, which now fails on a value that is not a boolean,NULLincluded, rather than converting it. This can break existing queries and preconditions: writeWHERE e.data.price == NULLrather thanWHERE NOT e.data.price, and put a comparison in parentheses, as inNOT (e.type == "io.eventsourcingdb.library.book-borrowed"), sinceNOTbinds more tightly than==. - Changed how EventQL queries and
isEventQlQueryTruepreconditions compare aDATEwith aDATETIME, which are now values of different types, whichever stands on the left:==isfalse,!=istrue,<,>,<=, and>=end the query, andMINandMAXrefuse a mix of them. This can break existing queries and preconditions: write a range of days withDATETIMEbounds, as ine.time >= "2026-10-20T00:00:00Z" AS DATETIME AND e.time < "2026-10-21T00:00:00Z" AS DATETIME, or compareDATE(e.time). - Changed how EventQL queries and
isEventQlQueryTruepreconditions compare a number with a string by==and!=: they are now unequal, as values of different types are, so10115 == "10115"isfalse. This can break existing queries and preconditions: compare with a value of the same type, as ine.data.zip == 10115.+still joins a number with a string. - Changed how EventQL queries and
isEventQlQueryTruepreconditions compare and order theidof an event, which is a string:ORDER BY e.idsorts the ids as strings, so10comes before9,e.id == 42isfalse, ande.id >= 1000ends the query. This changes the results of existing queries and preconditions: cast theidto compare or sort it as a number, as ine.id AS INT >= 1000orORDER BY e.id AS INT. - Changed dates and times in EventQL queries and
isEventQlQueryTruepreconditions, which now work in UTC throughout:NOW()returns UTC,DATE,TIME, and the parts of aDATETIMEare taken in UTC, and results hand out aDATETIMEin UTC. This changes the results of existing queries and preconditions on times whose offset is not UTC:DATE("2026-10-20T23:30:00-02:00" AS DATETIME)is now2026-10-21. - Changed
+in EventQL queries andisEventQlQueryTruepreconditions, which now joins a string only with a string or a number, whichever side it stands on, so that a field that an event lacks no longer joins asnull. This can break existing queries and preconditions that join a boolean,NULL, or an object with a string; a boolean, a date, or a time joins throughAS STRING, as in"Active: " + (e.data.isActive AS STRING). - Changed how EventQL queries run, which makes them considerably faster, most of all those that read many events, group, sort, or join several sources, run on a server with a signing key, or ask for one subject, one event type, a range of times, or only the IDs of events, which they now read through an index; the same holds for EventQL preconditions of these shapes. A query with
TOPends as soon as it has its rows, aGROUP BYthat hands out aggregates alone takes memory for each group rather than for each event, but forMEDIAN,STDDEV, andVARIANCE, and a sort withTOPin a query without grouping or aggregates holds only the rows it keeps. A join whose sources keep more rows than a query may hold at once now fails, as sorting or grouping that many rows does. - Changed how EventSourcingDB stores writes, which makes writing considerably faster, for one client as for many at once, and keeps it from slowing down as the store grows. If storing in the background keeps failing, for example because the disk is full, the database switches to read-only mode.
- Changed how EventSourcingDB reads events, which makes reading events and EventQL queries over many events faster, at the cost of some more memory while they run.
- Changed how EventSourcingDB keeps its indexes, which now take far fewer files, so that checkpoints no longer slow down with new subjects and reading the events of a subject gets faster in large databases. After upgrading, it rebuilds them once in the background and refuses some requests until it is done, see First Start After Upgrading.
- Changed restoring a backup, which now writes the events in batches, so that it no longer takes longer per event the larger the backup is, and starting after a crash, which no longer hashes a whole file of the store again for every event appended to it; both get considerably faster.
- Changed how event schemas are stored in the data directory, now with a checksum that every read checks, so that a damaged schema is refused, and in files named after a hash of their event type, so that event types that are very long, or differ only in case on a file system that does not tell case apart, each keep a schema of their own, which they could not before. Verifying the integrity of the store checks them as well, and a data directory from before is converted once, at its first start.
- Changed registering event schemas and restoring a backup, which now refuse a schema with a number of more than 1,000 digits before or after its decimal point, see Limits. So the v1 API refuses a number such as
1e-999999, which it stored as0. - Changed the data directory so that earlier versions refuse to start on one that this version has used. Downgrading is not supported; to go back, restore a backup.
- Changed the data directory so that EventSourcingDB refuses to start where one of its own entries, such as
eventsorwal, is a link or lies on another file system, since writes there could not be made durable. The data directory itself may still be a link or a volume of its own. - Changed verifying the integrity of the store, which now runs considerably faster and reports every inconsistency it finds, up to 100, rather than only the first.
- Changed restoring a backup so that it respects the license: a backup with more events than the license permits, or a restore after the license has expired, is rejected.
- Changed the management UI, which now talks to the database through the v2 API alone and shows an error with its code and description. Its dashboard says in a status tile what needs attention, lists the latest events live, and shows the license, the stored events, the disk space, and the open files in a band of their own.
- Changed the output of
eventsourcingdb version, which now prints the version without the leadingvof its Git tag, as in1.3.0, the way theServerheader has always named it.
Fixed#
- Fixed unexpected internal errors of a request, which could end the database for every client, or leave a lock held so that every later write waited until a restart. Such a request now fails on its own and is logged, and the database goes on; a restore that fails this way empties the store again.
- Fixed answers that handed internal error texts to the client, such as paths in the data directory or messages of the operating system, also once a response was streaming. Such a failure is now answered with a generic message and logged in full, while a mistake in a query is still answered with its message and position.
- Fixed connections that answered reads with nothing after a request without a body took longer than five seconds: later requests on the same connection, which the client SDKs reuse, answered reads with an empty result and ended observations at once, so that a client could miss stored events. A restore no longer fails where storing a part of the backup takes that long.
- Fixed streamed responses of both APIs, such as reading and observing events, which cut off a client that took longer than 10 seconds to take in a line, for example behind a proxy that buffers the response, and got a line of plain text appended where the server failed once they had begun. They now wait up to 60 seconds for a line, and a failure breaks the stream off.
- Fixed requests whose body does not arrive in full, for example because the client drops the connection or a restore upload stalls, which were answered with a 500 and logged as an error of the server. They are now answered with a 400, or with a 408 where a body other than that of a restore did not arrive within the read timeout, and logged as a warning, as every other mistake of a client is.
- Fixed answers over HTTP/2, which the HTTPS listener speaks by default, whose stream the server could reset after it had answered it in full, which a client may treat as an error of the whole connection.
- Fixed the answer to a request for an API route that does not exist while the management UI is enabled, which was a 405 for any method but
GET, rather than a 404. A route called with a method it does not have gets a 405, also forGET, where it was a 404. - Fixed the
Authorizationheader, which both APIs took only with the scheme written exactly asBearerand a single space before the token. They now take the scheme in any case and after one or more spaces, as RFC 9110 has it, and answer a 401 withWWW-Authenticate: Bearer. - Fixed long-running requests that held up every write, and every read behind it, for as long as they ran: reads and observations whose client takes in the results slowly, auditing the event store, verifying its integrity, registering an event schema, listing event types, and writes of event types whose schema takes long to validate. Writes and reads now go on meanwhile, and a read, an audit, or a verification covers the store as of its start. A restore ends running observations with an error, so that they observe again once it is done.
- Fixed writes that could wait forever after a client read a single event type, or stopped listing subjects or event types before their end, until the database was restarted.
- Fixed two servers on the same data directory, which both started and wrote the same files at once, which can damage the data. A server now locks its data directory, and a second one refuses to start, as does one on a file system that cannot lock, so that a rollout has to stop the old server first, for example with the strategy
Recreateof a Kubernetes Deployment. Versions up to 1.2 neither take nor respect the lock. - Fixed EventSourcingDB on Windows, where reading events and running EventQL queries could hang for good together with every write after them, moving stored writes into the data files could keep failing after a momentary failure until the database switched to read-only mode, and a restore that failed could leave the database unable to start until its data directory was emptied by hand.
- Fixed the Linux binary for amd64, which was linked against glibc and did not start on systems without it, such as Alpine Linux. It is now statically linked, as the one for arm64 is.
- Fixed the health endpoint, which collected expensive metrics on every request, held up the start in large stores, and answered with a 500 for a healthy database now and then under write load, or for as long as the data directory held an entry it cannot read, such as
lost+found. It now measures in the background, counts only what the database keeps in the data directory, and reports a size without a value until it has measured it once. - Fixed values of the health endpoint that were wrong or misdescribed:
store:eventCountafter a failed restore, the uptime, which counted from opening the data rather than from the start of the process, the averages and maxima of the last minute, which took in every write under constant load, the maximum write time, which is that of the longest single write, andprocess:cpuUsage, which warned on machines with several cores and now counts the cores the process may use.system:diskRemainingnow warns before writes are refused and fails while they are, so that the health endpoint then answers with a 500, on the v2 API with a 503, see Read-Only Mode While the Disk Is Almost Full. - Fixed the OpenMetrics output of the health endpoint of the v1 API, which Prometheus could not scrape at all. It now follows OpenMetrics, with
store_event_countas a gauge and the uptime asprocess_uptime_seconds, and leaves out sizes it cannot measure rather than reporting0. - Fixed the OpenAPI documents, which disagreed with what the server does: they described the API token as a JWT, listed status codes that endpoints cannot produce and missed others, such as the 429 of rate limiting, and described the answers of
verify-store,audit-history,diagnostics, andread-event-type, the lines of backups, and thesignatureof events in shapes the server never sends, so that a client that validates against them rejected real answers. They now describe what the server sends and takes. - Fixed listing subjects below a subject, which missed its descendants where a sibling began with its name and a hyphen, such as
/books/42next to/books-archive. - Fixed reading a specific event type when reading the event types failed, which answered with a second error saying that the event type was not found.
- Fixed reading and observing events with an exclusive
lowerBoundat the largest event ID, which returned every event instead of none. Such a bound is now rejected, as is an exclusiveupperBoundof0. - Fixed observing events with
fromLatestEventandifEventIsMissingset towait-for-event, which never handed on an event where the event to wait for lay in a subject that the observation does not cover and was written only after the observation began. - Fixed the v1 API, which took an event ID with leading zeros, such as
007, as7in the bounds of reading and observing events, in preconditions, and in audits. Such an ID is now refused as malformed, as is one with a sign, such as+5. - Fixed writing events with a
traceparentortracestatethat W3C Trace Context allows but the API refused, such as the trace flags03. - Fixed auditing the event store through the v1 API, which refused a request without a body over HTTP/2 with a 400, reported a conflict for an intact history where
latestEventHashwas written in uppercase, and named an event0with an empty hash on a store with schemas but no events. - Fixed verifying the integrity of the store, auditing it, and running EventQL queries while the database does not permit reading, for example during a restore, which answered with a 200 and an error line in their stream, or with a 500. On the v1 API, they now answer with a 503, as every other read does.
- Fixed writes with an EventQL precondition, which held up every other write and every read for as long as their query ran, without any bound. Reads now go on meanwhile, and the preconditions of a write are evaluated for at most five seconds; a write whose preconditions take longer is rejected, and the other writes go on.
- Fixed
isEventQlQueryTruepreconditions whose query cannot be answered with a single boolean, which were answered with a 500 without a reason and could leave their query running for good. Such a write is now rejected with a 400 that names the reason, and the query stops as soon as the precondition has its answer. - Fixed EventQL queries and
isEventQlQueryTruepreconditions that terminated the database process:SUBSTRINGwith an index beyond its string or below zero, and the modulo by a decimal between -1 and 1. Such a call ofSUBSTRINGnow fails the query, and the modulo of decimals now keeps the fractional part, so5.5 % 2is1.5rather than1. - Fixed numbers in EventQL queries: integers could overflow silently, an integer too large for its type made the server drop the request and leave a lock held, and an integer and a decimal of the same value, such as
1and1.0, counted as different inCONTAINS,GROUP BY, andDISTINCT. Integers are now exact up to 1,000 digits, a larger one is reported as a mistake of the query, and numbers compare by their value. - Fixed EventQL queries and
isEventQlQueryTruepreconditions that computed a value that is not a number, such asSQRT(-1), which sorted, compared, and aggregated by chance, or failed without a position. Such a computation now ends the query at the expression that computed it. This can break existing queries and preconditions that relied on such a value being left out silently: leave such events out first, as inWHERE e.data.x >= 0 AND SQRT(e.data.x) >= 0. - Fixed comparisons in EventQL queries and
isEventQlQueryTruepreconditions: values of different types are unequal whichever stands on the left, where1 == trueended the query whiletrue == 1wasfalse; aDATETIMEcompares as an instant whatever its offset, also by==,!=,<=, and>=, and inGROUP BYandDISTINCT; and two objects compare by their content, whatever the order of their keys. A precondition whose query failed this way now holds or is violated, rather than refusing the write. - Fixed aggregates and casts in EventQL queries that returned wrong results:
MEDIANof an even number of values now averages the two in the middle,GROUP BYno longer merges decimals that share their integer part, a projection that calls the same aggregate twice computes all of them, and a cast toINTbeyond 64 bits is exact, while one ofNaNor an infinity is an error.STDDEVandVARIANCEof a single value returnNULL, soHAVING STDDEV(x) > 1now fails for a group of one unless it is guarded, as inHAVING COUNT() > 1 AND STDDEV(x) > 1. - Fixed
ORDER BYon theidof an event in EventQL queries which depended on the case in which a source was written and on a cast to a type other thanINT, and which, in a query with several sources, ignored the order for any source but the outermost one and did not reverse the rows that share anidforDESC. Such a query now sorts as written. - Fixed names in EventQL queries, which took in
*,#, a leading digit, and every character beyond ASCII, such as a typographic minus or a no-break space pasted from a word processor, so thate.data.qty*2read as a field namedqty*2and silently matched no event. A name now follows the Unicode standard for identifiers (UAX #31) and no longer takes*,#, emoji, or symbols; read such a field with a string in brackets, as ine.data["2fa"], which a key inPROJECT INTOcan no longer be named. - Fixed function calls in EventQL queries: a query that names a function without calling it, such as
LENinPROJECT INTO { a: LEN }, failed with messages about internal types or silently matched nothing, and is now refused before it runs, and a call of a function whose name a source bears, such asupper(upper.subject), now calls the function, so that formatting a query no longer changes what it means. Aggregates written after such a call that a query leaves unevaluated, as in a branch ofIFthat it does not take, now take in every event rather than answering as though there were none. - Fixed subqueries in EventQL queries with a mistake, such as a source that does not exist or an aggregate function in their
WHERE, which were refused only once a row reached them, or evaluated for each row, so that on an empty store a precondition with such a query held and let its write through. Such a query is now refused before it runs. - Fixed EventQL queries with an aggregate next to a key or the type of a cast that bears the name of a source, such as
{ count: COUNT(), keys: { e: 1 } }, which were refused as mixing aggregates with fields of the events. - Fixed EventQL queries, which handed out every event with a hash computed anew from the stored event, and signed that hash, rather than the hash stored with the event, so that an event changed in the data directory came with a matching hash and a valid signature. EventQL now hands out and signs the stored hash.
- Fixed EventQL queries that went on computing after their client had gone, while they combined several sources, sorted, grouped, or computed
STDDEVorVARIANCE. - Fixed error messages of EventQL queries for an operator on values it does not take, which named the wrong types or depended on which value stood on the left, and errors that wrote a long value into their text whole. They now name the types as the query has them, and a value longer than 100 bytes by its type alone.
- Fixed writes that could not be stored, for example on a full or failing disk, after which the database carried on as if nothing had happened, so that a later write could reuse an event ID or break the event chain. Such a write now puts the database into read-only mode until it is restarted, which the health endpoint reports. Backups and restores are refused while the database does not permit reading or writing, rather than reading an incomplete state or writing anyway.
- Fixed the maximum event size of 64 KiB, which let events somewhat above it through, and answered with a 500 where it refused one: an event whose stored form, including its hash, exceeds 64 KiB is now rejected with a 400 that names the reason, also in a backup that is restored. Events already stored are not affected, but a backup that holds one above the limit can no longer be restored.
- Fixed numbers in the
dataof events that a 64-bit floating-point number cannot hold exactly, such as integers beyond9007199254740992, which were changed silently when the event was written, with its hash computed over the changed value. They are now kept exact everywhere, up to 1,000 digits, and a number with more digits, or beyond the range of a 64-bit floating-point number, is rejected. Events written before stay as they are; verifying event signatures now says to hashdataexactly as it is received. - Fixed writing events, which changed the
sourceof an event, such as by percent-encoding a space or by writing its scheme in lowercase, with the hash computed over the changed value, and accepted values that are no URI. The source is now stored exactly as it is sent and has to be a URI as RFC 3986 defines it, not a relative one; any other value is rejected, also in a backup that is restored. Events written before keep their source, but the backup of a database that holds such a source can no longer be restored. - Fixed events that were older than the event before them once the clock of the server went back, after which verifying the store, auditing it, and restoring its backups failed. New events now keep the time of the latest event until the clock has caught up, see Watching the Clock. Events written before stay as they are, so a database that already holds such an event still fails these checks, and its backups are still refused.
- Fixed reading from a damaged data directory, where a damaged index could terminate the database process or take a great deal of memory. Such damage now fails the read that meets it, or the start.
- Fixed the logged errors for a damaged index file, which named nothing of the damage, and for an index of subjects that fails to initialize, which was misspelled.
- Fixed registering an event schema after a registration or a restore that failed, which refused the schema until a restart although none was stored, or kept validating writes against the schemas of the failed restore.
- Fixed writing events of a type whose stored schema cannot be compiled, after which the database answered no request at all until it was restarted. Such a write is now rejected, and everything else goes on.
- Fixed registering an event schema for a type with events, which answered with a violation of the schema rather than with a damaged file where it met both, so that the damage did not show. It now reports every events file that it could not read.
- Fixed the v1 API, which rounded numbers in event schemas that a 64-bit floating-point number cannot hold exactly, such as
9007199254740993, when registering, listing, backing up, and restoring them, so that the database enforced another bound than the one that was sent. Schemas are now kept exactly as they are written; a backup that the v1 API wrote before already holds the rounded number. - Fixed event schemas with a bound of a length or a count close to the largest 64-bit integer or beyond it, which bound differently on amd64 than on arm64. Such a maximum now refuses nothing and such a minimum everything, on every platform; a backup with events that such a minimum let through on amd64 can no longer be restored.
- Fixed the reasons given when an event does not match the schema of its type, or a request body the schema of its endpoint, which came without a reason within
anyOf,oneOf, and the like, wrote the allowed values ofconstandenumwrongly, and grew with every violation. Each violation now names where it lies and what is wrong, at most the first ten, and a schema that is not a valid JSON Schema is refused with the same message every time, naming every keyword that is wrong. - Fixed backups, which kept the database read-only while they ran, at the pace of their client, and until a restart where their client went away. Writes now go on during a backup, which holds the state at its start, and a backup ends as soon as its client is gone.
- Fixed backups that silently left out event schemas they could not read and finished as if they had succeeded. Such a backup now ends with an error, and so do listing event types and EventQL queries that meet such a schema.
- Fixed restores that fail or are interrupted: one whose emptying failed left the database serving a partly restored state, a server stopped during a restore started on the partly restored data, a restore answered before its events were on disk, and one that ended early left the reading of its backup behind, which over HTTPS could even end the process. Such a database now refuses reads and writes, and a server refuses to start, until the data directory is emptied, and a restore writes everything to disk before it answers.
- Fixed restoring a backup that holds what writing never lets through, which a restore took, or ended with a 500: an event without its source, subject, type, or data, one that does not match the schema of its type, or one dated more than an hour beyond the clock of the server, a schema that registering refuses or a second, different schema for the same event type, and lines of the v1 API without an ID, or a backup whose keys are spelled otherwise than documented throughout. Such a backup is now refused and the database left empty, also where it is the backup of a database that already holds such an event; the same schema twice is taken once.
- Fixed the findings of verifying the integrity of the store, which reported an intact store as corrupt after a failed write left an empty directory behind, a file it could not read as damage of the store, the same failure several times, and missing events files as a break of the chain, and which missed index entries for the wrong subject or event type and schemas whose
typeis written in another case. A finding now names the part of the store by its path in the data directory, the missing events and the files around them, and a file it cannot read as one, so that the store counts as not verified rather than as damaged. - Fixed EventQL queries in the management UI: a query that failed before its first result or broke off showed as successful, and a canceled one in the wrong color; Clear and leaving the view did not stop a query, nor did Cancel while it was being formatted; a large result could freeze the tab; numbers beyond what JavaScript holds exactly were rounded; copying said it had succeeded before it had; and the editor became slow after a while, marked mistakes in the wrong place, and showed
EXPandSUBSTRINGwithout their return types. - Fixed the formatting of EventQL queries in the management UI, which failed on some valid queries, such as
NOT NOT e.data.flag, misplaced comments and the clauses of nested subqueries, and took long for long queries. The formatter no longer adds parentheses that a query does not have. - Fixed the dashboard and the language of the management UI: the dashboard showed zeros where it could not measure, a meaningless number of remaining events for a license without a limit, and one line in English in the German UI, and the UI was German only in a browser set to exactly
de-DEand kept the language it was first shown in. It now follows the language of the browser unless one is chosen in the settings, so that a language chosen before the update has to be chosen once more. - Fixed serving the management UI: a browser kept running the version from before an upgrade for up to a day, ranges and
HEADrequests got wrong lengths, a client that refuses compressed content got it compressed, and a shared cache could hand an uncompressed file to every client. A browser that holds the files of an earlier version keeps them until their day runs out, also on the upgrade to this version, unless the page is reloaded bypassing the cache. - Fixed mistakes in the documentation: the Docker deployment guide now passes
--data-directory, Writing Events says that the hash chain spans the whole store, the EventQL reference and the editor of the management UI describe the third argument ofSUBSTRINGas the index of the last character, the API reference and the guides on verifying and auditing the store describe their streams as the server sends them, and read-only mode warns that switching it on through the API lasts only until the next restart, which--read-onlydoes not.
Deprecated#
- Deprecated the v1 API, which is supported until January 31, 2027, inclusive, and which a later release removes; use the v2 API instead. Its responses say so in the headers
DeprecationandSunset, with a link to the API overview.
Removed#
- Removed EventAI from the management UI, and with it the flags
--ai-modeland--ai-option, so that a server started with them no longer starts. - Removed the routes of the v1 API that only the management UI used:
format-eventql-query,ui-settings, andgenerate-eventql-query. - Removed the
generatecommand; SynthEvents generates sample events instead. - Removed the
docscommand; the documentation is on eventfoundation.io, with the state of every release.
Security#
- A server now refuses to start with an API token that could be guessed or that not every client can present: one with anything but printable ASCII or with whitespace at its start or end, and, on a data directory, one shorter than 32 characters. Change such a token before upgrading, and give your clients the new one.
- Fixed the check of the API token, whose time depended on how many of its leading characters a request got right, so that a client could in principle narrow down the token from the time of the answers. The check now takes the same time for every token.
- Fixed connections that a client could hold open for good, by sending a request slowly, never finishing the TLS handshake, or never reading the answers, many of them without an API token, so that they could exhaust the connections and the memory of the server. Every request, answer, and idle connection is now bounded in time, see Limits; keep the idle timeout of a reverse proxy or of a pool of connections below that of EventSourcingDB.
- Fixed the global rate limit, which charged requests without a valid API token against the limit that all clients share, so that a flood of them could lock out every client. They now draw from a global limit of their own; during such a flood, the management UI does not load, since a browser requests its files without the token.
- Fixed the profiles under
/debug/pprof, which a server served without an API token whenever its license permitted development mode, among them the command line with the API token and the license. They are now served only in development mode and with the API token, and the command line not at all. - Fixed the ping of the v1 API, which needs no API token but took the IDs of its answers from a counter shared with other answers, so that a client without a token could tell how many of those were answered.
- Verifying a license now also requires the audience and the issuer that every license names, so that a token signed with the same key for another purpose does not pass as one. Every license issued since the end of 2024 names both, so a license that is still valid keeps working.
- The pre-built binaries and the Docker image are now built with Go 1.27.4, which fixes vulnerabilities of the standard library that the server reaches, some of which a client could trigger without an API token.
- The Docker image is now based on Docker Hardened Images (DHI), which are CVE-free and provide a minimal attack surface.
- Fixed EventQL queries whose parsing could end the server or hold up every write, for anyone with an API token: a query that nests very deeply or chains very many operands overflowed the stack, block comments that are never closed and some other queries took time growing with the square of their length, and the query of a precondition was bounded only by the body of its write. A query may now nest at most 256 levels and chain at most 10,000 operands, and the query of a precondition may have at most 100 KiB, see Limits.
- Fixed EventQL queries that could take more memory than the server has, for anyone with an API token, by building huge strings, arrays, or objects, or by holding large values in a sort, a grouping,
DISTINCT, or a join. A query may now build strings of at most 1 MiB and arrays and objects of at most 64 MiB, and hold at most 1 GiB at once, see Rows Held in Memory. - Fixed registering event schemas, which let a schema refer to a file on the server with
$ref, so that anyone with an API token could make the server read files outside the database. A schema may now refer only to its own parts; one registered before that refers to a file no longer compiles, so that writes of its event type fail, and a new event type is needed. - Fixed registering event schemas, which accepted a small schema whose validation took time doubling with every level, so that anyone with an API token could hold up all writes for good. A schema is now refused where it reaches more than 10,000 subschemas, refers back to a part of itself, or uses
$dynamicRefor$recursiveRef, see Limits; one registered before stays, but every write of its event type is refused, so that a new event type is needed, and a backup that holds it can no longer be restored. An event that is too large is now refused as such before its schema is checked, also where an event of the same write does not match its schema, which the v1 API answers with a 400 rather than a 409. - Fixed writing events with a very deep or very long subject, which took memory and disk space growing with the square of its length, so that a single write could end the server. A subject may now have at most 32 segments and 1,024 characters, see Limits; events that already hold such a subject can no longer be read by it, only through a subject above it or with EventQL, and a backup that holds one can no longer be restored.
- Fixed restoring a backup with a single line longer than the memory, which could end the server. A line longer than any item of a backup can be is now refused without being read to its end, and so is a schema larger than registering takes, see Limits.
- Fixed the permissions in the data directory, which let every user of the machine read many of its files, among them the write-ahead log. EventSourcingDB now creates its files with
0640and its directories with0750, and at its first start takes every permission of other users away from the files that exist and lets its group read all of them, also those that only their user could read before, logging a warning where it cannot, for example for files of another user. On Windows, it leaves the permissions to the ACL of the data directory. - The management UI is now served with security headers, among them a
Content-Security-Policyunder which it runs no scripts from elsewhere and cannot be shown in a frame. - Fixed the sessions of the management UI, which kept what a session had read after logging out, so that Back showed the dashboard to whoever used the browser next, and stayed open where the server refused their API token. Logging out, and a refused token, now load the UI anew on the login page.
1.2.0 (November 3rd, 2025)Added in 1.2#
Added#
- Added the
isSubjectPopulatedprecondition, which only allows writes if the given subject has at least one event - Added a link to the documentation in the management UI
- Added a blog to the documentation
- Added an External Resources page to the documentation
- Added a warning when starting the server if the management UI is enabled on a browser-blocked port
- Added support for the
signatureandhashfields in EventQL, including auto-completion, syntax highlighting, and query execution - Added a warning when starting the server with an evaluation license, informing about the limited event capacity and where to get support and unlimited events
- Added German language support to the management UI with language switching
- Added comprehensive documentation on Configuring EventAI, including setup instructions, security considerations, privacy implications, example prompts, and troubleshooting
Changed#
- Updated the CLI Overview documentation to include the
--signing-key-fileflag
Fixed#
- Fixed a bug where observing events with
fromLatestEventandifEventIsMissing: "wait-for-event"would only return events of the same type after the first matching event, instead of returning all subsequent events for the subject - Fixed a bug where some numbers were shown malformed in the management UI
- Fixed a bug where long values in the EventQL tree graph would overflow their containers instead of being truncated with ellipsis
- Fixed a bug where alerts in the management UI could be hidden behind other UI elements
- Fixed a bug where pressing Enter in the login form would not submit the credentials
1.1.0 (September 1st, 2025)Added in 1.1#
Added#
- Added documentation on the Rust client SDK
- Added the
isEventQlQueryTrueprecondition, which only allows writes if the given EventQL query evaluates totrue - Added documentation on Dynamic Consistency Boundaries
- Added suggestions for available event types when comparing against the
typefield in the EventQL editor - Added the
/read-event-typeendpoint to read a specific event type from the database - Added a
Serverheader to every HTTP response containingEventSourcingDB/<version> - Added support for event signatures using Ed25519 and PKCS#8 PEM private keys via the new CLI flag
--signing-key-file
Fixed#
- Fixed a bug where queries inserted from the EventAI panel were not formatted automatically
- Fixed a bug where formatting an EventQL query with
DISTINCTin thePROJECT INTOclause would remove theDISTINCTkeyword - Fixed a bug where an EventQL projection with
COUNTorSUMwould not return any result if the query processed zero events - Fixed a bug where an EventQL query would fail silently if it didn't return a row within 5 seconds
- Fixed a bug that prohibited aggregation functions in EventQL subqueries
- Fixed a bug in the EventQL editor that resulted in a broken light theme when using light mode in the management UI
- Fixed the EventQL formatter to include manually placed parentheses in the formatted query
- Fixed a bug in the documentation where the EventQL function
SUBSTRINGwas incorrectly referred to asSUBSTR
1.0.3 (June 3rd, 2025)#
Added#
- Added missing fields to the EventQL event representation for parity with the
/read-eventsendpoint
Changed#
- Changed the link to the NDJSON website in the documentation to point to the GitHub repository instead
Fixed#
- Fixed running an EventQL query with aggregate functions crashing an empty database
- Fixed management UI not working when using HTTP on a remote server
- Fixed management UI not showing the correct number of events after restarting the server with a persistent directory
- Fixed EventAI panel remaining visible in the UI after disabling it and restarting the server
- Fixed a typo in the documentation on running EventSourcingDB with Kubernetes
1.0.2 (May 21st, 2025)#
Added#
- Added documentation on the upcoming .NET client SDK
- Added documentation on the upcoming Java client SDK
Fixed#
- Fixed another issue where running an EventQL query blocked writing new events
1.0.1 (May 12th, 2025)#
Added#
- Added documentation on the upcoming PHP client SDK
Changed#
- Removed an unnecessary dependency from the management UI
Fixed#
- Fixed an issue where running an EventQL query blocked writing new events
- Fixed missing tiles in the management UI
- Fixed incorrect rendering of the login page in Safari
- Fixed typos and broken links in the documentation
1.0.0 (May 5th, 2025)#
- Initial release