This is the full developer documentation for Cratis
# Software gets built differently now
> In the age of AI, software gets built differently. Cratis is one connected, purposeful ecosystem — an event store, an application framework, a UI library, developer tooling, and AI skills — designed together, from one company, so the pieces fit instead of being assembled. It is database-agnostic (MongoDB by default, PostgreSQL, SQL Server, or SQLite) with clients for .NET, TypeScript, Kotlin/Java, and Elixir. The open-source Cratis products are MIT licensed and free to use; Studio is a paid product in Preview.
One Ecosystem Event Sourcing CQRS Application Framework The Stack Any Language Any Database AI-Native Studio Screenplay Prologue
## Cratis facts
Cratis is an open-source (MIT) event sourcing and CQRS platform for .NET, built around the Chronicle event-sourcing database. The facts below are the short, citable version; [The Cratis Stack](/cratis-stack/#capability-map) has the full product table.
* **Products.** Chronicle is the event store and runtime. Arc is the CQRS application framework with generated TypeScript proxies. Components is a React UI library for Arc. Fundamentals holds the shared building blocks, and Specifications provides BDD-style testing. The CLI, Workbench, and Chronicle MCP server inspect and operate a running Chronicle. Cratis AI ships rules and skills for AI agents.
* **Model-first layer.** Screenplay, Stage, and Prologue are experimental. Studio is a paid product in Preview.
* **Licence.** The open-source Cratis products are MIT licensed. Studio is paid.
* **Languages.** Chronicle has a first-class .NET client and clients for TypeScript, Kotlin/Java, and Elixir, plus an experimental Go client. A Python client is in development and not yet published. Arc runs on ASP.NET Core and Spring Boot, and Components targets React.
* **Storage.** Chronicle stores events in MongoDB (default), PostgreSQL, Microsoft SQL Server, or SQLite.
* **Downloads.** over 2.5 million downloads across NuGet, npm, Docker Hub and Hex, counted October 2026. Chronicle, including its predecessors Aksio Cratis and the Dolittle Runtime: over 1.5 million downloads. Arc, including Aksio Cratis Applications and the Dolittle SDK: over 1.2 million downloads.
* **Where to get it.** NuGet, npm, Maven Central, Hex, Docker Hub, and [GitHub](https://github.com/Cratis). See the [support matrix](/compatibility/#support-matrix) for what each combination supports.
* **Who builds it.** Cratis, a small company that builds and maintains the platform.
## Start here
New to Cratis? Pick the route that matches what you want to do first:
[Build your first full-stack feature ](/build-a-full-app/)Scaffold an app from the Cratis template, then add one slice from command to event to live React screen.
[Choose where to start ](/adopting-cratis/)New system or existing one, .NET or another language: pick the product to adopt first.
[Follow a learning path ](/learning-paths/)An ordered reading route for event-sourcing beginners, builders, evaluators, operators, and frontend developers.
## Why this matters
Most stacks are assembled: a database here, a framework there, a UI kit from somewhere else, and glue code everywhere in between. Every seam is something a team — and now every AI assistant — has to learn, maintain, and keep in sync.
Cratis takes the opposite bet. One company designs the whole path — from the facts your system records, through the application that shapes them, to the screens people use and the tools that operate it — around one shared model and one set of conventions. We believe software built this way is better, more robust, faster to deliver, higher quality, and more cost-effective, because the seams you would normally hand-write and babysit simply are not there.
That same coherence is what makes the ecosystem AI-ready: conventions that are explicit and consistent are conventions an AI agent can follow. Cratis ships its conventions as [AI skills, rules, and diagnostics](/ai/), so assistants build with the grain of the platform instead of guessing at it.
## One connected ecosystem — adopt it one piece at a time
Designed together does not mean all-or-nothing. Each product stands on its own, and you choose what you need: run Chronicle alone as your event store, build with Arc without event sourcing at all, or take the whole stack and let value flow through the entire chain. Start with the piece that solves today’s problem — the rest are there when you want them, not before:
[Chronicle — the event store ](/chronicle/)Record what happened as immutable facts and derive read models from them. Database-agnostic — MongoDB (default), PostgreSQL, SQL Server, or SQLite — so it runs on the database you already operate.
[Arc — the application framework ](/arc/)Turn commands and queries into a full-stack CQRS application with validation, authorization, and generated TypeScript proxies — no hand-written API layer.
[Components — the UI library ](/components/)React components that render Arc's generated contracts as forms, dialogs, and live tables, so screens follow the model.
[CLI and Workbench — the tooling ](/cli/)See what the running system is doing — inspect events, observers, and read models, replay, and diagnose — from the terminal or the browser.
[AI skills — the accelerant ](/ai/)The same conventions developers use, packaged for AI agents: Cratis AI rules and skills, analyzers, the CLI command catalog, and an MCP server into the running store.
[Clients for your language ](/chronicle-clients/)Use Chronicle from .NET (first-class), TypeScript, Kotlin/Java (JVM), Elixir, or Go (experimental) — with a Python client coming soon.
## Built to deliver outcomes, not just features
Cratis is opinionated on purpose. The opinions are what carry the value:
* **Built to keep history you can trust.** Events are immutable facts, and read models derive from them — audit and replay are part of the design, not an afterthought.
* **Built to remove glue.** One C# model becomes typed frontend contracts; drift between backend and frontend becomes a build error instead of a production incident.
* **Built to fit your infrastructure.** A database-agnostic event store and multiple language clients mean the ecosystem adapts to you — not the other way around.
* **Built to be adopted piecemeal.** Every product is independently adoptable. Chronicle works without Arc, Arc works without event sourcing, Components is optional on top of Arc — you never buy the whole stack to get one piece, and nothing is wasted when you later add the next one.
* **Built for people and AI together.** Predictable, convention-driven codebases are easier for new developers to join and for AI agents to extend.
We believe this adds up to software that is more robust, faster to build, and more cost-effective to own. [Why developers choose Cratis](/why-cratis/) walks through the reasoning; [The Cratis Stack](/cratis-stack/) shows the whole journey end to end.
## Use one piece — or the whole thing
Modularity is a design commitment, not a marketing line. The products compose, but none of them holds the others hostage — each seam below is an intentional, supported adoption boundary:
* Chronicle stands alone. Use it purely as an event store — from .NET, TypeScript, Kotlin/Java, or Elixir — with no Arc, no Components, and no other Cratis product involved.
* Arc does not require event sourcing. Use application services, MongoDB, or Entity Framework Core for current-state behavior, and add Chronicle only where an event-sourced path fits.
* Arc does not require Components. Generated TypeScript clients can be consumed directly or through Arc’s React packages.
* Chronicle is not .NET-only. The [client landing page](/chronicle-clients/) maps each shipped client to its documentation and packages; the protocol details live in the [Chronicle architecture](/chronicle/architecture/) pages.
* Chronicle and its bundled local Workbench are available as MIT-licensed self-hosted software; authorized local use is separate from paid Cratis support, hosted coordination, or managed operational responsibility.
These boundaries do not establish compatibility with every package, provider, browser, runtime, topology, or product version. Check the documentation and package manifests for the combination you use.
## Go deeper
Technical depth lives in each product’s own documentation:
[Start Chronicle locally ](/chronicle/get-started/)Run the development image, wait for readiness, and open the bundled Workbench.
[Understand Chronicle architecture ](/chronicle/architecture/)Locate protocol surfaces, the kernel, event stores, namespaces, processing, and subscriptions.
[Run Chronicle on your database ](/chronicle/hosting/configuration/storage/)Configure the storage provider — MongoDB (default), PostgreSQL, SQL Server, or SQLite.
[Pick a Chronicle client ](/chronicle-clients/)Start from .NET, TypeScript, Kotlin/Java, Elixir, or Go (experimental), with links to each client's documentation and packages.
[Explore Arc ](/arc/)Choose commands, queries, observable-query APIs, frontend contracts, identity, tenancy, persistence, or testing.
[Browse Components ](/components/)Find package setup, component areas, styling boundaries, examples, and migration guidance.
[Use the CLI ](/cli/)Install the CLI, connect to Chronicle, inspect events and read models, and diagnose observer failures.
[Run a sample ](/samples/)Use Cratis-owned sample applications to evaluate one bounded technical path.
## About this site
Product repositories own their technical documentation, examples, packages, and released contracts. This site aggregates that material into one searchable set of tutorials, guides, explanations, and references.
* [Browse the Cratis repositories](https://github.com/Cratis)
* [Join the Cratis community](https://discord.gg/kt4AMpV8WV) to ask questions and get help from the team
* [Report a documentation problem](https://github.com/Cratis/Documentation/issues)
* [Read company, fit, and trust information](https://cratis.no/)
# 404
> That page wandered off. Let's get you back to something useful.
Looking for something specific? Try the **search** in the top bar, or jump to a product:
* [Chronicle](/chronicle/) — the event sourcing platform
* [Arc](/arc/) — the full-stack application framework
* [Components](/components/) — the React component library
# Adopting Cratis
> Where to start with Cratis — default to the full event-sourced stack for new information systems, or adopt one product at a time in existing systems.
Getting oriented
## Where to start
Cratis products stand on their own and compose when you want them to. For a new information system, our default is the full loop: Chronicle for event sourcing, Arc for CQRS, Components for React, and AuthProxy at the edge. For an existing system or a bounded current-state slice, you can adopt one piece at a time without losing the path back to the full stack.
## Pick your entry point
Three things decide where you start: **what you’re building on** — a new codebase or an existing one — **which boundary you are changing first**, and **which language your services are written in**. Event sourcing is the default architecture we recommend for information systems; CQRS, generated contracts, and edge identity can still be adopted independently.
```
New
a corner where current
state is genuinely enough
Existing
Frontend/backend glue
Audit & history
New information system
or existing?
Default stack:
Arc + Chronicle + Components
Arc + Components
over MongoDB / EF Core
What hurts
most?
Add Arc to your app
Introduce Chronicle,
one slice at a time
```
| Your situation | Start with | Why |
| --- | --- | --- |
| New information system | **Arc + Chronicle + Components** | The default Cratis loop: CQRS at the boundary, event history underneath, React generated from C# — the [full-stack capstone](/build-a-full-app/) builds the shape. |
| Bounded slice where current state is genuinely enough | **Arc + Components** over a database | Typed commands, queries, and a generated React client with CQRS but no event log — [CQRS without event sourcing](/arc/arc-without-event-sourcing/). |
| Existing .NET API, frontend glue is the pain | **Arc**, added to your app | A typed TS client for new endpoints, without rewriting what you have. |
| Existing system, you need history or audit | **Chronicle**, incrementally | Event-source one bounded context; leave the rest as it is. |
| New full-stack app on Kotlin or Java | **Arc + Chronicle** on Spring Boot | The [Cratis Kotlin](/templates/cratis-kotlin/) and [Cratis Java](/templates/cratis-java/) templates scaffold the same full-stack shape with a React frontend. |
| Services in TypeScript, Kotlin/Java, or Elixir that need an event store | **Chronicle** with your language’s client | Chronicle’s clients talk to the same kernel over gRPC — see [Chronicle in your language](/chronicle-clients/). |
| Need to separate CQRS from event sourcing | Read first | [CQRS without event sourcing](/arc/arc-without-event-sourcing/), then [Why developers choose Cratis](/why-cratis/). |
## Starting from scratch (greenfield)
A new project is the easy case — nothing to migrate, no constraints. For an information system, start with the whole loop unless you have a clear reason not to:
* **Default to the full experience.** Follow [Build a full-stack feature](/build-a-full-app/): it scaffolds the application from the [Cratis templates](/templates/getting-started/) and adds one slice by hand. You’ll have the command → event → projection → query → React loop running, with history available from the event log. The template wires Arc and Chronicle into one host through the [Cratis package](/arc/backend/csharp/chronicle/cratis-package/)’s `AddCratis`/`UseCratis`.
* **Use Arc-only for deliberate current-state slices.** If a bounded part of the app is just reference data, settings, or an adoption step, [Arc over a database](/arc/arc-without-event-sourcing/) still gives you CQRS, generated proxies, and live queries. Adopting Chronicle later is mostly a write-side change: the query contract and the React screens that use it can stay the same, though the query’s implementation moves to the read model a projection builds.
CQRS and event sourcing are independent
Arc gives you CQRS: commands for information entering the system, queries for information leaving it. Chronicle gives you event sourcing: facts as the durable source of truth. They fit together extremely well, but either one can stand without the other.
## Adding to an existing system (brownfield)
You rarely get to start clean. Cratis is built to be adopted a slice at a time, alongside code that already works — you don’t rewrite, you grow into it.
### Add Arc to an existing ASP.NET Core app
If your pain is the **frontend-to-backend boundary** — hand-written controllers, DTOs duplicated in TypeScript, fetch wrappers that drift out of sync — Arc slots into an app you already have. Register it on your existing host and start expressing *new* endpoints as commands and queries; Arc generates their typed proxies while your existing controllers keep running untouched. [Set up a standalone Arc backend](/arc/backend/csharp/getting-started/) shows the packages and the proxy-generation settings, with no Chronicle or template involved.
Arc’s persistence meets your data where it already lives: its [MongoDB](/arc/backend/csharp/mongodb/) and [Entity Framework](/arc/backend/csharp/entity-framework/) integrations read and write the database you already use — EF Core even has a [direct-registration path](/arc/backend/csharp/entity-framework/getting-started/) designed for slotting into an existing application without taking over the whole framework. For controllers and MediatR handlers, the [MediatR, MVC, and Arc](/arc/backend/csharp/coming-from-mediatr-and-mvc/) bridge maps familiar concepts onto Arc’s model.
### Introduce Chronicle into an existing domain
If your pain is **history** — you need an audit trail, a “how did this order get into this state?” view, or a new read model the current schema can’t serve — you don’t have to event-source the whole system in one move. Pick the bounded context where the model is clearest and event-source that first:
1. Model the facts for that area as [events](/chronicle/concepts/event/), and append them when the corresponding things happen — for new behavior, append the event; for existing writes, append alongside the current write. Existing rows do not become events on their own; decide whether you need historical events at all, and if so, write them deliberately.
2. Build [projections](/chronicle/projections/) that fold those events into exactly the read models that context needs. They can sit right next to your existing tables.
3. Let the rest of the system keep working as it is. Event sourcing earns its place one context at a time, not as a big-bang rewrite.
The [CRUD, EF Core, and Chronicle](/chronicle/coming-from-crud/) guide maps tables and `SaveChanges` onto events — that’s the mental shift that makes this click.
Writing to two places
Appending an event alongside an existing write is a dual write: the two can succeed or fail independently. Decide which one is the source of truth for that context, make the other derived from it where you can, and plan how you detect and reconcile a mismatch. The [CRUD, EF Core, and Chronicle](/chronicle/coming-from-crud/) guide maps the concepts.
Keep the boundary honest
Event sourcing is the default for information systems, not a mandate that every settings table needs a stream. Use Chronicle for the facts and processes that define the domain, and leave genuinely current-state-only corners on Arc-over-a-database. [When to use event sourcing](/chronicle/concepts/when-to-use-event-sourcing/) is the honest filter.
## Grow into the full stack
However you start, the pieces are designed to be *added*, not swapped out. The common growth paths:
* **Arc-over-a-database → add Chronicle.** Move a slice’s write side from a direct insert to appending an event with a projection behind it. The query’s contract and the React that calls it can stay the same, while the query now reads the projected read model; see the side-by-side in [CQRS without event sourcing](/arc/arc-without-event-sourcing/).
* **Chronicle-only service → add Arc and a frontend.** Already event-sourcing from a worker or service? Put Arc in front to expose typed commands and queries, and Components to render them — without changing how your events are stored.
* **Any backend → add Components.** The React library consumes Arc’s generated proxies, so adding it is a frontend-only step.
## Next steps
[ Why developers choose Cratis](/why-cratis/)
[How the three products stand alone and compose — the map this page navigates.](/why-cratis/)
[ CQRS without event sourcing](/arc/arc-without-event-sourcing/)
[The standalone Arc shape, and the write-side change that moves a slice to Chronicle.](/arc/arc-without-event-sourcing/)
[ Build the full loop](/build-a-full-app/)
[The full-stack capstone — command to event to live React screen, with history.](/build-a-full-app/)
# Agentic development with event sourcing: a guide
> Why event-sourced systems give AI coding agents verifiable history, explicit contracts and replayable tests, and how to set it up in practice.
Agentic development with event sourcing means letting AI coding agents design, change and operate a system whose state is an append-only log of named events, whose behavior is expressed as commands, and whose rules are written as executable Given/When/Then specifications. The agent reads and writes the same explicit contracts a human reviewer reads. The log records what happened, and the specifications state what must stay true. Neither depends on what the agent remembers or guesses.
This page explains why that combination helps, which Cratis tools implement each part, how they fit together, and what is still experimental. It is written for tech leads who are adopting coding agents and worry about correctness and auditability. Cratis is built by a team that works this way, and the tooling described here is the tooling we use. Where something is not finished, the page says so.
Maturity at a glance
The Chronicle MCP server, the CLI, Chronicle and Cratis Specifications are released. Cratis AI (rules and skills) and Studio are in Preview. Screenplay, Stage and Prologue are experimental. The [Cratis Stack](/cratis-stack/) page has a capability map with maturity labels.
## Why agents fail on CRUD systems
A coding agent works from what it can see: the files in the repository, the prompt, and whatever it can query. Three properties of a typical create-read-update-delete (CRUD) system work against it.
* **Hidden state.** The current row is the only truth. The agent cannot tell why a value is what it is, which code path last wrote it, or which of three similar columns the business actually relies on.
* **No history.** An update overwrites the previous value. When an agent’s change corrupts data, nothing in the database says what the data looked like before, so a fix is a guess and an audit is a reconstruction from logs.
* **Implicit contracts.** Behavior lives inside service methods, triggers and ORM hooks. An agent that changes one path cannot see which other behavior depended on it, and neither can the reviewer reading the diff.
None of this is a defect in the agent. A human who joins a CRUD system cold has the same problems and solves them by asking colleagues. An agent cannot ask, so it fills the gap with the most plausible answer, and a plausible wrong answer is the dangerous one.
Event sourcing removes each of these. State is the history of events, so nothing is hidden and nothing is overwritten. Every change is a named command with a typed shape, so the contract is explicit. The rest of this page builds on that.
If you are new to the idea, [what is event sourcing](/concepts/event-sourcing/) covers the basics, and [coming from CRUD](/chronicle/coming-from-crud/) maps the concepts you already use.
## Events, commands and specifications as a contract agents can read
Three artifacts carry the contract in a Cratis application. Each is plain code in the repository, so an agent reads it the same way it reads any other source file.
| Artifact | What it states | Why it helps an agent |
| --- | --- | --- |
| **Command** | One intention a caller can express, with its inputs, validation and the events it may record | The agent finds every way the system can change, named and typed, in one place |
| **Event** | A fact that happened, named in the past tense (`BookBorrowed`), immutable | The agent sees what the system records and cannot silently rewrite it |
| **Specification** | A Given/When/Then example of the behavior: facts that already happened, the action, the expected facts | The agent has an executable definition of “correct” that does not depend on its own reading of the code |
Given/When/Then maps one to one onto event sourcing. The given is the events that already happened, the when is the command or event under test, and the then is the events, read models or side effects that result. That is why a specification reads in the vocabulary of the event model, and why a domain expert can check it without reading the implementation. [Testing with Cratis](/testing-with-cratis/) shows the mapping in C#, Kotlin, Java and TypeScript, and [Specifications](/specifications/) documents the `Establish()`, `Because()` and `Destroy()` lifecycle.
Here is a real specification from the [Decisions that hold](/decisions-that-hold-walkthrough/) walkthrough. It checks a rule that is easy to get wrong: two callers borrow the last copy of a book at the same time.
```csharp
public class when_the_last_copy_is_borrowed_concurrently : Specification
{
static readonly ISBN _isbn = new("978-0316389704");
readonly CommandScenario _scenario = new();
CommandResult _result;
void Establish()
{
_scenario.Given.ForEventSource(_isbn).Events(new BookAddedToInventory("The Last Wish", AuthorId.New(), 1));
_scenario.AppendConcurrently(_isbn, new BookBorrowed(_isbn, "The Last Wish", MemberId.New(), DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddDays(21)));
}
async Task Because() => _result = await _scenario.Execute(new BorrowBook(_isbn, MemberId.New()));
void Destroy() => _scenario.Dispose();
[Fact] void should_not_succeed() => _result.ShouldNotBeSuccessful();
[Fact] void should_report_a_conflict() => _result.ShouldHaveValidationErrorBecauseOf(ValidationResultReason.ConcurrencyViolation);
[Fact] void should_append_nothing() => _scenario.AppendedEvents.Count.ShouldEqual(0);
}
```
The scenario seeds one copy, so the validator sees one available copy and passes. A competing `BookBorrowed` is appended after the command reads and before it commits. The command is refused with a concurrency violation and appends nothing. The spec runs in process, without a Chronicle server or a database, so an agent can run it after every change.
An agent that changes `BorrowBook` has a precise target: keep this spec green. An agent that weakens the rule fails the spec, not a reviewer’s patience.
## Event models as the shared source for humans and AI
Contracts are cheaper to agree on before the code exists. [Event modeling](/event-modeling/) lays a system out as a timeline of screens, commands, events and read models. Because each step is named and typed, a model is a description an agent can read, and a plan a human can review before any code is written.
Cratis puts the model in a file. [Screenplay](/screenplay/) is a declarative `.play` language for one bounded context: its events, commands, queries, projections, screens and rules. [Studio](/studio/) is the collaborative environment for editing Screenplay models. [Stage](https://github.com/Cratis/Stage) compiles Screenplay, verifies the specifications declared in the model, and renders a reviewable Arc and Chronicle application on disk.
The practical effect is a shared source. The model is what the team reviews. The specifications declared in the model are what Stage verifies. The generated application is what the agent starts from and extends. When the model changes, the discussion happens on the model, in terms the business understands, rather than in a diff of generated code.
The [event model is also the test](/event-modeling/): one column of a model becomes the Given/When/Then rows of a specification, as the [testing guide](/testing-with-cratis/#the-event-model-becomes-the-spec) shows step by step.
If you already use [EventModelers.ai](/eventmodelers-ai/), its Build Kits give an agent a model-to-code path for C#, Kotlin and Java. The page [EventModelers.ai](/eventmodelers-ai/) explains what a Build Kit does and how to pick one.
## Letting agents inspect the runtime: Chronicle MCP and the CLI
A specification covers behavior you thought of. Production shows the rest. An agent that is asked “why is this order cancelled?” or “why is this projection behind?” should read the system, not infer from the code.
Cratis gives the agent two documented routes into a running [Chronicle](/chronicle/) store.
### The Chronicle MCP server
The [Chronicle MCP server](/chronicle-mcp/) speaks the [Model Context Protocol](https://modelcontextprotocol.io). It runs over stdio as the `cratis/chronicle-mcp` container image and connects to a running Chronicle server. Releases are also listed in the official MCP Registry as `io.github.Cratis/chronicle-mcp`. Because it talks to Chronicle directly, it works with every Chronicle client language: .NET, TypeScript, Kotlin/Java and Elixir.
It exposes two kinds of tools:
* **Operate-side tools** inspect a live store: `list_event_types`, `get_events`, `get_tail_sequence_number`, `list_observers`, `list_failed_partitions`, `list_jobs` and others.
* **Design-time tools** turn a request into a reviewable proposal: describe an event type, build an event catalog, find event types nothing consumes, run an ad-hoc projection, and scaffold a read model.
The design-time tools follow one rule: introspect the store before generating anything. Field names, types and event names come from the schema registered with Chronicle, not from the model’s memory. When a requested event type is not registered, the tool says so and does not invent a shape.
The tool that most directly answers the audit question is `explain_causal_trace`. It reads the ordered events of one event source together with each event’s correlation id and causation chain, so the agent can say what happened and why. The documentation calls it the equivalent of `git blame` for state.
A minimal configuration for an MCP host that runs the server through Docker looks like this (taken from the [Chronicle MCP getting started](/chronicle-mcp/getting-started/) guide):
```json
{
"servers": {
"Chronicle": {
"type": "stdio",
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"-eCratis__Chronicle__Mcp__ConnectionString=chronicle://host.docker.internal:35000",
"cratis/chronicle-mcp"
]
}
}
}
```
### Read-only by default
The server starts in the `ReadOnly` profile. In that profile the tools that change job state (`stop_job`, `resume_job` and `delete_job`) are not in the tool list and cannot be called. The `Mutation` profile exposes them, and it refuses to start unless the deployment names a principal, an expiry in the future and explicit grants for each store, namespace, operation and target. Operation wildcards are invalid, so a new mutating tool is never granted by accident. Chronicle stays the authority and applies its own authentication and authorization to every call. See [deployment profiles](/chronicle-mcp/deployment-profiles/).
Local defaults
Without configuration the server uses Chronicle’s development credentials and `localhost:35000`. Those defaults are for a local development store. Give the server real credentials before you point it at a shared store.
### The CLI
The [Cratis CLI](/cli/) covers the same ground from a terminal and is the better fit for agents that run shell commands. Running `cratis init` in your project writes a `CHRONICLE.md` that lists every command the CLI can run, adds instruction files for the AI tools it detects, and adds a `chronicle-diagnose` slash command. The CLI can also replay an observer, which rebuilds a read model; read [replay rebuilds read models](#replay-rebuilds-read-models) before an agent runs it.
Neither route changes the event log. Both read it and operate observers and jobs; job control and replay change read-side state, so they need care and, over MCP, an explicit policy. To change state, the system goes through commands and events, so the history stays honest.
## Rules and skills that teach agents the conventions
A general-purpose agent dropped into a Cratis codebase invents handler classes, misses the vertical-slice layout and hand-writes the API client that proxy generation already produces. [Cratis AI](/ai/) is a shared corpus of rules, skills, prompts and subagents that carries the conventions to the agent.
The main path installs the corpus into a repository with the CLI:
```bash
cratis ai install
```
Run without options it asks what the repository needs. To script it, select the harnesses, profiles and languages explicitly:
```bash
cratis ai install \
--harnesses claude,codex,copilot,cursor,opencode,pi \
--profiles cratis/application \
--languages csharp,typescript
```
The CLI installs the managed content under `.cratis/ai`, creates native adapters for each selected coding tool, and records hashes in `.cratis/ai.manifest.json`. It refuses to overwrite files you own, and `cratis ai status` shows what is installed and whether a newer revision is available. Two other channels exist: the `@cratis/pi` package for Pi, and marketplace plugins for Claude Code, Codex, GitHub Copilot and Cursor, which deliver skills only. [Trust and distribution](/ai/trust-and-distribution/) explains how each channel is versioned, and the [harness guide](/ai/harnesses/) has the command for each tool.
Some of what the corpus contains, by name:
* The `new-vertical-slice` prompt scaffolds a feature: command, events, projection, query, React and specs.
* Skills such as `cratis-arc-command`, `cratis-chronicle-read-model` and `cratis-chronicle-projection` build one artifact by convention.
* The `write-specs` prompt and the `cratis-code-review` skill cover a feature with BDD specs and review it against the project’s standards.
Rules and skills do not prove a change is correct. They make the first attempt look like the rest of the codebase, so review and specifications spend their time on behavior instead of style. The [code-analysis rules](/code-analysis/) enforce the same framework contracts during the build, whoever wrote the code.
Cratis AI is in Preview. One practical limit applies to every agent: a large profile such as `cratis/full` can exceed the budget some agents give to skill descriptions, so install the profiles a repository needs rather than everything.
## Guardrails: specifications, review and replay
An agent that can write code quickly needs checks that are as fast and that do not depend on the agent’s judgment. A guardrail model for agent-written changes has four layers. Each one is a mechanism you can point at.
| Layer | Mechanism | What it catches |
| --- | --- | --- |
| Build | [Code-analysis rules](/code-analysis/) and typed commands, events and concepts | Contract mistakes before anything runs |
| Specifications | In-process Given/When/Then scenarios for commands, read models and reactors | A behavior change the spec did not expect |
| Constraints | Rules Chronicle checks when an event is appended, such as uniqueness, and append conditions for decisions that depend on state | A change that is accepted by the code but violates a rule under concurrency |
| Review | A human reads the model, the specs and the diff | Wrong intent that a green build cannot see |
### Specifications as the first gate
Specifications give an agent a fast loop. It changes the code, runs the specs and reads the failure. Because specs are written in the language of events, a failing spec names the fact that was expected and the fact that happened. [Testing with Cratis](/testing-with-cratis/) lists the in-process scenarios for each side: command scenarios for the write side, read-model scenarios for projections and reducers, and reactor scenarios for side effects.
The specs prove behavior, so the first review question for an agent-written change is whether the specs describe the behavior you want. An agent can write a spec that passes and asserts the wrong thing. Review the specification diff as carefully as the implementation diff, and prefer specifications written or approved from the event model.
### Constraints and decisions at append time
Some rules cannot be checked by a unit spec alone, because they depend on what other callers do at the same moment. Chronicle checks constraints when an event is appended and rejects the append if the rule fails. For decisions that read state, it can check the state the decision was based on: if a matching event was written since, the append is refused. The [keeping decisions correct](/concepts/keeping-decisions-correct/) page explains the model. This matters for agents because the rule is enforced where the data is written, not only where the agent happened to remember it.
### Replay rebuilds read models
The log also makes read models disposable. When you change how a projection or reducer builds a read model, you do not migrate the read model: you replay the observer and it rebuilds from history. The CLI does this with the `observers replay` command, and `replay-partition` does it for a single event source. The [replay a projection](/cli/scenarios/replay-a-projection/) scenario walks through it.
Replay is an operation, not a check. The event log is untouched, but the observer discards its accumulated state and rebuilds from sequence zero, so the read model is unavailable or incomplete while it catches up. Run it deliberately, after the changed projection is deployed, and prefer `replay-partition` when only one event source is affected. Specifications are the pre-deployment check: they run in process against seeded events and touch no store. Reactors need extra care, because Chronicle’s .NET client replays them unless the class is marked `[OnceOnly]`; read the [reactor replay rules](/chronicle/clients/where-the-clients-differ/#reactor-replay-and-delivery-identity) before replaying one.
### Review stays human
Nothing here removes the reviewer. It changes what the reviewer reads: an event model and a set of specifications that state intent, plus a diff that has already passed the build and the specs. The record that remains after a change is the log itself. Each event carries correlation, causation and identity, so a later audit can follow a state back to the command that caused it.
## Brownfield: start from signals with Prologue
Most teams are not starting from nothing. The expensive step in moving an existing system to this way of working is the event model: someone has to read years of accreted code and write down what the system does.
[Prologue](/prologue/) adds observed evidence to that step. It stands beside the running system, without application code changes, and watches selected signals: state-changing HTTP requests, which tables and columns changed after each one, and the telemetry the system already emits. It correlates these into captures, and the Interpreter proposes modules, features and slices with candidate commands, events, read models and projections. It produces a structured result and a draft [Screenplay](/screenplay/) file.
Treat the output as another input to human modeling, not a recovered specification. Prologue records no database row values and no HTTP request or response bodies, but endpoint paths, changed column names and telemetry attributes can still be sensitive, so minimize access and protect the capture files. It cannot see a rule the system enforces without leaving a trace. It is the wrong tool for a greenfield system, for a system with no HTTP or database write path to observe, and for data migration. See [why Prologue](/prologue/why-prologue/) for the full list.
Prologue is experimental.
## What is still experimental
Maturity labels below follow the [capability map](/cratis-stack/). A label says how far the product has come, not how well it works for you.
| Part | Role in this practice | Maturity |
| --- | --- | --- |
| Chronicle, Arc, Specifications | Event store, application framework and spec library | Released |
| Chronicle MCP server | Agent access to a running store, read-only by default | Released |
| CLI | Inspection, diagnosis and `cratis ai install` | Released |
| Cratis AI | Rules, skills, prompts and agents installed into a repository | Preview |
| Studio | Collaborative design of Screenplay event models | Preview, paid product |
| Screenplay | The `.play` modeling language | Experimental |
| Stage | Compiles Screenplay, verifies modeled specifications, renders an application | Experimental |
| Prologue | Proposes an event model from an existing system | Experimental |
Three limits are worth stating plainly:
* **Stage is partial.** Its renderer is the priority path. Its runtime performs only the subset of modeled backend behavior it understands, and its specification verification is useful but does not prove every Screenplay construct has executable semantics.
* **Cratis AI is unpinned.** `cratis ai install` reads the current `main` of the Cratis AI repository. To fix a revision, point `--source` at a checkout of a known commit.
* **Agents still make mistakes.** The mechanisms on this page make mistakes visible and cheap to catch. They do not make an agent correct. Keep the review step, and keep the specs honest.
The [roadmap](/roadmap/) and [what’s new](/whats-new/) track movement on these.
## Frequently asked questions
### What does “agentic development” mean here?
It means an AI agent does a meaningful share of the work: reading the codebase, writing changes, running the checks and diagnosing a running system, with a person setting direction and reviewing. On this page it is paired with event sourcing because the log, the commands and the specifications give the agent explicit things to read and verify.
### Why does event sourcing help AI coding agents?
It makes state, history and behavior explicit. The log records every change as a named fact, so the agent can inspect why state is what it is. Commands name every way the system can change. Specifications give an executable definition of correct behavior. An agent working against explicit contracts has less to guess.
### Do I have to use Cratis to do this?
No. The practice does not depend on one product: a typed command model, an append-only log and executable specifications work with other tools. Cratis supplies one integrated set, from the event store and framework to the rules, specifications and MCP server. For a comparison of event stores, see [event sourcing in .NET](/compare-event-sourcing-dotnet/) and [on the JVM](/compare-event-sourcing-jvm/).
### Which coding agents does Cratis AI support?
The managed install targets Claude Code, Codex, GitHub Copilot, Cursor, OpenCode and Pi. Marketplace plugins cover Claude Code, Codex, GitHub Copilot and Cursor with skills only. The [harness guide](/ai/harnesses/) has the command for each.
### Can an agent change my production data through the MCP server?
Not by default. The server starts read-only and exposes no tool that changes state. The `Mutation` profile adds job control (stop, resume, delete) behind an explicit, time-limited policy. Application state changes only through commands and events, never through the MCP server.
### Does the MCP server work with my Chronicle client language?
Yes. It connects to the Chronicle server over gRPC, so it works with the .NET, TypeScript, Kotlin/Java and Elixir clients alike. A running Chronicle server is required.
### Where do I start?
Install the rules and skills with `cratis ai install`, connect the MCP server or run `cratis init`, and build one slice by following [build a full-stack feature](/build-a-full-app/). Then read the [Decisions that hold](/decisions-that-hold-walkthrough/) walkthrough to see a rule enforced from model to specification to frontend. [Start using Cratis AI](/ai/getting-started/) covers the first step in detail.
### How do I stop an agent from writing specs that pass but assert the wrong thing?
Treat the specification diff as the main review artifact, and derive specs from the event model. A spec should read as a sentence a domain expert accepts. If the agent changed both a rule and the spec that checked it in one commit, that is the change to read first.
### Is this ready for production?
Chronicle, Arc, Specifications, the CLI and the MCP server are released. See [production readiness](/production-readiness/) for what to check before you depend on them. The model-first layer (Screenplay, Stage, Prologue) is experimental and is best used to explore and draft, with review, rather than as an unattended pipeline.
## Where to go next
* [AI-native development](/ai/): the full tour of Cratis AI, the CLI and the MCP server.
* [Chronicle MCP server](/chronicle-mcp/): tools, configuration and deployment profiles.
* [Testing with Cratis](/testing-with-cratis/) and [Specifications](/specifications/): the specification model in detail.
* [Event modeling](/event-modeling/): the model the agent and the team share.
* [The Cratis stack](/cratis-stack/): the products and a capability map with maturity labels.
# AI-native development
> Give your coding agent Cratis conventions through Cratis AI rules and skills, and let it inspect a running Chronicle store through the CLI and the Chronicle MCP server.
The Cratis Stack
## Build and operate with AI agents
Give your coding agent the same conventions, framework boundaries, and checks your team works to. Install the Cratis AI rules and skills into a repository with the Cratis CLI, or add the skills to one tool through its plugin. Then connect the agent to a running Chronicle store through the CLI and the Chronicle MCP server.
For the case behind this approach — why explicit events, commands and specifications make agent-written changes verifiable — read [Agentic development with event sourcing](/agentic-development/).
## Why “AI-native”
An assistant is only as good as what it knows about your framework and your system. Drop a general-purpose agent into a Cratis codebase and it guesses: it invents handler classes, misses the vertical-slice conventions, and hand-writes the API client that proxy generation already produces. Point it at a running event store and it has no idea how to read the log or find a failed partition.
Cratis addresses both halves. On the build side, Cratis AI gives the agent the conventions. On the operate side, the CLI and the Chronicle MCP server give it a documented way into the store.
## Build with AI: skills that know the Cratis way
**Cratis AI** is a shared corpus of rules, skills, prompts, and subagents that teaches an assistant the conventions: vertical slices, `[Command]` with `Handle()` on the record, model-bound projections, and `ConceptAs` instead of raw primitives. It is authored in [Cratis/AI](https://github.com/Cratis/AI) and reaches you through one of three channels:
| Channel | What you get | Versioning |
| --- | --- | --- |
| `cratis ai install` | Rules, skills, prompts, agents, and hooks for every selected coding tool, filtered by profile and committed to your repository | Takes the current `main`; `--source` reads a checkout of a known commit |
| `@cratis/pi` on npm | Skills, rules, prompts, agents, and hooks inside Pi | Versioned per release |
| `cratis` marketplace plugin for Claude Code, Codex, GitHub Copilot, or Cursor | Skills only | Follows the default branch |
A few of what you get, by name:
* **`new-vertical-slice`** (prompt) scaffolds a whole feature end to end: command, events, projection, query, React, and specs.
* **`cratis-arc-command`**, **`cratis-chronicle-read-model`**, **`cratis-chronicle-projection`**, **`cratis-chronicle-reactor`**, and **`cratis-fundamentals-concept`** (skills) build one artifact by convention. The `add-projection`, `add-reactor`, and `add-concept` prompts start the same work as a slash command.
* **`scaffold-feature`** and **`write-specs`** (prompts) and **`cratis-code-review`** (skill) set up a feature folder, cover it with BDD specs, and review the result against the project’s standards.
Prompts come with the managed install and `@cratis/pi`; the marketplace plugin has the skills only. Because the skills encode the conventions, an agent that uses them produces slices that look like the rest of your codebase instead of a layered approximation of it.
Start with [Start using Cratis AI](/ai/getting-started/). The [agent harness guide](/ai/harnesses/) has the command for each coding tool, [profiles](/ai/profiles/) explains how to choose the guidance a repository needs, and the [scenarios](/ai/scenarios/solo-developer/) cover solo, team, and multi-tool setups. The [code-analysis rules](/code-analysis/) enforce the same framework contracts during the build, whether or not an agent wrote the code.
Model first, then build
You can pair this with [Studio](/studio/) or [Screenplay](/screenplay/): model the feature, then let an agent build the slices around it. Studio is in Preview; review generated output from Stage, which remains experimental.
## Operate with AI: teach your assistant your store
Building is half the loop. The other half is *operating* what you built. Cratis gives an assistant two ways into a running store.
### `cratis init` — the CLI, made AI-aware
Run it once inside your project:
```bash
cratis init
```
It writes a **`CHRONICLE.md`** describing every command the CLI can run, adds instruction files for the AI tools it detects (Claude Code, GitHub Copilot, Cursor, Windsurf, and Pi), and adds a **`chronicle-diagnose`** slash command. Detection looks at the project’s files and at environment variables each tool sets. If it detects none, it configures none; pass `--tool ` to target one.
From then on your assistant can browse events, watch observers, and diagnose a failed partition through the [CLI](/cli/), because the command catalog is in its context. After a CLI upgrade, refresh the catalog with `cratis init --refresh`. The [CLI getting started](/cli/getting-started/) guide covers it in full.
### The Chronicle MCP server — an agent, connected to the store
For tools that speak the **Model Context Protocol**, Cratis publishes the [Chronicle MCP server](/chronicle-mcp/) as the `cratis/chronicle-mcp` container image. It connects straight to a running Chronicle server. The setup depends on your MCP host; [Chronicle MCP getting started](/chronicle-mcp/getting-started/) has a working configuration and [configuration](/chronicle-mcp/configuration/) covers connection strings and credentials.
Local defaults
Without configuration, the server uses Chronicle’s development client credentials and `localhost:35000`. Those defaults are for a local development store only. Give it real credentials, or an API key, before you point it at anything shared.
Once connected, your assistant can:
* **Explore**: list event stores, namespaces, event types, projections, and read models; read events and the tail sequence number; look at read model instances.
* **Check observer health**: list observers and failed partitions, get one observer’s state, and list the recommendations the store has raised.
* **Manage jobs**: list jobs and their steps, and stop, resume, or delete a job.
* **Design against real data**: describe the system and its event types, build an event catalog, find event types nothing consumes, trace why an event source ended up in its state, run an ad-hoc projection, suggest next event types, and scaffold a read model.
To replay an observer or recover a failed partition, use the [CLI](/cli/chronicle/) or the Workbench after the agent has helped you diagnose the cause.
Operate, not change history
The MCP server and the CLI inspect and operate the store: they read the log and manage observers and jobs. To change application state you still go through commands and events, so history stays honest.
## The whole loop, AI-accelerated
```
Operate with AI
Build with AI
scaffold slices
run it
inspect, diagnose, replay
browse, check health, manage jobs
Cratis AI
rules · skills · prompts
cratis init
CLI command catalog
Chronicle MCP server
connected to the store
Your Cratis app
Running Chronicle store
```
## Where to go next
[ Get started with AI](/ai/getting-started/)
[Install Cratis AI into a repository and confirm your agent sees it.](/ai/getting-started/)
[ Ecosystem support](/ai/ecosystems/)
[Which coding tools get what, through the managed install and each plugin.](/ai/ecosystems/)
[ Distribution](/ai/trust-and-distribution/)
[The three channels, how each is versioned, and how your own guidance stays yours.](/ai/trust-and-distribution/)
[ The Cratis Stack](/cratis-stack/)
[How design, build, and operate fit together end to end.](/cratis-stack/)
[ CLI getting started](/cli/getting-started/)
[Install the CLI, connect to your store, and run `cratis init` to set up AI tooling.](/cli/getting-started/)
[ Chronicle MCP](/chronicle-mcp/)
[The operate-side and design-time tools an agent gets against a live store.](/chronicle-mcp/)
[ Skills and plugins](/plugins/)
[What the corpus contains, and which channel delivers rules, skills, prompts, and hooks.](/plugins/)
[ Code analysis](/code-analysis/)
[The Roslyn analyzers and ESLint rules that enforce the same conventions during the build.](/code-analysis/)
[ Vertical slices](/arc/vertical-slices/)
[The convention the build-side skills follow: everything for a feature in one folder.](/arc/vertical-slices/)
# Cratis AI concepts
> The terms used across the Cratis AI pages, including corpus, profile, rule, skill, prompt, managed manifest, harness adapter, and native plugin.
The Cratis AI pages use a small set of terms. This page defines them in one place.
| Concept | Meaning |
| --- | --- |
| **Corpus** | The shared rules, skills, prompts, agents, hooks, and harness assets. It is authored in [Cratis/AI](https://github.com/Cratis/AI) and installed into a repository under `.cratis/ai`. |
| **Configuration** | `.cratis/ai.json`, the repository’s own record of which harnesses, profiles, and languages it selected. It has no version field. |
| **Profile** | A named selection. `cratis/application/*` profiles are for building applications on Cratis, `cratis/engineering/*` for contributing to the framework, `cratis/documentation` for documentation. Product profiles such as `cratis/arc` select skills for one product. See [Choose Cratis AI profiles](/ai/profiles/). |
| **Rule** | Standing guidance the harness loads without being asked. Some rules apply to every file; others are scoped to paths such as `**/*.cs`. |
| **Skill** | A folder with a `SKILL.md` and optional references that the agent loads when a task matches. Cratis skill names start with `cratis-`, for example `cratis-arc-command`. |
| **Prompt** | A slash command that starts a workflow, such as `new-vertical-slice` or `add-projection`. |
| **Agent** | A subagent definition, such as a spec writer or a security reviewer, that a harness with subagent support can delegate to. |
| **Managed manifest** | `.cratis/ai.manifest.json`, which records the corpus source revision and every Cratis-installed file and harness integration with its hash. |
| **Harness** | The coding tool that runs the agent: Claude Code, Codex, GitHub Copilot, Cursor, OpenCode, or Pi. |
| **Harness adapter** | A native path such as `.claude/skills`, `.cursor/rules`, `.pi/extensions`, or `AGENTS.md` that points into `.cratis/ai`, usually as a symbolic link. |
| **Native plugin** | A single-harness install that bypasses the CLI: the `cratis` marketplace plugin (skills only) or the `@cratis/pi` npm package. |
The repository owns its configuration and every file it adds. The installer owns only the paths listed in the managed manifest. A file it did not install is never claimed or overwritten, even with `--force`; if such a file is in the way, the run reports a conflict and changes nothing.
Use [Start using Cratis AI](/ai/getting-started/) for the managed workflow and the [agent harness guide](/ai/harnesses/) to compare native plugins.
# Use AI as a Cratis maintainer
> Work on a Cratis framework or product repository with an AI agent, using the engineering profiles, the repository's own context, fresh verification, and reviewed pull requests.
You maintain a Cratis repository (Arc, Chronicle, a client SDK, Components, the CLI, this documentation) and want an agent to do real work in it. The agent should not guess how the framework works; the repository already carries the instructions it needs. Your part is to start it in the right place, state the outcome, and insist on fresh verification before anything ships.
## Work from the repository root
1. **Start the assistant in the repository you intend to change**
The root `AGENTS.md` (or `CLAUDE.md`) links to the repository’s own instructions and the installed Cratis rules. Profile selection matters: application vertical-slice rules do not apply to Chronicle, Arc, Components, or client-library source. A framework repository installs a `cratis/engineering/` profile, for example:
```bash
cratis ai install \
--harnesses claude,codex,copilot,cursor,opencode,pi \
--profiles cratis/engineering/csharp,cratis/engineering/typescript,cratis/documentation \
--languages csharp,typescript
```
Add a product profile such as `cratis/arc` or `cratis/chronicle` when you want that product’s skills as well. [Choose Cratis AI profiles](/ai/profiles/) explains the split.
2. **Read project-owned context**
Project-specific instructions live in `.cratis/ai/rules/project.md` and the concern files it lists. Some repositories still carry a legacy `.cratis/PROJECT.md`; the installer can split it into the new layout. Keep that context in the repository you are working in. Never replace, concatenate, or copy another repository’s managed AI corpus.
3. **Ask for the outcome, not an implementation pattern**
Good requests name the behavior and the constraints:
* “Model the registration flow before implementation.”
* “Add a strongly typed invoice number.”
* “Diagnose why this projection stopped updating.”
* “Review this change for Cratis architecture and security.”
A matching skill guides the workflow. If you know the skill or prompt, name it through your harness’s skill picker or slash command.
4. **Let the repository answer routine questions**
The assistant should inspect local rules, source, generated patterns, and specs before interrupting you. It should stop for product choices, missing authority, credentials, risky production actions, or evidence that genuinely conflicts.
5. **Require fresh gates**
Compilation is not evidence of behavior. Run the repository’s current build, specs, lint, generated-file, and documentation checks, and run the failing check again after a fix.
6. **Ship through a reviewed pull request**
Use logical commits, green CI, and the repository’s merge policy. When the work creates a new repository in the Cratis organization, prepare a Strategy intake proposal with the facts Strategy needs (purpose, owner, lifecycle, dependencies, release and security expectations). The proposal is a draft for a maintainer to route; an agent does not open, comment on, or link `Cratis/Strategy` issues on its own.
## Common maintainer workflows
Skills are installed when a selected profile includes them; the marketplace plugins carry all of them. Prompts are installed with every managed profile.
| Intent | Skill or prompt |
| --- | --- |
| Design commands, events, streams, views, or reactions before building | `cratis-chronicle-event-modeling`, or `cratis-screenplay-event-modeling` for a `.play` model |
| Build a complete application behavior | `new-vertical-slice` prompt |
| Add a command, read model, projection, reactor, reducer, or concept | `cratis-arc-command`, `cratis-chronicle-read-model`, `cratis-chronicle-projection`, `cratis-chronicle-reactor`, `cratis-chronicle-reducer`, `cratis-fundamentals-concept`; or the `add-projection`, `add-reactor`, `add-reducer`, `add-concept` prompts |
| Diagnose a stale read model or a stopped reactor | `cratis-chronicle-projection`, `cratis-chronicle-reactor` |
| Inspect a running Chronicle store | `cratis-chronicle-cli-operations`, `cratis-chronicle-mcp-inspection`, `cratis-cli-terminal-workbench` |
| Review correctness, architecture, security, or performance | `cratis-code-review`, `cratis-security-review`, `cratis-performance-review`; or the `code-review` and `review-pr` prompts |
| Write product or site documentation | `cratis-documentation-writing`, `cratis-engineering-docs-authoring`, `cratis-technical-examples` |
| Record an architectural decision | `cratis-engineering-decision-record` |
| Commit, push, open, monitor, and merge a pull request | `ship-changes` prompt |
Skills guide the work; analyzers still enforce contracts
The AI workflow does not replace Arc or Chronicle analyzers, compiler errors, specifications, CI, or human review. It helps an agent reach the same gates a maintainer would use.
## Do not propagate the corpus
Do **not** copy shared `.cratis/ai`, `.claude`, `.github`, `.agents`, or `.pi` trees from one repository to another. Copying created mixed versions, unclear ownership, and changes that fanned out by accident, so it is no longer done.
Shared guidance reaches a repository through one of three channels instead:
1. `cratis ai install` and `cratis ai update`, which copy the current `main` of `Cratis/AI` (or a `--source` checkout) and record the source commit and file hashes;
2. the `@cratis/pi` npm package, versioned per release; or
3. the native marketplace plugins, which carry skills only.
Change shared guidance in `Cratis/AI` through a reviewed pull request, then update consuming repositories through their channel. Roll back by reinstalling from a known-good commit with `--source`, or by pinning an earlier `@cratis/pi` version. [Cratis AI distribution](/ai/trust-and-distribution/) describes the channels in full.
Some repositories still carry repository-local AI files from before the managed install. Leave them in place unless a reviewed change retires them, and do not restart copying between repositories.
## Keep shared and project knowledge separate
Shared skills own Cratis conventions and reusable workflows. The consuming repository owns architecture decisions, commands, environments, how credentials are obtained, fixtures, and verification details. Keeping them apart lets the shared corpus update without overwriting the facts that make one project different from another. Remove private or project-specific facts before you propose a reusable improvement to `Cratis/AI`.
Read [Cratis AI distribution](/ai/trust-and-distribution/) for the ownership rules and [Start using Cratis AI](/ai/getting-started/) for the setup steps.
# AI ecosystem support
> Which Cratis AI content reaches Claude Code, Codex, GitHub Copilot, Cursor, OpenCode, and Pi through the managed install and through each native plugin.
Cratis authors one corpus and exposes it through each harness’s native folders. The adapter changes how a harness discovers the content, not what the rule or skill says. What a harness receives depends on the channel you install through.
## Support matrix
| Harness | Managed install (`cratis ai install`) | Native channel | Native channel content |
| --- | --- | --- | --- |
| Claude Code | `CLAUDE.md`, `.claude` | `cratis` marketplace plugin | Skills |
| OpenAI Codex | `AGENTS.md`, `.agents/skills` | `cratis` marketplace plugin | Skills |
| GitHub Copilot | `.github` | `cratis` marketplace plugin | Skills |
| Cursor | `.cursor` | `cratis` marketplace plugin | Skills |
| OpenCode | `AGENTS.md`, `.opencode` | None | Use the managed install |
| Pi | `AGENTS.md`, `.pi` | `@cratis/pi` on npm | Skills, rules, prompts, agents, subagent tool, quality hooks |
The managed install gives each selected harness the integrations it supports (rules, skills, prompts, and agents; Codex gets only `AGENTS.md` and skills) from one committed copy, filtered by profile and language. It is the recommended path for a repository that more than one tool or person works in. The [agent harness guide](/ai/harnesses/) lists exactly what it creates for each harness.
Harnesses not in this table are not supported by the CLI or the marketplace manifests. A tool that reads `AGENTS.md` may still pick up the root instructions, but Cratis does not test or document that.
## Versions
| Channel | Where it reads from | Can you pin it? |
| --- | --- | --- |
| Managed install | Current `main` of `Cratis/AI`, or a local checkout with `--source` | Only by pointing `--source` at a checkout of a known commit |
| Marketplace plugins | Current default branch of `Cratis/AI` | No |
| `@cratis/pi` | Published npm releases | Yes, by package version |
[Cratis AI distribution](/ai/trust-and-distribution/) explains the ownership and update rules behind these channels.
## Quality
The `Cratis/AI` repository checks corpus structure, profile composition, the harness adapters, and the Pi package in CI, and loads the corpus through Pi’s resource loader to prove the skills, prompts, and extensions are discovered. Per-skill checks are static: they confirm that a skill contains required guidance. They do not run a model against the skill, compile its examples, or measure how well an agent performs with it.
# Start using Cratis AI
> Install Cratis rules, skills, agents, and prompts into a repository with the Cratis CLI, or add the skills to a single coding tool through its native plugin.
You want your coding agent to write Cratis code the way the framework expects: commands with `Handle()` on the record, model-bound projections, `ConceptAs` instead of raw primitives. Cratis AI is the shared guidance that teaches it that. This page gets it into a repository and shows how to tell it worked.
The guidance comes in four kinds. **Rules** are standing instructions; some load for every file, others only for matching paths such as `**/*.cs`. **Skills** are focused task workflows the agent loads when a task matches. **Prompts** are slash commands such as `new-vertical-slice`, and **agents** are subagent definitions.
## Pick a channel
| You want | Use | What you get |
| --- | --- | --- |
| One consistent setup for a repository, its team, and several coding tools | `cratis ai install` (this page) | Rules, skills, prompts, agents, and hooks, filtered by profile and committed to the repository |
| Cratis skills in one tool on your own machine | The tool’s [native plugin](/ai/harnesses/#claude-code-plugin) | Skills only |
| Cratis guidance in Pi, pinned to a package version | [`@cratis/pi`](/ai/harnesses/#pi-package) | Skills, rules, prompts, agents, and quality hooks |
[Cratis AI distribution](/ai/trust-and-distribution/) explains how the three channels are versioned and why the CLI path is the only one that manages a shared repository copy.
## Complete repository setup
**Prerequisites:** the [Cratis CLI](/cli/getting-started/), and `git` with network access to GitHub. The CLI clones the current `main` branch of [Cratis/AI](https://github.com/Cratis/AI) on each install and update, unless you point it at a local checkout with `--source`.
1. From the repository root, preview what would be written:
```bash
cratis ai install --dry-run
```
In a terminal, the CLI asks you to pick harnesses (your coding tools), profiles, and languages. The choices come from the corpus’s own `.cratis/ai/manifest.json`, so they always match what is published.
2. Run the same command without `--dry-run`. In a script, CI, or an agent’s terminal there is no prompt, so pass the selection explicitly:
```bash
cratis ai install \
--harnesses claude,codex,copilot,cursor,opencode,pi \
--profiles cratis/application/csharp,cratis/documentation \
--languages csharp,typescript
```
[Choose Cratis AI profiles](/ai/profiles/) explains which profile fits your repository. Name your languages: rules scoped to C# or TypeScript files are installed only for the languages you list.
3. Check the result:
```bash
cratis ai status
```
It prints the selected profiles, harnesses, and languages, the corpus revision installed, and whether any managed file was changed locally.
4. Commit `.cratis/ai.json`, `.cratis/ai.manifest.json`, the `.cratis/ai/` tree, and the harness adapters (`AGENTS.md`, `CLAUDE.md`, `.claude/`, `.pi/` and so on). Everyone on the team and every CI run then reads the same guidance.
To confirm the agent actually sees it, start your coding tool from the repository root and ask it to list the Cratis skills or prompts it can use. The names start with `cratis-`, for example `cratis-arc-command` and `cratis-chronicle-projection`.
Scaffolded from a template?
The Cratis `dotnet new` templates already contain a `.cratis/ai.json`. Skip `install` and run `cratis ai update` once to download the guidance it selects. `cratis new` runs that step for you.
### What the installer writes
The CLI copies the selected corpus into `.cratis/ai/`, records each installed file and its hash in `.cratis/ai.manifest.json`, and creates native adapters for each selected harness. Most adapters are symbolic links into `.cratis/ai/`, so every tool reads the same copy. The [agent harness guide](/ai/harnesses/) lists what each harness receives.
Pi is configured directly from the managed copy. The managed `cratis-rules` extension puts the universal rules from `.cratis/ai/rules` into the system prompt, and `cratis-path-guidance` attaches path-scoped rules and skill hints the first time a matching file is touched. You do not need the `@cratis/pi` package as well.
Where your own files can go
The installer never overwrites a file it did not install. If one of your files sits where a Cratis file would go, the run reports a conflict and changes nothing, even with `--force`. Rename your file or move it.
Adapter folders such as `.claude/skills` are links into `.cratis/ai/`. A file you create inside a linked folder lands in the shared managed tree and is seen by every harness. Keep tool-private files outside the linked folders.
Selecting a Screenplay profile also writes a managed `screenplay` MCP entry into each harness’s MCP configuration, including files you already own, alongside your own entries. Set `mcpServers.screenplay.enabled` to `false` in `.cratis/ai.json` to disable it. The [agent harness guide](/ai/harnesses/) lists the files.
## Share project-specific guidance
Put guidance that belongs to this repository alone in `.cratis/ai/rules/project.md`. When it covers several concerns, keep that file a short index and give each concern its own file:
```text
.cratis/ai/rules/
├── project.md
└── project/
├── build-and-test.md
├── architecture.md
├── security.md
└── deployment.md
```
`project.md` tells harnesses that read one context file to read every concern. Native rule adapters and Pi’s rules extension discover the concern files directly. The CLI creates `AGENTS.md` and `CLAUDE.md` as links to `project.md` when those paths are free.
If `AGENTS.md` or `CLAUDE.md` already exists as your own file, the CLI leaves it alone. Your existing file then does not mention the managed rules; add a line that tells the agent to read `.cratis/ai/rules/project.md`.
### Moving from a legacy project file
The installer detects `.cratis/PROJECT.md`, `.ai/PROJECT.md`, and `.agents/PROJECT.md`. It copies their headings into `project.md` and one concern file per heading, without deleting the original. Review the split, remove the legacy file and anything that points to it, and run `cratis ai update` again so the shared adapters are created.
## Maintain the installation
```bash
cratis ai status # read-only; exits non-zero if a managed file was edited
cratis ai update --dry-run # what an update would change
cratis ai update # take the current corpus for the recorded selection
cratis ai uninstall # remove unchanged Cratis-managed content and adapters
```
`update` re-reads `.cratis/ai.json` and takes whatever the corpus `main` branch holds now. There is no version pin on this channel. To change the selection, run `install` again with new options.
`update` and `uninstall` act only on files the manifest records as Cratis-managed. When one of those files was edited locally, both stop before changing anything and list the paths. Restore the file, or pass `--force` once you have decided the local edit can be replaced or removed.
Because `status` exits non-zero on local edits, you can run it in CI to catch drift in managed guidance. [Updates and rollback](/ai/scenarios/updates-and-rollback/) covers previewing, pinning with `--source`, and going back.
## Choose the integration that fits your repository
### Managed by Cratis
Use `cratis ai install` when several people or several coding tools work in the repository, or when you want the guidance reviewed and committed like code. It is the only channel that installs rules for every harness, filters by profile, and protects local edits on update and uninstall.
### Native plugin or marketplace
Use a native plugin when one developer wants Cratis skills in one tool without touching the repository. The Claude Code, Codex, GitHub Copilot, and Cursor marketplace plugins deliver the skills only, from the current `Cratis/AI` default branch. `@cratis/pi` also contributes rules, prompts, agents, a subagent tool, and quality hooks, and it is versioned on npm. Neither creates `.cratis/ai`, configures other harnesses, or protects local edits. The [agent harness guide](/ai/harnesses/) has the commands.
## Contribute and verify skills
Skills are authored and reviewed in [Cratis/AI](https://github.com/Cratis/AI). A skill can carry a `verification.json` with an example input and assertions that the skill contains the guidance that input needs. These checks are static: they do not run the input through a model, compile the skill’s examples, or judge its writing. Judging how an agent behaves with a skill still takes a real task and a review of the output.
Questions? Ask on Discord
Ask questions and get help from the Cratis team and other developers on the [Cratis Discord](https://discord.gg/kt4AMpV8WV). See [Community and help](/community/) for where else to go.
# Agent harness guide
> What the managed Cratis AI install creates for Claude Code, Codex, GitHub Copilot, Cursor, OpenCode, and Pi, and the commands for each native plugin.
A *harness* is the coding tool that runs the agent: Claude Code, Codex, GitHub Copilot, Cursor, OpenCode, or Pi. Each one looks for instructions and skills in its own folders. This page lists what Cratis puts in those folders, and how to install the native plugin when you only need one tool.
Cratis AI reaches a harness in one of two ways:
* **Managed repository setup.** `cratis ai install` resolves the repository’s profiles and languages, installs the shared corpus in `.cratis/ai`, and wires every selected harness to it. See [Start using Cratis AI](/ai/getting-started/).
* **Native plugin setup.** You install one harness’s plugin or package without the Cratis CLI. You get no shared repository copy, no multi-harness wiring, and no protection for local edits on update.
Use the managed path when a team wants the same guidance across tools. Use a native plugin when one developer wants Cratis skills in one tool.
## Managed harness support
```bash
cratis ai install \
--harnesses claude,codex,copilot,cursor,opencode,pi \
--profiles cratis/application/csharp \
--languages csharp,typescript
```
The accepted harness values come from the corpus’s `.cratis/ai/manifest.json`. The CLI creates adapters only for the harnesses you select:
| Harness | Managed project integration |
| --- | --- |
| Claude Code | `CLAUDE.md`, plus `.claude` rules, agents, commands, prompts, skills, and hook settings |
| OpenAI Codex | `AGENTS.md` and `.agents/skills` |
| GitHub Copilot | `.github/copilot-instructions.md`, plus `.github` instructions, agents, prompts, and skills |
| Cursor | `.cursor` rules, agents, commands, and skills |
| OpenCode | `AGENTS.md`, plus `.opencode` agents, commands, and skills |
| Pi | `AGENTS.md`, plus `.pi` extensions (rules loading, path guidance, Screenplay MCP, subagents, quality hooks), agents, prompts, and skills |
Most adapters are symbolic links into `.cratis/ai`, so no harness gets its own diverging copy. `AGENTS.md`, `CLAUDE.md`, and `.github/copilot-instructions.md` are created only when you do not already have your own file at that path. Run `cratis ai status` to see local edits to managed files, and `cratis ai uninstall` to remove the integrations the CLI created.
The Screenplay profile also registers an MCP server
When a selected profile resolves to `cratis/screenplay`, the CLI also adds a managed `screenplay` MCP server entry (`cratis screenplay mcp`) to each harness’s MCP configuration, next to your own entries: `.mcp.json` (Claude Code), `.vscode/mcp.json` (Copilot), `.cursor/mcp.json`, `.codex/config.toml` and `opencode.json`. Pi reaches the same server through its managed `cratis-mcp` extension. To turn it off, set `mcpServers.screenplay.enabled` to `false` in `.cratis/ai.json`.
Your harness folders remain yours
The installer refuses to overwrite a file it did not install. You can add your own files next to the Cratis links in any harness folder. A folder that is itself a link, such as `.claude/skills`, points into `.cratis/ai`: anything you put there becomes part of the shared tree that every harness reads. `--force` only replaces or removes files already recorded as Cratis-managed.
## Native plugins
The Claude Code, Codex, GitHub Copilot, and Cursor plugins come from the marketplace manifests in [Cratis/AI](https://github.com/Cratis/AI). They carry the **skills only**, read from the repository’s current default branch, so there is no version to pin. Rules, prompts, agents, and hooks are not included; use the managed install for those.
## Claude Code plugin
```text
/plugin marketplace add Cratis/AI
/plugin install cratis@cratis
```
Remove it with `/plugin uninstall cratis@cratis`.
## OpenAI Codex plugin
```bash
codex plugin marketplace add Cratis/AI
codex plugin add cratis@cratis
```
Remove it with `codex plugin remove cratis@cratis`.
## GitHub Copilot plugin
These commands are for the GitHub Copilot CLI:
```bash
copilot plugin marketplace add Cratis/AI
copilot plugin install cratis@cratis
```
## Cursor plugin
Add `Cratis/AI` in Cursor’s plugin settings. Cursor reads the committed `.cursor-plugin/marketplace.json` and installs the Cratis skills.
## Pi package
`@cratis/pi` is published to npm and versioned per release, which makes it the one native channel you can pin.
```bash
# Project-scoped (written to .pi/settings.json)
pi install -l npm:@cratis/pi
# User-wide
pi install npm:@cratis/pi
# Pinned to an exact version
pi install -l npm:@cratis/pi@
```
Pi treats a versioned npm source as pinned: `pi update` does not move it. Find the published versions with `npm view @cratis/pi versions`.
The package reads `.cratis/ai.json` when present and exposes the skills for the selected profiles and languages. Without that file, it exposes every packaged skill. Unlike the marketplace plugins, it also contributes rules, prompts, agents, a subagent tool, and Cratis quality hooks.
It does not create `.cratis/ai`, configure other harnesses, or protect local edits. When the repository already has a managed install (`.cratis/ai.manifest.json` exists), the package steps aside so the same rules and hooks are not registered twice.
## OpenCode without the CLI
There is no Cratis OpenCode plugin or marketplace entry. OpenCode reads `AGENTS.md` and `.opencode/agents`, `.opencode/commands`, and `.opencode/skills` from the project, so to get Cratis content into OpenCode, use the managed install with `--harnesses opencode`. You can add your own content to those folders either way.
# Choose Cratis AI profiles
> Pick the Cratis AI profiles and languages that match what your repository builds, so the agent gets application, framework, or documentation guidance and not the wrong set.
A profile decides which Cratis guidance a repository receives. The choice that matters is *what the repository builds*: an application on Cratis, part of the Cratis framework itself, or documentation. Get that wrong and the agent follows rules written for a different kind of project.
## Choose the profile
| Repository | Profile |
| --- | --- |
| .NET application on Arc and Chronicle, with a React frontend | `cratis/application/csharp` |
| .NET application on Arc without Chronicle | `cratis/application/arc-only` |
| Application on the Kotlin clients | `cratis/application/kotlin` |
| Application on the TypeScript Chronicle client | `cratis/application/typescript` |
| A Cratis framework repository (library, kernel, client SDK) | `cratis/engineering/`, for example `cratis/engineering/csharp` |
| Any repository with documentation | add `cratis/documentation` |
`cratis/application` composes every application language profile. Prefer the language-specific one unless the repository really spans them.
Only a `cratis/application*` profile brings in the application rules: vertical slices, model-bound commands and read models, the MVVM frontend. Only a `cratis/engineering*` profile brings in the framework-contributor rules. The product profiles, such as `cratis/arc` or `cratis/chronicle`, select skills for that product but deliver neither rule set on their own.
The corpus’s `.cratis/ai/manifest.json` is the authoritative list of the profiles, languages, and harnesses `cratis ai install` accepts. An unknown value fails validation with the rejected value. Use the profile table above and the [CLI reference](/cli/ai/) to choose a supported selection.
## Configure the repository
1. Run the installer. In a terminal it prompts for each choice:
```bash
cratis ai install
```
2. Or pass every selection, which is required in scripts, CI, and agent terminals:
```bash
cratis ai install \
--harnesses claude,cursor,opencode,pi \
--profiles cratis/application/typescript \
--languages typescript
```
3. Review and commit `.cratis/ai.json`:
```json
{
"schemaVersion": "1.0",
"harnesses": ["claude", "cursor", "opencode", "pi"],
"profiles": ["cratis/application/typescript"],
"languages": ["typescript"]
}
```
The CLI expands composed profiles, keeps the language-specific parts that match your languages, and installs the matching skills. Agents, prompts, and hooks are always installed. Rules are filtered by profile and by language.
Always list your languages
`--languages` narrows the skills a profile composes, and omitting it leaves the skills unconstrained. Rules behave differently: a rule scoped to C# or TypeScript files is installed only when that language is listed. Name every language the repository uses.
Configuration is not ownership
`.cratis/ai.json` records what your repository asked for. There is no version field: the corpus version is whatever the CLI read at install or update time. `.cratis/ai.manifest.json` records which installed files and integrations Cratis owns, with their hashes. A file you create is never claimed because it lives in a managed or harness folder.
## Update or change a selection
To change products, languages, or harnesses, run `install` again with the new options. To pick up a newer corpus for the selection you already have, run:
```bash
cratis ai update
```
The update adds newly selected content, removes Cratis-owned content that is no longer selected, and reconnects the harness links. It stops before changing anything if a managed file was edited locally. Review that edit before you use `cratis ai update --force`.
## Use a native plugin instead
A native plugin is independent of the managed path. It suits one person using one harness. The Claude Code, Codex, GitHub Copilot, and Cursor plugins deliver every Cratis skill and do not read profiles. The Pi package does:
```bash
pi install -l npm:@cratis/pi
```
`@cratis/pi` reads `.cratis/ai.json` when it exists and exposes the skills for those profiles and languages; without it, the package exposes all skills. Neither kind of plugin creates the shared `.cratis/ai` corpus, configures other harnesses, or protects local edits.
# Scenario: Cratis maintainer
> Configure a Cratis framework or product repository with the engineering profiles and its native agent harness folders.
You contribute to a Cratis framework or product repository instead of building an application on it. The framework-contributor rules apply; the application vertical-slice rules do not.
## Configure the repository
Use the managed installer with a `cratis/engineering/` profile for each language the repository contains. Add `cratis/documentation` when it has documentation, and a product profile when you also want that product’s skills:
```bash
cratis ai install \
--harnesses claude,codex,copilot,cursor,opencode,pi \
--profiles cratis/engineering/csharp,cratis/engineering/typescript,cratis/documentation,cratis/arc \
--languages csharp,typescript
```
A product profile such as `cratis/arc` on its own brings skills but neither the engineering nor the application rules. See [Choose Cratis AI profiles](/ai/profiles/).
Start the agent from the repository root so it reads `AGENTS.md`, the installed Cratis rules, and the repository’s own guidance.
## Keep repository guidance local
Put product-specific or confidential guidance in the owning repository, in `.cratis/ai/rules/project.md` and its concern files. Add your own skills under `.cratis/ai/skills/` with names that do not start with `cratis-`. The managed manifest records only the files Cratis installed, so your additions survive update and uninstall.
Generalize reusable guidance before proposing it to `Cratis/AI`, and leave private product facts out of the shared corpus.
## Use a native plugin for one harness
A maintainer who works in one harness on their own machine can install the `cratis` marketplace plugin (skills only) or `@cratis/pi` instead. Neither installs the engineering rules into the repository for the rest of the team.
See [Use AI as a Cratis maintainer](/ai/cratis-maintainers/) for the complete workflow.
# Scenario: multiple harnesses
> Configure several AI coding tools from one managed Cratis AI corpus and one repository-owned selection.
Your team uses more than one coding tool in the same repository: some people in Claude Code, some in Cursor, CI in Codex. You want all of them to follow the same Cratis guidance. Use the managed install.
## Configure the repository
```bash
cratis ai install \
--harnesses claude,codex,copilot,cursor,opencode,pi \
--profiles cratis/application/csharp \
--languages csharp,typescript
```
Selecting a harness nobody uses only adds an unused adapter, so most repositories select all of them.
Commit `.cratis/ai.json`, `.cratis/ai.manifest.json`, the managed `.cratis/ai` content, and the harness adapters.
## What you get
* **One corpus.** Every adapter points into `.cratis/ai`; no harness gets a separately maintained copy.
* **One selection.** Profiles and languages are resolved once for all selected harnesses.
* **Native folders.** Each tool still reads `.claude`, `.cursor`, `.opencode`, `.pi`, or whichever folders it expects.
* **Protected edits.** `cratis ai update` stops before overwriting a locally edited managed file, and never touches files Cratis did not install.
Run `cratis ai status` before an update and `cratis ai uninstall` when you remove the managed integration.
## Native plugins remain optional
A developer can still install a marketplace plugin or `@cratis/pi` on their own machine without the CLI. That path covers one harness, and the marketplace plugins carry skills only. It does not configure the other harnesses or protect local edits.
# Scenario: solo developer
> One person, one coding tool. Install the Cratis AI plugin for your harness and start; nothing to add to your repository.
You work alone, in one coding tool, building on Cratis.
## What you do
Install the plugin for your harness. The [agent harness guide](/ai/harnesses/) has the exact command for each one. For Claude Code:
```text
/plugin marketplace add Cratis/AI
/plugin install cratis@cratis
```
That is the whole setup. There is no `.cratis/` directory to create and no configuration file to commit.
## What you get
Your assistant loads Cratis skills when a task matches one: writing a command, modeling an event flow, adding a projection, debugging a stale read model. The skills are Markdown instructions and references. The marketplace plugin adds no hooks, scripts, MCP server, or files in your project.
The plugin carries skills only. It does not install the always-on Cratis rules, the slash-command prompts such as `new-vertical-slice`, or the subagents. If you want those, run `cratis ai install` in the repository (see [Start using Cratis AI](/ai/getting-started/)); in Pi, `@cratis/pi` includes rules and prompts.
Your own configuration stays yours. Keep whatever you already have in `.claude/` (or your harness’s equivalent). The plugin does not touch it.
## Keeping current
The marketplace plugin reads the current default branch of `Cratis/AI`. There is no version to pin; updating means updating the plugin in your harness, and going back means uninstalling it. `@cratis/pi` is the exception: it is published to npm with version numbers. See [updates and rollback](/ai/scenarios/updates-and-rollback/).
## Things to avoid
Do not copy the `Cratis/AI` repository’s `.cratis/ai`, `.claude`, `.github`, or skill folders into your own repository by hand. The copy goes stale without warning. Use a plugin, or the managed install, which records what it installed and can update it.
# Scenario: team repository
> Commit one managed Cratis AI corpus and selection so everyone on the team, in every supported harness, sees the same guidance.
Several people work in one repository, and you want every one of them, and CI, to get the same Cratis guidance. Commit it with the code.
## What you do
Run `cratis ai install` in the repository root, select the harnesses, profiles, and languages the repository needs, then review and commit the result.
| Path | What it carries |
| --- | --- |
| `.cratis/ai.json` | The selected harnesses, profiles, and languages. |
| `.cratis/ai/` | Cratis-managed rules, skills, prompts, agents, and hooks, plus anything you add. |
| `.cratis/ai.manifest.json` | The source commit, and each managed file’s source identity and installed hash. |
| Harness adapters | Symbolic links or configuration entries pointing to `.cratis/ai`. |
A configuration can select more than one harness and profile:
```json
{
"schemaVersion": "1.0",
"harnesses": ["claude", "codex", "pi"],
"profiles": ["cratis/application/csharp", "cratis/documentation"],
"languages": ["csharp", "typescript"]
}
```
## What you get
* **One source.** Harness integrations point to `.cratis/ai`; they do not carry diverging skill folders.
* **Reviewable updates.** An update is a diff in a pull request. Run `cratis ai update --dry-run` first to see what will change.
* **Protected edits.** The manifest hashes every managed file. Update and uninstall stop before changing anything if one was edited locally.
* **Your own extensions.** Local rules and skills under `.cratis/ai` are never claimed, deleted, or overwritten by Cratis.
To catch hand edits to managed guidance in CI, run `cratis ai status`; it exits non-zero when a managed file was modified.
Resolving a modified managed file
Run `cratis ai status` to see which paths changed. Restore the file, or use `cratis ai update --force` or `cratis ai uninstall --force` only when you deliberately want to replace or remove that edited managed content. If the edit is an improvement others would want, propose it to `Cratis/AI`.
# Scenario: updates and rollback
> How Cratis AI guidance changes for each channel, how to preview an update, and how to get back to a known-good state.
An update to Cratis AI changes what your agent is told to do, so you want to see it coming and be able to undo it. How you do that depends on the channel you installed through.
| Channel | What an update takes | How to go back |
| --- | --- | --- |
| Managed install | The current `main` of `Cratis/AI` | Reinstall from a checkout of a known-good commit with `--source`, or revert the commit that updated it |
| `@cratis/pi` | A published npm version | Install the earlier version |
| Marketplace plugins | The current default branch of `Cratis/AI` | Uninstall the plugin |
## Managed install
`cratis ai update` is not pinned. It takes whatever the source holds when it runs. Preview first:
```bash
cratis ai status # installed revision, available revision, local edits
cratis ai update --dry-run # the actions an update would take, without writing
cratis ai update
```
Because the managed content is committed, an update arrives as a diff you can review in a pull request, run your gates against, and merge like any other change.
To stay on a specific revision, or to roll back after an update you do not want, point the CLI at a local checkout of that commit:
```bash
git clone https://github.com/Cratis/AI ../cratis-ai
git -C ../cratis-ai checkout
cratis ai update --source ../cratis-ai --dry-run
cratis ai update --source ../cratis-ai
```
The source commit of the current install is recorded in `.cratis/ai.manifest.json`. If the update is still an unmerged branch, discard it; if it was merged, reverting that commit restores the previous files as well.
Edited managed files block updates
If someone edited a managed file, `update` stops before changing anything. Decide whether the edit should be kept (and proposed to `Cratis/AI`) or replaced, then restore it or use `--force`.
## `@cratis/pi`
The Pi package is the one channel with real version numbers:
```bash
pi install -l npm:@cratis/pi # latest release
pi install -l npm:@cratis/pi@ # a specific release, pinned
npm view @cratis/pi versions # what has been published
```
To roll back, install the version you last trusted. A versioned source stays at that version until you change it.
## Marketplace plugins
The Claude Code, Codex, GitHub Copilot, and Cursor plugins read the current default branch of `Cratis/AI`. There is no version to pin. Updating means updating the plugin in your harness; the only way back is to uninstall it. If you need a fixed version, use the managed install with `--source` instead.
## Uninstalling
`cratis ai uninstall` removes unchanged Cratis-managed files and the adapters the CLI created. It never removes files you created yourself, such as your own `.cratis/ai` rules and skills or an `AGENTS.md` you wrote. Run it with `--dry-run` first to see the list.
See [Cratis AI distribution](/ai/trust-and-distribution/) for the channels and ownership rules.
# Cratis AI distribution
> How Cratis AI guidance reaches a repository through three channels, how each is versioned, and how a managed install keeps repository-owned guidance intact.
Cratis AI is authored in one place, the [Cratis/AI](https://github.com/Cratis/AI) repository, and reaches your repository or your coding tool through one of three channels. They differ in what they deliver, how you pin them, and who owns the files afterwards. This page explains those differences so you can pick a channel and know what an update will do.
## Three channels
| Channel | Delivers | Version semantics |
| --- | --- | --- |
| `cratis ai install` / `cratis ai update` | Rules (filtered by profile and language), skills (filtered by profile), prompts, agents, hooks, and harness adapters, copied into the repository | Unpinned. Each run clones the current `main` of `Cratis/AI`. `--source ` reads a local checkout instead, which is how you fix a known commit or work offline. |
| `@cratis/pi` (npm) | Skills, rules, prompts, agents, a subagent tool, and quality hooks, inside Pi | Versioned per release. Pin and roll back by package version. |
| Marketplace plugins (Claude Code, Codex, GitHub Copilot, Cursor) | Skills only | Unpinned. Read from the current default branch of `Cratis/AI`. |
There is no separately published, per-profile versioned package, and `.cratis/ai.json` has no version field. The managed install records the source commit it used in `.cratis/ai.manifest.json`, so you can always see which revision a repository has, and `cratis ai status` shows whether a newer one is available.
```
git clone, or --source checkout
left alone
marketplace manifests
Cratis/AI main
cratis ai install / update
.cratis/ai.json
selection
managed .cratis/ai content
.cratis/ai.manifest.json
source commit + hashes
harness adapters
mostly symlinks
your own .cratis/ai files
native plugins
skills only
npm release
@cratis/pi
```
## Selection, not a universal copy
A repository selects its harnesses, profiles, and languages:
```json
{
"schemaVersion": "1.0",
"harnesses": ["claude", "cursor", "opencode", "pi"],
"profiles": ["cratis/application/csharp", "cratis/documentation"],
"languages": ["csharp", "typescript"]
}
```
The CLI resolves that selection against the corpus it reads. The `@cratis/pi` package reads the same file and exposes the matching skills; when the file is absent, it exposes every packaged skill. The marketplace plugins ignore the file and deliver every skill.
## Ownership and synchronization
Cratis marks each managed text file with a `cratis-ai-managed` source identity and records source identities and installed hashes in `.cratis/ai.manifest.json`. The local corpus lives in `.cratis/ai`, and each harness adapter is a symbolic link or configuration entry that points there. On update:
* new and changed upstream files are synchronized;
* upstream files that moved or were removed are deleted only when their installed hash is unchanged;
* a locally edited managed file stops update or uninstall before anything changes;
* `--force` replaces or removes an edited managed file on purpose; and
* files Cratis did not install, including your own `.cratis/ai` content, are left intact. If one of them sits where a Cratis file would go, the run reports a conflict and writes nothing.
Use the lifecycle commands
Run `cratis ai status` before updating; it exits non-zero when a managed file was edited, so it also works as a CI check. Run `cratis ai update --dry-run` to see what an update would change. Run `cratis ai uninstall` to remove unchanged Cratis-owned content and the harness configuration the CLI created.
## Keep project knowledge in the repository
Shared guidance covers Cratis conventions and reusable workflows. Your repository owns the rest: architecture decisions, build commands, environments, approved secret mechanisms, fixtures. Put that in `.cratis/ai/rules/project.md` and its concern files, and add repository-specific skills under `.cratis/ai/skills/` with names that do not start with `cratis-`. No channel merges, overwrites, or removes them.
Never copy `.cratis/ai`, `.claude`, `.github`, `.agents`, or `.pi` trees from one repository into another, and never hand-edit managed files. A copied tree goes stale without anyone noticing, and a hand edit blocks the next update. Change shared guidance in `Cratis/AI` through a reviewed pull request, after removing anything private or project-specific.
## Quality is verification
Cratis does not gate corpus content on author identity, provenance records, or approval chains. The `Cratis/AI` CI checks corpus structure, profile composition, harness adapters, and the Pi package, and loads the corpus through Pi’s resource loader to prove skills, prompts, and extensions are discovered.
A skill can carry a `verification.json` with an example input and assertions that the skill contains the guidance that input needs. These are static checks. They do not run a model, compile the skill’s code examples, or prove the guidance leads an agent to good results. That still takes a real task and a review of what the agent produced.
## Plugins and marketplaces
Plugins are the quickest way to get Cratis skills into one tool. They are deliberately narrower than a repository install: the marketplace plugins carry no rules, no profile filtering, no ownership records, and no protection for local edits. Use a plugin for convenient skill access; use `cratis ai install` to configure a repository. [Updates and rollback](/ai/scenarios/updates-and-rollback/) shows how to preview, pin, and undo each channel.
# API reference
> Where to find the generated .NET and TypeScript API reference, and how it's produced.
Use this page to find an exact API signature when a guide does not cover your case. The generated references describe documented public APIs at the version built by the site; check your installed package version when a signature differs.
## .NET / C\#
The .NET API reference is generated with **DocFX** from the XML documentation comments across the Chronicle client SDK, Arc, and Fundamentals assemblies.
**[Browse the .NET API reference →](/api/)** — public types and members from the Chronicle clients, Arc (including Arc.Core and MongoDB), and Fundamentals, organized by namespace. Shared extension classes list their contributing assemblies; existing per-library type and member URLs redirect to the canonical pages.
XML comments also appear in IDE IntelliSense. Relevant NuGet packages include [`Cratis.Chronicle`](https://www.nuget.org/packages/Cratis.Chronicle), [`Cratis.Arc`](https://www.nuget.org/packages/Cratis.Arc), [`Cratis`](https://www.nuget.org/packages/Cratis), [`Cratis.Testing`](https://www.nuget.org/packages/Cratis.Testing), [`Cratis.Arc.MongoDB`](https://www.nuget.org/packages/Cratis.Arc.MongoDB), and [`Cratis.Specifications.XUnit`](https://www.nuget.org/packages/Cratis.Specifications.XUnit). Install only the packages your application needs.
## TypeScript
The TypeScript API reference is generated with **TypeDoc** from the `@cratis/*` packages and surfaced alongside the rest of the site:
* [`@cratis/arc`](/api/arc/javascript/arc/) — the Arc client core
* [`@cratis/arc.react`](/api/arc/javascript/arc.react/) — React hooks and bindings
* [`@cratis/arc.react.mvvm`](/api/arc/javascript/arc.react.mvvm/) — the MVVM layer
* [`@cratis/arc.vite`](/api/arc/javascript/arc.vite/) — Vite integration for metadata and queries; the .NET build generates proxies
* [`@cratis/fundamentals`](/api/fundamentals/javascript/) — shared utilities and concepts
## How it’s produced (for contributors)
We deliberately **combine tooling**: a modern site for the narrative docs, and the best generator for each kind of API reference. The reference is built in the documentation pipeline from the product source — there’s nothing to hand-maintain. See the documentation site’s `README.md` for the build details.
## When to reach for what
* **Learning or solving a problem?** Stay in the guides, [tutorial](/chronicle/tutorial/), and [scenarios](/chronicle/scenarios/) — they explain the *why* and the *how*.
* **Looking up an exact signature?** The API reference and your IDE’s IntelliSense are the fastest path.
# Arc
> Commands, queries, validation, authorization, and generated TypeScript clients for ASP.NET Core, Spring Boot, and the TypeScript source preview.
Arc
## CQRS application framework for ASP.NET Core, Spring Boot, and Node.js
Arc is an opinionated CQRS application framework with commands, queries, validation, authorization, and TypeScript proxy generation. Write the backend in C#, Kotlin or Java, or the TypeScript source preview; each can generate a typed frontend client. Event sourcing is not required.
## Start here
When commands, queries, validation, authorization, frontend contracts, and persistence each grow their own conventions, Arc gives them one application boundary. Start with current-state persistence or application services. Add Chronicle only where event sourcing fits, and add Components only where its React compositions fit.
[ Start an Arc host](#start-an-arc-host)
[Add Arc to an ASP.NET Core, Spring Boot, or TypeScript host.](#start-an-arc-host)
[ Commands and queries](#commands-and-queries)
[Execute changes, expose reads, and generate the client contracts for both.](#commands-and-queries)
[ Frontend contracts](#frontend-contracts)
[Use the generated TypeScript runtime directly or compose it with React.](#frontend-contracts)
[ Persistence choices](#persistence-choices)
[Keep current state in application services, MongoDB, or EF Core, or add Chronicle.](#persistence-choices)
[Why Arc ](/arc/why-arc/)The problem Arc solves, why it leans on CQRS and proxy generation, and how it keeps a full-stack app type-safe end to end.
[Coming from MediatR and MVC (C#) ](/arc/backend/csharp/coming-from-mediatr-and-mvc/)Map controllers, DTOs, and MediatR handlers onto Arc's command and query model.
[Coming from Spring MVC (Kotlin and Java) ](/arc/backend/kotlin/coming-from-spring-mvc/)Map @RestController handlers and DTOs onto Arc's command and query model.
[Samples ](/samples/)Runnable event sourcing and CQRS samples for the Cratis stack, including full Arc applications.
## Start an Arc host
Arc runs on ASP.NET Core and Spring Boot; a third implementation for TypeScript on Node.js is a [source preview](/arc/backend/typescript/) with no published npm package or full parity yet. Choose a backend below and check its capabilities before relying on a shared example. The shared [HTTP contract](/arc/http-contract/) is verified for C# and JVM; TypeScript’s coverage is documented separately.
[C# on ASP.NET Core ](/arc/backend/csharp/getting-started/)Install Cratis.Arc, configure the host, and reach the first command and query checkpoint. Arc.Core is a lighter alternative host.
[Kotlin or Java on Spring Boot ](/arc/backend/kotlin/get-started/)Apply the io.cratis.arc Gradle plugin, declare a command and a read model, and call them. A Java walkthrough sits alongside it.
[TypeScript source preview on Node.js ](/arc/backend/typescript/getting-started/)Build an Arc host from source; inspect the capability reference before using preview APIs.
Starting the host verifies the setup path only. Commands, queries, generated proxies, persistence integrations, and frontend packages each have additional configuration and version requirements.
## Commands and queries
Arc supports model-bound records and methods as well as controller-based endpoints. The command and query pipelines handle dependency injection, validation, authorization, filters, execution scopes, result conversion, and HTTP exposure in C# and on the JVM. TypeScript implements a subset of these capabilities; consult its [capability reference](/arc/backend/typescript/reference/capabilities/). The [scenario recipes](/arc/scenarios/) show the same task, such as [validating a command](/arc/scenarios/validate-a-command/), with backend examples where the capability is available.
Queries can return one result or use Arc’s observable-query APIs. The TypeScript generator creates clients for recognized commands, queries, referenced types, validation rules, and identity details. See the detailed product documentation for the exact transport, subscription, and lifecycle behavior.
[Command pipeline (C#) ](/arc/backend/csharp/commands/)Model intentions, validate and authorize them, provide dependencies, execute handlers, and convert typed results into HTTP responses.
[Command pipeline (Kotlin and Java) ](/arc/backend/kotlin/guides/commands/)The same command model on Spring Boot: KSP-generated handlers, dependency resolution, validation and typed results.
[Query pipeline (C#) ](/arc/backend/csharp/queries/)Expose purpose-shaped reads with paging, sorting, filtering, result rendering, and diagnostics.
[Query pipeline (Kotlin and Java) ](/arc/backend/kotlin/guides/queries/)Model-bound read models with GET, RFC QUERY, HTTP snapshots, SSE and WebSocket hosting on Spring Boot.
[Observable-query APIs (C#) ](/arc/backend/csharp/queries/observable-query-demultiplexer/)Work with documented query subscriptions, collection changes, subscription guards, and query health information.
[Observable-query APIs (Kotlin and Java) ](/arc/backend/kotlin/guides/observable-queries/)Subscribe to collection changes over SSE or WebSocket, and the same observable-query lifecycle on the JVM host.
## Frontend contracts
`@cratis/arc` supplies the TypeScript command, query, validation, identity, and messaging runtime used by generated proxies. `@cratis/arc.react` adds hooks, command forms, dialog infrastructure, identity composition, and query components. The composed `CommandDialog` used in the tutorial comes from the separate `@cratis/components` package. The optional React/MVVM package adds view models, dependency injection, route parameters, and browser abstractions.
[Explore Arc’s frontend packages](/arc/frontend/) to choose the TypeScript, React, or React/MVVM path. Components is separate: applications may use Arc without Components, and may use the generated TypeScript clients without Arc’s React packages.
## Identity and tenancy
Arc includes [pluggable identity and access behavior](/arc/understanding-identity-and-access/), identity-detail providers, server-side authorization and role checks, and frontend identity context. Tenant resolution can use a header, query parameter, claim, subdomain, fixed value, or development setting before provider-specific database or namespace mapping — see [tenancy on C#](/arc/backend/csharp/tenancy/) or [tenancy on Kotlin and Java](/arc/backend/kotlin/guides/ambient-tenancy/).
Frontend visibility helpers do not replace server-side authorization. Tenant resolution also does not by itself establish isolation for an application or storage topology.
## Persistence choices
Arc does not depend on Chronicle. Commands and queries can use application services or current-state persistence without an event log. Each backend integrates with the data access its own ecosystem already uses.
[MongoDB (C#) ](/arc/backend/csharp/mongodb/)Arc's serialization conventions, tenant-aware database resolution, resilient collection access, and observable change streams.
[Entity Framework Core (C#) ](/arc/backend/csharp/entity-framework/)Discover read models, configure value conversions and mappings, run migrations, and observe SQL Server, PostgreSQL or SQLite changes.
[Spring Data (Kotlin and Java) ](/arc/backend/kotlin/guides/spring-data/)Inject JPA or MongoDB repositories into queries and commands, with Arc paging, tenant routing, transactions and observable change streams.
[Chronicle (C#) ](/arc/backend/csharp/chronicle/)Add event-sourced commands, concurrency behavior, tenant namespaces and reactor side effects.
[Chronicle (Kotlin and Java) ](/arc/backend/kotlin/guides/chronicle/)The same event-sourced commands, concurrency behavior, tenant namespaces and reactor side effects on Spring Boot.
## Inspection, contracts, and testing
Arc can describe registered commands, queries, identity schemas, and OpenAPI operations at runtime. Repository packages also include analyzers, source generators, command-scenario testing, proxy-generation checks, Vite helpers, and Arc-specific ESLint rules.
* [Inspect registered application contracts (C#)](/arc/backend/csharp/introspection/) — commands, queries and identity schemas are also introspectable on Kotlin and Java; a dedicated guide is not yet published
* Generate OpenAPI descriptions: [C#](/arc/backend/csharp/open-api/) or [Kotlin and Java](/arc/backend/kotlin/guides/openapi/)
* Test command behavior: [C#](/arc/backend/csharp/testing/), [Kotlin and Java](/arc/backend/kotlin/guides/testing/), or [TypeScript preview](/arc/backend/typescript/testing/)
* Review analyzer rules: [C# Roslyn analyzers](/arc/backend/csharp/code-analysis/) or [Kotlin KSP diagnostics](/arc/backend/kotlin/reference/diagnostics/)
Check the combination you use
These are separate packages and integration surfaces. Their existence does not establish compatibility with every frontend, runtime, persistence provider, browser, or product version. Check the package manifests and the evidence for the combination you evaluate.
## Continue
[ Arc source and README](https://github.com/Cratis/Arc)
[Repository structure, package roles, contribution guidance, and current limits.](https://github.com/Cratis/Arc)
[ Arc packages (C#)](https://www.nuget.org/packages/Cratis.Arc)
[Published .NET package metadata and versions.](https://www.nuget.org/packages/Cratis.Arc)
[ Arc packages (Kotlin and Java)](https://central.sonatype.com/namespace/io.cratis)
[Published Maven Central package metadata and versions.](https://central.sonatype.com/namespace/io.cratis)
[ Chronicle](/chronicle/)
[The optional event-sourced runtime integration for Arc application behavior.](/chronicle/)
[ Components](/components/)
[Optional React components aligned with Arc application patterns.](/components/)
# CQRS without event sourcing
> Arc's CQRS model runs over current-state storage without Chronicle, and this page shows where CQRS ends and event sourcing begins.
It’s easy to assume Arc and [Chronicle](/chronicle/) are a package deal, because they work well together. We think event sourcing is the default architecture for information systems, and Chronicle is the Cratis event-sourcing platform. But Arc itself is CQRS: **commands, queries, and generated TypeScript proxies that keep your React frontend in lockstep with your backend.** Where the data actually lives is a separate decision.
This page shows Arc on its own — the same typed full-stack experience, backed by a plain database instead of an event log — so the line between CQRS and event sourcing is explicit. CQRS and event sourcing fit naturally together, but neither depends on the other.
## The line between Arc and Chronicle
Arc is a layer that can sit *on top of* Chronicle; Chronicle never depends on Arc. That direction is the whole point — it’s why a bounded current-state slice can keep everything Arc gives you without storing events. Each backend has its own current-state integrations: [MongoDB](/arc/backend/csharp/mongodb/) and [Entity Framework](/arc/backend/csharp/entity-framework/) on C#, [Spring Data](/arc/backend/kotlin/guides/spring-data/) on JVM, and [MongoDB or Drizzle](/arc/backend/typescript/reference/capabilities/) in the TypeScript preview. Chronicle adds the optional event-sourced path.
```
Arc.Core — commands · queries · generated proxies
MongoDB
EF Core / SQL
Chronicle — event sourcing
```
Pick a supported current-state store for your backend and you can build a typed CQRS app without an event log. Pick Chronicle and the same Arc boundary records facts, builds projections, and keeps history.
## A standalone slice, end to end
Choose your backend
The document-store examples and host excerpts below have tabs for each backend. The proxy-generation and testing guidance that follows is .NET-specific. For JVM setup and testing see [Spring Data read models](/arc/backend/kotlin/guides/spring-data/) and [testing](/arc/backend/kotlin/guides/testing/); for TypeScript see [getting started](/arc/backend/typescript/getting-started/) and [testing](/arc/backend/typescript/testing/).
These **illustrative excerpts** show registering an author and listing authors live in MongoDB. For a runnable project, use [standalone .NET setup](/arc/backend/csharp/getting-started/) and its [backend checkpoint](/arc/backend/csharp/getting-started/your-first-command/), [Kotlin setup](/arc/backend/kotlin/get-started/), [Java setup](/arc/backend/kotlin/get-started/java/), or [TypeScript setup](/arc/backend/typescript/getting-started/) for domain declarations, host, database configuration, and replica-set bootstrap. The explanation here does not replace those prerequisites.
**The read model is just a document.** Declare a read model and Arc exposes its query methods. The model-bound query returns a live stream:
* C#
```csharp
[ReadModel]
public record Author(AuthorId Id, AuthorName Name)
{
// This static method is the query — served over HTTP, and live.
public static ISubject> AllAuthors(IMongoCollection authors) =>
authors.Observe();
}
```
* Java
```java
import org.springframework.data.mongodb.core.mapping.Document;
@Document
@ReadModel
@AllowAnonymous
public record Author(AuthorId id, AuthorName name) {
public static Flow.Publisher> allAuthors(@FromServices MongoObservableQuery queries) {
return queries.observePublisher(Author.class);
}
}
```
* Kotlin
```kotlin
import org.springframework.data.mongodb.core.mapping.Document
@Document
@ReadModel
@AllowAnonymous
data class Author(val id: AuthorId, val name: AuthorName) {
companion object {
@JvmStatic
fun allAuthors(@FromServices queries: MongoObservableQuery): Flow> =
queries.observe()
}
}
```
* TypeScript (preview)
```typescript
import { field } from '@cratis/fundamentals';
import { query, readModel, service, type ObservableSource } from '@cratis/arc.core';
@readModel()
export class Author {
@field(AuthorId) id!: AuthorId;
@field(AuthorName) name!: AuthorName;
@query({ observable: true }, service(AuthorRepository))
static allAuthors(authors: AuthorRepository): ObservableSource {
return authors.observeAll();
}
}
```
**The command writes the document directly.** Inject the database dependency and save. This example chooses no response payload; no event is involved:
* C#
```csharp
[Command]
public record RegisterAuthor(AuthorId Id, AuthorName Name)
{
public Task Handle(IMongoCollection authors) =>
authors.InsertOneAsync(new Author(Id, Name));
}
```
* Java
```java
@Command
@AllowAnonymous
public record RegisterAuthor(AuthorId id, AuthorName name) {
public void handle(@FromServices AuthorRepository authors) {
authors.save(new Author(id, name));
}
}
```
* Kotlin
```kotlin
@Command
@AllowAnonymous
data class RegisterAuthor(val id: AuthorId, val name: AuthorName) {
suspend fun handle(@FromServices authors: AuthorRepository) {
authors.save(Author(id, name))
}
}
```
* TypeScript (preview)
```typescript
import { field } from '@cratis/fundamentals';
import { command, inject } from '@cratis/arc.core';
@command()
export class RegisterAuthor {
@field(AuthorId) id!: AuthorId;
@field(AuthorName) name!: AuthorName;
@inject(AuthorRepository)
handle(authors: AuthorRepository): Promise {
return authors.save({ id: this.id, name: this.name });
}
}
```
**A command can take the read model it acts on.** Mark the key on the command and configure the read model’s database lookup, so the command decides against current state instead of fetching it first. On the JVM MongoDB branch, `Author` must be a Spring Data-mapped `@Document` as well as an Arc `@ReadModel`; otherwise the MongoDB resolver cannot claim it from the mapping context at startup:
* C#
```csharp
using System.ComponentModel.DataAnnotations;
[Command]
public record RenameAuthor([property: Key] AuthorId Id, AuthorName NewName)
{
public Task Handle(Author author, IMongoCollection authors) =>
authors.ReplaceOneAsync(_ => _.Id == author.Id, author with { Name = NewName });
}
```
* Java
```java
import io.cratis.arc.artifacts.CommandKey;
@Command
@AllowAnonymous
public record RenameAuthor(@CommandKey AuthorId id, AuthorName newName) {
public void handle(Author author, @FromServices AuthorRepository authors) {
authors.save(new Author(author.id(), newName));
}
}
```
* Kotlin
```kotlin
import io.cratis.arc.artifacts.CommandKey
@Command
@AllowAnonymous
data class RenameAuthor(@CommandKey val id: AuthorId, val newName: AuthorName) {
suspend fun handle(author: Author, @FromServices authors: AuthorRepository) {
authors.save(author.copy(name = newName))
}
}
```
* TypeScript (preview)
```typescript
import { field } from '@cratis/fundamentals';
import { command, commandReadModel, inject, key } from '@cratis/arc.core';
@command()
export class RenameAuthor {
@field(AuthorId) @key() id!: AuthorId;
@field(AuthorName) newName!: AuthorName;
@inject(commandReadModel(Author), AuthorRepository)
async handle(author: Author, authors: AuthorRepository): Promise {
author.name = this.newName;
await authors.save(author);
}
}
// Configure Author as a MongoDB-owned read model; Arc will look it up by the declared key.
```
On .NET this works both for a MongoDB read model and for one carried by an Entity Framework `ReadOnlyDbContext`. On TypeScript, configure a `ReadModelForCommandResolver` for the keyed model (the MongoDB integration registers one for configured `readModels`). [Read models in commands](/arc/backend/csharp/chronicle/read-models/injecting-into-commands/) covers what a nullable parameter means, and [Read models from other providers](/arc/backend/csharp/chronicle/read-models/other-providers/#declaring-the-key-without-chronicle) how to declare the key when it is not a single property.
Separate the decision from the write
These direct collection calls remain supported. On C# and TypeScript, for immediate inline work that a model-bound command can describe before execution, prefer returned command operations ([C#](/arc/backend/csharp/commands/operations/), [TypeScript](/arc/backend/typescript/commands/operations/)). The JVM backend has no operation mechanism; there, perform the work in `handle` through an injected service. With operations, the handler declares the write and Arc performs it afterward, without requiring Chronicle. Optional compensation is best-effort and in-process, not atomic database rollback or durable recovery. The [migration recipe](/arc/backend/csharp/commands/operations/migrating/) shows how to preserve an existing response contract.
That’s the backend behavior. Start the host with the database configured as in the backend-specific setup guides. The .NET excerpt assumes `Cratis:MongoDB`; Arc auto-discovers collections and serializers rather than requiring per-model registrations. For the TypeScript excerpt, place the `AuthorRepository` abstract class shown below in `Features/Authors/AuthorRepository.ts` and import it into the command, query, validators, and host; it is the runtime token used by `@inject` and `service`, not a type-only interface. Import your `Author`, `AuthorId`, `AuthorName`, `RegisterAuthor`, and `RenameAuthor` declarations from their feature files. Keep the TypeScript [setup guide’s `generate.mjs`](/arc/backend/typescript/getting-started/create-an-application/#generate-the-metadata) on `--project ./tsconfig.json` and `--artifacts ./Features`; import its generated metadata and discover the same `Features/` root in the host. Regenerate after chapter 2 adds validators, or the host will not register them. Install `@cratis/arc.mongodb`, set `MONGODB_URI` (or use the loopback default), and use a replica set for live MongoDB change streams. The scoped `MongoAuthorRepository` supplies the persistence methods used in the preceding excerpts:
* C#
```csharp
var builder = WebApplication.CreateBuilder(args);
builder.AddCratisArc();
builder.UseCratisMongoDB();
var app = builder.Build();
app.UseCratisArc();
app.Run();
```
* Java
```java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class LibraryApplication {
public static void main(String[] args) {
SpringApplication.run(LibraryApplication.class, args);
}
}
```
* Kotlin
```kotlin
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
@SpringBootApplication
class LibraryApplication
fun main(args: Array) {
runApplication(*args)
}
```
* TypeScript (preview)
```typescript
import { ArcApplication, type ObservableSource } from '@cratis/arc.core';
import { mongoCollection, type MongoCollection } from '@cratis/arc.mongodb';
import type { Document, Filter } from 'mongodb';
import { metadata } from './Features/generatedMetadata.js';
// Features/Authors/AuthorRepository.ts — import Author and its concepts here.
// Import this runtime token into main.ts, commands, queries, and validators.
export abstract class AuthorRepository {
abstract save(author: Author): Promise;
abstract all(): Promise;
abstract observeAll(): ObservableSource;
abstract findById(id: AuthorId, signal?: AbortSignal): Promise;
abstract existsByName(name: AuthorName, signal?: AbortSignal): Promise;
}
// main.ts — import AuthorRepository from ./Features/Authors/AuthorRepository.js.
// Also import Author, AuthorId, AuthorName, RegisterAuthor, and RenameAuthor from their feature files.
class MongoAuthorRepository extends AuthorRepository {
constructor(private readonly collection: MongoCollection) { super(); }
async save(author: Author): Promise {
const document = this.collection.codec.serialize(author);
await this.collection.native.replaceOne({ _id: document._id } as Filter, document, { upsert: true });
}
all(): Promise { return this.collection.find(); }
observeAll(): ObservableSource { return this.collection.observe(); }
async findById(id: AuthorId): Promise {
return await this.collection.findById(id) ?? undefined;
}
async existsByName(name: AuthorName): Promise {
return (await this.collection.find({ [this.collection.codec.fieldName('name')]: name.value })).length > 0;
}
}
export async function start(): Promise {
const builder = ArcApplication.createBuilder({ tenancy: { resolve: () => 'default' } });
builder.useGeneratedMetadata(metadata);
builder.withMongoDB({
server: process.env.MONGODB_URI ?? 'mongodb://127.0.0.1:27017',
database: 'Library', readModels: [Author]
});
builder.services.addScoped(AuthorRepository, async scope =>
new MongoAuthorRepository(await scope.resolve(mongoCollection(Author))));
// Use the same Features root as arc-proxygenerator's --artifacts argument.
await builder.discover(new URL('./Features/', import.meta.url));
const app = await builder.build();
await app.run({ port: 3000 });
}
await start();
```
On .NET, install `Cratis.Arc.ProxyGenerator.Build`, configure `CratisProxiesOutputPath`, and build in Debug as shown in setup to generate `RegisterAuthor` and `AllAuthors`. Building with Arc alone does not generate them. On the JVM, run `generateArcProxies`; on TypeScript, run your application’s source-based generator script and regenerate metadata after changing artifacts. This React **fragment** assumes the [frontend setup](/arc/frontend/react/getting-started/) imports/providers and the [per-operation `id` state](/arc/tutorial/first-slice/) (`Guid.create()` from `@cratis/fundamentals`), with a fresh dialog instance for each registration:
```tsx
const [authors] = AllAuthors.use(); // live — re-renders when the collection changes
command={RegisterAuthor} title="Add author" initialValues={{ id }}>
value={i => i.name} title="Name" />
```
Note
`AllAuthors` is live with no event sourcing involved. On C#, `IMongoCollection.Observe()` watches MongoDB’s change stream; the JVM and TypeScript tabs use their own observable sources. Subscribed browsers update when the configured source emits. [Observed DbSets](/arc/backend/csharp/entity-framework/observing/) receive same-host `SaveChanges` notifications through Arc’s configured interceptor. SQLite does not detect arbitrary external-process writes; other EF providers need their database notification setup for that.
## Test the slice without Chronicle
On C#, Arc’s command testing does not depend on Chronicle either. Start with `Cratis.Arc.Testing`, drive the command through a `CommandScenario`, and assert the `CommandResult` exactly as you would in an event-sourced slice. The only difference is what you assert after the command runs: a current-state slice checks the database or application service it wrote to, while a Chronicle-backed slice can also assert the appended events.
That keeps the CQRS boundary testable before you decide whether the slice needs an event log. See [Arc testing](/arc/backend/csharp/testing/) and [command scenarios](/arc/backend/csharp/testing/command-scenario/) for the base testing loop; add the [Chronicle testing extension](/arc/backend/csharp/testing/chronicle/) only when the command appends events.
## What actually changes when you add Chronicle
For C#, set this slice next to the same slice with [Chronicle added later](/arc/backend/csharp/chronicle/add-event-sourcing/). The TypeScript Chronicle integration is experimental; do not assume the same migration procedure applies. You can preserve the query and frontend **contracts**, while replacing direct writes with events and configuring a projection. The provider lookup/observation implementation may need to change; adding a package does not migrate existing data automatically:
| | Standalone (this page) | With Chronicle |
| --- | --- | --- |
| What the handler does | inserts a document | appends an event |
| What fills the read model | the command, directly | a projection over the event |
| What you can read | current state | current state **and full history** |
Adopting Chronicle changes persistence and adds projection/bootstrap work. If you preserve the query’s contract and route, its generated caller and screen can remain unchanged.
## The trade-off
Storing current state directly is simpler for bounded CRUD surfaces, reference data, settings, and adoption steps. What you don’t get is everything an event log buys you: an audit trail, the ability to rebuild a read model a brand-new way from history, temporal queries, and reactors that fire on facts. For information systems, we prefer starting with Chronicle because those needs show up often. [Why Event Sourcing](/chronicle/why-event-sourcing/) explains the default.
The reassuring part, from the table above: the boundary stays clean. If a direct-database slice later belongs in the event-sourced model, move persistence to Chronicle and deliberately preserve the public read contracts so the frontend need not change. [Adopting Cratis](/adopting-cratis/) walks through doing exactly that, one step at a time.
## Go deeper
* [MongoDB integration](/arc/backend/csharp/mongodb/) — setup, serializers, class mapping, and [observing collections](/arc/backend/csharp/mongodb/observing-collections/) for live queries.
* [Entity Framework integration](/arc/backend/csharp/entity-framework/getting-started/) — DbContexts, read-only contexts, and [observing DbSets](/arc/backend/csharp/entity-framework/observing/).
* [Commands](/arc/backend/csharp/commands/) and [Queries](/arc/backend/csharp/queries/) — the full model-bound and controller-based reference.
* [Why Arc](/arc/why-arc/) — the problem Arc solves, and how CQRS relates to event sourcing.
# Backend
> Choose the Arc backend implementation for your language and compare shared concepts with capability-specific behavior.
Arc’s backend gives commands, queries, validation, authorization and proxy generation one application boundary. That shape is the same whichever language you write it in — a command is a declared intention that is validated, authorized and handled, and a query is a purpose-shaped read. What differs is the host you run it on and the API surface you write against.
## Pick your language
* [C#](/arc/backend/csharp/) — ASP.NET Core and the lightweight Arc.Core host, with MongoDB and Entity Framework Core integrations, Roslyn analyzers, and post-build TypeScript proxy generation.
* [Kotlin and Java](/arc/backend/kotlin/) — Spring Boot, with Spring Data JPA and MongoDB integrations, KSP compile-time diagnostics, and Gradle-driven TypeScript proxy generation.
* [TypeScript](/arc/backend/typescript/) — Node.js, on its own HTTP host or in Express, Fastify, or Hono, with MongoDB and Drizzle SQL integrations, an experimental Chronicle integration, ESLint rules, and TypeScript proxies generated from your source. It is a source preview: no package is published to npm and it does not have full parity with the C# implementation. Its [capability reference](/arc/backend/typescript/reference/capabilities/) gives the status of each capability.
## What the implementations share
These implementations share the application-boundary model and frontend packages. Wire compatibility is capability-specific; implementation references document supported behavior and deliberate differences. The [HTTP contract](/arc/http-contract/) describes the shared contract verified for C# and JVM, while the TypeScript [capability reference](/arc/backend/typescript/reference/capabilities/) identifies what its source preview supports.
## The frontend is shared
Every backend generates proxies for the same `@cratis/arc` and `@cratis/arc.react` packages, so the [frontend documentation](/arc/frontend/) applies whichever backend you chose. The steps for running the generator differ by language; see your backend’s proxy generation page.
# Backend
> How you express commands and queries as plain records in C# and let Arc handle endpoints, validation, authorization, and proxies.
The backend is where you express *what your application does* — the commands that change state and the queries that read it. Arc’s job is to make that expression the only thing you write: you define a command or query as a plain record, and Arc handles the HTTP endpoint, validation, authorization, and a typed TypeScript proxy for the frontend. CQRS without the ceremony. In the full Cratis loop those commands append events through Chronicle; in bounded current-state slices they can write MongoDB or EF Core directly.
```
Arc discovers
compiled assembly/PDB · configured post-build tool
commands write
queries read
Command / query record
HTTP endpoint
Typed TS proxy
MongoDB / EF Core
Read models
```
## Start here
New to the backend? Walk through [Getting started](/arc/backend/csharp/getting-started/) to build your first command and query end to end. Then the two pillars:
* [Commands](/arc/backend/csharp/commands/) — intents that change state through `Handle()`.
* [Queries](/arc/backend/csharp/queries/) — reads, exposed to the frontend as typed proxies (including live, observable ones).
The magic that ties it to the frontend is [Proxy generation](/arc/backend/csharp/proxy-generation/) — read it early; it’s why the whole stack is type-safe.
## Integrations
Arc meets the rest of your stack:
| Topic | What it covers |
| --- | --- |
| [MongoDB](/arc/backend/csharp/mongodb/) | Document storage for read models and other data. |
| [Entity Framework](/arc/backend/csharp/entity-framework/) | EF Core integration for relational read models. |
| [Chronicle](/arc/backend/csharp/chronicle/) | Event sourcing integration — append events from commands, build read models, feed state back into business rules. |
| [ASP.NET Core](/arc/backend/csharp/asp-net-core/) | How Arc plugs into the ASP.NET Core pipeline. |
## Cross-cutting
| Topic | What it covers |
| --- | --- |
| [Core](/arc/backend/csharp/core/) | Commands, queries, and dependency injection at the lower level. |
| [Identity](/arc/backend/csharp/identity/) | Who the user is — authentication and identity details. |
| [Tenancy](/arc/backend/csharp/tenancy/) | Tenant selection and provider-specific storage routing; membership authorization is separate. |
| [Open API](/arc/backend/csharp/open-api/) | OpenAPI/Swagger generation. |
| [Code Analysis](/arc/backend/csharp/code-analysis/) | Analyzers and fixers that catch mistakes at compile time. |
Building the UI on top? Head to the [frontend](/arc/frontend/), which consumes everything here through the generated proxies.
# Application calendar
> Resolve the time zone that applies to an operation and re-evaluate live answers when the local date changes.
The application calendar answers one question: which time zone’s calendar applies to this operation? It pairs an instant you supply with that zone, so code can ask for the local date without reading the clock itself.
## Configure the application time zone
Set the zone under `Cratis:Arc:Calendar:TimeZone`, or in code:
```json
{
"Cratis": {
"Arc": {
"Calendar": {
"TimeZone": "Europe/Oslo"
}
}
}
}
```
The default is `UTC`. Only IANA ids are accepted. When the host starts, an unknown id, a Windows-only id such as `W. Europe Standard Time`, and a non-zero fixed-offset id such as `Etc/GMT+5` all throw `TimeZoneIsNotAnIanaZone`, which names the id. Arc never replaces a zone it cannot resolve with UTC, because that would move every local date without anyone noticing. A zone set in code is applied after configuration binding, so it wins over the bound value.
## Zone precedence
`IApplicationCalendar.ResolveZone` picks the first zone that exists:
1. The zone the operation declares.
2. The tenant’s zone, from the first registered `ICanProvideTenantCalendar` that returns an id.
3. The application zone.
Only the operation, a provider you register, and the configuration can choose a zone. A request header or query parameter never does. A provider that returns an unusable id throws `TimeZoneIsNotAnIanaZone`, and a provider that throws propagates the exception.
```csharp
using Cratis.Arc.Calendar;
using Cratis.Arc.Tenancy;
public class TenantCalendarFromSettings : ICanProvideTenantCalendar
{
public ValueTask Provide(TenantId tenant, CancellationToken token) =>
new(tenant.Value == "acme" ? "Asia/Tokyo" : null);
}
```
## Read an instant
`IApplicationCalendar.Read` returns a `ClockReading`. It never reads a clock; you pass the instant.
```csharp
ClockReading reading = await calendar.Read(DateTimeOffset.UtcNow, tenant, null, token);
DateOnly today = reading.Today;
DateOnly yesterday = reading.Today.AddDays(-1);
DateOnly sameInstantYesterday = reading.DateOf(reading.Instant.AddDays(-1));
```
`Today.AddDays(-1)` steps calendar days. `DateOf(Instant.AddDays(-1))` steps elapsed time, which differs across a daylight-saving change.
## Re-evaluate at clock boundaries
An answer such as “items due today” changes when the local date changes, even though no data changed. `ReevaluateAtBoundaries` takes a subject of candidates and re-evaluates the latest candidates whenever they change and whenever a boundary passes.
```csharp
var context = await clockBoundaries.Begin(tenant, null, token);
ISubject> answer = candidates.ReevaluateAtBoundaries(
context,
(items, reading) => new ClockEvaluation>(
[.. items.Where(item => item.Due <= reading.Today)],
LocalDateChanges.NextLocalDateChange(reading.Zone, reading.Instant)));
```
`LocalDateChanges.NextLocalDateChange` handles a local midnight, a gap that skips midnight, a wholly skipped date, and a transition that moves the local date back.
The next boundary of an evaluation must be after `reading.Instant`, or `null` when the result cannot change because of the clock. A boundary that is not after it ends the subject with a `ClockBoundaryIsNotInTheFuture` error instead of re-evaluating in a loop.
An evaluation whose result equals the last emitted result is not emitted, but only inside this operator. The first evaluation is always emitted. Collections are compared positionally, so a reorder is a change. Evaluations run one at a time inside the tenant of the context.
The subject connects on its first subscription. It releases its timer and its subscription to the candidates when the last subscriber leaves, the context’s token is cancelled, or the stream errors or completes. Calling `OnNext` on it is not supported.
## Subscription lifetime
The zone is resolved once, when `IClockBoundaries.Begin` runs, and is fixed for the subscription. Changing the application zone needs a restart, and a changed tenant zone applies to new subscriptions.
## Test with a fake clock
The operator arms its timers on the registered `TimeProvider`, so a test can use `FakeTimeProvider` from `Microsoft.Extensions.TimeProvider.Testing` and `Advance` past a boundary.
## Platform prerequisites
Zone resolution uses the host’s time-zone data. Containers need `tzdata`, and the host must not run in a configuration that lacks time-zone data. In invariant-globalization mode zone ids still resolve, but verify that your image carries the data. A host without it fails at startup rather than falling back.
## Out of scope
A live `now` that ticks continuously, and an advanceable clock for query scenarios, are not part of this feature.
# ASP.NET Core Integration
> What the Cratis.Arc package adds for ASP.NET Core hosts, and where to find configuration, authorization, and integration guides.
The Arc provides enhanced capabilities for ASP.NET Core applications, building upon the core Arc features with web-specific functionality. Install `Cratis.Arc` for this host (`Cratis.Arc.Core` alone is the lightweight alternative). Register with `WebApplicationBuilder.AddCratisArc()` and activate with `WebApplication.UseCratisArc()`. No event store is required. OpenAPI/Swagger have separate optional packages.
## Features
* **[Configuration](/arc/backend/csharp/asp-net-core/configuration/)** - Configure Arc through appsettings.json or programmatically
* **[Authorization](/arc/backend/csharp/asp-net-core/authorization/)** - Arc role checks versus separately configured ASP.NET endpoint policies
* **[Microsoft Identity](/arc/backend/csharp/asp-net-core/microsoft-identity/)** - Integration with Microsoft Client Principal for Azure services
* **[FromRequest Attribute](/arc/backend/csharp/asp-net-core/from-request/)** - Advanced model binding combining multiple HTTP request sources
* **[Swagger](/arc/backend/csharp/asp-net-core/swagger/)** - Enhanced OpenAPI documentation with Arc-specific schema generation
* **[Validation](/arc/backend/csharp/asp-net-core/validation/)** - Comprehensive validation with FluentValidation support
* **[Without Wrappers](/arc/backend/csharp/asp-net-core/without-wrappers/)** - Control response wrapping behavior for specific endpoints
* **[Invariant Culture](/arc/backend/csharp/asp-net-core/invariant-culture/)** - Configure invariant culture defaults and understand request-level overrides
## When to Use ASP.NET Core Integration
Use the ASP.NET Core integration when you need:
* Full web framework capabilities (Kestrel, middleware pipeline, static files)
* Razor views or MVC features
* Swagger UI for API documentation
* Advanced middleware scenarios
* Kestrel’s HTTP server capabilities
* Traditional web application patterns
## When to Use Arc.Core Instead
Consider using [Arc.Core](/arc/backend/csharp/core/overview/) (without ASP.NET Core) when you need:
* Minimal dependencies and smaller binary size
* A simplified `HttpListener` host rather than the ASP.NET middleware ecosystem
* A deployment whose performance and Native AOT compatibility you verify with your actual dependencies
* Console applications or background services
* Scenarios where full web framework is unnecessary
## See Also
* [Arc.Core Overview](/arc/backend/csharp/core/overview/) - Lightweight alternative without ASP.NET Core
* [Commands](/arc/backend/csharp/commands/) - Command handling patterns
* [Queries](/arc/backend/csharp/queries/) - Query patterns and conventions
* [Tenancy](/arc/backend/csharp/tenancy/) - Tenant isolation and management
# Authorization in ASP.NET Core
> Evaluate named policies and authentication schemes through the ASP.NET Core Arc host.
When your application uses `Cratis.Arc`, two layers can reject a request: ASP.NET Core middleware at the HTTP boundary and Arc filters in the command/query pipelines. Keep their configuration separate so a policy tested through HTTP is not mistaken for an in-process guarantee.
## The two boundaries
| Boundary | Checks | Applies to |
| --- | --- | --- |
| ASP.NET Core authorization | Microsoft endpoint/controller metadata, registered policies, schemes, default/fallback policy | Requests that traverse the configured middleware and matching endpoint metadata |
| Arc authorization filters | Authentication, roles, named Arc or ASP.NET Core policies, and selected authentication schemes | Commands and queries executed through Arc pipelines |
`Cratis.Arc.Authorization.RolesAttribute` derives from **Arc’s** `AuthorizeAttribute`, not Microsoft’s. Every authorization attribute on a declaration applies, so stacked attributes require all of them, as they do in ASP.NET Core. A policy name must resolve to exactly one registered Arc or ASP.NET Core policy; a missing or ambiguous policy fails startup.
With `Cratis.Arc`, Arc’s pipeline also enforces **Microsoft’s** `[Authorize]` and `[AllowAnonymous]` on model-bound commands and read models - authentication, roles, policies, and schemes, as it does for its own attributes. This check also runs for mapped HTTP commands/queries and new hub subscriptions, which ASP.NET Core middleware cannot protect on its own. Direct calls with no identity transition retain their existing scope; a scheme-selected direct call must use a safe Arc-owned execution scope, as described below. Both attribute families can be mixed; a method’s declaration replaces its type’s whichever family each comes from, and a declaration that is both anonymous and restricted is rejected as ambiguous. Arc’s own attributes remain the portable choice: they are enforced on every Arc host, including Arc Core’s own HTTP host, where nothing reads the Microsoft ones.
Arc v18.2.0 through v22.20.0 did not enforce the Microsoft attributes
When Arc Core gained its own attributes in v18.2.0, the ASP.NET Core evaluators meant to keep honoring Microsoft’s resolved Arc’s same-named attributes instead, so a model-bound command protected only with Microsoft’s `[Authorize]` was open to every caller. Upgrade, or replace those attributes with `Cratis.Arc.Authorization`’s.
## Setup
Configure a real authentication scheme for your deployment before adding the middleware. The following **configuration fragment** belongs in an existing `WebApplication` program after authentication service registration:
```csharp
using Cratis.Arc;
using Microsoft.AspNetCore.Authorization;
builder.AddCratisArc();
builder.Services.AddAuthorization(options => options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build());
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.UseCratisArc();
app.Run();
```
See [Microsoft Identity integration](/arc/backend/csharp/asp-net-core/microsoft-identity/) for one authentication option and its trusted-ingress prerequisites. A fallback policy is not a substitute for authentication service configuration.
## Protecting all endpoints by default
ASP.NET’s `AuthorizationOptions.FallbackPolicy`, configured above with `builder.Services.AddAuthorization(...)`, applies to HTTP endpoints without applicable authorization metadata, including MVC controller actions. Its default policy applies when Microsoft `[Authorize]` supplies no named policy. Explicit anonymous metadata bypasses these policies.
The observable hub routes remain anonymous, but the hub evaluates `FallbackPolicy` separately for every non-anonymous query subscription before the query executes. It authenticates any schemes named by the fallback policy against the live WebSocket upgrade or SSE subscribe POST, then evaluates the policy with that `HttpContext` as its resource. Queries allowing anonymous access or opted-in guest policy evaluation bypass the host fallback, matching their mapped direct endpoints. A denial or policy failure produces an `Unauthorized` frame, and SSE subscribe returns 401. This host check does not add a fallback policy to in-process pipeline calls.
Arc’s [`IFallbackAuthorizationEvaluator`](/arc/backend/csharp/core/authorization/#set-baseline-requirements-without-overriding-anonymous-access) is separate: it supplies baseline requirements for model-bound commands and queries on both Arc hosts, and for controller-based queries reached through Arc’s observable hub path in the ASP.NET Core host. It also covers server-side pipeline invocations that never pass through ASP.NET middleware. Direct HTTP GET and WebSocket requests to controller-based queries are MVC actions, not covered by `IFallbackAuthorizationEvaluator`. Use ASP.NET Core’s `FallbackPolicy` to require authorization for those actions and controller-based commands without explicit authorization metadata.
Caution
Normal Arc activation maps the discovery endpoints (command/query introspection, development-user/tenant discovery and the identity schema) anonymously only in Development; elsewhere they require an authenticated caller unless `ArcOptions.Introspection.RequireAuthentication` is `false`. A fallback policy does **not** protect endpoints that are explicitly anonymous. Review [production discovery exposure](/arc/backend/csharp/introspection/#production-access).
## Role-based authorization
For model-bound commands and queries, use explicit Arc imports. These are complete type fragments for an existing Arc host; neither needs a database or Chronicle:
```csharp
using Cratis.Arc.Authorization;
using Cratis.Arc.Commands.ModelBound;
[Roles("Admin", "Manager")]
[Command]
public record ReviewRequest(Guid RequestId)
{
public Guid Handle() => RequestId;
}
```
Arc requires authentication and at least one listed role. The returned `Guid` remains ordinary response data.
```csharp
using Cratis.Arc.Authorization;
using Cratis.Arc.Queries.ModelBound;
[ReadModel]
public record ServiceStatus(string State)
{
[Roles("Admin", "Auditor")]
public static ServiceStatus Internal() => new("Running");
}
```
Arc query method attributes take precedence over read-model type attributes. For the full Arc rules, see [Core authorization](/arc/backend/csharp/core/authorization/).
## Authorization in controllers
For ASP.NET controller policy enforcement, use **Microsoft** attributes explicitly. This complete controller fragment assumes MVC is registered and controllers are mapped in the host:
```csharp
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/reports")]
[Authorize(Roles = "Manager")]
public class ReportsController : ControllerBase
{
[HttpGet]
public IActionResult Get() => Ok(new { State = "Available" });
}
```
Microsoft controller/action requirements compose according to ASP.NET Core rules; do not transfer Arc’s method-overrides-type rule to them. See [ASP.NET Core authorization](https://learn.microsoft.com/aspnet/core/security/authorization/introduction).
## Policy-based authorization
Register ASP.NET Core policies before building the host. Arc also evaluates these policies on model-bound artifacts reached through mapped HTTP endpoints and hub subscriptions, independently of middleware. This is a **configuration fragment** in an existing ASP.NET host:
```csharp
builder.Services.AddAuthorizationBuilder()
.AddPolicy("ReportsReader", policy => policy
.RequireAuthenticatedUser()
.RequireClaim("permission", "reports:read"));
```
After building the app and enabling its authentication/authorization middleware:
```csharp
app.MapGet("/reports/availability", () => new { Available = true })
.RequireAuthorization("ReportsReader");
```
This protects that HTTP endpoint. To protect a model-bound command or query through **every** Arc route instead, annotate its type or query method with `[Cratis.Arc.Authorization.Authorize(Policy = "ReportsReader")]` or Microsoft’s `[Authorize(Policy = "ReportsReader")]`. Arc evaluates the named policy inside its pipeline; it never assumes middleware has run. ASP.NET Core policy handlers receive the Arc `CommandContext` or `QueryContext` as their resource, not an MVC action resource. For an Arc pipeline invocation, that resource’s `ReceivedAt` is fixed at Arc dispatch before binding and scheme authentication; it survives the scheme-selected service scope. It is not network arrival time or a timestamp from before application middleware. MVC authorization is a separate boundary and does not use this Arc resource.
Arc requires authentication before evaluating ASP.NET Core policies by default. For a policy that intentionally permits guests, register it with ASP.NET Core and then opt it in for Arc pipeline evaluation with `builder.Services.AddArcAnonymousAspNetAuthorizationPolicy("PublicOrMember")`. The policy must not call `RequireAuthenticatedUser()` (or include `DenyAnonymousAuthorizationRequirement`) or specify `AuthenticationSchemes`: a scheme-selected policy cannot evaluate a guest. Arc asks the ASP.NET Core policy provider for the opt-in name at startup and rejects missing, ambiguous, native Arc, authentication-required, or scheme-selected policies. The opt-in name must match the name in the command or query’s policy declaration exactly, including case. Even if a provider resolves names without regard to case, an opt-in for `Guest` does not opt in `guest`. The standalone Core host cannot use this ASP.NET Core opt-in. Only declarations made entirely of opted-in policies without roles or authentication schemes can evaluate guests; the policies still decide whether to grant access.
For mapped model-bound command and query endpoints, Arc emits anonymous HTTP metadata only when the entire effective declaration qualifies for guest evaluation. This lets guests reach the Arc policy even with an authentication-requiring `FallbackPolicy`; command validation and query GET/QUERY endpoints use the same rule. The Arc pipeline still evaluates every policy and returns 403 on denial without running command/query logic. Do not combine the policy declaration with `[AllowAnonymous]`. Ordinary policies, mixed guest/ordinary requirements, roles, and authentication schemes retain their existing HTTP authentication behavior. The opt-in does not change MVC authorization or unrelated endpoints.
An attribute’s `AuthenticationSchemes` and any schemes contributed by the policy select the identities used for authentication, role checks, and policy evaluation. Arc calls ASP.NET Core’s authentication service for each named scheme and combines the authenticated identities from successful schemes. A failed scheme does not cancel another scheme’s success; if none authenticates, Arc denies access rather than falling back to the default principal. For a selected-scheme request, Arc runs the command or query in a fresh execution service scope; both `ICurrentPrincipalAccessor` and `IHttpContextAccessor.HttpContext.User` expose the selected identity while it executes. The HTTP accessor presents an operation-local native context: its selected `User` and `RequestServices` do not replace those on the shared connection object, and unrelated request features still come from the live request. Existing HTTP accessor registrations made **before** Arc is added are decorated, retaining their ordinary no-scheme behavior; replacing that registration **after** Arc disables safe scheme-selected execution and Arc rejects it rather than mutating a shared request. The built-in Arc ASP.NET request adapter also keeps selected `User` and `RequestServices` operation-local; a custom `IHttpRequestContext` that shares a native request should implement `IAuthorizationRequestContext` to avoid changing that shared object during identity selection. The prior request services, principal, and tenant cache are restored afterward. Arc rejects a scheme-selected invocation through an arbitrary caller-supplied `IServiceProvider`: an already-resolved tenant-bound service cannot be safely rebound. Use mapped HTTP/hub entry points, or the command pipeline’s scope-owning `Execute(command)` overload from a live HTTP request. Queries with explicit schemes must enter through mapped HTTP or the hub, not a direct `IQueryPipeline.Perform(..., serviceProvider)` call. A call without a live HTTP context cannot satisfy a named scheme. A nested server-side execution using `BeginScope` and no named scheme remains supported; changing to a different scheme-selected identity after an outer command has bound a unit of work is rejected before joining that work. Configure and register every scheme before startup. On SSE, the live subscribe POST supplies the identity, not the older stream GET. Direct observable SSE/WebSocket emissions retain the live request and its selected identity, tenant, and execution provider until the stream ends; an ordinary no-scheme direct stream keeps the original request context and principal subtype. Hub emissions after subscription admission use the captured Arc principal and tenant, but deliberately expose **no native `HttpContext`**: the subscribe POST may already be disposed, and the emitting thread may belong to another user. Use `ICurrentPrincipalAccessor`, the Arc request context, and the emission context’s provider in hub guards/interceptors instead of `IHttpContextAccessor` for those later emissions. Custom host runtimes can implement `IAuthorizationEmissionRuntime` to capture a live direct request and suppress unavailable native HTTP in hub emissions. Policy checks gate admission, not revocation. See [observable emission guards](/arc/backend/csharp/queries/observable-query-emission-guards/) for revocation after admission.
To register a portable scoped policy instead, use [Core named policies](/arc/backend/csharp/core/authorization/#register-a-named-policy). Register a name in either the native Arc registry or ASP.NET Core, not both.
## Authorization results for commands and queries
Arc authorization failures return results with `IsAuthorized = false` and skip the handler/query logic. Mapped Arc results use HTTP 403; ASP.NET middleware can challenge or forbid before Arc executes and may return a different response shape.
An ordinary call to `ServiceStatus.Internal()` bypasses the query pipeline and **does not evaluate attributes**. Calling a command’s `Handle()` directly likewise bypasses authorization, validation, filters, and result handling. Use the [query pipeline](/arc/backend/csharp/queries/query-pipeline/), [command pipeline](/arc/backend/csharp/commands/command-pipeline/), or mapped HTTP endpoint when those guarantees matter.
## Custom authorization
A command filter denying permission returns `CommandResult.Unauthorized(context.CorrelationId)`; a query filter uses `QueryResult.Unauthorized(context.CorrelationId)`. `CommandResult.Error(...)` describes an exception result, not an authorization rejection. See [command filters](/arc/backend/csharp/commands/command-filters/) for filter contracts.
Read the trusted principal through `ICurrentPrincipalAccessor` (`Cratis.Arc.Authorization`), or `IHttpContextAccessor.HttpContext.User` when intentionally writing ASP.NET-specific code. `IProvideIdentityDetails` composes frontend details; returning roles or `IsUserAuthorized` in its payload does not add claims or authorize every command/query. The [identity details](/arc/backend/csharp/identity/identity-provider-service/) are presentation data and must not be used as authorization evidence.
## Verification checklist
Test anonymous, authenticated-without-role, and permitted-role callers through both HTTP and direct pipelines where the latter are supported. Where HTTP policies or schemes matter, test wrong-policy/wrong-scheme principals against the actual endpoint and verify scoped collaborators use the selected tenant. Include discovery endpoints with Production settings and a fallback policy enabled. Test custom ownership and tenant membership checks independently of UI visibility.
## See also
* [Core authorization](/arc/backend/csharp/core/authorization/) — host-independent attribute and principal contracts.
* [Identity](/arc/backend/csharp/identity/) — identity enrichment and its trust boundary.
* [Microsoft Identity](/arc/backend/csharp/asp-net-core/microsoft-identity/) — authentication setup.
* [Tenancy](/arc/backend/csharp/tenancy/) — selection is not membership authorization.
* [Proxy generation](/arc/backend/csharp/proxy-generation/) — clients transport results; they do not enforce server permissions.
# Configuration
> Configure Arc in an ASP.NET Core host through appsettings.json or code, including route generation and JSON settings.
Cratis Arc can be configured both through `appsettings.json` and programmatically to customize its behavior. The main configuration is handled through the `ArcOptions` class.
This page covers the ASP.NET Core host specifically. Code blocks are configuration/service fragments for an existing `Cratis.Arc` web project; import `Cratis.Arc`, `System.Text.Json`, and `Microsoft.Extensions.Options` as used. Types such as `MyCustomIdentityDetailsProvider` and `MyCustomConverter` are application-owned examples, not built-in Arc types. For the lightweight Arc.Core host and its listen URL configuration, see [Arc.Core Getting Started](/arc/backend/csharp/core/getting-started/).
## Default Configuration Section
By default, Arc looks for configuration under the `Cratis:Arc` section in your `appsettings.json` file. The same keys can be supplied through environment variables — .NET maps the `__` separator onto nested keys, so `Cratis:Arc:GeneratedApis:RoutePrefix` becomes `Cratis__Arc__GeneratedApis__RoutePrefix`.
## Configuration Options
### Configuration example
Here’s an example covering the most common options, bound from `appsettings.json` under `Cratis:Arc`. The [complete options tree](/arc/backend/csharp/configuration/#the-arcoptions-tree) also documents `ExposeExceptionDetails`, `Tenancy.BaseDomain`, and host-only settings.
```json
{
"Cratis": {
"Arc": {
"CorrelationId": {
"HttpHeader": "X-Correlation-ID"
},
"Tenancy": {
"ResolverType": "Header",
"HttpHeader": "x-cratis-tenant-id"
},
"GeneratedApis": {
"RoutePrefix": "api",
"SegmentsToSkipForRoute": 0,
"IncludeCommandNameInRoute": true,
"IncludeQueryNameInRoute": true
},
"Query": {
"KeepAliveInterval": "00:00:30"
}
}
}
}
```
Note
`ArcOptions.Hosting` (the listen URL) applies only to **Arc.Core** hosts — in an ASP.NET Core app the URL comes from Kestrel and `launchSettings.json`, not from Arc. For the Arc.Core hosting shape, see [Arc.Core Getting Started](/arc/backend/csharp/core/getting-started/).
### Configuration Properties
#### CorrelationId
Controls how correlation IDs are handled in HTTP requests.
* **HttpHeader** (string, default: `"X-Correlation-ID"`): The HTTP header name to use for correlation ID tracking.
#### Tenancy
Controls how the active tenant is resolved on each request.
* **ResolverType** (`TenantResolverType`, default: `Header`): How to resolve the tenant — `Header`, `Query`, `Claim`, `Subdomain`, `Development`, or `Fixed`.
* **HttpHeader** (string, default: `"x-cratis-tenant-id"`): The HTTP header used when `ResolverType` is `Header`, and the fallback header when it is `Subdomain`.
* **QueryParameter** (string, default: `"tenantId"`): The query-string parameter used when `ResolverType` is `Query`.
* **ClaimType** (string, default: `"tenant_id"`): The claim used when `ResolverType` is `Claim`.
* **FixedTenantId** (string, default: `"development"`): The tenant every request resolves to when `ResolverType` is `Fixed` or `Development`. Prefer `Fixed` for a single-tenant production deployment — `Development` resolves identically but is named for an environment it does not check.
* **DevelopmentTenantId** (string, default: `"development"`): The same value under its original name — reading or writing either key sets both. Supply only one; if both are present the binder’s property order decides.
#### GeneratedApis
Controls how automatically generated API endpoints are configured for commands and queries.
* **RoutePrefix** (string, default: `"api"`): The base route prefix for all generated API endpoints.
* **SegmentsToSkipForRoute** (int, default: `0`): Number of namespace segments to skip when constructing routes from type namespaces.
* **IncludeCommandNameInRoute** (bool, default: `true`): Whether to include the command type name as the last segment of the route for command endpoints.
* **IncludeQueryNameInRoute** (bool, default: `true`): Whether to include the query name as the last segment of the route for query endpoints.
* **EnableQueryHttpMethod** (bool, default: `true`): Whether generated queries also accept HTTP `QUERY` with arguments in a JSON body. GET remains available. See [HTTP QUERY](/arc/backend/csharp/queries/using-the-http-query-method/).
#### Query
Controls observable (real-time) queries.
* **KeepAliveInterval** (`TimeSpan`, default: `00:00:30`): How often a keep-alive frame is sent on an open observable-query connection.
#### IdentityDetailsProvider
* **IdentityDetailsProvider** (Type, default: `null`): Specifies a custom identity details provider type. If not specified, the system will use type discovery to find one automatically.
## Setup Methods
The ASP.NET Core host bootstraps with `WebApplication.CreateBuilder`, registers Arc with `AddCratisArc`, and activates it with `UseCratisArc`.
### Using Configuration File
The most common approach is to use the configuration file with the default section:
```csharp
var builder = WebApplication.CreateBuilder(args);
// Bind the default configuration section (Cratis:Arc)
builder.AddCratisArc();
var app = builder.Build();
app.UseCratisArc();
app.Run();
```
### Using Custom Configuration Section
You can specify a custom configuration section path:
```csharp
var builder = WebApplication.CreateBuilder(args);
// Bind a custom configuration section
builder.AddCratisArc(configSectionPath: "MyApp:CratisConfig");
var app = builder.Build();
app.UseCratisArc();
app.Run();
```
### Programmatic Configuration
You can configure the options through code with the `configureOptions` callback:
```csharp
var builder = WebApplication.CreateBuilder(args);
builder.AddCratisArc(options =>
{
// Configure correlation ID
options.CorrelationId.HttpHeader = "X-My-Correlation-ID";
// Configure tenancy
options.Tenancy.HttpHeader = "X-Custom-Tenant";
// Configure generated APIs
options.GeneratedApis.RoutePrefix = "myapi";
options.GeneratedApis.SegmentsToSkipForRoute = 2;
options.GeneratedApis.IncludeCommandNameInRoute = false;
options.GeneratedApis.IncludeQueryNameInRoute = false;
// Set a custom identity details provider
options.IdentityDetailsProvider = typeof(MyCustomIdentityDetailsProvider);
});
var app = builder.Build();
app.UseCratisArc();
app.Run();
```
### Hybrid Configuration
`AddCratisArc` binds `appsettings.json` first and then applies the `configureOptions` callback, so the callback acts as a programmatic override on top of file-based settings:
```csharp
var builder = WebApplication.CreateBuilder(args);
// Values come from Cratis:Arc; the callback overrides specific settings
builder.AddCratisArc(options =>
{
options.GeneratedApis.RoutePrefix = "v1/api";
});
var app = builder.Build();
app.UseCratisArc();
app.Run();
```
## Turning controllers off
`AddCratisArc` registers ASP.NET Core MVC and discovers the controllers in your project assemblies. An application that only uses model-bound commands and queries doesn’t need MVC. Turn it off with `WithoutControllers` on the Arc builder:
```csharp
var builder = WebApplication.CreateBuilder(args);
builder.AddCratisArc(configureBuilder: arc => arc.WithoutControllers());
var app = builder.Build();
app.UseCratisArc();
app.Run();
```
Controllers are on unless you call `WithoutControllers` or turn them off with the `CratisArcControllersSupport` property (see below). With controllers off:
* Model-bound commands and queries, observable queries included, work as before. `UseCratisArc` maps their endpoints without MVC.
* Controller-based commands and queries aren’t available, because no controllers are discovered.
* MVC-only features are unavailable. Calling `MapControllers` fails, because MVC isn’t registered, and `[FromRequest]` model binding, `[AspNetResult]` and MVC model validation are unavailable. Arc still validates model-bound commands and queries.
* OpenAPI documents describe the model-bound endpoints only.
* The application registers the ASP.NET Core services it used to get from MVC. Add `builder.Services.AddCors()` before `UseCors`, and `builder.Services.AddEndpointsApiExplorer()` when you use Swashbuckle. Arc registers the ASP.NET Core authentication and authorization services, so `UseAuthentication`, `UseAuthorization` and policies work without calling `AddAuthorization`. Authentication schemes still come from your own `AddAuthentication(...)`, and protected introspection needs a default scheme, as it does with controllers on.
`WithoutControllers` works with `AddCratis` too, through `configureArcBuilder`. The `IHostBuilder` overload of `AddCratisArc` doesn’t support `configureBuilder` (passing one throws), so there it’s only the MSBuild property below that turns controllers off.
### Removing MVC from a trimmed or NativeAOT publish
`WithoutControllers` is a runtime choice. `AddCratisArc` still references the code that registers MVC, so a trimmed or NativeAOT publish keeps it, along with the trim warnings MVC raises. To let the trimmer remove it, turn controllers off in the application’s project file instead:
```xml
false
```
The `Cratis.Arc` package turns the property into the `Cratis.Arc.Controllers.IsSupported` runtime switch in the application’s `runtimeconfig.json`. With it set to `false`:
* Every `AddCratisArc` overload, the `IHostBuilder` one included, registers no MVC and discovers no controllers, exactly as `WithoutControllers` does, and everything listed above applies. You don’t need to call `WithoutControllers` as well.
* A trimmed or NativeAOT publish removes the code that registers MVC, so MVC’s trim warnings and most MVC assemblies drop out of the output. Arc still references a few MVC types elsewhere, so `Microsoft.AspNetCore.Mvc.Core` and `Microsoft.AspNetCore.Mvc.Abstractions` stay. This works for `net8.0`, `net9.0` and `net10.0`.
* The rest of Arc isn’t trim or NativeAOT compatible yet, so a trimmed publish still reports trim warnings from Arc itself.
The switch is written into the `runtimeconfig.json` of the project that sets the property, so it applies to whichever process runs. A test project that hosts the application, for example through `WebApplicationFactory`, runs with its own `runtimeconfig.json` and keeps MVC unless it sets the property too. Set the property in a `Directory.Build.props` shared by the application and its tests, so tests run the way the published application does.
Leave the property unset, or set it to `true`, to keep controllers on. The property only has an effect through the `Cratis.Arc` package; a project that references Arc’s source through a `ProjectReference` has to import `build/Cratis.Arc.targets` itself.
### Configuring MVC when controllers are on
With controllers on, `AddCratisArc` registers MVC after the `configureBuilder` callback returns. Configure MVC after `AddCratisArc` returns, not inside `configureBuilder`:
```csharp
builder.AddCratisArc();
builder.Services.Configure(options => { /* ... */ });
builder.Services.PostConfigure(options => { /* ... */ });
```
MVC settings made inside the callback run before Arc’s own setup and before MVC’s defaults exist. Arc’s JSON configuration then overrides yours, Arc’s model binder goes ahead of one you insert, and removing a default formatter finds nothing to remove.
## Hosting under a path prefix
For an app reached at `/workbench`, configure ASP.NET Core’s `UsePathBase` **before** `UseRouting` and `UseCratisArc`. It removes the prefix from the request path before endpoint matching; leave Arc’s generated API route prefix (normally `/api`) unchanged.
```csharp
var builder = WebApplication.CreateBuilder(args);
builder.AddCratisArc();
var app = builder.Build();
app.UsePathBase("/workbench");
app.UseRouting();
app.UseCratisArc();
app.Run();
```
Set `` in the frontend and set the application router’s `basename` to `/workbench` separately. Neither setting changes the server’s routes; see [React provider configuration](/arc/frontend/react/arc/#hosting-under-a-path-prefix). If a reverse proxy already removes the prefix, the server sees unprefixed paths, so configure the frontend and ingress accordingly.
## Environment-Specific Configuration
You can use different configurations for different environments using the standard ASP.NET Core configuration pattern:
**appsettings.json** (base configuration):
```json
{
"Cratis": {
"Arc": {
"GeneratedApis": {
"RoutePrefix": "api"
}
}
}
}
```
**appsettings.Development.json** (development overrides):
```json
{
"Cratis": {
"Arc": {
"CorrelationId": {
"HttpHeader": "X-Dev-Correlation-ID"
}
}
}
}
```
**appsettings.Production.json** (production overrides):
```json
{
"Cratis": {
"Arc": {
"GeneratedApis": {
"RoutePrefix": "v1"
}
}
}
}
```
## Route Generation Examples
The `GeneratedApis` configuration affects how routes are generated for your commands and queries. Here are some examples:
Given a command class `MyApp.Sales.Commands.CreateOrderCommand`:
### Default Configuration
```json
{
"GeneratedApis": {
"RoutePrefix": "api",
"SegmentsToSkipForRoute": 0,
"IncludeCommandNameInRoute": true,
"IncludeQueryNameInRoute": true
}
}
```
**Generated route**: `/api/my-app/sales/commands/create-order-command`
### Skip Namespace Segments
```json
{
"GeneratedApis": {
"RoutePrefix": "api",
"SegmentsToSkipForRoute": 2,
"IncludeCommandNameInRoute": true,
"IncludeQueryNameInRoute": true
}
}
```
**Generated route**: `/api/commands/create-order-command` (skips `MyApp` and `Sales`).
### Exclude Type Names
```json
{
"GeneratedApis": {
"RoutePrefix": "api",
"SegmentsToSkipForRoute": 2,
"IncludeCommandNameInRoute": false,
"IncludeQueryNameInRoute": false
}
}
```
**Generated route**: `/api/commands` for this single command. A single query with location `MyApp.Sales.Queries` would use `/api/queries` with these options.
Route conflicts restore the type name
When `IncludeCommandNameInRoute` or `IncludeQueryNameInRoute` is set to `false`, Arc detects route conflicts. If multiple commands or queries remain in the same namespace after skipping segments, Arc includes the type name in the route. Within those generated command or query groups:
* Single command/query in a namespace: Route remains clean without the type name
* Multiple commands/queries in the same namespace: Type names are automatically added to prevent route collisions
* Both runtime endpoint mapping and proxy generation apply this logic consistently
For example, with the configuration above:
* If you have only `MyApp.Sales.Commands.CreateOrder`, the route will be `/api/commands`
* If you have both `MyApp.Sales.Commands.CreateOrder` and `MyApp.Sales.Commands.UpdateOrder`, the routes will be `/api/commands/create-order` and `/api/commands/update-order` respectively (type names added automatically to avoid conflict)
Note
The runtime `GeneratedApis` settings and the build-time proxy-generation settings must agree. If you change route generation here, mirror it in the `CratisProxies*` MSBuild properties so the generated TypeScript clients call the same routes. See [Proxy Generation Configuration](/arc/backend/csharp/proxy-generation/configuration/).
## JSON Serialization
Arc-generated endpoints use `ArcOptions.JsonSerializerOptions`. Arc’s MVC post-configuration copies only the property naming policy and appends Arc converters; it does not copy the entire options object. Settings such as `DefaultIgnoreCondition`, `NumberHandling`, and other MVC serializer options require separate MVC configuration if controller output must match. Plain ASP.NET minimal API endpoints receive only Arc’s concept converters by default (single concepts, concept collections and dictionaries keyed by concepts). Concepts in request and response bodies use their underlying primitive JSON values; other types retain ASP.NET’s JSON defaults. Manual serialization must explicitly use the Arc options.
### Default Configuration
Arc’s own `JsonSerializerOptions` has the following defaults (not all are copied to MVC):
* **Property Naming**: Camel case with acronym-friendly handling (e.g., `XMLParser` becomes `xmlParser`)
* **Null Handling**: Null values are ignored when writing JSON
* **Enums**: Serialized as integers (not strings)
* **Concepts**: Full support for Cratis Concepts (strongly-typed primitives)
* **Date/Time**: Support for `DateOnly` and `TimeOnly` types
* **Types**: Support for `System.Type` and `System.Uri` serialization
* **Derived Types**: Polymorphic serialization support when derived types are discovered
### Accessing JsonSerializerOptions
The configured `JsonSerializerOptions` is available through dependency injection:
```csharp
using System.Text.Json;
public class MessageSerializer(JsonSerializerOptions jsonOptions)
{
public string Serialize(string message) =>
JsonSerializer.Serialize(new { Message = message }, jsonOptions);
}
```
Or through `ArcOptions`:
```csharp
public class MyService
{
public MyService(IOptions arcOptions)
{
var jsonOptions = arcOptions.Value.JsonSerializerOptions;
}
}
```
### Customizing JSON Serialization
You can add custom converters or modify the configuration through the options pattern:
```csharp
builder.AddCratisArc(options =>
{
// Add a custom converter
options.JsonSerializerOptions.Converters.Add(new MyCustomConverter());
});
```
For controllers, configure null omission, number handling, and other non-copied settings separately through MVC’s `AddJsonOptions`. Arc’s post-configuration still sets the naming policy, removes the first `JsonStringEnumConverter` if present, and appends Arc converters. `ConfigureHttpJsonOptions` configures ASP.NET minimal API serialization; it does **not** replace Arc’s serializer for generated endpoints. For minimal APIs, ASP.NET Core’s camel-case naming remains unchanged, including when you explicitly choose `JsonNamingPolicy.CamelCase`. Configure `ConfigureHttpJsonOptions` to choose another naming policy, opt out of primitive concept serialization by registering an application converter for that concept type (for example, a `JsonConverter` that writes and reads `{ "value": "..." }`), or set null omission, number handling, and other serializer settings. Application converters keep their order and take precedence over Arc’s appended concept converters. Converter order matters: the first matching converter wins. Enum, date/time, URI, type, geospatial and derived-type converters are **not** added to plain minimal APIs; configure those explicitly if needed. Test the actual request/response contract; see [OpenAPI enum schemas](/arc/backend/csharp/open-api/enums/).
### Adding Source-Generated Metadata
Arc resolves the metadata for its own wire types - `CommandResult`, `QueryResult`, change sets, observable query messages and the identity responses - from a source-generated `JsonSerializerContext` it ships. Everything else, such as your read models, command responses and query arguments, resolves through reflection. In a trimmed or NativeAOT application reflection-based serialization is disabled, so Arc needs metadata for those types from you.
Declare a `JsonSerializerContext` for your types and add it with `AddJsonTypeInfoResolver`:
```csharp
using System.Text.Json.Serialization;
using Cratis.Arc.Commands;
[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata)]
[JsonSerializable(typeof(Order))]
[JsonSerializable(typeof(IEnumerable))]
[JsonSerializable(typeof(PlaceOrder))]
[JsonSerializable(typeof(CommandResult))]
public partial class AppJsonSerializerContext : JsonSerializerContext;
```
```csharp
builder.AddCratisArc(options => options.AddJsonTypeInfoResolver(AppJsonSerializerContext.Default));
```
`Order` and `PlaceOrder` stand in for your own read model and command, and `OrderId` for the response `PlaceOrder` returns. Arc’s `JsonSerializerOptions` look up a type’s metadata in this order:
1. The resolvers you add with `AddJsonTypeInfoResolver`. Your contracts win, including any you customize for Arc’s own types. They are consulted first whether you add them before or after you wrap, replace or clear `TypeInfoResolver`; added while Arc’s resolver is still in the chain, they are consulted in the order you add them.
2. Resolvers you append to `JsonSerializerOptions.TypeInfoResolverChain` with `Add`, in the order you append them.
3. Arc’s source-generated metadata for its own wire types.
4. The reflection-based resolver, only when reflection-based serialization is enabled (`JsonSerializer.IsReflectionEnabledByDefault`). This is the resolver `System.Text.Json` falls back to for options without one, so an application that adds nothing serializes exactly as before.
The serializer options still decide naming, null handling, number handling and converters, so the JSON is the same whichever resolver supplies the metadata. Use `JsonSourceGenerationMode.Metadata`, as Arc does. Arc’s options carry custom converters, so `System.Text.Json` cannot use a generated serialization fast path with them; generating one only adds code.
Keep these in mind:
* Register `CommandResult` for every response type your commands return, such as `CommandResult` above. Arc writes a command’s response wrapped in `CommandResult`, where `TResponse` is the response’s runtime type - a generic type its own metadata cannot cover, so registering `TResponse` alone is not enough. Without it, a command that returns a response fails with a `NotSupportedException` when reflection is disabled.
* Register the runtime types of query data, not only the declared ones. `QueryResult.Data` and change-set items are written by their runtime type, so if a query returns a `List`, register `List`.
* Add resolvers while configuring Arc. `JsonSerializerOptions` become read-only once they have been used.
* Arc keeps a single resolver of its own in `TypeInfoResolverChain`, after the resolvers added with `AddJsonTypeInfoResolver` and before anything you append. That resolver consults the resolvers appended after it first, so a resolver you append - for instance a `DefaultJsonTypeInfoResolver` with `Modifiers` that rename or ignore members - still decides the contract of every type it knows, Arc’s own included, as it did when it was the only resolver in the chain. Types it does not know fall through to Arc’s metadata and, when enabled, reflection. The same holds for a copy of Arc’s options, `new JsonSerializerOptions(options)`: a resolver appended to the copy wins for the copy and leaves Arc’s options alone.
* Assigning `JsonSerializerOptions.TypeInfoResolver` replaces the whole chain, Arc’s resolver included. The resolver you assign then serializes every type on its own, so with reflection disabled it has to cover Arc’s wire types too. Prefer `AddJsonTypeInfoResolver`.
* To add modifiers to every contract, wrap the resolver the options carry: `options.TypeInfoResolver = options.TypeInfoResolver!.WithAddedModifier(...)`. Arc’s resolver stays inside the wrapper, so the modifiers apply to what it resolves too. On .NET 8 and .NET 9, wrap `TypeInfoResolver` before appending anything to `TypeInfoResolverChain`, or build the combination explicitly with `JsonTypeInfoResolver.Combine(...)`: wrapping after appending crashes with a stack overflow on those versions. That is `System.Text.Json` behavior, not Arc’s.
* Arc’s options always carry a resolver. `TypeInfoResolver ??= x`, or `if (TypeInfoResolver is null) { ... }`, therefore no longer assigns anything - use `AddJsonTypeInfoResolver`, `TypeInfoResolverChain.Add` or an explicit assignment instead. And `TypeInfoResolver?.WithAddedModifier(...)` now takes effect.
* Assigning `options.TypeInfoResolver = JsonTypeInfoResolver.Combine(options.TypeInfoResolver, other)` flattens into Arc’s resolver followed by `other`, so `other` wins for the types it knows, like any appended resolver. Only appending a combination that contains Arc’s resolver, `options.TypeInfoResolverChain.Add(JsonTypeInfoResolver.Combine(options.TypeInfoResolver, other))`, resolves in the order you compose it: Arc’s resolver, and with it Arc’s metadata and reflection, before `other`.
* `AddJsonTypeInfoResolver` resolvers are consulted first in all these cases: after you wrap `TypeInfoResolver` they go ahead of the wrapper, and after you set it to `null` Arc’s resolver is composed anew behind them, so Arc’s metadata and the reflection fallback are kept. Once you have wrapped the resolver, a resolver you add later is consulted before the ones you added earlier.
* The resolvers apply to Arc’s own endpoints and serialization. MVC controllers and plain minimal APIs keep their own serializer options.
When you create options with `ConfigureArcDefaults()` outside `ArcOptions`, a resolver the options already carry stays first, followed by Arc’s resolver: `new JsonSerializerOptions { TypeInfoResolver = AppJsonSerializerContext.Default }.ConfigureArcDefaults()`.
## Best Practices
1. **Use Configuration Files**: For most scenarios, use `appsettings.json` configuration as it allows easy environment-specific overrides without code changes.
2. **Environment-Specific Settings**: Use `appsettings.{Environment}.json` files for environment-specific configurations.
3. **Programmatic Configuration**: Use programmatic configuration when you need to:
* Set configuration based on runtime conditions
* Use custom identity providers
* Override specific settings that can’t be easily expressed in JSON
4. **Route Planning**: Consider your API route structure carefully when configuring `GeneratedApis` options, especially in public-facing APIs where route stability is important.
5. **Header Standardization**: Use standard HTTP header names for correlation IDs and tenant IDs that align with your organization’s conventions and any API gateways or load balancers in use.
6. **JSON Consistency**: Use the injected `JsonSerializerOptions` for manual serialization that must match generated Arc endpoints. Configure and test MVC’s remaining options separately when controller output must match too.
# Combine request values with FromRequest
> Bind JSON and URL values into one MVC request model, then validate them together.
Use `[FromRequest]` when an MVC action needs a single input model assembled from JSON and URL values. Arc handles the merging, so the action and its validator can work with one object instead of stitching parameters together.
This is an ASP.NET Core integration feature, not the binding contract for model-bound Arc commands or queries.
## Overview
Arc binds the JSON body first, then uses MVC’s other request sources to fill properties that still hold their type’s default value. Non-default body values win.
See [default values and fallback](#default-values-and-fallback) when zero, false, or property initializers matter.
## Usage Examples
### Basic Usage
In an existing ASP.NET Core Arc application, declare the request model and use it in your controller action:
```csharp
using Cratis.Arc.ModelBinding;
using Microsoft.AspNetCore.Mvc;
public class UserUpdateRequest
{
public string Name { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
[FromRoute] public int UserId { get; set; }
[FromQuery] public bool NotifyUser { get; set; }
}
[HttpPut("users/{userId}")]
public IActionResult UpdateUser([FromRequest] UserUpdateRequest request)
{
// Apply the update through your application service after validation.
return Ok();
}
```
**Request Example:**
```http
PUT /users/123?notifyUser=true
Content-Type: application/json
{
"name": "John Doe",
"email": "john@example.com"
}
```
The action receives `UserId = 123`, `NotifyUser = true`, and the name and email from JSON. The example shows binding; `Ok()` alone does not persist an update.
Note
A non-default body value can also override a merged route ID. When the URL must identify the resource being changed, bind that ID separately with `[FromRoute]` and use it in the authorization and update—not the merged property.
### Fallback Scenarios
```csharp
public class SearchRequest
{
public string Query { get; set; } = string.Empty;
public int Page { get; set; } = 1;
public int PageSize { get; set; } = 10;
[FromRoute] public string Category { get; set; } = string.Empty;
}
[HttpPost("search/{category}")]
public IActionResult Search([FromRequest] SearchRequest request)
{
return Ok();
}
```
**Request with partial JSON:**
```http
POST /search/electronics?page=2&pageSize=20
Content-Type: application/json
{
"query": "laptop"
}
```
Result:
* `Query` = “laptop” (from JSON body)
* `Page` = 1 (initializer survives; it is not CLR default `0`)
* `PageSize` = 10 (initializer survives)
* `Category` = `""` (initializer survives; it is not CLR default `null`)
**Request with complete JSON:**
```http
POST /search/electronics?page=2&pageSize=20
Content-Type: application/json
{
"query": "laptop",
"page": 5,
"pageSize": 50
}
```
Result:
* `Query` = “laptop” (from JSON body)
* `Page` = 5 (from JSON body, overrides query parameter)
* `PageSize` = 50 (from JSON body, overrides query parameter)
* `Category` = `""` (initializer survives)
To make the category eligible for route fallback, declare it as `[FromRoute] public string? Category { get; set; }` and validate it after binding. To allow paging fallback, do not initialize the numeric properties to nonzero values; apply business defaults after merging. `[FromRequest]` does not track field presence, so it cannot distinguish an omitted integer from an explicitly supplied zero.
[]()
## Default values and fallback
Fallback compares a property’s value with its type’s default; it does not track whether the JSON field was present.
| Body-bound value | Eligible for fallback? |
| --- | --- |
| `0` for an integer | Yes |
| `false` for a Boolean | Yes |
| `null` for a reference | Yes |
| An empty string | No |
| A nonzero property initializer, such as `Page = 1` | No |
An explicit JSON zero or false can therefore be replaced by a route/query value. Apply application defaults after binding when you want missing values to remain eligible for fallback.
## Benefits
* **Cross-parameter validation:** a rule can check related values together—for example, require an email address when the request asks to notify the user.
* **One validation model:** the validator receives the combined input, regardless of which request source supplied each value.
* **Less controller plumbing:** the action receives one model instead of assembling it from separate parameters.
* **Focused validator specs:** test rules directly with request objects; use separate HTTP tests to verify binding.
Adding merging can change an existing endpoint’s contract. Keep the precedence deliberate and test the request combinations your clients use.
### Validation Scenarios
With one request model, a validator can check the name, email, and notification preference together.
**Traditional approach (multiple parameters):**
```csharp
[HttpPut("users/{userId}")]
public IActionResult UpdateUser(
[FromRoute] int userId,
[FromQuery] bool notifyUser,
[FromBody] UserUpdateData data)
{
// Combine the inputs before applying cross-property rules.
return Ok();
}
```
**With \[FromRequest]:**
```csharp
using Cratis.Arc.ModelBinding;
using FluentValidation;
using Microsoft.AspNetCore.Mvc;
public class UserUpdateRequest
{
[FromRoute] public int UserId { get; set; }
[FromQuery] public bool NotifyUser { get; set; }
public string Name { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
}
public class UserUpdateRequestValidator : AbstractValidator
{
public UserUpdateRequestValidator()
{
RuleFor(x => x.UserId).GreaterThan(0);
RuleFor(x => x.Name).NotEmpty().MaximumLength(100);
RuleFor(x => x.Email).EmailAddress();
// Cross-property validation is now possible
RuleFor(x => x.NotifyUser)
.Equal(false)
.When(x => string.IsNullOrEmpty(x.Email))
.WithMessage("Cannot notify user without email address");
}
}
[HttpPut("users/{userId}")]
public IActionResult UpdateUser([FromRequest] UserUpdateRequest request)
{
// Validate the merged request before applying the update.
return Ok();
}
```
Register and invoke the validator through your MVC validation setup. `[FromRequest]` performs binding; declaring an `AbstractValidator` alone does not establish when it runs. Test the validator directly with merged request objects, and use an HTTP test to check binding.
## Integration with Other Features
### Swagger/OpenAPI
Arc’s `FromRequestOperationFilter` describes `[FromRequest]` parameters as body schemas. Check the generated operation’s route and query parameters too; see [Swagger integration](/arc/backend/csharp/asp-net-core/swagger/) for its configuration and limits.
### Proxy Generation
Model binding and client generation have separate requirements. Check the [proxy generator’s supported endpoint shapes](/arc/backend/csharp/proxy-generation/) when choosing the client for a mixed-source MVC action.
## Best Practices
* Use `[FromRequest]` when combining request sources reduces manual binding work.
* Keep fallback-eligible properties unset until merging finishes.
* Document the permitted sources and precedence for your clients.
* Validate the merged input and authorize the resource before writing.
## Limitations
Merging compares values, not JSON field presence. For partial updates that must distinguish “omitted” from an explicit zero, false, or null, use a request contract that represents that distinction. Test nested models and conflicting input sources through HTTP; a direct validator test does not exercise binding.
# Invariant Culture
> Make an ASP.NET Core Arc host use invariant culture with UseInvariantCulture() so parsing and formatting do not depend on machine settings.
Arc provides a `.UseInvariantCulture()` extension method that configures your application to use invariant culture throughout, ensuring consistent and predictable behavior regardless of the host machine’s regional settings.
## Why Invariant Culture?
Modern distributed applications run across different machines, containers, and cloud regions—each potentially configured with different regional settings. Without explicit culture configuration, operations like number parsing, date formatting, or string comparisons may produce inconsistent results depending on where the code runs.
Invariant culture supplies predictable defaults for culture-sensitive code; explicit per-call or per-thread culture choices can still override them:
* **DateTime formatting and parsing** uses invariant conventions by default; it does not choose a time zone
* **Number formatting and parsing** produces consistent results (e.g., decimal separator is always `.`)
* **String comparisons and sorting** are culture-independent
* **Serialization and deserialization** still follows each serializer’s contract and explicit options
## Configuration
In an ASP.NET Core web project with `Cratis.Arc`, import `Cratis.Arc`. Call `UseInvariantCulture()` on the `WebApplicationBuilder` before building the application, and on the resulting `WebApplication` to activate the request localization middleware:
```csharp
var builder = WebApplication.CreateBuilder(args);
builder.UseInvariantCulture();
builder.AddCratisArc();
var app = builder.Build();
app.UseInvariantCulture();
app.UseCratisArc();
await app.RunAsync();
```
> **Important**: Call `app.UseInvariantCulture()` before any middleware that processes request data to ensure invariant culture is applied to all incoming requests.
## What Gets Configured
Calling `UseInvariantCulture()` on the builder:
* Sets `CultureInfo.DefaultThreadCurrentCulture` to `CultureInfo.InvariantCulture`
* Sets `CultureInfo.DefaultThreadCurrentUICulture` to `CultureInfo.InvariantCulture`
* Configures `RequestLocalizationOptions` to use only `InvariantCulture`
* Removes all request culture providers, preventing clients from overriding the culture via `Accept-Language` headers or query strings
Calling `UseInvariantCulture()` on the application:
* Registers the ASP.NET Core request localization middleware with the invariant culture settings
## Practical Example
Consider an API that accepts a price value. Without invariant culture, `"1,5"` might parse as `1.5` on a European machine but fail on an English-locale machine. Invariant culture stabilizes culture-sensitive parsing defaults. JSON numeric values already use JSON’s culture-independent syntax; request localization does not define that wire format. This complete ASP.NET `Program.cs` example receives a numeric JSON `price`:
```csharp
using Cratis.Arc;
var builder = WebApplication.CreateBuilder(args);
builder.UseInvariantCulture();
builder.AddCratisArc();
var app = builder.Build();
app.UseInvariantCulture();
app.UseCratisArc();
app.MapPost("/products", (ProductRequest request) =>
{
// JSON binding uses the serializer's numeric contract
return Results.Ok(new { Price = request.Price });
});
await app.RunAsync();
record ProductRequest(decimal Price, DateOnly ExpiryDate);
```
## When to Use
Invariant culture is recommended for:
* **APIs and services** that exchange data between systems
* **Applications deployed in multiple regions** or cloud environments
* **Data processing pipelines** where consistency is critical
* **Any scenario** where culture-sensitive behavior could lead to bugs or data inconsistencies
It is generally safe to apply invariant culture in back-end services, as the locale-specific formatting should be handled on the client side (e.g., a frontend application) rather than in the API itself.
# Microsoft Identity
> Build application identity details from Microsoft identity platform headers supplied by a trusted ingress.
Cratis’ Arc provides a way to easily work with providing an object that represents properties the application finds important for describing the logged in user. The purpose of this is to provide details about the logged in user on the ingress level of an application and letting it provide the details on the request going in. Having it on the ingress level lets you expose the details to all microservices behind the ingress.
The values provided by the provider are values that are typically application specific and goes beyond what is already found in the token representing the user. This is optimized for working with Microsoft Azure well known HTTP headers passed on by the different app services, such as Azure ContainerApps or WebApps. Internally, it is based on the following HTTP headers to be present.
| Header | Description |
| --- | --- |
| x-ms-client-principal | The unsigned principal payload holding the details, base64 encoded [Microsoft Client Principal Data definition](https://learn.microsoft.com/en-us/azure/static-web-apps/user-information?tabs=csharp#client-principal-data) |
| x-ms-client-principal-id | The unique identifier from the identity provider for the identity |
| x-ms-client-principal-name | The name of the identity, typically resolved from claims within the token |
> Important note: Since local development is not configured with the identity provider, but you still need a way to test that both the backend and the frontend deals with the identity in the correct way. This can be achieved by creating the correct token and injecting it as request headers using a browser extension. Read more about [generating principal tokens for local development](/arc/backend/csharp/development/generating-principal/).
The token in the `x-ms-client-principal` should be a base64 encoded [Microsoft Client Principal Data definition](https://learn.microsoft.com/en-us/azure/static-web-apps/user-information?tabs=csharp#client-principal-data).
Caution
Use these headers only behind ingress that authenticates callers, strips caller-supplied identity headers, writes trusted replacements, and prevents direct backend access. Base64 is not a signature. Generated principals are local test fixtures, not credentials to accept on an internet-facing service. The identity Arc returns from enrichment is likewise not an authentication ticket.
## Authentication / Authorization
To get the Microsoft Client Principal supported in your backend, the Arc offers an `AuthenticationHandler` that supports the HTTP headers and does the right thing to put ASP.NET Core and every `HttpContext` in the right state.
You can add this by calling the `AddMicrosoftIdentityPlatformIdentityAuthentication()` method on your services.
This is a registration fragment for an ASP.NET Core application using `Cratis.Arc` (not `ArcApplication`):
```csharp
using Cratis.Arc;
using Cratis.Arc.Identity;
var builder = WebApplication.CreateBuilder(args);
builder.AddCratisArc(options => options.TrustForwardedIdentityHeaders = true);
builder.Services.AddMicrosoftIdentityPlatformIdentityAuthentication();
builder.Services.AddAuthorization();
```
The above code will then also call the `.AddAuthentication()` with the default scheme name (**MicrosoftIdentityPlatform**) and register the appropriate `AuthenticationHandler` for that scheme.
### Trusting the forwarded headers
Registering the scheme is not enough on its own. Because the headers are not signed, the handler ignores them and returns no result, so requests stay anonymous unless another scheme authenticates them, until the host opts in with `TrustForwardedIdentityHeaders`. Set it in code, as above, or in configuration:
```json
{
"Cratis": {
"Arc": {
"TrustForwardedIdentityHeaders": true
}
}
}
```
Opt in only when every request reaches the application through an ingress that strips caller-supplied identity headers and sets its own, such as Azure App Service or Container Apps authentication (EasyAuth) or Cratis AuthProxy. A host that calls `AddMicrosoftIdentityPlatformIdentityAuthentication()` (including through `AddCratis()`) while the headers are not trusted logs a warning that names this setting on every startup. The built-in header handlers also warn on the first request that carries untrusted headers. The setting applies to every endpoint, including protected [introspection catalogs](/arc/backend/csharp/introspection/).
You can override the scheme name on the extension method by passing your own string as an argument.
For it to be appropriately setup, you’ll need to enable the default authentication and authorization on your app, like below:
```csharp
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.UseCratisArc();
app.Run();
```
This establishes the host authentication mechanism; apply [authorization requirements](/arc/backend/csharp/asp-net-core/authorization/) to private operations. Identity details do not automatically protect every endpoint.
## Knowing which identity provider signed the caller in
The `x-ms-client-principal` payload carries an `identityProvider` field describing the provider the ingress authenticated the caller with. Without it, everything downstream sees is a set of claims that look the same whether they came from Entra ID or GitHub — so the application has no way to tell one federation apart from another.
Arc keeps that value on the reconstructed principal as a claim, so both normal request authorization and the `/.cratis/me` identity resolution can read it:
```csharp
using Cratis.Arc.Identity;
using Microsoft.AspNetCore.Http;
public class IdentityProviderReader(IHttpContextAccessor httpContextAccessor)
{
public string? Current =>
httpContextAccessor.HttpContext?.User.FindFirst(MicrosoftIdentityPlatformClaims.IdentityProvider)?.Value;
}
```
| Aspect | Detail |
| --- | --- |
| Claim type | `urn:cratis:arc:identity:provider` — use the `MicrosoftIdentityPlatformClaims.IdentityProvider` constant |
| Value | The exact `identityProvider` value the ingress forwarded, for example `aad` or `github` |
| When absent | The forwarded principal carried no `identityProvider` field, or the field held only blank characters |
The claim type is **reserved for Arc**. The `x-ms-client-principal` header is base64, not a signature, so any caller that can reach the application can put whatever it likes in the serialized payload — including a claim of this very type. Arc therefore removes every claim of the reserved type from the deserialized payload, ignoring casing, *before* writing its own value.
Caution
**What the strip guarantees is single provenance, not authenticity.** When a nonblank provider value is supplied, the claim carries exactly one value, and that value always comes from one place — the `identityProvider` field of the forwarded principal — so a claim of the reserved type passed through by the ingress or by the identity provider can never displace it. It does **not** make the value trustworthy. The same unsigned header carries that field too, and Arc does not check who sent the header, so a caller that can reach the application authors the whole document; the strip only decides which of its fields wins. **Trust this claim exactly as far as you trust the `x-ms-client-principal` header itself — that is, only insofar as your ingress is the only thing that can set it.** Terminate the header at the ingress and reject or overwrite an inbound one; nothing inside Arc can do that for you.
Read the claim with `FindFirst` or `FindAll`, as in the example above, and **never normalize the claim type yourself**. Those lookups compare the claim type the same way the strip does, so what they return is exactly what Arc wrote. Enumerating `User.Claims` and folding the type with `ToUpperInvariant()` or `Trim()` widens the match beyond what was removed — a forged type differing only by Unicode case folding (`urn:cratiſ:arc:identity:provider`, U+017F) or by trailing whitespace then matches, and because forwarded claims are added before Arc’s own, a `FirstOrDefault()` over that widened set returns the forgery.
Note
The value is the exact `identityProvider` field the ingress forwarded, verbatim and untrimmed — Arc neither interprets nor normalizes it. What it *means* is therefore the ingress’s choice, not Arc’s. Cratis AuthProxy forwards the canonical provider key, the same value it publishes as its own `urn:cratis:identity:provider-key` claim, so in a canonical AuthProxy deployment the two carry identical values. Another ingress may forward an authentication scheme name or a provider display name that changes when the provider is renamed. **Arc guarantees neither**, so use this claim for telling federations apart, for diagnostics, and for provider-aware behavior. If you need a durable provider key, read the claim the ingress publishes for that purpose — `urn:cratis:identity:provider-key` in an AuthProxy deployment — rather than this one. Arc copies such ingress-authored claims through untouched.
## Identity Details
For information about providing additional identity details for logged-in users, including authorization checks and custom identity information, see the [Identity documentation](/arc/backend/csharp/identity/).
Use Arc’s identity-details provider to add application information to the principal supplied by Microsoft Identity. Those details help the UI; authorization still checks the authenticated principal.
# Swagger
> Describe Arc commands, queries, and concepts in Swashbuckle Swagger documents with the Cratis.Arc.Swagger filters.
Cratis Arc provides Swagger/OpenAPI filters through the optional `Cratis.Arc.Swagger` package for ASP.NET Core hosts. These describe Arc conventions, but do not guarantee that every generated schema matches the runtime contract. Review the [current limitations](#current-limitations) before generating clients.
## Overview
The Swagger extension adds filters for:
* **Concepts** - Maps concept types to their underlying primitive types
* **Commands** - Adds result-wrapper and error response schemas
* **Queries** - Adds result-wrapper schemas and paging/sorting parameters where applicable
* **FromRequest attributes** - Describes request-body binding
* **Model-bound endpoints** - Attempts to enrich generated command and query operations
## Setup
Add the `Cratis.Arc.Swagger` package, which brings Swashbuckle with it, then register the document generator with Arc’s filters and expose the document and UI in `Program.cs`:
```csharp
using Cratis.Arc.Swagger;
var builder = WebApplication.CreateBuilder(args);
builder.AddCratisArc();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options => options.AddConcepts());
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseCratisArc();
await app.RunAsync();
```
`AddConcepts()` registers the schema and operation filters; it does not serve anything by itself. `UseSwagger()` serves `/swagger/v1/swagger.json`, which lists the Arc command, validate, and query routes alongside Arc’s `/.cratis/*` endpoints, and `UseSwaggerUI()` serves the browser UI at `/swagger`. The example serves both only in Development; decide deliberately whether a production host should publish its API description.
## Features
### Concept Schema Filter
Automatically maps concept types (types inheriting from `ConceptAs`) to their underlying primitive types in the Swagger schema. This ensures that concepts appear as their actual data types (string, int, Guid, etc.) rather than complex objects in the API documentation.
**Example:**
```csharp
using Cratis.Concepts;
public record UserId(Guid Value) : ConceptAs(Value);
// In Swagger, UserId parameters will appear as string (UUID format)
// instead of a complex object with a Value property
```
### Command Result Operation Filter
Enhances command endpoints by:
* Adding `CommandResult` or `CommandResult` response schemas based on inferred return types
* Including standard HTTP status codes (200, 400, 403, 500) with appropriate error schemas
* Handling void/Task return types correctly
* Supporting concept return types
### Query Result Operation Filter
Enhances query endpoints by:
* Adding `QueryResult` response schemas
* Including standard HTTP status codes with error handling
* Adding pagination and sorting parameters where the filter detects paging support
* Supporting concept return types
### FromRequest Operation Filter
Properly handles the `[FromRequest]` attribute by:
* Removing the parameter from the query string/path parameters
* Adding it as a request body with the correct JSON schema
* Supporting complex model binding scenarios
### Model-Bound Operation Filters
Attempts to match generated command and query operations to registered handlers and performers, then adds request and result schemas. Matching and type inference have the limitations below.
### Pagination and Sorting Parameters
For matched model-bound query performers with `SupportsPaging = true` (`IQueryable` results), the filter adds the following parameters:
| Parameter | Type | Description |
| --- | --- | --- |
| `sortby` | string | Field name to sort by |
| `sortDirection` | string | Sort direction (`asc` or `desc`) |
| `pageSize` | integer | Number of items per page |
| `page` | integer | Page number (0-based) |
## Enum Schema Filter
Replaces enum values with their names, without changing the schema type. This can leave string members in an integer schema and does not change Arc’s default numeric wire format. See [enum limitations](/arc/backend/csharp/open-api/enums/).
## Response Schemas
The filters add the following response descriptions when they match an operation. These are generated descriptions, not proof of the runtime payload or every possible status:
### Success Responses (200)
* Commands: `CommandResult` or `CommandResult`
* Queries: `QueryResult`; the model-bound filter does not supply a concrete typed `data` contract
### Error Responses
* **400 Bad Request**: Validation errors or malformed requests
* **403 Forbidden**: Authorization failures
* **500 Internal Server Error**: Unexpected server errors
The model-bound filters reuse the result schema for the listed error responses; compare it with actual error responses from your host.
## Current limitations
The Swagger implementations share the limitations of the [Microsoft OpenAPI model-bound transformers](/arc/backend/csharp/open-api/model-bound/) and [enum transformer](/arc/backend/csharp/open-api/enums/):
* **Command adapter inference:** The model-bound command filter inspects the registered adapter’s `Handle`, not the command record’s return contract. The standard adapter returns `ValueTask