---
title: Contributing to Clients
editUrl: https://github.com/Cratis/Chronicle/edit/main/Documentation/contributing/clients/index.mdx
description: Chronicle's shared public documentation is language agnostic. It uses unified language tabs for examples that apply across clients. The shared Chronicle…
---

> Starting a brand-new client from scratch? [Building a Chronicle Client](/chronicle/building-a-client/)
> is the onboarding story — starting points, the idiomatic-client layering, authentication,
> clustering, and the real history of the TypeScript, Elixir, and Kotlin clients. This page and the
> rest of this section are the deep reference for how Chronicle's own clients are built and
> maintained.

Below are the core principles for building public facing APIs exposed in our clients:

- APIs should be lovable
- Provide sane defaults but make them flexible, extensible and overridable
- Consistency is king
- It should be easy to do things right, hard to do things wrong
- Never expose more than is needed

## Shared documentation snippets

Chronicle's shared public documentation is language agnostic. It uses unified language tabs for examples that apply across clients. The shared Chronicle page owns the explanation and where the tab group appears, but each client repo owns its own code snippet.

This keeps the published docs readable while keeping client-specific APIs close to the code that can verify them.

### Ownership

Place snippets in `Documentation/client-snippets/**` in the repo that owns the client:

- C#: `Chronicle/Documentation/client-snippets`
- Kotlin: `Chronicle.Kotlin/Documentation/client-snippets`
- Java: `Chronicle.Kotlin/Documentation/client-snippets-java`
- Elixir: `Chronicle.Elixir/Documentation/client-snippets`
- TypeScript: `Chronicle.TypeScript/Documentation/client-snippets`

Snippet files can be `.md` or `.mdx`. Prefer `.md` for ordinary fenced code snippets; use `.mdx` only when the snippet file itself needs MDX syntax.

A shared page that uses `ChronicleClientTabs` must itself be `.mdx`. Put every placeholder on its own line in the self-closing form shown below. The documentation sync deliberately recognizes only that shape, so an inline, multiline, or paired component is not expanded.

Do not create a public client-specific Chronicle section just to hold equivalent snippets. The snippet folders are source-only and are expanded into the shared pages by the documentation site sync.

Shared pages (`Documentation/**` except `client-snippets/` and `clients/`) must contain **no direct client-language fences**. This includes `csharp`, `cs`, `c#`, `java`, `kotlin`, `kt`, `kts`, `elixir`, `ex`, `exs`, `typescript`, `ts`, and `tsx`. Put client code in `Documentation/client-snippets/<id>.md` and reference it from a shared `.mdx` page with `<ChronicleClientTabs snippet="<id>" />`. `bash Documentation/verify-markdown.sh` enforces this locally, and Markdown Verification CI runs the same authoring check and its detector self-test. The detector parses Markdown/GFM and MDX rather than matching fence-looking text inside other code blocks.

### Add a shared example

Shared Chronicle pages reference those snippets with a placeholder:

```mdx
<ChronicleClientTabs snippet="events/appending/example" />
```

The snippet id is extensionless. Add the same id under each participating client repo. A partial tab set is valid: a client participates only when that client should have a tab for this example.

```text
Chronicle/Documentation/client-snippets/events/appending/example.md
Chronicle.Kotlin/Documentation/client-snippets-java/events/appending/example.md
Chronicle.Kotlin/Documentation/client-snippets/events/appending/example.md
Chronicle.Elixir/Documentation/client-snippets/events/appending/example.md
Chronicle.TypeScript/Documentation/client-snippets/events/appending/example.md
```

Use one fenced code block in each snippet file unless the language needs more than one block. The fenced language must match the owning client, for example `csharp`, `java`, `kotlin`, `elixir`, or `typescript`.

`<ChronicleClientTabs />` has no `clients` property. Chronicle's shared docs never declare which clients apply to an example — the sync discovers that itself: it checks every registered client's snippet root for a file at that id and renders a tab only for the clients where one exists. When only some clients expose a concept, simply don't add a snippet file for the others; there is nothing to configure on the placeholder.

Do not use an absent snippet to claim that a participating client lacks support. When the shared workflow should be visible for that client but its SDK does not support it yet, keep the tab visible with an explicit unsupported snippet:

````md
```text
This Chronicle client does not support this workflow yet.
Track the client SDK issue before using this API from this language.
```
````

The default synchronized tab key is `chronicle-client`, so choosing TypeScript in one tab group selects TypeScript in the other tab groups on that page. Use a custom `syncKey` only when tab groups should not follow each other.

### Where "client X doesn't support this" belongs

That unsupported snippet is the **only** place to state that a client does not support a workflow. Do not write it as prose on a shared page — no "this is currently C#-only", no "Kotlin and Java don't have an equivalent yet".

A shared page cannot know when a client gains a capability. It lives in a different repo, on a different release cadence, with no CI that fails when the claim stops being true — so the claim silently rots, and the first person to notice is a reader who believes it. An unsupported snippet, by contrast, sits in the client repo next to the code, and the person adding the capability is already editing that folder.

The rule is about **claims of absence**, not about describing differences. It is fine for a shared page to say that a client expresses the same capability through a different shape, as long as it does not assert that some client cannot do something:

- ❌ "Kotlin, Java, and Elixir don't currently support targeting a non-default event sequence at all."
- ✅ "TypeScript reaches the same result through the decorator's event-sequence parameter rather than a separate attribute."

When a client does not support a workflow at all, leave the shared prose alone and let the tab carry it.

### Validation and CI

Each client repo must also keep `Documentation/validate-client-snippets.py` wired into its `Client Snippet Verification` workflow. That workflow compiles the snippets against the client source when either `Documentation/client-snippets/**` or `Source/**` changes, so a client API change cannot silently leave the shared Chronicle docs stale.

Run the validator in each repo whose snippets changed:

```bash
python3 Documentation/validate-client-snippets.py
```

Then run the documentation site build from the Documentation repo:

```bash
cd web
npm run build
```

External client repos also need a documentation sync workflow that triggers the `build-docs` workflow in the `Cratis/Documentation` repo after snippet changes land on `main`.

### Client-specific pages

Use snippets for examples that belong inside shared Chronicle pages. Use client-specific pages only when the content is genuinely different for that client, such as installation, connection setup, runtime integration, language-specific APIs, framework integration, decorators or annotations, package behavior, generated APIs, or troubleshooting.

Chronicle concepts and feature workflows belong in the shared Chronicle docs. Do not add client-specific concept or guide trees for events, reactors, reducers, read models, projections, constraints, seeding, transactions, migrations, or compliance when the same explanation applies across clients.

Client-specific pages still live in the owning client repo's `Documentation/**` folder:

- .NET pages render under `/chronicle/clients/dotnet/` from `Chronicle/Documentation/clients/dotnet/**`
- Kotlin pages render under `/chronicle/clients/kotlin/`
- Elixir pages render under `/chronicle/clients/elixir/`
- TypeScript pages render under `/chronicle/clients/typescript/`

The documentation site imports those pages into the Chronicle navigation under **Client SDKs**. The `Documentation/client-snippets/**` folders are skipped and never become public pages.

If a client-specific page later becomes generally useful across clients, move the explanation into the shared Chronicle docs and use `<ChronicleClientTabs />` for the code.

From `Documentation/web`, `npm run variant-docs:check` audits public client docs for shared-topic overlap using `variant-docs.yml`. That count is a ratchet: it can go down as duplicated client pages are consolidated into shared docs, but it must not increase.

If an old client URL must remain as a pointer to the shared docs, mark it as a bridge page:

```yaml
---
sharedTopicBridge: true
---
```

Bridge pages should only explain where the canonical shared topic lives and where to find client-specific setup or reference details.

### Add another Chronicle client

To add another client language later:

1. Add the client repo to the Documentation repo checkout/submodule setup.
2. Add it to `Documentation/web/variant-docs.yml`.
3. Add `Documentation/client-snippets/**` to the client repo when it participates in shared tabs. A new client only shows up in a tab group once it has a matching snippet file — no other wiring makes it opt-in or required.
4. Add `Documentation/validate-client-snippets.py` that compiles snippets against that client.
5. Add a `Client Snippet Verification` workflow in the client repo.
6. Add the documentation sync workflow that triggers the Documentation repo after `Documentation/**` changes land on `main`.
7. Add the new language's snippet files for the shared Chronicle pages that should include it.
8. Run the affected snippet validators, `npm run variant-docs:check`, and `npm run build` in `Documentation/web`.

### Migration audit

Shared Chronicle pages have **zero** allowed direct client-language fences, enforced by `Documentation/verify-authoring.mjs` locally and in CI. The Documentation site's aggregate audit also has an empty Chronicle client-fence baseline in `web/scripts/variant-docs-baseline.json`; it checks missing snippets and audits other products as well.

The aggregate check reports public client pages that still look like shared Chronicle topic pages. Treat those counts as migration debt: move the shared explanation into Chronicle docs, replace code with synchronized tabs, then lower the `publicDocsAudit` baseline in `web/variant-docs.yml`.

From `Documentation/web`, run `npm run audit:variant-docs:strict` to audit shared pages across products without the site's baseline. When migrating a page, replace each client SDK example with `<ChronicleClientTabs snippet="..." />`, add matching snippets in the participating client repos, and run each affected client snippet validator.

## Topics

| Topic | Description |
| --- | --- |
| [Client Types](/chronicle/contributing/clients/client-types/) | The different client types and relationships between them. |
| [.NET](/chronicle/contributing/clients/dotnet/) | The .NET Client. |
| [API](/chronicle/contributing/clients/api/) | The REST API Client. |
| [Internalization](/chronicle/contributing/clients/internalization/) | The process of internalization. |
