Skip to main content
Version: Next 🚧

Clients

Orisun exposes gRPC services. You can use the official typed clients, grpcurl, or generate bindings for another language from the protobuf files.

note

The examples on this page target client builds that include SaveEventsV2, write IDs, and GetWriteContext. Clients retain SaveEvents as a single-query convenience API and SaveEventsV2 for multiple complete observations. Both use the canonical SaveEventsV2 RPC. The server does not expose a separate single-query write RPC.

Use the client repository revisions referenced by the current Orisun source checkout for these features. The package-manager commands below may install older published clients. Updating source revisions does not publish new packages.

Official Clients​

LanguagePackageRepository
Gogithub.com/oexza/orisun-client-goorisun-client-go
Node.js@orisun/eventstore-clientorisun-node-client
Javacom.orisunlabs:orisun-java-clientorisun-client-java

Install​

go get 'github.com/oexza/orisun-client-go@<revision>'

Use the typed clients when you want request/response objects and subscription helpers. Use grpcurl for operational checks, debugging, and quick experiments.

Boundary prerequisite​

Client event operations target an active boundary; a boundary name is not created implicitly by the first write. Before starting an application, define the storage with Admin/CreateBoundary, then wait for Admin/GetBoundary to report BOUNDARY_LIFECYCLE_STATUS_ACTIVE. See the Admin boundary API for placements, lifecycle states, and errors.

The Node client exposes typed helpers:

import { AdminClient, BoundaryStatus } from '@orisun/eventstore-client';

const admin = new AdminClient({
host: 'localhost',
port: 5005,
username: 'admin',
password: 'changeit',
});

const {boundary: definition} = await admin.createBoundary({
name: 'accounts',
description: 'Account lifecycle events',
placement: {backend: 'postgres', namespace: 'public'},
});

let boundary = definition;
while (boundary.status === BoundaryStatus.PROVISIONING) {
await new Promise(resolve => setTimeout(resolve, 100));
({boundary} = await admin.getBoundary('accounts'));
}
if (boundary.status !== BoundaryStatus.ACTIVE) {
throw new Error(`Boundary provisioning failed: ${boundary.lastError}`);
}

const {boundaries} = await admin.listBoundaries();

Use the same createBoundary(...) shape to attach existing supported storage. Retain its original boundary name and placement; there is no import flag. The command returns a definition in PROVISIONING; do not treat the response as readiness. In clustered deployments, retry an EventStore request that briefly returns FAILED_PRECONDITION after the shared catalog becomes ACTIVE; the selected node is still completing its local runtime installation.

First program​

A complete command loop: connect, save an event with a Command Context Consistency check, read the latest carried state from one snapshot, handle a conflict, and subscribe a projector. The examples assume the accounts boundary has already reached ACTIVE. Pick your language once; the same groupId carries over to the EventStore API and Tutorial pages.

package main

import (
"context"
"errors"
"log"
"os"
"os/signal"

orisun "github.com/oexza/orisun-client-go"
eventstore "github.com/oexza/orisun-client-go/eventstore"
)

func main() {
client, err := orisun.New(
"localhost:5005",
orisun.WithCredentials("admin", "changeit"),
orisun.WithInsecure(),
)
if err != nil {
log.Fatal(err)
}
defer client.Close()

ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
accountOpenedID := "018f2d5e-0001-7000-8000-000000000001"
accountRootQuery := &eventstore.Query{
Criteria: []*eventstore.Criterion{{
Tags: []*eventstore.Tag{
{Key: "__eventType", Value: "AccountOpened"},
{Key: "accountOpenedId", Value: accountOpenedID},
},
}},
}
accountContextQuery := &eventstore.Query{
Criteria: []*eventstore.Criterion{
accountRootQuery.Criteria[0],
{Tags: []*eventstore.Tag{{Key: "scopes.accountOpenedId", Value: accountOpenedID}}},
},
}

// 1. Open the account (context must be empty).
_, err = client.SaveEventsV2(ctx, &eventstore.SaveEventsV2Request{
Boundary: "accounts",
Consistency: []*eventstore.ConsistencyObservation{{
Query: accountRootQuery,
Position: &eventstore.Position{CommitPosition: -1, PreparePosition: -1},
}},
Events: []*eventstore.EventToSave{{
EventId: accountOpenedID, EventType: "AccountOpened",
Data: `{"accountOpenedId":"018f2d5e-0001-7000-8000-000000000001","balance":0}`,
}},
})
if err != nil {
var conflict *orisun.OptimisticConcurrencyException
if errors.As(err, &conflict) {
log.Printf("conflict: expected=%v actual=%v", conflict.ExpectedVersion(), conflict.ActualVersion())
}
log.Fatal(err)
}

// 2. Read the latest carried state and construct its V2 observation.
latest, err := client.GetLatestByCriteria(ctx, &eventstore.GetLatestByCriteriaRequest{
Boundary: "accounts",
Criteria: accountContextQuery.Criteria,
})
if err != nil {
log.Fatal(err)
}
observed := &eventstore.ConsistencyObservation{
Query: accountContextQuery,
Position: latest.ContextPosition,
}

// 3. Save a decision only while that exact context is current.
write, err := client.SaveEventsV2(ctx, &eventstore.SaveEventsV2Request{
Boundary: "accounts",
Consistency: []*eventstore.ConsistencyObservation{observed},
Events: []*eventstore.EventToSave{{
EventId: "018f2d5e-0002-7000-8000-000000000002",
EventType: "AccountReviewed",
Data: `{"scopes.accountOpenedId":"018f2d5e-0001-7000-8000-000000000001"}`,
}},
})
if err != nil {
var conflict *orisun.OptimisticConcurrencyException
if errors.As(err, &conflict) {
// Re-read and re-decide. Do not retry this stale observation.
}
log.Fatal(err)
}

// 4. Subscribe a projector after the command just committed.
handler := orisun.NewSimpleEventHandler().
WithOnEvent(func(e *eventstore.Event) error { return nil }).
WithOnError(func(e error) { log.Printf("subscription: %v", e) })
sub, err := client.SubscribeToEvents(ctx, &eventstore.CatchUpSubscribeToEventStoreRequest{
Boundary: "accounts",
SubscriberName: "balance-projector",
AfterPosition: write.LogPosition,
}, handler)
if err != nil {
log.Fatal(err)
}
defer sub.Close()
<-ctx.Done() // Keep the process alive until interrupted.
}

Write IDs and recorded consistency contexts​

Save results and returned events expose WriteId in Go, writeId in Node.js, and getWriteId() in Java. Treat this value as opaque and retain its boundary. It identifies the complete atomic write, including when a write saves several events. Historical events without recorded contexts return an empty write ID.

Use the write ID returned by a save or read to retrieve the observations checked when that write committed:

writeContext, err := client.GetWriteContext(ctx, &eventstore.GetWriteContextRequest{
Boundary: "accounts",
WriteId: write.WriteId,
})
if err != nil {
return err
}
for _, observation := range writeContext.Consistency {
// observation.Query is the complete query; observation.Position is its observation.
_ = observation
}

An unconditional write has an empty observation list. An unknown write context returns NOT_FOUND; clients preserve the underlying transport error. Recorded observations describe the original write and are not fresh reads for a new command. Read the command's current context before another checked write.

The clients send application data unchanged by storage-envelope conventions: use the ordinary event-type property on save, use __eventType in criteria and index definitions, and do not add reserved top-level __* fields to application data. Returned data has already been stripped by the backend. See the upgrade guide before upgrading an existing deployment. Both client save methods are supported.

Authenticating from a client​

Every call needs credentials. The typed clients take a username and password at construction and reuse the session token the server returns on later calls, so you only supply Basic credentials once. After changing that user's password, construct a new client with the new password because the server revokes the cached session and the existing client retains its original Basic credentials.

With grpcurl, send HTTP Basic in the authorization metadata header:

grpcurl -plaintext -H 'Authorization: Basic YWRtaW46Y2hhbmdlaXQ=' localhost:5005 orisun.EventStore/Ping

Each authenticated response sets an x-auth-token header. A long-lived client can capture that token once and send it as x-auth-token on subsequent calls instead of re-sending Basic credentials; Orisun validates the token first and falls back to Basic. Read the full model in Security & Authorization.

Proto Files​

The service definitions live in the main repository:

Generated server Go bindings are kept in orisun/grpcapi/; SDK bindings live in each client repository.

Generate stubs for another language​

If no official client exists for your language, generate stubs directly from the protobuf files with protoc. For example, for Python:

python -m grpc_tools.protoc \
-I proto \
--python_out=. --grpc_python_out=. \
proto/eventstore.proto proto/admin.proto

Swap the *_out plugins for your target language. With gRPC reflection enabled (the default), you can also explore the API live:

grpcurl -plaintext -H "$AUTH" localhost:5005 list
grpcurl -plaintext -H "$AUTH" localhost:5005 describe orisun.EventStore

Single-query saves​

The existing SDK SaveEvents/saveEvents request shape remains supported. Its query.subsetQuery and query.expectedPosition are translated into one SaveEventsV2.consistency observation. A missing position with a non-empty query means {-1, -1}; no query or an empty criteria list means an unconditional append. The server exposes only the SaveEventsV2 write RPC, so upgrade old SDK binaries before connecting to this release. Keeping the method name does not make an old binary use the new RPC.

Subscription lifecycle and checkpoints​

after_position is exclusive when supplied. Omit it to receive the latest matching stored event once, then newer matches; use {0, 0} for a complete replay. Subscriber names coordinate a lease, not a stored application checkpoint. SDK helpers do not automatically reconnect from durable progress. Persist your own checkpoint and recreate the subscription after transport failure or expiry.

The server advances its cursor when a gRPC send succeeds, before client-side processing is acknowledged. Go and Node callbacks run sequentially, but their current helpers log/report a callback error and continue receiving. To avoid checkpointing beyond a failed event, stop or cancel processing, then reconnect from your durable checkpoint. In Go, cancel the subscription context rather than calling Close() from its own handler. Node returns a cancellable gRPC stream; call cancel() to terminate it.

Java subscriptions inherit the configured RPC timeout as a stream deadline (default 30 seconds). EventSubscription.close() currently suppresses callbacks and invokes completion locally; it does not cancel the underlying RPC immediately. Close the client/channel or let its deadline terminate the RPC before expecting the server lease to be released. These are SDK lifecycle limits, not event acknowledgements. Embedded callbacks differ: returning an error ends the subscription immediately. See Go embedding.

Compatibility​

Client libraries are generated from the public protobuf definitions. When upgrading Orisun, regenerate or update clients if the protobuf files changed.