---
title: Configuration reference
description: Arc Spring Boot properties, Gradle plugin extension fields, defaults, coordinates, and tasks.
---


## Published identities

| Component | Identity |
| --- | --- |
| Runtime | `io.cratis:arc:<version>` |
| KSP processor | `io.cratis:arc-ksp:<version>` |
| Spring Boot integration | `io.cratis:arc-spring-boot-starter:<version>` |
| Spring Data JPA | `io.cratis:arc-spring-data-jpa:<version>` |
| Spring Data MongoDB | `io.cratis:arc-spring-data-mongodb:<version>` |
| OpenAPI | `io.cratis:arc-openapi-spring-boot-starter:<version>` |
| Observability | `io.cratis:arc-observability-spring-boot-starter:<version>` |
| Chronicle integration | `io.cratis:arc-chronicle-spring-boot-starter:<version>` |
| Testing | `io.cratis:arc-testing:<version>` |
| Gradle plugin | Plugin ID `io.cratis.arc`; implementation artifact `io.cratis:arc-gradle-plugin:<version>` |

Local builds default to `0.0.0-SNAPSHOT`; `-Pversion=<version>` overrides it. A local coordinate is not automatically available in a consumer build until it is published or supplied by a composite build.

## Arc tenancy options

`Source` exposes immutable `TenancyOptions` for request adapters and tests. The defaults match Arc on .NET:

| Option | Default | Meaning |
| --- | --- | --- |
| `headerName` | `x-cratis-tenant-id` | Case-insensitive request header used by header resolution and subdomain fallback. |
| `queryParameterName` | `tenantId` | Case-sensitive query parameter. |
| `claimType` | `tenant_id` | Claim read from explicit context claims, then principal claims. |
| `baseDomain` | Empty | Required by `SubdomainTenantIdResolver`; must contain at least two valid DNS labels and cannot be an address. |
| `fixedTenantId` | `development` | Value returned by fixed and development resolvers. |

Every resolver receives a `TenantResolutionContext` containing explicit headers, query parameters, host, claims, and principal. No resolver reads thread-local or ambient request state. `CompositeTenantIdResolver` tries resolvers in list order and returns the first nonblank result, so declaration order is the complete precedence rule.

`SubdomainTenantIdResolver` accepts exactly one DNS label before `baseDomain` and otherwise falls back to `headerName`. For example, `acme.myapp.com` resolves `acme` for `myapp.com`; `myapp.com`, `one.two.myapp.com`, unrelated hosts, invalid labels, and address literals use the fallback.

## Spring Boot properties

All properties use the `cratis.arc` prefix.

| Property | Default | Meaning |
| --- | --- | --- |
| `endpoints.route-prefix` | `api` | Prefix for conventional routes. |
| `endpoints.segments-to-skip-for-route` | `0` | Leading package segments omitted from routes. Must be nonnegative. |
| `endpoints.include-command-name-in-route` | `true` | Includes command names in conventional routes. |
| `endpoints.include-query-name-in-route` | `true` | Includes query names in conventional routes. |
| `endpoints.enable-query-http-method` | `true` | Adds RFC QUERY alongside GET for one-shot and observable snapshot routes. |
| `correlation-header` | `X-Correlation-ID` | Java-friendly request and response header name used consistently by every route in the host. Must not be blank. |
| `correlation-enabled` | `true` | Registers the host-wide correlation filter for `/*`. Disable it when the application owns correlation itself. |
| `tenant-header` | `x-cratis-tenant-id` | Backward-compatible alias and default for `tenancy.header-name`. |
| `tenancy.resolvers` | `header` | Ordered resolver precedence. Values are `fixed`, `header`, `query`, `claim`, `subdomain`, and `development`. |
| `tenancy.required` | `false` | Rejects unresolved command, query, observable, and identity requests with 400 when enabled. |
| `tenancy.header-name` | Unset | Case-insensitive header name; an unset value uses `tenant-header`. |
| `tenancy.query-parameter-name` | `tenantId` | Exact case-sensitive query parameter name. |
| `tenancy.claim-type` | `tenant_id` | Principal claim used for resolution and authenticated membership checks. |
| `tenancy.base-domain` | Empty | Valid multi-label DNS base domain required by subdomain resolution. |
| `tenancy.fixed-tenant-id` | `development` | Value returned by fixed and development resolution. |
| `tenancy.constrain-to-authenticated-claims` | `true` | When authenticated claims of `claim-type` exist, the resolved tenant must be one of them. |
| `spring-data.jpa.command-transactions-enabled` | `false` | Opts a verified fixed JPA persistence unit into an imperative, thread-bound command transaction scope. Dynamic tenant units are never auto-enrolled. |
| `spring-data.mongodb.command-transactions-enabled` | `false` | Opts a verified fixed MongoDB template with an identity-aligned manager into an imperative, thread-bound command transaction scope. Dynamic tenant stores are never auto-enrolled. |
| `request-timeout` | `30s` | Async servlet request and bounded first-result ceiling. |
| `coroutine-parallelism` | `4` | Maximum actively executing Arc request coroutines; must be greater than zero. |
| `coroutine-queue-capacity` | `256` | Maximum admitted Arc operations waiting for execution; zero permits no waiting. |
| `overload-retry-after-seconds` | `5` | `Retry-After` value when general Arc request admission is exhausted. |
| `maximum-request-body-bytes` | `1048576` | Maximum command or QUERY body, enforced for declared and streamed lengths. |
| `command-scope-completion-timeout` | `5s` | Independent cooperative timeout for each best-effort command execution-scope completion. |
| `platform-identity.enabled` | `false` | Opts into the optional Spring Security platform-header bridge. The default trust policy denies all ingress; supply an application `ArcPlatformIdentityTrust` and secure, header-rewriting ingress. With no application chain, all routes require authentication and CSRF stays enabled; see [platform security](/arc/backend/kotlin/guides/security/#establish-trust-before-accepting-platform-headers). |
| `identity-cookie-secure-policy` | `auto` | `always`, `never`, or `auto`; auto secures HTTPS and every non-development profile. |
| `expose-exception-details` | Profile-derived | When unset, enabled only for `dev`, `development`, or `local` profiles. |
| `observable-queries.wait-for-first-result-timeout` | `30s` | Maximum default wait for the first HTTP snapshot result. A request may shorten it. |
| `observable-queries.keep-alive-interval` | `30s` | Idle interval between hub `Ping` messages; zero disables heartbeats. |
| `observable-queries.connection-timeout` | `30m` | Maximum SSE connection lifetime; zero disables the transport timeout. |
| `observable-queries.maximum-connections` | `1000` | Concurrent direct and multiplexed streaming connections. Exhaustion returns 503. |
| `observable-queries.maximum-subscriptions-per-connection` | `100` | Active subscriptions on one hub connection. Exhaustion returns 429 for SSE POST. |
| `observable-queries.outbound-buffer-capacity` | `64` | Pending outbound frames per connection before fail-closed cleanup. |
| `observable-queries.maximum-inbound-message-size` | `65536` | Maximum WebSocket text message size in bytes. |
| `observable-queries.overload-retry-after-seconds` | `5` | `Retry-After` value on 503/429 overload responses. |
| `observable-queries.web-socket-enabled` | `true` | Registers WebSocket routes when Spring WebSocket is on the classpath. |
| `observable-queries.allowed-origins` | _(empty)_ | Browser origins allowed to open an observable-query WebSocket. Empty means same-origin only. |

:::caution[A dev server needs its origin named]
Spring answers a cross-origin WebSocket handshake with `403`, and a browser reports that as a socket
that never opens rather than as an error you can see. Serving the page from a dev server on another
port makes every handshake cross-origin, so set `cratis.arc.observable-queries.allowed-origins` to
that origin — `http://localhost:5173`, for example. Leave it empty in a deployment: the handshake is
not subject to the same-origin policy `fetch` obeys, so this check is what stops another site from
opening a socket with your visitor's cookies attached.
:::

The optional observability starter adds properties under `cratis.arc.observability`:

| Property | Default | Meaning |
| --- | --- | --- |
| `enabled` | `true` | Decorates Arc execution contracts when a non-no-op `ObservationRegistry` bean exists. |
| `correlation-baggage-enabled` | `true` | Places command and query correlation identifiers in OpenTelemetry baggage when its API is available. |
| `correlation-logging-enabled` | `true` | Places command and query correlation identifiers in SLF4J MDC when its API is available. |

Correlation identifiers are observation context, baggage, and logging values, not metric or span tags. The starter never records tenant identifiers, user identifiers, command values, query arguments, claims, headers, or cookies.

## Gradle plugin extension

`cratisArc` fields:

| Field | Default | Meaning |
| --- | --- | --- |
| `moduleName` | Project name | Stable KSP artifact module name; cannot be blank. |
| `dependencyVersion` | Plugin implementation version | Version for managed `arc` and `arc-ksp` dependencies. |
| `manageDependencies` | `true` | Adds missing runtime and KSP dependencies. |
| `endpoints.routePrefix` | `api` | Proxy route prefix. |
| `endpoints.segmentsToSkip` | `0` | Package segments omitted from proxy routes. |
| `endpoints.includeCommandNames` | `true` | Includes command names. |
| `endpoints.includeQueryNames` | `true` | Includes query names. |
| `endpoints.enableQueryHttpMethod` | `true` | Permits QUERY selection in proxies. |
| `proxies.enabled` | `true` | Enables generation when an output directory exists. |
| `proxies.outputDirectory` | Unset | Generated TypeScript directory. An unset value skips generation. |
| `proxies.removeStaleGeneratedFiles` | `true` | Removes only stale Arc-marked files. |
| `proxies.segmentsToSkip` | `0` | Package segments omitted from generated import layout. |

Resolver configuration is validated at startup: the resolver list cannot be empty or contain duplicates, selected resolver options cannot be blank, fixed/development identifiers must be nonblank, and subdomain configuration must have a valid base domain. `CompositeTenantIdResolver` follows the listed order exactly. An application-defined `TenantIdResolver` or `TenantAccessEvaluator` bean replaces the default with Spring Boot's normal `@ConditionalOnMissingBean` behavior.

The host captures case-insensitive headers, each query parameter's first value, host, principal, and claims into an explicit `TenantResolutionContext` at request or subscription entry. Resolution is performed once and the resulting value is supplied as both `tenantId` and `tenantNamespace`; no thread-local state is used. Optional unresolved tenancy remains `null` for backward compatibility. Fixed or development resolution must be configured explicitly before either can supply a default. Access denial is generic and returns 403.

The plugin applies Kotlin/JVM `2.4.10` and KSP `2.3.11` (KSP2), targets JDK 17, and treats Kotlin and Java warnings as errors. Use Gradle 8.14.4, the repository's wrapper baseline. See the [compiler compatibility notes](/arc/backend/kotlin/get-started/#compiler-compatibility) before retaining older consumer compiler or runtime pins. `generateArcProxies` depends on main classes, KSP, and resources, and `build` depends on it. Use the standard active JDK selection through `JAVA_HOME` and `PATH`; the repository requires no machine-specific absolute JDK path.

KSP diagnostics use stable `ARCKSP` codes. Configuration, command, query, proxy-shape, validation-metadata, enum-wire-value, and interoperability diagnostics are cataloged in `CodeGeneration/KSP/DIAGNOSTICS.md`; errors stop generation, while warnings identify risky but compilable conventions.

Published Kotlin APIs use checked `.api` baselines. The root `apiCheck` gate covers `arc`, KSP, the Gradle plugin implementation artifact, every published integration, and testing support; contract tests and runnable samples are intentionally unpublished and excluded. An API change therefore requires an explicit baseline update rather than silently changing compiler or plugin contracts. The temporal/UUID proxy slice changes generated TypeScript source types. Recursive shape metadata and source summaries were introduced in manifest format 6. Format 7 added optional typed command `eventMetadata`; current format 8 requires explicit property `ignoreValidation` metadata. Immutable public JVM shape descriptors, earlier constructor descriptors, and compatibility getters remain available. See the [format-8 migration and Jakarta factory contract](/arc/backend/kotlin/reference/validation/#manifest-format-8-migration). Because the reader accepts exactly the current version, a dependency built against an earlier format must be rebuilt.

The runtime proxy harness has five wired unit tests plus 33 behavioral runtime tests. It runs four ignore-validation tests, four shared-fluent tests, and 15 general tests in separate UTC Node processes, then five calendar tests in a separate UTC process and the same five in a separate `America/Los_Angeles` process. Counts are parsed from each TAP summary, which must report its exact expected test/pass total and zero fail, cancelled, skipped, or todo results. Spring process-spawn errors fail cleanly instead of hanging startup.

Repository proxy verification has three distinct gates: deterministic generation plus strict TypeScript compilation with `verbatimModuleSyntax`, comparison with a repository-local semantically normalized .NET-derived expected fixture intended for source control, and `:ContractTests:typeScriptRuntimeTest` against the executable Kotlin Spring Boot sample. The differential compares sorted paths for all regular files and untouched JVM bytes, including headers, against prepared expected bytes. Only the expected side receives LF/trailing-whitespace handling, FixtureModel quote/indent preparation, CreateFixtures quote/import/layout/hook preparation with exact-site suppression, and five literal type-only import rewrites for `verbatimModuleSyntax`. An independent literal source table, cross-checked against fixture descriptors, validates 16 artifacts; queries use their declaring model. Uppercase SHA-256 headers are reconstructed from prepared expected bodies, never actual output, and three indexes stay headerless. Historical capture-time namespace/query-name casing and removed timestamps/hashes remain embedded in the fixture. `SetCommandValues`, `ClearCommandValues`, and query helper types such as `PerformQuery`, `SetSorting`, `SetPage`, `SetPageSize`, and `ChangeSet` become type-only imports. One additional expected-side correction at `Commands/CreateFixtures.ts` changes `Command<ICreateFixtures, FixtureModel>` to `Command<ICreateFixtures, FixtureModel[]>`. The captured command already calls `super(FixtureModel, true)`, so its scalar generic contradicts enumerable runtime behavior. Only `Models/Observe.ts` also prepares the zero-space blank line immediately before the four-space-indented `filter: string;` member in exactly one known `ObserveParameters` block as four spaces; `ObserveOne.ts` is excluded. Missing/duplicate correction anchors and misplaced suppressions fail preparation. The .NET-derived `FixtureModel.labelsByCategory` capture already uses `@field(Object)` and `Record<string, string>`; no dictionary-shape rewrite is applied to it, although its file receives the documented formatting preparation. It proves that one string-key/string-value Record fixture, not non-string keys, nullable entries, typed model values, `ValueMap`, or broader dictionary parity. `Contracts/Shape.ts` is a class, not interface-emission proof. No normalization transforms JVM output. Capture SDK/tool versions remain unverified; this remains a normalized-fixture drift gate, not raw .NET-output equivalence, and overall parity remains Partial. The current .NET fixture contains no `Guid`, `DateOnly`, or `TimeOnly`, so focused generator and contract tests cover that mapping. See [Generate TypeScript proxies](/arc/backend/kotlin/guides/typescript-proxies/) for the complete rewrite inventory, source table, independent hash contract, and remaining capture-time reproducibility limitation.

## Binary validation annotation inputs

The Arc plugin supplies KSP option `arc.validationClasspath` for each compilation: sorted absolute
file URIs for the real compile-classpath directories/JARs, joined by `|`. The same file collection is
a **nonincremental classpath input**, not a compile-ABI-only input. Private record field annotations
can change without a public ABI change. KSP reads class-file field/accessor annotations without
loading application classes, combining both sites instead of losing a field constraint when its
accessor has another constraint of the same type.

Manual KSP wiring needs the matching option and input declaration; see the actual Kotlin sample
and ContractTests build configurations linked from [manual validation tooling](/arc/backend/kotlin/reference/validation/#manual-build-tooling).
Do not pass an unrelated runtime classpath. Missing field evidence or conflicting/unreadable record
definitions fail with `ARCKSP0311`; malformed input locations fail with `ARCKSP0001`. This option
creates no new metadata resource and does not replace the format-8 manifest or the independent
fluent declaration fingerprint. `ArcFluentValidationNativeFunctionalTest` proves private-field edits,
unchanged-consumer invalidation, and fresh-output equivalence; the existing response-handler native
fixture also proves ordinary unannotated binary records remain supported.

## Dependency response-handler metadata

Arc KSP exports validated local `@HandlesCommandResponseValues` declarations to
`META-INF/cratis/arc-response-handlers/<moduleName>.json`. This is a separate format-1
compiler/build transport, separate from the format-8 artifact manifest.
A handler-only producer emits the declaration resource without manufacturing an artifact
module, service entry, or artifact manifest. Imported declarations are never re-exported.

The producer document has exactly `formatVersion`, `moduleName`, and `handlers`:

```json
{"formatVersion":1,"moduleName":"Audit","handlers":[{"handlerTypeName":"example.AuditHandler","handledTypeNames":["example.AuditEntry"]}]}
```

The aggregate index has exactly `formatVersion: 1` and `modules: [producer documents]`;
no dependencies produces `{"formatVersion":1,"modules":[]}`. Output is canonical UTF-8,
with fixed property order, sorted modules, handlers, and handled names, and one terminal
newline. Paths and timestamps are not serialized. Identical module documents deduplicate;
the same module name with different declarations fails. Identical handler claims may appear
in different modules, but conflicting claims fail. Different handlers may handle the same
value. Duplicate handler entries or handled names within one document, duplicate JSON
fields, unknown fields, malformed types, and unsupported versions fail closed.

Binary export initially supports public top-level handlers and public top-level handled
types with ASCII canonical names. Other supported source-local declarations remain
source-local. Consumers resolve indexed names through the current KSP resolver each round
and verify their annotation and supported Kotlin/blocking/async handler SPI. Missing classes
or disagreement fail with `ARCKSP0102`, naming the binary resource rather than inventing a
source location. Invalid or unreadable `arc.responseHandlerMetadata` options fail with
`ARCKSP0001`. The option accepts an absolute `file:` URI, including percent-encoded spaces;
omitting it retains source-only discovery.

The Arc plugin registers `extractMainArcResponseHandlerMetadata`,
`extractTestArcResponseHandlerMetadata`, and corresponding tasks for other JVM compilations.
Each resolves its compilation's dependency artifacts as JARs, not its own outputs. Main and
test indexes are independent. Index wiring does not add a processor dependency to additional
compilations: for generated test artifacts, also add `io.cratis:arc-ksp:<version>` to `kspTest`. `ExtractArcResponseHandlerMetadata.dependencyArtifacts` is
`@Classpath` because resource changes matter, and the output is written only when its bytes
change. `ArcResponseHandlerMetadataArgumentProvider.metadataFile` is path-insensitive.
The index is also a **nonincremental task input**: a changed index requires complete KSP
reclassification in that compilation and can rerun other processors there. Ordinary source
edits and no-op builds retain the existing incremental behavior. Runtime registration and
`canHandle` remain separate; missing or nonmatching runtime handlers still fail closed.

For manual Gradle/KSP builds, put `io.cratis:arc-gradle-plugin:<version>` on the buildscript
classpath without applying `io.cratis.arc`. The following Groovy wiring is exercised by the
native producer/consumer contract; retain your runtime, KSP, and repository declarations.
Use the same published Arc version throughout (the local workspace version is
`0.0.0-SNAPSHOT`, available only after supplying its artifacts).

```groovy
buildscript {
    repositories { mavenCentral(); gradlePluginPortal() }
    dependencies { classpath 'io.cratis:arc-gradle-plugin:0.0.0-SNAPSHOT' }
}
plugins { id 'org.jetbrains.kotlin.jvm'; id 'com.google.devtools.ksp' }
repositories { mavenCentral() }
dependencies {
    implementation 'io.cratis:arc:0.0.0-SNAPSHOT'
    ksp 'io.cratis:arc-ksp:0.0.0-SNAPSHOT'
}
kotlin { jvmToolchain(17) }
ksp { arg('arc.moduleName', 'Consumer') }
def extract = tasks.register('extractHandlers', io.cratis.arc.gradle.ExtractArcResponseHandlerMetadata) {
    dependencyArtifacts.from(configurations.compileClasspath.incoming.artifactView {
        attributes.attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE,
            objects.named(LibraryElements, LibraryElements.JAR))
    }.files)
    outputFile.set(layout.buildDirectory.file('handler index/main.json'))
}
tasks.withType(com.google.devtools.ksp.gradle.KspAATask).configureEach {
    if (name == 'kspKotlin') {
        def provider = objects.newInstance(io.cratis.arc.gradle.ArcResponseHandlerMetadataArgumentProvider)
        provider.metadataFile.set(extract.flatMap { it.outputFile })
        commandLineArgumentProviders.add(provider)
        inputs.file(provider.metadataFile).withPropertyName('arcResponseHandlerMetadata')
            .withPathSensitivity(PathSensitivity.NONE)
    }
}
```

Repeat this task-local wiring with the corresponding dependency configuration and a distinct
output for each additional compilation. Do not use a global KSP argument provider for all
source sets, include the compilation's own classes, or mark the index input incremental.

## Manual KSP setup

Without the plugin, apply Kotlin/JVM and `com.google.devtools.ksp`, add `io.cratis:arc` to `implementation`, add `io.cratis:arc-ksp` to `ksp`, and set `arc.moduleName` in the KSP extension. Kotlin Spring applications also apply Kotlin's Spring plugin. Add the Arc Spring starter and `spring-boot-starter-webmvc` for HTTP hosting.

Spring Boot 4 and Arc use the same Jackson 3 mapper. The starter contributes Arc's module and builder customizer, so conventional MVC controllers and generated Arc endpoints share the same naming, inclusion, temporal, enum, concept, and polymorphism policy without an additional property. An application-supplied mapper remains authoritative; construct its Arc-configured replacement with `ArcObjectMapper.configure(mapper)` because Jackson 3 mappers are immutable.

## Jackson 3 migration

Arc's JSON API uses Jackson 3.1.x. Runtime types moved from `com.fasterxml.jackson` to `tools.jackson`, including `ObjectMapper`, `JsonNode`, modules, serializers, deserializers, and naming strategies. This is binary- and source-breaking for applications that called `ArcObjectMapper`, constructed `ChangeSetComputer` with a mapper, consumed introspection schemas, supplied an OpenAPI mapper, or referenced `ArcJacksonModule` directly. Replace those imports with their `tools.jackson` equivalents.

Jackson annotations are the deliberate exception: Jackson 3 still uses `com.fasterxml.jackson.annotation`, so application model annotations do not change package. Separate Java-time modules are no longer required because Jackson 3 embeds Java-time support.

`ArcObjectMapper.configure(mapper)` no longer mutates its argument. Jackson 3 mappers are immutable, so capture the configured copy:

```kotlin
val mapper = ArcObjectMapper.configure(JsonMapper.builder().build())
```

Java follows the same contract:

```java
ObjectMapper mapper = ArcObjectMapper.configure(JsonMapper.builder().build());
```

The mapper must be a JSON `JsonMapper`; Jackson 3 does not permit a generic mapper to be repurposed for another format.
