eventsourcingdb 0.1.0-beta.1
eventsourcingdb: ^0.1.0-beta.1 copied to clipboard
Community Dart client for EventSourcingDB with typed HTTP and NDJSON APIs.
eventsourcingdb #
An independent, community-maintained Dart client for EventSourcingDB, a purpose-built database for event sourcing.
This project is not an official SDK from the native web GmbH. It targets Dart VM and Flutter mobile/desktop. Browser support is experimental and is not part of the compatibility test matrix.
The integration suite targets the immutable EventSourcingDB preview snapshot
sha256:169073c2e0231aed5d9a7acadddb70ca222a5f0cc18874c2671b5b6efe6c50d7
from 2026-09-06. Compatibility claims are limited to that exact server build.
For more information on EventSourcingDB, see its official documentation.
Getting Started #
Add the client SDK to your pubspec.yaml:
dart pub add eventsourcingdb
Import the Client class and create an instance by providing the URL of your EventSourcingDB instance and the API token to use:
import 'package:eventsourcingdb/eventsourcingdb.dart';
final client = Client(Uri.parse('http://localhost:3000'), 'secret');
Then call ping to check whether the instance is reachable. If it is not, the call throws:
await client.ping();
Note that ping does not require authentication, so the call may succeed even if the API token is invalid.
If you want to verify the API token, call verifyApiToken. If the token is invalid, the call throws:
await client.verifyApiToken();
Writing Events #
Call writeEvents and pass one or more EventCandidates. data is any JSON-encodable value (a Map, List, String, num, bool, or null):
final writtenEvents = await client.writeEvents([
EventCandidate(
source: 'https://www.eventsourcingdb.io',
subject: '/books/42',
type: 'io.eventsourcingdb.library.book-acquired',
data: {'title': 'Dune', 'author': 'Frank Herbert'},
),
]);
writeEvents writes either all or none of the given events, depending on optional preconditions:
await client.writeEvents(
[eventCandidate],
preconditions: [
Precondition.isSubjectPristine('/books/42'),
// or: Precondition.isSubjectPopulated('/books/42'),
// or: Precondition.isSubjectOnEventId('/books/42', '0'),
// or: Precondition.isEventQlQueryTrue('FROM e IN events PROJECT INTO COUNT() == 0'),
],
);
Reading Events #
Call readEvents with a subject and options. It returns a Stream<Event> that ends once all matching events have been read:
await for (final event in client.readEvents(
'/books/42',
const ReadEventsOptions(recursive: false),
)) {
print(event.id);
}
recursive controls whether events of nested subjects are included. Further options let you control ordering and bounds:
const ReadEventsOptions(
recursive: true,
order: Order.antichronological,
lowerBound: Bound(id: '0', type: BoundType.inclusive),
upperBound: Bound(id: '10', type: BoundType.exclusive),
fromLatestEvent: ReadFromLatestEvent(
subject: '/books/42',
type: 'io.eventsourcingdb.library.book-borrowed',
ifEventIsMissing: ReadIfEventIsMissing.readEverything,
),
);
Reading Subjects #
Call readSubjects with a base subject to read all subjects recursively below it (streamed as they are found):
await for (final subject in client.readSubjects('/books')) {
print(subject);
}
Observing Events #
Call observeEvents to keep reading events as they are written. The returned Stream<Event> only ends when you stop listening (e.g. by cancelling the subscription) or the connection is lost:
final subscription = client
.observeEvents('/books/42', const ObserveEventsOptions(recursive: false))
.listen((event) => print(event.id));
// ... later:
await subscription.cancel();
Reading and Registering Event Types #
await for (final eventType in client.readEventTypes()) {
print('${eventType.type} (phantom: ${eventType.isPhantom})');
}
final eventType = await client.readEventType(
'io.eventsourcingdb.library.book-acquired',
);
await client.registerEventSchema(
'io.eventsourcingdb.library.book-acquired',
{
'type': 'object',
'properties': {
'title': {'type': 'string'},
'author': {'type': 'string'},
},
'required': ['title', 'author'],
},
);
Running EventQL Queries #
runEventQlQuery yields each row's decoded JSON payload as-is (a Map, a scalar, or null, depending on what the query projects):
final rows = await client
.runEventQlQuery('FROM e IN events PROJECT INTO COUNT()')
.toList();
Use runEventQlQueryAs for a typed result, or
runEventQlQueryForEvents when the query projects whole events:
final counts = client.runEventQlQueryAs<int>(
'FROM e IN events PROJECT INTO COUNT()',
(value) => value as int,
);
await for (final event in client.runEventQlQueryForEvents(
'FROM e IN events PROJECT INTO e',
)) {
print(event.id);
}
Reading Event Data #
Event.data is already JSON-decoded (a Map, List, String, num, bool, or null). Use getData with your own decode function for a typed value:
final title = event.getData(
(json) => (json as Map<String, Object?>)['title'] as String,
);
Verifying Hashes and Signatures #
Every event carries a SHA-256 hash over its metadata and data, tamper-evident by construction. Call verifyHash to recompute and compare it; it throws HashVerificationException on a mismatch:
event.verifyHash();
If the server was started with a signing key, events also carry an Ed25519 signature. Call verifySignature with the server's PKIX/SPKI-encoded public key; it throws SignatureVerificationException if the event is unsigned, malformed, or the signature does not verify:
event.verifySignature(verificationKeySpki);
Using a Custom HTTP Client #
Provide your own package:http client, e.g. to control timeouts or add custom headers:
import 'package:http/http.dart' as http;
final client = Client(
Uri.parse('http://localhost:3000'),
'secret',
httpClient: http.Client(),
);
Injected HTTP clients remain owned by the caller and are not closed by
Client.close(). Network methods also accept an optional abortTrigger:
import 'dart:async';
final abort = Completer<void>();
final pending = client.ping(abortTrigger: abort.future);
abort.complete();
await pending;
The constructor also accepts a JsonCodec dataCodec for custom event payload
conversion and a package:logging Logger. Logs contain operation names and
paths, never API tokens or event payloads. Depend on the
EventSourcingDbClient interface when the client needs to be mocked.
Docker Test Container #
VM tests can import the optional container helper without adding a separate dependency:
import 'package:eventsourcingdb/eventsourcingdb_test.dart';
final container = EventSourcingDbContainer()
.withImageTag('preview@sha256:169073c2e0231aed5d9a7acadddb70ca222a5f0cc18874c2671b5b6efe6c50d7')
.withPort(4000)
.withSigningKey();
await container.start();
final client = container.getClient();
addTearDown(container.stop);
Error Handling #
All exceptions this package throws extend EventSourcingDbException:
EventSourcingDbHttpException– the server responded with a non-200 status; carriesstatusCodeand the response body.ServerErrorException– a streamed response reported anerrorline.InvalidValueException– a response violated the protocol this client expects.HashVerificationException/SignatureVerificationException–Event.verifyHash/Event.verifySignaturefailed.
Connection-level failures (e.g. the server being unreachable) surface as whatever package:http throws (typically a SocketException or http.ClientException), not one of the types above.
Running the Example #
See example/example.dart for a runnable end-to-end walkthrough (ping, write, read, EventQL) against a local EventSourcingDB instance.
Development #
dart pub get # install dependencies
dart format . # format
dart analyze # static analysis
dart test test/unit # unit tests (no Docker required)
dart test --concurrency=2 --tags docker test/integration
make qa runs formatting, analysis, and the unit test suite in one go.
Integration tests spin up a real, digest-pinned EventSourcingDB container per
test through EventSourcingDbContainer, so they require a running Docker
daemon. See doc/architecture.md for the design model.