---
title: 'Cratis Stage: customize a rendered application'
editUrl: https://github.com/Cratis/Stage/edit/main/Documentation/guides/customize-rendered-application.md
description: Customize a Cratis Stage application with service hooks, dependencies and CSS, and check which files your publisher preserves when you render again.
lastUpdated: 2026-11-05T00:00:00.000Z
---


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

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`:

```bash
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:

```csharp title="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](/stage/guides/sign-in-with-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

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](/stage/reference/plan-from-sources/#reducer-rendering-convention).

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](/stage/reference/plan-from-sources/#migrating-a-classic-read-model).
A caller that does not supply the new-application profile keeps classic behavior.

## 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:

```csharp title="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:

```csharp title="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:

```csharp title="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

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`

`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:

```bash
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](/stage/guides/sign-in-with-entra-id/) for the `Cratis:Authentication` keys.

## Behind an authenticating proxy

:::danger
These are ordinary request headers: anyone can forge them. Enable this option only when every request passes
through a trusted authenticating proxy that strips incoming identity headers and sets its own, and the backend
cannot be reached directly. Examples include Azure App Service authentication and Cratis AuthProxy.
:::

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:

```csharp title="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.

## 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:

```xml title="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`:

```xml title="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.

## 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:

```css title="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](/components/styling/themed/) and check token names
in the [Cratis token reference](/components/styling/cratis-tokens/). 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

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

- 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](/stage/reference/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](/stage/guides/sign-in-with-entra-id/), 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](#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](/stage/guides/sign-in-with-entra-id/).
If you publish plans yourself, [Build a renderer target](/stage/guides/build-renderer-target/) explains the write and ownership
checks needed to preserve custom files.
