Skip to content

Writing Events

This guide explains how to store events in EventSourcingDB using its HTTP API. At the end, you will have successfully written your first events.

Before You Begin

All examples on this page and the following ones assume that you have a running EventSourcingDB instance and a valid API token. If not, see Running EventSourcingDB.

Whenever a curl command contains <API_TOKEN>, replace it with your actual token. Otherwise, EventSourcingDB will reject the request due to failed authentication.

No further configuration is required – all examples work out of the box with the default setup.

Modeling an Event

Before you can store an event, you first need to think about what the event should represent. This depends on your business domain, as the event's meaning is tightly coupled to it.

For example, if you're building an online shop, you might have an order submitted event. If you're developing a chess server, perhaps a knight moved event. If you're managing a library, a book acquired event. We'll use the library example in this guide because it's easy to follow, includes some interesting aspects, and might even feel familiar.

For now, we'll go with the book acquired event because acquiring a book logically happens at the beginning – otherwise, the library wouldn't have any books.

So, you already know that the type of your event should be book acquired. Since EventSourcingDB is based on the CloudEvents standard, event types must be specified as reverse domain names – for example, io.eventsourcingdb.library.book-acquired. For more details, see Event Types.

The CloudEvents standard requires a few additional fields:

  • The source (the application that emits the event)
  • The subject (this is essentially the object the events refer to)
  • The actual data, containing the event-specific business payload

Let's assume that your application is intended to run under https://library.eventsourcingdb.io. Then the source would simply be this URL. For more details, see Sources.

Let's also assume that you assign a random ID to the newly acquired book, e.g. 42. The subject could then look like this: /books/42. For more details, see Subjects.

The data is a standard JSON object containing the book's details:

{
  "title": "2001 – A Space Odyssey",
  "author": "Arthur C. Clarke",
  "isbn": "978-0756906788"
}

That gives us everything we need to construct the event:

{
  "source": "https://library.eventsourcingdb.io",
  "subject": "/books/42",
  "type": "io.eventsourcingdb.library.book-acquired",
  "data": {
    "title": "2001 – A Space Odyssey",
    "author": "Arthur C. Clarke",
    "isbn": "978-0756906788"
  }
}

Events vs. Event Candidates

Technically, events contain additional fields, such as the timestamp when the event was created and the version of the CloudEvents specification. However, these fields are automatically added by EventSourcingDB when the event is stored.

Internally, what you send to the database is called an event candidate. It's not an actual event yet – it's more like a proposal to become one. Only when the database accepts and stores the candidate does it turn into a real event.

Storing an Event

To store the event, you send a POST request to EventSourcingDB's /api/v1/write-events endpoint. You can do this from the command line using curl. Make sure to include the API token for authentication that was set when starting the database:

curl \
  -i \
  -X POST \
  -H "authorization: Bearer <API_TOKEN>" \
  -H "content-type: application/json" \
  -d "{
    \"events\": [
      {
        \"source\": \"https://library.eventsourcingdb.io\",
        \"subject\": \"/books/42\",
        \"type\": \"io.eventsourcingdb.library.book-acquired\",
        \"data\": {
          \"title\": \"2001 – A Space Odyssey\",
          \"author\": \"Arthur C. Clarke\",
          \"isbn\": \"978-0756906788\"
        }
      }
    ]
  }" \
  http://localhost:3000/api/v1/write-events

If Something Goes Wrong

If you receive an error stating that the connection is not authorized, check whether the API token was set correctly. You need to replace <API_TOKEN> in the example with your actual token.

Understanding the Response

If everything worked as expected, the server will respond with HTTP status code 200 OK. The first line should look like this:

HTTP/1.1 200 OK

You can ignore the other headers.

What's more interesting is the response payload. It looks like this, with the exception of the dynamically generated time field:

[
  {
    "specversion": "1.0",
    "id": "0",
    "time": "...",
    "source": "https://library.eventsourcingdb.io",
    "subject": "/books/42",
    "type": "io.eventsourcingdb.library.book-acquired",
    "datacontenttype": "application/json",
    "data": {
      "title": "2001 – A Space Odyssey",
      "author": "Arthur C. Clarke",
      "isbn": "978-0756906788"
    },
    "predecessorhash": "0000000000000000000000000000000000000000000000000000000000000000",
    "hash": "...",
    "signature": null
  }
]

About predecessorhash, hash, and signature

EventSourcingDB adds cryptographic hashes and, optionally, signatures to each event to ensure integrity and authenticity:

  • The predecessorhash field contains the hash of the previous event for this subject, or all zeroes if this is the first event.
  • The hash field is a cryptographic fingerprint of the current event. It is used to ensure data integrity.
  • The signature field indicates whether the event is cryptographically signed. For now, you can ignore this field – more details will follow later in the documentation.

For details on how hashes and signatures work in EventSourcingDB, see Verifying Event Signatures.

Writing Multiple Events at Once

You may have noticed that the parameter in the curl example was called events (plural), not event (singular), and that the event was wrapped in an array.

That's because you can send multiple events in a single request. EventSourcingDB handles all events in a request as part of a transaction, guaranteeing atomicity – meaning either all events are written, or none.

Using Preconditions

Sometimes you only want to store events under certain conditions – for example, only if no events for a subject exist yet, or only if no new events have been written since the last read, or only if a specific EventQL query evaluates to true.

To support this, EventSourcingDB lets you define preconditions along with the events. For more details see Preconditions.

Transactions and Preconditions

EventSourcingDB will only store the submitted events if all specified preconditions are met. This preserves the atomicity of the operation.

Using isSubjectPristine

To ensure that events are only written if the subject has no existing events, use the isSubjectPristine precondition. This is useful when an event is meant to introduce a subject, like the book-acquired event above – it must be the first event and must not appear multiple times for the same subject.

If you resend the request as shown above, another event will be added to subject /books/42. But if you include an isSubjectPristine precondition, EventSourcingDB will reject the write – which in this case is what you want:

curl \
  -i \
  -X POST \
  -H "authorization: Bearer <API_TOKEN>" \
  -H "content-type: application/json" \
  -d "{
    \"events\": [
      {
        \"source\": \"https://library.eventsourcingdb.io\",
        \"subject\": \"/books/42\",
        \"type\": \"io.eventsourcingdb.library.book-acquired\",
        \"data\": {
          \"title\": \"2001 – A Space Odyssey\",
          \"author\": \"Arthur C. Clarke\",
          \"isbn\": \"978-0756906788\"
        }
      }
    ],
    \"preconditions\": [
      {
        \"type\": \"isSubjectPristine\",
        \"payload\": {
          \"subject\": \"/books/42\"
        }
      }
    ]
  }" \
  http://localhost:3000/api/v1/write-events

The response will include HTTP status code 409 Conflict:

HTTP/1.1 409 Conflict

Double-Check the Subject

Make sure you define the precondition for the correct subject. Technically, the precondition doesn't have to match the subject of the events being written, but that's typically what you want.

Using isSubjectPopulated

The isSubjectPopulated precondition is the opposite of isSubjectPristine. It ensures that the subject already has at least one event – useful when you want to update or modify an existing subject.

For example, if you want to borrow a book, the book must have been acquired first. Using isSubjectPopulated prevents writing events to subjects that haven't been initialized yet:

curl \
  -i \
  -X POST \
  -H "authorization: Bearer <API_TOKEN>" \
  -H "content-type: application/json" \
  -d "{
    \"events\": [
      {
        \"source\": \"https://library.eventsourcingdb.io\",
        \"subject\": \"/books/42\",
        \"type\": \"io.eventsourcingdb.library.book-borrowed\",
        \"data\": {
          \"borrowedBy\": \"/readers/23\",
          \"borrowedUntil\": \"2025-11-17\"
        }
      }
    ],
    \"preconditions\": [
      {
        \"type\": \"isSubjectPopulated\",
        \"payload\": {
          \"subject\": \"/books/42\"
        }
      }
    ]
  }" \
  http://localhost:3000/api/v1/write-events

If the book hasn't been acquired yet (subject is pristine), the request will fail with HTTP status code 409 Conflict. Once you've written the book-acquired event first, the borrow operation will succeed.

Using isSubjectOnEventId

The isSubjectOnEventId precondition also ensures that the subject already contains events – i.e., it is not empty.

In this case, you must specify both the subject and the ID of the most recent event. This lets you check that no new events have been added since you last read the subject – essentially enabling optimistic locking on a per-subject basis.

The following example tries to borrow a book from the library – which should only be allowed if the last known event was the acquisition of the book and no other events (like another borrow) have occurred since:

curl \
  -i \
  -X POST \
  -H "authorization: Bearer <API_TOKEN>" \
  -H "content-type: application/json" \
  -d "{
    \"events\": [
      {
        \"source\": \"https://library.eventsourcingdb.io\",
        \"subject\": \"/books/42\",
        \"type\": \"io.eventsourcingdb.library.book-borrowed\",
        \"data\": {
          \"borrowedBy\": \"/readers/23\",
          \"borrowedUntil\": \"...\"
        }
      }
    ],
    \"preconditions\": [
      {
        \"type\": \"isSubjectOnEventId\",
        \"payload\": {
          \"subject\": \"/books/42\",
          \"eventId\": \"0\"
        }
      }
    ]
  }" \
  http://localhost:3000/api/v1/write-events

Run this immediately after acquiring the book, and it will succeed. If you repeat the request, it will fail – confirming that the optimistic locking worked as intended.

Using isEventQlQueryTrue

Sometimes, you want to ensure that an event is only written if a more complex condition holds – for example, if no similar event has ever been recorded before. The isEventQlQueryTrue precondition lets you define such conditions using EventQL.

In the following example, we make sure that no other book with the same title already exists in the database. If a book titled 2001 – A Space Odyssey has already been acquired before, the write will be rejected:

curl \
  -i \
  -X POST \
  -H "authorization: Bearer <API_TOKEN>" \
  -H "content-type: application/json" \
  -d "{
    \"events\": [
      {
        \"source\": \"https://library.eventsourcingdb.io\",
        \"subject\": \"/books/42\",
        \"type\": \"io.eventsourcingdb.library.book-acquired\",
        \"data\": {
          \"title\": \"2001 – A Space Odyssey\",
          \"author\": \"Arthur C. Clarke\",
          \"isbn\": \"978-0756906788\"
        }
      }
    ],
    \"preconditions\": [
      {
        \"type\": \"isEventQlQueryTrue\",
        \"payload\": {
          \"query\": \"FROM e IN events WHERE e.data.title == '2001 – A Space Odyssey' PROJECT INTO COUNT() == 0\"
        }
      }
    ]
  }" \
  http://localhost:3000/api/v1/write-events

As with other preconditions, the response will include HTTP status code 409 Conflict if the condition is not met.

Arbitrary Conditions

The isEventQlQueryTrue precondition gives you full control over whether events should be written, based on the current state of the database. You can use any valid EventQL query that returns a single boolean value.

Your Turn

So far, we've only written events for a single book. But if you look at the book borrowed event, you'll notice that it references a reader with the ID /readers/23. That reader doesn't exist yet – so let's fix that.

As an exercise, try modeling and writing two new events:

  1. A reader applied event, indicating that someone has applied for a library card. This event should contain the reader's first and last name as data – for example:

    {
      "firstName": "Jane",
      "lastName": "Doe"
    }
    
  2. A reader accepted event, confirming that the reader has been approved and their library card has been issued. This event doesn't carry any domain-specific data – it should contain an empty object as payload:

    {}
    

Write the two events in two separate POST requests – one per event. Make sure to use the appropriate preconditions:

  • Use isSubjectPristine when writing the reader applied event to ensure that the subject doesn't exist yet.
  • Use isSubjectOnEventId when writing the reader accepted event to make sure it follows the application and that no other events have been written in the meantime.

You can use /readers/23 as the subject to match the book borrowed event above – or pick a different ID if you prefer.