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.
| File | What it is for | Picked up when |
|---|---|---|
Customizations/Program.cs | Service registrations, options, and extra endpoints or middleware | The C# project compiles |
Customizations/*.cs | Your adapters and other types | The C# project compiles |
Customizations/Dependencies.props | Project references, package references, and MSBuild properties | MSBuild evaluates the project |
Customizations/styles.css | Font and --cratis-* token overrides for the frontend | Vite 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.
Before you start
Section titled “Before you start”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:
docker compose up -d --waitThe 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:
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.
Keep the reducer convention stable
Section titled “Keep the reducer convention stable”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.
Register an adapter and its options
Section titled “Register an adapter and its options”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:
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:
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:
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:
export ExchangeRates__BaseAddress=https://rates.example.com/v1/dotnet runEnd 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.
Behind an authenticating proxy
Section titled “Behind an authenticating proxy”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:
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.
Add project and package references
Section titled “Add project and package references”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:
<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:
<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.
Restyle the frontend
Section titled “Restyle the frontend”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:
: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.
Re-render without losing your files
Section titled “Re-render without losing your files”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.
What the hooks do not do
Section titled “What the hooks do not do”- 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 modeledfilerealization 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
GETandHEADon/healthzrequires 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. ConfigureApplicationruns 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 afterBuild, before static files,UseCratisand your middleware.ConfigureServicesruns afterAddCratis, 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.csschanges styling only. Screens come fromscene.jsonand 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.