diff --git a/Chronicle/TypeScript/README.md b/Chronicle/TypeScript/README.md
new file mode 100644
index 0000000..148bae9
--- /dev/null
+++ b/Chronicle/TypeScript/README.md
@@ -0,0 +1,126 @@
+
+
+# Chronicle TypeScript Client
+
+### Append a fact from Node.js. Let a reactor respond. Read the combined history.
+
+**Chronicle · Node.js · TypeScript**
+
+[Back to all samples](../../README.md)
+
+
+
+---
+
+## The idea
+
+A guestbook records visitors. The program appends one immutable `VisitorArrived` event, a reactor responds by appending a `VisitorWelcomed` event, and the program reads the complete history back — all from Node.js with the [`@cratis/chronicle`](https://www.npmjs.com/package/@cratis/chronicle) client.
+
+```mermaid
+flowchart LR
+ program[Node.js program] -->|append VisitorArrived| log[Chronicle event log]
+ log -->|observe| reactor[ConciergeReactor]
+ reactor -->|append VisitorWelcomed| log
+ log -->|history| program
+```
+
+There is no HTTP API, projection, or frontend in the way. This sample is about the TypeScript client and the event log itself.
+
+## Pinned versions
+
+| Piece | Version |
+| --- | --- |
+| [`@cratis/chronicle`](https://www.npmjs.com/package/@cratis/chronicle) | `3.1.1` |
+| [`@cratis/fundamentals`](https://www.npmjs.com/package/@cratis/fundamentals) | `7.18.2` |
+| Chronicle server image | `cratis/chronicle:latest-development` |
+| Node.js | 23 or newer |
+
+The npm dependencies are pinned exactly in [`package.json`](./package.json). The `latest-development` image tag is a moving development tag; the sample intentionally tracks the current development build of the Chronicle server.
+
+## Run it
+
+You need Node.js 23 or newer, npm, and Docker.
+
+Start Chronicle with the included [`docker-compose.yml`](./docker-compose.yml) — the development image bundles MongoDB, so one container is enough:
+
+```bash
+cd Chronicle/TypeScript
+docker compose up -d
+```
+
+Install the dependencies and run the sample:
+
+```bash
+npm install
+npm start
+```
+
+You should see the three steps in order:
+
+```text
+[append] VisitorArrived('Ada') appended to 'reception-guestbook' at sequence 0.
+[react] ConciergeReactor saw VisitorArrived('Ada') at sequence 0 and responds with a VisitorWelcomed event.
+[read] Guestbook 'reception-guestbook' history — 2 event(s):
+ [seq 0] VisitorArrived: {"name":"Ada"}
+ [seq 1] VisitorWelcomed: {"name":"Ada","greeting":"Welcome, Ada!"}
+```
+
+Pass a name to record someone else — every run appends new facts to the same history:
+
+```bash
+npm start -- Grace
+```
+
+Open Chronicle Workbench at and select the `TypeScriptGuestbook` event store to inspect the same history visually.
+
+## Clean up
+
+Stop and remove the container when you are done:
+
+```bash
+docker compose down
+```
+
+The event store lives inside the container, so removing it also removes the recorded history. Remove the sample's local artifacts with:
+
+```bash
+npm run clean
+```
+
+## Code tour
+
+| File | What it shows |
+| --- | --- |
+| [`events.ts`](./events.ts) | Two small, past-tense `@eventType()` classes |
+| [`reactor.ts`](./reactor.ts) | A `@reactor()` that returns a side-effect event |
+| [`index.ts`](./index.ts) | Connect, append, wait for observers, and read the history |
+| [`docker-compose.yml`](./docker-compose.yml) | The single-container local Chronicle server |
+
+The client setup is intentionally short:
+
+```typescript
+const client = new ChronicleClient(ChronicleOptions.development());
+const store = await client.getEventStore('TypeScriptGuestbook');
+const result = await store.eventLog.append(GUESTBOOK_ID, new VisitorArrived(name));
+```
+
+After the append, `result.waitForCompletion()` waits until every observer — here the `ConciergeReactor` — has caught up, so the read that follows sees the reactor's side effect instead of racing it.
+
+Set the `CHRONICLE_CONNECTION` environment variable to point the sample at a different Chronicle server (for example `chronicle://localhost:35000`).
+
+## Build check
+
+```bash
+npm run compile
+```
+
+Type-checks the sample with the TypeScript compiler.
+
+## Make it yours
+
+- Record a `VisitorLeft` event and react to it differently.
+- Give `VisitorWelcomed` its own event source to build a separate welcome log.
+- Move on to the [Chronicle Backend](../Backend/README.md) sample to see the same append-and-read journey from .NET behind an HTTP API.
+
+> [!NOTE]
+> This focused sample deliberately leaves out projections, read models, constraints, transactions, tenancy, authentication, and production configuration.
diff --git a/Chronicle/TypeScript/docker-compose.yml b/Chronicle/TypeScript/docker-compose.yml
new file mode 100644
index 0000000..62a73e7
--- /dev/null
+++ b/Chronicle/TypeScript/docker-compose.yml
@@ -0,0 +1,6 @@
+services:
+ chronicle:
+ image: cratis/chronicle:latest-development
+ ports:
+ - 35000:35000
+ - 8080:8080
diff --git a/Chronicle/TypeScript/events.ts b/Chronicle/TypeScript/events.ts
new file mode 100644
index 0000000..5ee4a75
--- /dev/null
+++ b/Chronicle/TypeScript/events.ts
@@ -0,0 +1,29 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+import { eventType } from '@cratis/chronicle';
+
+/**
+ * A visitor has signed the guestbook.
+ * This event is the source of truth for every visit — if there is no
+ * VisitorArrived event, the visit never happened.
+ */
+@eventType()
+export class VisitorArrived {
+ constructor(readonly name: string = '') {}
+}
+
+/**
+ * A visitor has been welcomed.
+ *
+ * Appended by {@link ConciergeReactor} as a side effect of a {@link VisitorArrived}
+ * event — the program never appends this event directly, which is what makes the
+ * reaction visible in the history this sample reads back.
+ */
+@eventType()
+export class VisitorWelcomed {
+ constructor(
+ readonly name: string = '',
+ readonly greeting: string = ''
+ ) {}
+}
diff --git a/Chronicle/TypeScript/index.ts b/Chronicle/TypeScript/index.ts
new file mode 100644
index 0000000..f703801
--- /dev/null
+++ b/Chronicle/TypeScript/index.ts
@@ -0,0 +1,61 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+import 'reflect-metadata';
+import { ChronicleClient, ChronicleOptions } from '@cratis/chronicle';
+import { VisitorArrived, VisitorWelcomed } from './events';
+
+// Side-effect import so the @reactor decorator runs and the reactor is
+// discovered and registered with the event store on connect.
+import './reactor';
+
+/** The single guestbook every visit in this sample is recorded against. */
+const GUESTBOOK_ID = 'reception-guestbook';
+
+async function run(): Promise {
+ // The sample keeps its artifacts (events, reactor) as flat files next to this
+ // one, so discovery only needs to scan the top level — this also keeps the
+ // sample's local node_modules out of the scan.
+ const discoveryPatterns = ['*.ts', '!*.d.ts', '!*.spec.ts'];
+ const options = process.env.CHRONICLE_CONNECTION
+ ? ChronicleOptions.fromConnectionString(process.env.CHRONICLE_CONNECTION, { discoveryPatterns })
+ : ChronicleOptions.development({ discoveryPatterns });
+
+ const client = new ChronicleClient(options);
+
+ try {
+ const store = await client.getEventStore('TypeScriptGuestbook');
+
+ // 1. Append — record the immutable fact that a visitor arrived.
+ const name = process.argv[2] ?? 'Ada';
+ const result = await store.eventLog.append(GUESTBOOK_ID, new VisitorArrived(name));
+ console.log(`[append] VisitorArrived('${name}') appended to '${GUESTBOOK_ID}' at sequence ${result.sequenceNumber.value}.`);
+
+ // 2. React — wait until every observer of the append (the ConciergeReactor)
+ // has either caught up or failed. Reading immediately without waiting can
+ // race the reactor's asynchronous processing and miss its side effect.
+ const completion = await result.waitForCompletion();
+ if (!completion.isSuccess) {
+ console.error(`[react] ${completion.failedPartitions.length} observer partition(s) failed while catching up on the append.`);
+ process.exitCode = 1;
+ return;
+ }
+
+ // 3. Read — the history now holds both the appended fact and the
+ // reactor's side-effect event.
+ const history = await store.eventLog.getForEventSourceIdAndEventTypes(GUESTBOOK_ID, [VisitorArrived, VisitorWelcomed]);
+ console.log(`[read] Guestbook '${GUESTBOOK_ID}' history — ${history.length} event(s):`);
+ for (const entry of history) {
+ console.log(` [seq ${entry.context.sequenceNumber}] ${entry.eventType.id.value}: ${JSON.stringify(entry.content)}`);
+ }
+ } finally {
+ client.dispose();
+ }
+
+ process.exit(process.exitCode ?? 0);
+}
+
+run().catch(error => {
+ console.error('Unhandled error:', error);
+ process.exit(1);
+});
diff --git a/Chronicle/TypeScript/package.json b/Chronicle/TypeScript/package.json
new file mode 100644
index 0000000..c3c8e29
--- /dev/null
+++ b/Chronicle/TypeScript/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "@cratis/sample-chronicle-typescript",
+ "version": "0.0.0",
+ "description": "Minimal Node.js TypeScript sample for the Cratis Chronicle client: append an event, react to it, and read the history",
+ "author": "Cratis",
+ "license": "MIT",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "compile": "tsc -b",
+ "build": "tsc -b",
+ "start": "tsx index.ts",
+ "clean": "rm -rf dist *.tsbuildinfo"
+ },
+ "dependencies": {
+ "@cratis/chronicle": "3.1.1",
+ "@cratis/fundamentals": "7.18.2",
+ "reflect-metadata": "0.2.2"
+ },
+ "devDependencies": {
+ "@types/node": "26.4.0",
+ "tsx": "4.23.12",
+ "typescript": "6.0.3"
+ }
+}
diff --git a/Chronicle/TypeScript/reactor.ts b/Chronicle/TypeScript/reactor.ts
new file mode 100644
index 0000000..bb9fe66
--- /dev/null
+++ b/Chronicle/TypeScript/reactor.ts
@@ -0,0 +1,25 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+import { reactor, EventContext } from '@cratis/chronicle';
+import { VisitorArrived, VisitorWelcomed } from './events';
+
+/**
+ * Reacts to visitors signing the guestbook by welcoming them.
+ *
+ * Reactors are the "if this then that" mechanism of event sourcing: they observe
+ * events and produce side effects. Returning an event from a handler appends it —
+ * here to the same event source that triggered the reactor — so the welcome
+ * becomes a fact in the history alongside the arrival.
+ *
+ * Key rules:
+ * - Handlers must be idempotent — the reactor may be called more than once for the same event.
+ * - Never query state inside a reactor; use the event data directly.
+ */
+@reactor()
+export class ConciergeReactor {
+ async visitorArrived(event: VisitorArrived, context: EventContext): Promise {
+ console.log(`[react] ConciergeReactor saw VisitorArrived('${event.name}') at sequence ${context.sequenceNumber} and responds with a VisitorWelcomed event.`);
+ return new VisitorWelcomed(event.name, `Welcome, ${event.name}!`);
+ }
+}
diff --git a/Chronicle/TypeScript/tsconfig.json b/Chronicle/TypeScript/tsconfig.json
new file mode 100644
index 0000000..4152969
--- /dev/null
+++ b/Chronicle/TypeScript/tsconfig.json
@@ -0,0 +1,24 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "ES2022",
+ "moduleResolution": "Bundler",
+ "lib": ["ES2022"],
+ "outDir": "./dist",
+ "rootDir": "./",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "experimentalDecorators": true,
+ "emitDecoratorMetadata": true,
+ "forceConsistentCasingInFileNames": true,
+ "ignoreDeprecations": "6.0"
+ },
+ "include": [
+ "./**/*.ts"
+ ],
+ "exclude": [
+ "node_modules",
+ "dist"
+ ]
+}
diff --git a/README.md b/README.md
index 81383fd..9782ed8 100644
--- a/README.md
+++ b/README.md
@@ -21,6 +21,7 @@ From a small Chronicle event-sourcing process to a React application composed wi
| Sample | Experience | Products | Start here |
| --- | --- | --- | --- |
| **[Chronicle Backend](./Chronicle/Backend/README.md)** | HTTP API | Chronicle | Append one immutable fact and read an event source's history. |
+| **[Chronicle TypeScript Client](./Chronicle/TypeScript/README.md)** | Terminal | Chronicle | Append a fact from Node.js, let a reactor respond, and read the history. |
| **[Chronicle Processing](./Chronicle/Processing/README.md)** | HTTP API | Chronicle, Fundamentals | Compare a projection, reducer, and reactor on one event stream. |
| **[Idea Loom — Arc + React](./Arc/React/README.md)** | React | Arc, Components, Fundamentals | Follow a typed command and observable query from C# to a polished UI. |
| **[Chronicle Multi-Tenancy](./Chronicle/MultiTenancy/README.md)** | HTTP API | Arc, Chronicle, Fundamentals | Isolate the same typed workflow across tenant namespaces. |
diff --git a/samples.json b/samples.json
index 162377b..70ce35b 100644
--- a/samples.json
+++ b/samples.json
@@ -66,6 +66,34 @@
],
"featured": true
},
+ {
+ "id": "chronicle-typescript",
+ "title": "Chronicle TypeScript Client",
+ "tagline": "Append a fact from Node.js, let a reactor respond, and read the combined history.",
+ "track": "getting-started",
+ "path": "Chronicle/TypeScript",
+ "sourceUrl": "https://github.com/Cratis/Samples/tree/main/Chronicle/TypeScript",
+ "ui": "Terminal",
+ "level": "Start here",
+ "products": ["Chronicle"],
+ "runtime": ["Node.js", "Chronicle"],
+ "prerequisites": ["Node.js 23 or newer", "npm", "Docker"],
+ "highlights": [
+ "Connect to Chronicle from Node.js with the @cratis/chronicle client",
+ "Append an immutable event and wait for its observers to catch up",
+ "Return a side-effect event from a reactor and read both facts back"
+ ],
+ "verification": [
+ "npm --prefix Chronicle/TypeScript install",
+ "npm --prefix Chronicle/TypeScript run compile"
+ ],
+ "limitations": [
+ "The sample is a single console run and intentionally leaves out projections, read models, and a frontend.",
+ "It runs against the local Chronicle development container and does not include automated specs.",
+ "The published @cratis/chronicle client must match the Chronicle server's gRPC contracts; the README lists the currently verified server tag."
+ ],
+ "featured": true
+ },
{
"id": "chronicle-processing",
"title": "Chronicle Processing",