Configuration
The same application runs on your laptop, in CI, and in production. The route prefix stays put, but the tenant source, the exception detail, and the listener address change between them. You want those differences in configuration, not in if statements around your startup code.
Arc accepts settings through one ArcOptions object. The serializable settings described below can come from appsettings.json, environment variables, and code; options marked code-only cannot be bound from configuration, and code always has the last word. The groups follow the same Cratis:Arc paths as Arc on .NET: CorrelationId, Tenancy, GeneratedApis, Query, Hosting, Introspection, and ExposeExceptionDetails, so one appsettings.json shape serves both. Node-specific transport limits and registration hooks live in those groups or alongside them, as noted below.
What each entry point reads
Section titled “What each entry point reads”Where options come from depends on how you create the application. The host you mount it in does not matter: Express, Fastify, and Hono take an application that is already built. See the hosting overview for choosing a host.
| Entry point | Reads appsettings.json and environment | Typical use |
|---|---|---|
ArcApplication.createBuilder() from @cratis/arc.core | Yes, unless you pass configuration: false | Node applications, with discovery and the standalone host |
CratisApplication.createBuilder() from @cratis/cratis | Yes, including Cratis:Chronicle | Arc and the experimental Chronicle integration in one call; see The Cratis package |
ArcApplication.createBuilder() from @cratis/arc.core/fetch | No, code options only | Fetch API runtimes without a filesystem; see Fetch API runtimes |
new ArcServer(options) | No, code options only | Low-level definitions and specs |
Three ways to configure
Section titled “Three ways to configure”ArcApplication.createBuilder() loads appsettings.json from the process working directory, then appsettings.{Environment}.json beside it, then Cratis__... environment variables, and finally applies code options. The environment comes from DOTNET_ENVIRONMENT, ASPNETCORE_ENVIRONMENT, or NODE_ENV, in that order. Keys are case-insensitive. Code overrides individual nested values rather than replacing the whole group.
{ "Cratis": { "Arc": { "CorrelationId": { "HttpHeader": "X-Request-ID" }, "Tenancy": { "ResolverType": "Fixed", "FixedTenantId": "default" }, "GeneratedApis": { "RoutePrefix": "api", "EnableQueryHttpMethod": true }, "Query": { "KeepAliveInterval": "00:00:30" }, "Hosting": { "ApplicationUrl": "http://127.0.0.1:3000/" }, "ExposeExceptionDetails": false } }}For example, Cratis__Arc__CorrelationId__HttpHeader=X-Other-ID overrides the file, while correlationId: { httpHeader: 'X-Code-ID' } overrides that environment variable:
import { ArcApplication } from '@cratis/arc.core';
const builder = ArcApplication.createBuilder({ correlationId: { httpHeader: 'X-Code-ID' } });const app = await builder.build();Use { configuration: false } to disable file and environment binding, or { configuration: { file: new URL('./appsettings.json', import.meta.url), env: suppliedEnvironment } } to choose both explicitly. A string path also works. Invalid JSON and invalid known values fail setup; unknown keys within Cratis:Arc, Cratis:Chronicle, and Cratis:MongoDB are reported to logger when configured, without including their values. Other configuration sections are ignored. Do not put real connection strings in committed files. Chronicle binds Cratis:Chronicle:{ConnectionString,EventStore}, and MongoDB binds Cratis:MongoDB:{Server,Database}; clients, handlers, tokens, and other non-serializable values belong in code.
new ArcServer(options) does not bind files or Cratis__... environment overrides. Its discovery and exception-detail defaults still consult the host environment variables when process is available. On a Fetch-only runtime without process, discovery defaults to requiring authentication and exception exposure defaults to false.
Add features through the builder
Section titled “Add features through the builder”Storage and event sourcing are optional packages. Importing one adds its method to the builder, and the method registers everything the integration needs:
| Package | Builder method | Configuration it binds |
|---|---|---|
@cratis/arc.mongodb | builder.withMongoDB({ ... }); see MongoDB | Cratis:MongoDB:{Server,Database} |
@cratis/arc.drizzle | builder.withDrizzle({ ... }); see SQL with Drizzle | None; pass the database in code |
@cratis/arc.chronicle, experimental | builder.withChronicle({ ... }); see Chronicle | Cratis:Chronicle:{ConnectionString,EventStore} |
Each package also exports a function form, such as withMongoDB(builder, options), which does the same. Calling a method whose package you did not import fails where you call it, so a missing integration never degrades into a silent no-op. Configuration covers only serializable values. Clients, connection pools, and model classes are passed in code.
The ArcOptions tree
Section titled “The ArcOptions tree”The paths below are relative to Cratis:Arc in configuration and camelCase in TypeScript. .NET settings with different value representations are called out explicitly. Unspecified options use the documented defaults.
| Configuration path / TypeScript path | Default | Effect |
|---|---|---|
ExposeExceptionDetails / exposeExceptionDetails | true only when the effective environment is Development | Include original exception messages and stack traces in serialized HTTP results; otherwise redact them. This does not enable development discovery. |
Development / development | false | Enable the development user and tenant discovery providers. TypeScript-only; it does not change exception exposure. |
Introspection:Enabled / introspection.enabled | true | Boolean. false unmaps command/query catalogs and HTTP OpenAPI in every environment, not identity discovery or in-process metadata. See Turn discovery off. |
CorrelationId:HttpHeader / correlationId.httpHeader | X-Correlation-ID | Correlation ID request and response header. |
Tenancy:ResolverType / tenancy.resolverType | header when tenancy is present | Single header, query, claim, subdomain, development, or fixed source. |
Tenancy:HttpHeader / tenancy.httpHeader | x-cratis-tenant-id | Header source; also the fallback for resolverType: TenantResolverType.Subdomain or an ordered [TenantResolverType.Subdomain, TenantResolverType.Header] list. |
Tenancy:BaseDomain / tenancy.baseDomain | None | Required for the verified subdomain source; exactly one preceding DNS label matches. |
Tenancy:QueryParameter / tenancy.queryParameter | tenantId | Query-string source. |
Tenancy:ClaimType / tenancy.claimType | tenant_id | Claim source; only own string claims on authenticated principals count. |
Tenancy:FixedTenantId / tenancy.fixedTenantId | development | Fixed or development source. The .NET DevelopmentTenantId configuration name also binds this value; do not supply both names. |
Tenancy:Required / tenancy.required | false | Answer 400 when no tenant is selected. TypeScript-only. |
Tenancy:MembershipClaim / tenancy.membershipClaim | None | Require the selected tenant in this comma-separated own claim of an authenticated principal, or answer 403. TypeScript-only. |
GeneratedApis:RoutePrefix / generatedApis.routePrefix | api | Prefix for convention routes. |
GeneratedApis:SegmentsToSkipForRoute / generatedApis.segmentsToSkipForRoute | 0 | Leading namespace segments removed from generated routes. |
GeneratedApis:IncludeCommandNameInRoute / generatedApis.includeCommandNameInRoute | true | Append command names to convention routes. |
GeneratedApis:IncludeQueryNameInRoute / generatedApis.includeQueryNameInRoute | true | Append query names to convention routes. |
GeneratedApis:EnableQueryHttpMethod / generatedApis.enableQueryHttpMethod | true | Accept HTTP QUERY with JSON arguments as well as GET; false answers 405 with Allow: GET. |
GeneratedApis:OpenApiVersion / generatedApis.openApiVersion | 0.1.0 | TypeScript-only OpenAPI info.version; .NET does not have this ArcOptions key. |
Query:KeepAliveInterval / query.keepAliveIntervalMs | 00:00:30 / 30000 ms | Idle hub Ping interval. Files and environment use .NET’s hh:mm:ss format, converted to milliseconds. In code zero disables keep-alive. |
Hosting:ApplicationUrl / hosting.applicationUrl | http://127.0.0.1:3000/ | Standalone Node HTTP listener URL; an explicit app.start({ host, port }) or app.run({ host, port }) overrides its host or port. HTTP adapters and Fetch-only dispatch do not open this listener. |
Hosting:MaxBodyBytes / hosting.maxBodyBytes | 1048576 | Maximum JSON command or QUERY body in bytes; TypeScript-only hosting limit. |
With no tenancy group at all, Arc retains its original behavior: it reads the default tenant header unchanged and does not check membership. When you supply the group, its built-in source validates and normalizes the tenant ID. tenancy.resolve(request, principal) is a code-only authoritative resolver; returning undefined does not fall back. tenancy.sources is a TypeScript-only ordered list of the resolver types above; the first nonempty result wins. Do not combine sources and resolverType. tenancy.required answers 400 when no tenant is selected, and tenancy.membershipClaim requires a matching own claim on an authenticated principal or answers 403. See Tenant resolvers.
development: true enables only development user and tenant discovery providers. It does not enable exception details. Conversely, exposeExceptionDetails: true does not authorize development providers. On Node, the exception-detail default uses DOTNET_ENVIRONMENT, then ASPNETCORE_ENVIRONMENT, then NODE_ENV; only Development (case-insensitive) exposes details by default. The Node builder uses configuration.env when supplied. The code-only environmentName option overrides discovery’s environment, not this exception-detail default or environment-file selection; it has no Cratis:Arc configuration key. Set exposeExceptionDetails explicitly in code or configuration if needed. Keep it false on public hosts.
Observable query limits
Section titled “Observable query limits”All of these TypeScript transport settings belong under query; positive numeric values in Cratis:Arc:Query accept decimal-integer strings from the environment. The Query:KeepAliveInterval key is the shared .NET setting; the remaining limits, guards, Origin policy, and query health are TypeScript extensions.
| TypeScript option | Default | Effect |
|---|---|---|
query.allowedOrigins | Same origin | Exact trusted HTTP(S) Origins or a code-only (origin, request, native) predicate; see WebSockets. |
query.observableEmissionGuards | [] | Code-only scoped emission policies; see Emission guards. |
query.enableObservableHealth | false | Authenticated caller-scoped health query; see Query health. |
query.maxObservableSubscriptions / query.maxObservableSubscriptionsPerCaller | 4096 / 4096 | Global and per-caller live and opening subscriptions. |
query.maxObservableHubConnections / query.maxObservableHubConnectionsPerCaller | 512 / 512 | Global and per-caller hub connections. |
query.maxObservableHubSubscriptionsPerConnection | 256 | Subscriptions on one hub. |
query.maxObservableInboundFrames / query.maxObservableOutboundFrames | 256 / 256 | Queued transport frames. |
query.maxObservablePendingEmissions | 256 | Pending snapshots from a structural subscribable. |
query.maxObservableInboundFrameBytes / query.maxObservableOutboundFrameBytes | 65536 / 1048576 | Incoming WebSocket frame or SSE control body / outgoing frame sizes. |
query.maxObservableTombstones | 1024 | Retained unsubscribe tombstones per hub connection for two minutes. |
query.observableHandshakeTimeoutMs | 10000 | WebSocket upgrade handshake deadline. |
query.observableShutdownTimeoutMs | 10000 | Hub and direct WebSocket cleanup deadline. |
Per-caller defaults equal global limits: set smaller per-caller budgets for internet-facing hosts. A caller is an authenticated principal in its tenant. An anonymous caller is its peer address in its tenant, and all anonymous callers without a peer address in a tenant share one budget; behind a reverse proxy, that address is the proxy’s unless the adapter’s native callback supplies a verified client address. See Per-caller budgets. Exhausted admission answers 503 with Retry-After: 1; an exhausted handshake answers HTTP 503 or WebSocket close 1013. The keep-alive interval alone accepts zero. query.allowedOrigins accepts a string array in code; the configuration binder does not accept Origin lists or predicates.
Artifacts and services
Section titled “Artifacts and services”These TypeScript-only ArcOptions values are code-only; the builder can also register discovered artifacts and matching services directly.
| Option | Default | Effect |
|---|---|---|
commands, queries, observableQueries | [] | Low-level definitions; see Low-level definitions. |
services | Owned empty registry | Registrations or an externally owned ServiceRegistry; see Dependency injection. |
commandResponseValueHandlers, commandContextValuesProviders, commandKeyResolvers | [] | Ordered scoped response handlers, context providers, and key rules. |
readModelForCommandResolvers | [] | Sources for commandReadModel(...) parameters. |
commandExecutionRunner, commandExecutionScopes | None / [] | Validated execution wrapper and per-command scopes. |
commandCompensationTimeoutMs | 30000 | Cooperative budget for operation compensation. |
queryRenderers, readModelInterceptors | [] | Ordered scoped read-side extensions. |
Routes and requests
Section titled “Routes and requests”A body larger than hosting.maxBodyBytes, measured by Content-Length or while reading, answers 400 malformedRequest for commands and QUERY alike. Unlike invalid QUERY JSON, the limit does not produce an exception envelope. Arc also rejects non-UTF-8 JSON, non-finite numbers, nesting beyond 32 levels, and the keys __proto__, prototype, and constructor. Fastify’s own bodyLimit applies first. Every result carries a correlation ID. Arc reuses a valid, non-zero UUID from correlationId.httpHeader in lowercase; otherwise it generates one.
Security
Section titled “Security”| Option | Default | Effect |
|---|---|---|
authentication, authenticationSchemes | [] / {} | Ordered and named handlers; see Authentication. |
authorizationPolicies | {} | Named authorization rules, also registered through addAuthorizationPolicy. |
nativePrincipal | false | Accept a host-verified principal, never a caller-supplied header; see Native principal. |
identityDetails | None | Registers /.cratis/me; see Identity. |
developmentUsers, developmentTenants | None | Code-only fixture discovery providers; require development: true, independently of endpoint access. |
environmentName | Environment variables, otherwise non-Development | Code-only discovery environment override; no Cratis:Arc:EnvironmentName key. Does not affect exception exposure. See Discovery access for precedence. |
introspection.enabled | true | Cratis:Arc:Introspection:Enabled, or Cratis__Arc__Introspection__Enabled=false in the deployment. Code wins; invalid boolean values fail setup. Disabling catalogs does not bypass identity-discovery authentication checks or warnings. |
introspection.requireAuthentication | Unset | Anonymous only in Development. false opts out; true requires authentication everywhere and fails startup without authentication configured. |
introspection.roles | None | Comma-separated nonempty roles, any one of which grants access. Implies authentication; cannot be combined with requireAuthentication: false. |
A note on CORS
Section titled “A note on CORS”CORS is not an Arc option. Arc sends no Access-Control-* headers, and it answers a preflight OPTIONS request to a command or query route with 405 and an Allow header. A browser on another origin therefore cannot call Arc until something in front of it handles CORS.
Choose one of these:
- Serve the frontend from the same origin. Proxy
/apiand/.cratisthrough your dev server, as the Library sample’s Vite configuration does, or serve the built frontend with static files. - Use your web framework’s CORS middleware, mounted before Arc:
corsfor Express,@fastify/corsfor Fastify, orhono/corsfor Hono. The middleware answers the preflight and adds the headers to Arc’s responses. - Handle CORS at your ingress in front of the standalone host, which has no middleware of its own.
Arc accepts HTTP QUERY for queries by default. If cross-origin clients use it, add QUERY to the allowed methods; it is not a simple method, so the browser always sends a preflight. A client that only uses GET does not need it.
WebSocket upgrades for observable queries are not covered by CORS. Arc checks their Origin against query.allowedOrigins itself; see WebSockets.
Errors and logging
Section titled “Errors and logging”The code-only logger(error, correlationId) receives the original error regardless of exposeExceptionDetails. A callback failure produces a 500 with hasExceptions: true; when exposure is off, HTTP callers receive ['An unexpected error occurred'] and no stack trace. Direct calls are neither redacted nor logged. If the logger throws or rejects, Arc attempts it once and returns a generic redacted 500. A handler failure and a scope cleanup failure arrive together as one AggregateError.
What startup rejects
Section titled “What startup rejects”build() and the ArcServer constructor fail before serving when:
- names, namespaces, paths, route prefixes, or namespace-skip counts are unsafe;
hosting.maxBodyBytesor a query limit is not a positive safe integer, except thatquery.keepAliveIntervalMsaccepts zero;query.allowedOriginsis not an exact HTTP(S) Origin list or predicate;tenancycombinesresolverTypeandsources, uses an invalid source/domain/tenant, or supplies an invalid claim name;- two operations have case-insensitively duplicate names or colliding routes, including reserved endpoints and
/validateroutes; - authorization combines anonymous with restricted access, names an unknown policy or scheme, or combines
nativePrincipalwithauthentication; - an input schema has case-insensitively duplicate property names or cannot become JSON Schema.
The builder also rejects misplaced decorators, duplicate validator targets, missing service registrations, dependency cycles, and captive lifetimes. A captive lifetime is a singleton that depends on a scoped service. It would keep the first request’s instance forever, which in a multi-tenant application means the first tenant’s data. Arc checks the declared graph without running any factory, so the check is safe in every environment.
Low-level definition fields
Section titled “Low-level definition fields”defineCommand and defineQuery share name (required), namespace, path, summary, schema (required), authorization, authorize, validate, filters, handlerDependencies, validatorDependencies, and clientOutput. A command also takes handle (required), provide, and scopes. A query takes perform (required); an observable query takes observe (required). See Low-level definitions.
Upgrading from v0.21
Section titled “Upgrading from v0.21”v0.22 grouped the flat options under the .NET configuration paths and renamed the options type. Code that still uses the old names no longer compiles:
| Before v0.22 | Now |
|---|---|
ArcServerOptions | ArcOptions |
correlationHeader | correlationId.httpHeader |
tenantHeader | tenancy.httpHeader |
resolveTenant | tenancy.resolve |
enableQueryMethod | generatedApis.enableQueryHttpMethod |
openApiVersion | generatedApis.openApiVersion |
maxBodyBytes | hosting.maxBodyBytes |
observableKeepAliveIntervalMs | query.keepAliveIntervalMs |
allowedOrigins | query.allowedOrigins |
maxObservable*, observableHandshakeTimeoutMs, observableShutdownTimeoutMs, enableObservableHealth, observableEmissionGuards | The same names under query |
In appsettings.json, use the grouped paths from the ArcOptions tree. A flat key such as Cratis:Arc:CorrelationHeader does not bind; with a logger configured, Arc reports it as an unknown key.