Skip to content

Cratis Stage: customize a rendered application

Use Customizations/ at the application root to add services, external-system adapters and styling to a Cratis Stage-rendered application without editing generated slices. The scaffold loads these files through C# hooks, an MSBuild import and a CSS import. Keeping the files separate makes later renders easier to apply, but their survival still depends on how your publisher writes the plan.

Scaffold files are the starting host and project setup: Program.cs, the project files, Directory.Packages.props and the frontend shell. You can edit them after the first render. Module, feature and slice plans exclude them; application-scope plans include them again. Before applying an application-scope plan to an existing application, check whether your publisher will overwrite those edits.

With RepositoryRoot = true, the scaffold also includes .github/workflows/build.yml and .github/dependabot.yml. Excluding the scaffold from later plans keeps those files out of the planned writes; it does not prevent a publisher from replacing or deleting them independently.

FileWhat it is forPicked up when
Customizations/Program.csService registrations, options, and extra endpoints or middlewareThe C# project compiles
Customizations/*.csYour adapters and other typesThe C# project compiles
Customizations/Dependencies.propsProject references, package references, and MSBuild propertiesMSBuild evaluates the project
Customizations/styles.cssFont and --cratis-* token overrides for the frontendVite builds or serves the frontend

You only need the files for the changes you make. The top-level Customizations/ directory is reserved: a model that would generate files there is rejected with STAGE-CRATIS-005, including case variants. The example below adds a configured HTTP client and an endpoint; the styling section changes the visible theme.

Start with an application you can already build and run, using the SDK and frontend tooling named in its generated README. The hooks come from an application-scope CratisRendering plan; module, feature and slice plans do not include Program.cs or the project file.

Check that Program.cs calls ConfigureServices(builder); and ConfigureApplication(app);, and that .frontend/main.tsx discovers Customizations/styles.css with import.meta.glob and awaits its loader before rendering React. If those hooks are missing, plan the scaffold again and review the affected files before publishing it over your application.

Before starting the backend, start the rendered development services with Docker Compose v2 supporting --wait:

Terminal window
docker compose up -d --wait

The generated compose file checks Chronicle’s TLS /health endpoint on port 35000. The command waits for Chronicle to become healthy before returning, so the backend does not try to connect while the kernel is still starting. Follow the generated README’s dotnet run step after this command completes.

The rendered Program.cs provides the hooks:

Program.cs (managed)
using Cratis.Arc.MongoDB;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHealthChecks();
builder.AddCratis(
configureArcBuilder: arc => arc.WithMongoDB(),
configureChronicleBuilder: chronicle => chronicle.WithCamelCaseNamingPolicy());
<RootNamespace>.GeneratedAuthentication.Authentication.Add(builder);
ConfigureServices(builder);
var app = builder.Build();
<RootNamespace>.GeneratedAuthentication.Authentication.Use(app);
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseCratis();
app.MapHealthChecks("/healthz");
app.MapFallbackToFile("/index.html");
ConfigureApplication(app);
await app.RunAsync();
// Optional partial methods disappear when Customizations/Program.cs supplies no implementation.
public partial class Program
{
static partial void ConfigureServices(WebApplicationBuilder builder);
static partial void ConfigureApplication(WebApplication app);
}

ConfigureServices runs after AddCratis and before Build. ConfigureApplication runs after UseCratis and the generated endpoints, and before RunAsync. When you do not implement a hook, the compiler removes the call.

The two Authentication calls come from the managed GeneratedAuthentication.cs; <RootNamespace> stands for your application’s root namespace. They do nothing until you configure sign-in; see Sign in with Microsoft Entra ID.

The generated /healthz endpoint runs all health checks. With Chronicle 19.59.6 or later, it reports healthy only after the client is connected and Chronicle has successfully registered the application’s artifacts, including its read models. Wait for a healthy response before querying the generated application. Earlier Chronicle versions can report healthy before registration completes. When Entra ID sign-in is enabled, GET and HEAD requests to /healthz are the only requests that skip sign-in.

A new application explicitly initialized with CreateNewApplicationProfile persists its self reducer convention in .cratis/stage/rendering.json. Keep that configuration and have your rendering host reload it for later plans. This file is application configuration, not a Screenplay behavior declaration or generated C# attachment. See the profile API.

Applications without the input stay classic, including newly added reducers. Do not switch an existing classic application to self mode by editing the setting or deleting its reducer file. To move an existing read model, migrate it instead: Stage keeps the old observer identity and plans the removal of the old class. See Migrating a classic read model. A caller that does not supply the new-application profile keeps classic behavior.

To read from an external exchange-rate service, add a typed HttpClient, bind its base address from configuration, and expose a read endpoint. The example uses only the ASP.NET Core shared framework, so it needs no extra package. You will need a reachable service that accepts rates/{currency} and returns the ExchangeRate JSON shape below; rates.example.com later in this guide is a placeholder, not a working service.

For another adapter and stylesheet to copy, see Samples/Customization/Customizations/ in this repository.

Create the options type:

Customizations/ExchangeRatesOptions.cs
namespace Customizations;
public class ExchangeRatesOptions
{
public const string Section = "ExchangeRates";
public Uri? BaseAddress { get; set; }
}

Create the adapter. A missing rate returns null; any other failed response throws, so an outage does not look like an empty result:

Customizations/ExchangeRatesClient.cs
using System.Net;
namespace Customizations;
public record ExchangeRate(string Currency, decimal Rate);
public class ExchangeRatesClient(HttpClient client)
{
public async Task<ExchangeRate?> GetRate(string currency, CancellationToken cancellationToken)
{
using var response = await client.GetAsync($"rates/{Uri.EscapeDataString(currency)}", cancellationToken);
if (response.StatusCode == HttpStatusCode.NotFound)
{
return null;
}
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<ExchangeRate>(cancellationToken);
}
}

Implement the hooks:

Customizations/Program.cs
using Customizations;
using Microsoft.Extensions.Options;
public partial class Program
{
static partial void ConfigureServices(WebApplicationBuilder builder)
{
builder.Services
.AddOptions<ExchangeRatesOptions>()
.Bind(builder.Configuration.GetSection(ExchangeRatesOptions.Section))
.Validate(options => options.BaseAddress is not null, "ExchangeRates:BaseAddress is required.")
.ValidateOnStart();
builder.Services.AddHttpClient<ExchangeRatesClient>((services, client) =>
client.BaseAddress = services.GetRequiredService<IOptions<ExchangeRatesOptions>>().Value.BaseAddress);
}
static partial void ConfigureApplication(WebApplication app)
{
app.MapGet(
"/api/customizations/exchange-rates/{currency}",
async (string currency, ExchangeRatesClient rates, CancellationToken cancellationToken) =>
await rates.GetRate(currency, cancellationToken) is { } rate ? Results.Ok(rate) : Results.NotFound());
}
}

Keep Customizations/Program.cs in the global namespace, with no namespace declaration, and declare the class as public partial class Program or partial class Program. The method signatures must match the managed declarations exactly. A namespace or a changed signature fails the build with CS0759.

The endpoint sits under /api because the generated Vite development server proxies /api to the backend. Pick a path that cannot collide with a route Arc generates from your model. If you already implement either hook, add the new registration or route to that method instead of declaring a second implementation.

Keep custom types out of generated namespaces

Section titled “Keep custom types out of generated namespaces”

Put adapter types in your own namespace, such as Customizations, not in the global namespace or a namespace Stage generates (including <RootNamespace>, <RootNamespace>.Common and slice namespaces). The global partial Program hook above is the supported exception. Remove obsolete generated declarations through your publisher’s managed-file cleanup rather than leaving stale siblings in those namespaces.

Generated C# uses short type names where Stage can resolve them from the application’s generated declarations and its framework catalog. Stage does not scan Customizations/ or packages added through Dependencies.props. A custom type in a generated or enclosing namespace can therefore shadow a reference that Stage intended to bind to a framework type. Keeping adapter types in Customizations avoids that enclosing-namespace collision.

Also check new global using directives, aliases and imported packages. They can introduce ambiguities or change what a generated reference means. Extension methods can change overload resolution without a type-name collision. After changing dependencies, build the application and run its specifications; resolve conflicts in your custom code or dependency choices rather than editing generated references.

Supply configuration without editing appsettings.json

Section titled “Supply configuration without editing appsettings.json”

appsettings.json is included in the application scaffold and can be overwritten when that scaffold is published again. Supply adapter settings through another ASP.NET Core configuration source, such as environment variables:

Terminal window
export ExchangeRates__BaseAddress=https://rates.example.com/v1/
dotnet run

End the base address with / so the relative rates/... request keeps the /v1/ segment. With the validation above, the host refuses to start when the setting is missing instead of failing on the first request. Keep secrets in a secret store or environment variables, not in files under Customizations/.

With your service running, request /api/customizations/exchange-rates/USD from the application. A returned rate produces a JSON response; an upstream 404 produces a 404 from this endpoint. Other unsuccessful HTTP responses throw rather than appearing as missing rates. The base-address check does not test service connectivity.

The host’s own sign-in settings use the same mechanism. See Sign in with Microsoft Entra ID for the Cratis:Authentication keys.

Generated applications register Arc’s Microsoft Identity Platform header authentication scheme. Forwarded x-ms-client-principal* headers are ignored by default. To accept identity from an authenticating proxy, opt in with the environment variable Cratis__Arc__TrustForwardedIdentityHeaders=true, or configure Arc through the existing services hook:

Customizations/Program.cs
using Cratis.Arc;
public partial class Program
{
static partial void ConfigureServices(WebApplicationBuilder builder)
{
builder.Services.Configure<ArcOptions>(options => options.TrustForwardedIdentityHeaders = true);
}
}

Merge this registration into your existing global ConfigureServices hook and keep the setting out of scaffolded appsettings.json. Without a trusted identity, /.cratis/me returns 401 and protected commands and queries reject anonymous callers.

Outside the Development environment, Arc’s discovery endpoints (/.cratis/commands, /.cratis/queries, /.cratis/users, /.cratis/tenants and /.cratis/identity-details/schema) also require an authenticated caller. The generated frontend does not use these discovery catalogs. The Docker sandbox keeps this secure default; its persona is not a proxy identity and does not authenticate discovery requests.

The rendered project file imports Customizations/Dependencies.props when it exists. Use it to move adapters into their own project or to add a package the shared framework does not provide:

Customizations/Dependencies.props
<Project>
<ItemGroup>
<ProjectReference Include="$(MSBuildThisFileDirectory)../../Adapters/Adapters.csproj" />
</ItemGroup>
</Project>

Replace the path with your own project. $(MSBuildThisFileDirectory) anchors the path to the Customizations/ folder, so this example points at an Adapters folder next to the rendered application.

New scaffolds enable central package management and transitive pinning in Directory.Packages.props. Add a PackageVersion there and use an unversioned PackageReference in your import. To keep an adapter’s pin entirely inside Customizations/Dependencies.props, use VersionOverride instead of Version:

Customizations/Dependencies.props (package reference shape)
<Project>
<ItemGroup>
<PackageReference Include="Your.Package" VersionOverride="1.2.3" />
</ItemGroup>
</Project>

Replace the illustrative package name and version with your dependency. Version on a PackageReference fails restore under central package management. Older inline-spec scaffolds that disable central package management still require Version instead; check your existing Directory.Packages.props before adding a package.

The import is the last element of the web project body: a property you set in this file replaces the generated value, so set only what you mean to change. Specifications compile into the same project in Debug, so they see these dependencies too.

The scaffolded .frontend/index.css imports @cratis/components/tokens, then @cratis/components/styles, then sets the default font. It does not import @cratis/components/theme. .frontend/main.tsx then loads Customizations/styles.css through an awaited lazy import when the file exists. React does not render until that import completes, so your declarations load after the defaults.

Override the root font and the --cratis-* tokens your product needs:

Customizations/styles.css
:root {
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
font-size: 16px;
--cratis-primary-color: #0f766e;
--cratis-primary-color-text: #ffffff;
--cratis-action-background: #0f766e;
--cratis-action-background-hover: #115e59;
--cratis-action-background-active: #134e4a;
--cratis-action-text: #ffffff;
--cratis-focus-ring: 0 0 0 3px #0f766e;
--cratis-border-radius: 4px;
}

To map a larger design system, follow Build a product theme and check token names in the Cratis token reference. The pinned Components version defines the token names; a name it does not use has no effect.

The file is plain CSS processed by Vite and can @import other CSS files beside it with a relative path. CSS does not install npm dependencies. Add any required package to package.json separately and protect that scaffold edit when publishing later application-scope plans. After creating or deleting Customizations/styles.css, restart npm run dev so Vite re-evaluates the import. Reload the application and check the primary controls and focus ring to see the token changes.

A Stage plan never contains a path under Customizations/. Stage also does not read your files: it cannot report a broken customization, and it produces the same managed bytes whether or not the folder exists. A mistake shows up when you build or run the application.

CratisRendering does not write files. Whether Customizations/ survives a render depends on the caller that publishes the plan. A publisher that writes only the planned paths leaves your files alone. A publisher that deletes files it did not plan, or that replaces the whole output folder, removes them. Keep Customizations/ under version control and check your publisher’s behavior before rendering over an application that has one.

  • Hooks do not bypass model admission. Event-triggered Automation slices render their reactions and own artifacts; captures, non-event triggers, opaque reaction bodies and Translate slices still fail STAGE-ESM-024. Registering a service does not bind it to a modeled file realization or make an unsupported construct renderable. See Event reaction rendering for the supported shape.
  • Endpoints you map are anonymous unless you secure them with ASP.NET Core authorization yourself. Modeled authorization covers only the artifacts Stage renders. When you turn on Entra ID sign-in, the perimeter covers your endpoints too: every route except GET and HEAD on /healthz requires sign-in, whatever your endpoint declares. It decides who is signed in; it does not add authorization policies to your code. For forwarded identity, see Behind an authenticating proxy.
  • ConfigureApplication runs last, so middleware you add there runs after the generated middleware. You cannot place middleware before static files or Cratis through this hook. With Entra ID sign-in enabled, the authentication perimeter runs right after Build, before static files, UseCratis and your middleware.
  • ConfigureServices runs after AddCratis, and the last registration of a service wins. Register your own abstractions; do not re-register Cratis types unless you intend to replace them.
  • Customizations/styles.css changes styling only. Screens come from scene.json and the generated bindings; CSS does not change those bindings.

For application-wide sign-in, continue with Sign in with Microsoft Entra ID. If you publish plans yourself, Build a renderer target explains the write and ownership checks needed to preserve custom files.