Skip to content

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.

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 pointReads appsettings.json and environmentTypical use
ArcApplication.createBuilder() from @cratis/arc.coreYes, unless you pass configuration: falseNode applications, with discovery and the standalone host
CratisApplication.createBuilder() from @cratis/cratisYes, including Cratis:ChronicleArc and the experimental Chronicle integration in one call; see The Cratis package
ArcApplication.createBuilder() from @cratis/arc.core/fetchNo, code options onlyFetch API runtimes without a filesystem; see Fetch API runtimes
new ArcServer(options)No, code options onlyLow-level definitions and specs

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.

appsettings.json
{
"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.

Storage and event sourcing are optional packages. Importing one adds its method to the builder, and the method registers everything the integration needs:

PackageBuilder methodConfiguration it binds
@cratis/arc.mongodbbuilder.withMongoDB({ ... }); see MongoDBCratis:MongoDB:{Server,Database}
@cratis/arc.drizzlebuilder.withDrizzle({ ... }); see SQL with DrizzleNone; pass the database in code
@cratis/arc.chronicle, experimentalbuilder.withChronicle({ ... }); see ChronicleCratis: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 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 pathDefaultEffect
ExposeExceptionDetails / exposeExceptionDetailstrue only when the effective environment is DevelopmentInclude original exception messages and stack traces in serialized HTTP results; otherwise redact them. This does not enable development discovery.
Development / developmentfalseEnable the development user and tenant discovery providers. TypeScript-only; it does not change exception exposure.
Introspection:Enabled / introspection.enabledtrueBoolean. 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.httpHeaderX-Correlation-IDCorrelation ID request and response header.
Tenancy:ResolverType / tenancy.resolverTypeheader when tenancy is presentSingle header, query, claim, subdomain, development, or fixed source.
Tenancy:HttpHeader / tenancy.httpHeaderx-cratis-tenant-idHeader source; also the fallback for resolverType: TenantResolverType.Subdomain or an ordered [TenantResolverType.Subdomain, TenantResolverType.Header] list.
Tenancy:BaseDomain / tenancy.baseDomainNoneRequired for the verified subdomain source; exactly one preceding DNS label matches.
Tenancy:QueryParameter / tenancy.queryParametertenantIdQuery-string source.
Tenancy:ClaimType / tenancy.claimTypetenant_idClaim source; only own string claims on authenticated principals count.
Tenancy:FixedTenantId / tenancy.fixedTenantIddevelopmentFixed or development source. The .NET DevelopmentTenantId configuration name also binds this value; do not supply both names.
Tenancy:Required / tenancy.requiredfalseAnswer 400 when no tenant is selected. TypeScript-only.
Tenancy:MembershipClaim / tenancy.membershipClaimNoneRequire the selected tenant in this comma-separated own claim of an authenticated principal, or answer 403. TypeScript-only.
GeneratedApis:RoutePrefix / generatedApis.routePrefixapiPrefix for convention routes.
GeneratedApis:SegmentsToSkipForRoute / generatedApis.segmentsToSkipForRoute0Leading namespace segments removed from generated routes.
GeneratedApis:IncludeCommandNameInRoute / generatedApis.includeCommandNameInRoutetrueAppend command names to convention routes.
GeneratedApis:IncludeQueryNameInRoute / generatedApis.includeQueryNameInRoutetrueAppend query names to convention routes.
GeneratedApis:EnableQueryHttpMethod / generatedApis.enableQueryHttpMethodtrueAccept HTTP QUERY with JSON arguments as well as GET; false answers 405 with Allow: GET.
GeneratedApis:OpenApiVersion / generatedApis.openApiVersion0.1.0TypeScript-only OpenAPI info.version; .NET does not have this ArcOptions key.
Query:KeepAliveInterval / query.keepAliveIntervalMs00:00:30 / 30000 msIdle 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.applicationUrlhttp://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.maxBodyBytes1048576Maximum 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.

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 optionDefaultEffect
query.allowedOriginsSame originExact 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.enableObservableHealthfalseAuthenticated caller-scoped health query; see Query health.
query.maxObservableSubscriptions / query.maxObservableSubscriptionsPerCaller4096 / 4096Global and per-caller live and opening subscriptions.
query.maxObservableHubConnections / query.maxObservableHubConnectionsPerCaller512 / 512Global and per-caller hub connections.
query.maxObservableHubSubscriptionsPerConnection256Subscriptions on one hub.
query.maxObservableInboundFrames / query.maxObservableOutboundFrames256 / 256Queued transport frames.
query.maxObservablePendingEmissions256Pending snapshots from a structural subscribable.
query.maxObservableInboundFrameBytes / query.maxObservableOutboundFrameBytes65536 / 1048576Incoming WebSocket frame or SSE control body / outgoing frame sizes.
query.maxObservableTombstones1024Retained unsubscribe tombstones per hub connection for two minutes.
query.observableHandshakeTimeoutMs10000WebSocket upgrade handshake deadline.
query.observableShutdownTimeoutMs10000Hub 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.

These TypeScript-only ArcOptions values are code-only; the builder can also register discovered artifacts and matching services directly.

OptionDefaultEffect
commands, queries, observableQueries[]Low-level definitions; see Low-level definitions.
servicesOwned empty registryRegistrations 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, commandExecutionScopesNone / []Validated execution wrapper and per-command scopes.
commandCompensationTimeoutMs30000Cooperative budget for operation compensation.
queryRenderers, readModelInterceptors[]Ordered scoped read-side extensions.

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.

OptionDefaultEffect
authentication, authenticationSchemes[] / {}Ordered and named handlers; see Authentication.
authorizationPolicies{}Named authorization rules, also registered through addAuthorizationPolicy.
nativePrincipalfalseAccept a host-verified principal, never a caller-supplied header; see Native principal.
identityDetailsNoneRegisters /.cratis/me; see Identity.
developmentUsers, developmentTenantsNoneCode-only fixture discovery providers; require development: true, independently of endpoint access.
environmentNameEnvironment variables, otherwise non-DevelopmentCode-only discovery environment override; no Cratis:Arc:EnvironmentName key. Does not affect exception exposure. See Discovery access for precedence.
introspection.enabledtrueCratis: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.requireAuthenticationUnsetAnonymous only in Development. false opts out; true requires authentication everywhere and fails startup without authentication configured.
introspection.rolesNoneComma-separated nonempty roles, any one of which grants access. Implies authentication; cannot be combined with requireAuthentication: false.

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 /api and /.cratis through 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: cors for Express, @fastify/cors for Fastify, or hono/cors for 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.

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.

build() and the ArcServer constructor fail before serving when:

  • names, namespaces, paths, route prefixes, or namespace-skip counts are unsafe;
  • hosting.maxBodyBytes or a query limit is not a positive safe integer, except that query.keepAliveIntervalMs accepts zero;
  • query.allowedOrigins is not an exact HTTP(S) Origin list or predicate;
  • tenancy combines resolverType and sources, 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 /validate routes;
  • authorization combines anonymous with restricted access, names an unknown policy or scheme, or combines nativePrincipal with authentication;
  • 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.

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.

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.22Now
ArcServerOptionsArcOptions
correlationHeadercorrelationId.httpHeader
tenantHeadertenancy.httpHeader
resolveTenanttenancy.resolve
enableQueryMethodgeneratedApis.enableQueryHttpMethod
openApiVersiongeneratedApis.openApiVersion
maxBodyByteshosting.maxBodyBytes
observableKeepAliveIntervalMsquery.keepAliveIntervalMs
allowedOriginsquery.allowedOrigins
maxObservable*, observableHandshakeTimeoutMs, observableShutdownTimeoutMs, enableObservableHealth, observableEmissionGuardsThe 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.