Skip to content

Commit 2f6ce3f

Browse files
committed
feat: allow overriding external domain
1 parent 025a69c commit 2f6ce3f

File tree

3 files changed

+128
-4
lines changed

3 files changed

+128
-4
lines changed

src/CommunityToolkit.Aspire.Hosting.Zitadel/README.md

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,46 @@ Then, in the _Program.cs_ file of `AppHost`, define a Zitadel resource, then cal
2020
builder.AddZitadel("zitadel");
2121
```
2222

23-
Zitadel *requires* a Postgres database, you can add one with `AddDatabase`:
23+
Zitadel *requires* a Postgres database, you can add one with `WithDatabase`:
2424
```csharp
2525
var database = builder.AddPostgres("postgres");
2626

2727
builder.AddZitadel("zitadel")
28-
.AddDatabase(database);
28+
.WithDatabase(database);
2929
```
3030
You can also pass in a database rather than server (`AddPostgres().AddDatabase()`).
3131

32+
### Configuring the External Domain
33+
34+
By default, Zitadel uses `{name}.dev.localhost` as the external domain, which works well for local development. For production deployments or custom scenarios, you can configure a custom external domain:
35+
36+
**Option 1: Using the parameter**
37+
```csharp
38+
builder.AddZitadel("zitadel", externalDomain: "auth.example.com");
39+
```
40+
41+
**Option 2: Using the fluent API**
42+
```csharp
43+
builder.AddZitadel("zitadel")
44+
.WithExternalDomain("auth.example.com");
45+
```
46+
47+
**Option 3: From configuration**
48+
```csharp
49+
var domain = builder.Configuration["Zitadel:ExternalDomain"];
50+
builder.AddZitadel("zitadel", externalDomain: domain);
51+
```
52+
53+
#### Why `.dev.localhost`?
54+
55+
`.dev.localhost` is a special top-level domain that:
56+
- Automatically resolves to `127.0.0.1` without requiring DNS configuration
57+
- Provides unique subdomains for each Zitadel instance (e.g., `zitadel1.dev.localhost`, `zitadel2.dev.localhost`)
58+
- Works reliably in local development and CI/CD environments
59+
- Satisfies Zitadel's requirement for stable hostnames in OIDC/OAuth2 flows
60+
61+
For production deployments, replace this with your actual domain name using one of the configuration methods above.
62+
3263
## Feedback & contributing
3364

3465
https://github.com/CommunityToolkit/Aspire

src/CommunityToolkit.Aspire.Hosting.Zitadel/ZitadelHostingExtensions.cs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,23 @@ public static class ZitadelHostingExtensions
1717
/// <param name="username">An optional parameter to set a username for the admin account, if <c>null</c> will auto generate one.</param>
1818
/// <param name="password">An optional parameter to set a password for the admin account, if <c>null</c> will auto generate one.</param>
1919
/// <param name="masterKey">An optional parameter to set the masterkey, if <c>null</c> will auto generate one.</param>
20+
/// <param name="externalDomain">The external domain for Zitadel. Defaults to <c>{name}.dev.localhost</c> which works for local development. For production deployments, specify the actual domain (e.g., "auth.example.com").</param>
2021
public static IResourceBuilder<ZitadelResource> AddZitadel(
2122
this IDistributedApplicationBuilder builder,
2223
[ResourceName] string name,
2324
int? port = null,
2425
IResourceBuilder<ParameterResource>? username = null,
2526
IResourceBuilder<ParameterResource>? password = null,
26-
IResourceBuilder<ParameterResource>? masterKey = null
27+
IResourceBuilder<ParameterResource>? masterKey = null,
28+
string? externalDomain = null
2729
)
2830
{
2931
ArgumentNullException.ThrowIfNull(builder);
3032
ArgumentNullException.ThrowIfNull(name);
3133

34+
// Use provided external domain or default to {name}.dev.localhost
35+
var domain = externalDomain ?? $"{name}.dev.localhost";
36+
3237
var usernameParameter = username?.Resource ?? new ParameterResource($"{name}-username", _ => "admin", false);
3338
var passwordParameter = password?.Resource ?? ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(builder, $"{name}-password", minSpecial: 1);
3439
var masterKeyParameter = masterKey?.Resource ?? ParameterResourceBuilderExtensions.CreateGeneratedParameter(builder, $"{name}-masterKey", true, new GenerateParameterDefault
@@ -64,7 +69,7 @@ public static IResourceBuilder<ZitadelResource> AddZitadel(
6469
.WithEnvironment("ZITADEL_MASTERKEY", masterKeyParameter)
6570
.WithEnvironment("ZITADEL_TLS_ENABLED", "false")
6671
.WithEnvironment("ZITADEL_EXTERNALSECURE", "false")
67-
.WithEnvironment("ZITADEL_EXTERNALDOMAIN", $"{name}.dev.localhost")
72+
.WithEnvironment("ZITADEL_EXTERNALDOMAIN", domain)
6873
.WithUrlForEndpoint(ZitadelResource.HttpEndpointName, e => e.DisplayText = "Zitadel Dashboard");
6974

7075
// Use ReferenceExpression for the port to avoid issues with endpoint allocation
@@ -121,4 +126,20 @@ public static IResourceBuilder<ZitadelResource> WithDatabase(this IResourceBuild
121126

122127
return builder;
123128
}
129+
130+
/// <summary>
131+
/// Configures the external domain for the Zitadel resource. This overrides the default domain set in <see cref="AddZitadel"/>.
132+
/// </summary>
133+
/// <param name="builder">The Zitadel resource builder.</param>
134+
/// <param name="externalDomain">The external domain to use (e.g., "auth.example.com"). Cannot be null or empty.</param>
135+
/// <returns>The resource builder for chaining.</returns>
136+
/// <exception cref="ArgumentException">Thrown if <paramref name="externalDomain"/> is null or whitespace.</exception>
137+
public static IResourceBuilder<ZitadelResource> WithExternalDomain(
138+
this IResourceBuilder<ZitadelResource> builder,
139+
string externalDomain)
140+
{
141+
ArgumentException.ThrowIfNullOrWhiteSpace(externalDomain);
142+
143+
return builder.WithEnvironment("ZITADEL_EXTERNALDOMAIN", externalDomain);
144+
}
124145
}

tests/CommunityToolkit.Aspire.Hosting.Zitadel.Tests/ZitadelHostingExtensionsTests.cs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,4 +127,76 @@ public void AddZitadel_With_Custom_Port()
127127

128128
Assert.Equal(8888, endpoint.Port);
129129
}
130+
131+
[Fact]
132+
public async Task AddZitadel_Uses_Custom_ExternalDomain()
133+
{
134+
var builder = DistributedApplication.CreateBuilder();
135+
136+
var zitadel = builder.AddZitadel("zitadel", externalDomain: "auth.example.com");
137+
138+
var env = await zitadel.Resource.GetEnvironmentVariableValuesAsync();
139+
140+
Assert.Equal("auth.example.com", env["ZITADEL_EXTERNALDOMAIN"]);
141+
}
142+
143+
[Fact]
144+
public async Task WithExternalDomain_Overrides_Default()
145+
{
146+
var builder = DistributedApplication.CreateBuilder();
147+
148+
var zitadel = builder.AddZitadel("zitadel")
149+
.WithExternalDomain("custom.domain.com");
150+
151+
var env = await zitadel.Resource.GetEnvironmentVariableValuesAsync();
152+
153+
Assert.Equal("custom.domain.com", env["ZITADEL_EXTERNALDOMAIN"]);
154+
}
155+
156+
[Fact]
157+
public async Task WithExternalDomain_Can_Override_Parameter()
158+
{
159+
var builder = DistributedApplication.CreateBuilder();
160+
161+
var zitadel = builder.AddZitadel("zitadel", externalDomain: "first.example.com")
162+
.WithExternalDomain("second.example.com");
163+
164+
var env = await zitadel.Resource.GetEnvironmentVariableValuesAsync();
165+
166+
// WithExternalDomain should override the parameter
167+
Assert.Equal("second.example.com", env["ZITADEL_EXTERNALDOMAIN"]);
168+
}
169+
170+
[Fact]
171+
public void WithExternalDomain_Throws_If_Null()
172+
{
173+
var builder = DistributedApplication.CreateBuilder();
174+
var zitadel = builder.AddZitadel("zitadel");
175+
176+
var act = () => zitadel.WithExternalDomain(null!);
177+
178+
Assert.Throws<ArgumentNullException>(act);
179+
}
180+
181+
[Fact]
182+
public void WithExternalDomain_Throws_If_Empty()
183+
{
184+
var builder = DistributedApplication.CreateBuilder();
185+
var zitadel = builder.AddZitadel("zitadel");
186+
187+
var act = () => zitadel.WithExternalDomain("");
188+
189+
Assert.Throws<ArgumentException>(act);
190+
}
191+
192+
[Fact]
193+
public void WithExternalDomain_Throws_If_Whitespace()
194+
{
195+
var builder = DistributedApplication.CreateBuilder();
196+
var zitadel = builder.AddZitadel("zitadel");
197+
198+
var act = () => zitadel.WithExternalDomain(" ");
199+
200+
Assert.Throws<ArgumentException>(act);
201+
}
130202
}

0 commit comments

Comments
 (0)