Skip to content

Connect to Chronicle

The getting started guide connects to a local development kernel with ChronicleOptions.development(). This page covers everything else: connecting to a real server, securing the connection, and knowing what the client does when the network or the kernel misbehaves.

import { ChronicleClient, ChronicleOptions } from '@cratis/chronicle';
const options = ChronicleOptions.fromConnectionString(process.env.CHRONICLE_CONNECTION!);
const client = new ChronicleClient(options);
const store = await client.getEventStore('Library');

Keep the connection string, which carries credentials, in configuration or a secret store rather than in source code.

Creating a ChronicleClient does not connect. The first getEventStore(...) or getEventStores() call connects, and later calls reuse the connection.

chronicle://[<client-id>:<client-secret>@]<host>[:<port>][,<host>[:<port>]...][/?<option>=<value>&...]
chronicle+srv://[<client-id>:<client-secret>@]<service-host>[/?<option>=<value>&...]
  • The port defaults to 35000. Write IPv6 addresses in brackets, such as [::1]:35000.
  • URL-encode reserved characters in the client id, client secret, and option values.
  • chronicle+srv:// resolves the _chronicle._tcp.<host> DNS SRV records into a list of servers. It accepts exactly one host.
OptionDefaultMeaning
apiKeyNot setAuthenticate with an API key instead of client credentials.
disableTlsfalsetrue connects over plaintext gRPC and requests tokens over http.
skipTlsValidationtrueAccept any server certificate. Set to false to validate the certificate.
loadBalancerleast-connectionsHow the client picks one server from several: least-connections, round-robin, or random.
srvNameServerSystem resolverIP address, with an optional port, of the DNS server used for chronicle+srv:// lookups, such as 10.0.0.2:53. A host name is rejected.
certificatePath, certificatePasswordNot setAccepted for connection-string compatibility with other clients. The TypeScript client does not use them.

ChronicleOptions.fromConnectionString(connectionString, options) also accepts these options in its second argument:

OptionDefaultMeaning
discoveryPatternsNone for compiled JavaScript; **/*.ts and **/*.tsx with exclusions when the program runs from TypeScriptGlob patterns for artifact discovery. [] turns it off.
defaultSinkTypeIdWellKnownSinks.MongoDBWhere registered read models are stored; see Sinks.
clientArtifactsProviderThe shared default providerSupplies the event types, projections, reducers, and reactors to register.
reactorResultHandlerNot setHandles values that reactors return; see Reactors.
telemetryEvent source identifiers omittedPer-client identifier privacy; see Observability.
loggerdiag compatibility adapterPer-client structured diagnostic sink; see Observability.

ChronicleOptions.development(options) takes the same second argument.

Spans always use the cratis.chronicle.client.* convention names. For upgrade actions, see Telemetry naming cutoff.

The client supports two authentication methods. Use one per connection string.

  • Client credentials. Put the client id and secret in the user-info part: chronicle://my-service:<secret>@chronicle.example.com. The client requests an OAuth access token from /connect/token on the server it connects to, using TLS unless disableTls=true, and sends it as a bearer token on every call.
  • API key. Add apiKey=<key>. The client sends it as api-key metadata on every call.

A connection string with a client id and secret and an API key fails. A client id without a secret fails too, unless an API key is present, in which case the client id is ignored.

The client uses TLS unless the connection string sets disableTls=true. Chronicle serves gRPC and the token endpoint over TLS on one port.

With skipTlsValidation=false, the server certificate must be trusted by the default Node.js and gRPC trust store. The TypeScript client has no option to supply a custom certificate authority bundle or a client certificate.

Use disableTls=true only when something else protects the traffic, for example a service mesh that terminates TLS. Credentials and events then cross the network unencrypted between the client and that proxy.

List several hosts, or use chronicle+srv://, to connect to a clustered deployment:

chronicle://my-service:<secret>@chronicle-1.internal:35000,chronicle-2.internal:35000/?skipTlsValidation=false&loadBalancer=round-robin

The client talks to one server at a time. It resolves SRV records again and picks a server with the loadBalancer strategy on every connection and reconnection attempt, so it follows membership changes. With client credentials, it requests tokens from the selected server too, so each server must serve /connect/token.

When getEventStore(...) needs a connection, the client:

  1. Resolves the servers and picks one.
  2. Verifies that the kernel’s gRPC contract is compatible with the client’s contract package. See Client and kernel compatibility.
  3. Calls the kernel and registers a keep-alive.
  4. Creates the event store if needed and registers its artifacts.

Two failures are permanent. When the kernel is incompatible, the client throws IncompatibleChronicleServer. When the token endpoint rejects the client credentials with an OAuth invalid_client, unauthorized_client, or invalid_grant error, or the kernel refuses an API key, and this repeats on three consecutive attempts, it throws RejectedChronicleCredentials. The repeats ride out a kernel that is still registering its clients at startup; other token-endpoint errors, such as a 403 from a proxy, keep retrying. In both cases it stops retrying and rejects every later call; fix the deployment or the connection string, then create a new client.

If one of the first three steps fails for any other reason, such as the kernel being unreachable, the client waits and tries again, indefinitely. The wait doubles from about one second up to 30 seconds, with random jitter so that many clients don’t return at once. Until a connection succeeds or you dispose the client, getEventStore(...) neither resolves nor rejects. Once connected, a failure to register artifacts, such as a schema error, rejects getEventStore(...) straight away.

After connecting, the client checks the connection every five seconds and watches the kernel keep-alive. When either fails, it reconnects with the same backoff and registers the artifacts again for every event store it has handed out. A registration failure during that reconnect is logged, not thrown. Reactor and reducer observations restart after the reconnect. If getEventStore(...) or getEventStores() fails with a connection error, the client reconnects and retries the call once. Other calls, such as appends, reject with the gRPC error, and your code decides whether to retry.

When the kernel rejects a cached token as unauthenticated, for example after a key rotation, the client requests a new token and retries the call once. Observation streams are not retried within the call; they are observed again after a reconnect.

Supply ChronicleOptions.logger to send structured diagnostics to your application logger. Records include error types, not exception messages or stacks; see the logging contract.

Without a custom logger, the client uses its OpenTelemetry diag adapter; this default stays after the naming migration. It discards messages until you register a diagnostics logger. Existing configuration still works:

import { diag, DiagConsoleLogger, DiagLogLevel } from '@opentelemetry/api';
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.WARN);

Failed attempts appear as @cratis/chronicle/ChronicleClient Connection attempt failed, retrying, with the attempt number, delay, and error.type/exception.type. gRPC failures also include numeric rpc.grpc.status_code, for example 14 (UNAVAILABLE), 16 (UNAUTHENTICATED), or 4 (DEADLINE_EXCEEDED). When a token request fails, the client logs Failed to obtain OAuth2 token; sending RPC without authorization with the error type and sends the call without a token, which a kernel with authentication turned off accepts. Tokens, exception messages, and stacks are not included in diagnostics.

To fail fast at startup instead of waiting forever, bound the first call and dispose the client when the time runs out:

let timer: NodeJS.Timeout | undefined;
const timeout = new Promise<never>((_, reject) => {
timer = setTimeout(() => reject(new Error('Chronicle did not respond within 30 seconds')), 30_000);
});
try {
const store = await Promise.race([client.getEventStore('Library'), timeout]);
// Use the store.
} catch (error) {
client.dispose();
throw error;
} finally {
clearTimeout(timer);
}

The TypeScript client and the Chronicle kernel have separate version numbers, and the client also depends on a @cratis/chronicle.contracts version. None of these numbers promises which kernel versions work.

Instead, the client sends its contract descriptor to the kernel on every new connection, and the kernel reports whether it can serve it. When the kernel reports an incompatibility, or does not implement the check, the client throws IncompatibleChronicleServer, does not retry, and rejects every later call. Deploy a compatible kernel and create a new client. Unlike the .NET client, the TypeScript client has no option to skip this check.

There is no published compatibility matrix. The kernel-backed specifications use the cratis/chronicle:19.26.2-development image. Before you upgrade in production, test the client against the kernel version you run. Preserve existing append routes describes the upgrade that needs a kernel supporting kernel-owned append routing.

Create one ChronicleClient per process and share it. On shutdown, call client.dispose(). It stops the health checks and the keep-alive, ends reactor and reducer observations, and closes the channel. A disposed client rejects every later call with ChronicleClient is disposed, including a getEventStore(...) that is still waiting to connect.

Once it has disposed every client, the process can exit; the client leaves no open connections or timers behind.