diff --git a/docs/architecture/adr-0003-default-fill.md b/docs/architecture/adr-0003-default-fill.md
new file mode 100644
index 0000000..c37e9bb
--- /dev/null
+++ b/docs/architecture/adr-0003-default-fill.md
@@ -0,0 +1,44 @@
+# ADR-0003: ACL default-fill strategy
+
+- **Status:** Accepted
+- **Date:** 2026-06-04
+- **Deciders:** Respellion engineering
+- **Relates to:** S-04 (#5); builds on ADR-0001 (loose coupling)
+
+## Context
+
+The ACL is the only code that talks to ZGW APIs (ADR-0001 / CLAUDE.md §8.1). When the
+domain asks it to "open a zaak", the domain payload is intentionally free of ZGW
+specifics — it carries domain facts (e.g. the registrant's BSN), not OpenZaak fields. But
+OpenZaak's `POST /zaken` requires ZGW-mandatory fields: `bronorganisatie`,
+`verantwoordelijkeOrganisatie`, `startdatum`, `vertrouwelijkheidaanduiding`, and a
+`zaaktype` URL. Something has to supply those, and it must not leak into the domain.
+
+## Decision
+
+**The ACL default-fills the ZGW-mandatory zaak fields; the domain never sees them.**
+
+- `bronorganisatie`, `verantwoordelijkeOrganisatie`, `vertrouwelijkheidaanduiding`, and the
+ `zaaktype` URL come from **ACL configuration** (`AclDefaults` options) — not hardcoded,
+ not from the domain. This keeps them operationally manageable (the beheer portal will
+ edit them in S-15) and environment-specific (the seeded BIG zaaktype URL differs per env).
+- `startdatum` is derived from an injected **clock** (today's date), so it is
+ deterministic in tests.
+- The mapping from domain payload → ZGW `ZaakRequest` lives entirely inside the ACL
+ (`Application` builds the request from payload + defaults; `Infrastructure` serialises and
+ POSTs it). No other service constructs ZGW payloads or URLs.
+
+## Consequences
+
+- **Positive:** the domain stays ZGW-agnostic; ZGW knowledge is in one named place; defaults
+ are config (testable, env-specific, later editable via the beheer portal).
+- **Cost:** the ACL must be configured per environment (the seeded zaaktype URL, the
+ organisation RSINs). Missing/invalid config fails fast at the ACL boundary.
+- **Follow-ups:** mapping the BSN onto the zaak (eigenschap/rol), status transitions, and
+ documents are explicitly out of scope for S-04 and get their own slices.
+
+## Alternatives considered
+
+- **Defaults in the domain payload** — rejected: leaks ZGW concerns into the domain,
+ violating ADR-0001.
+- **Hardcoded defaults in code** — rejected: not env-specific, not operationally editable.
diff --git a/services/acl/Acl.Api/Acl.Api.csproj b/services/acl/Acl.Api/Acl.Api.csproj
new file mode 100644
index 0000000..0a0116c
--- /dev/null
+++ b/services/acl/Acl.Api/Acl.Api.csproj
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
diff --git a/services/acl/Acl.Api/Program.cs b/services/acl/Acl.Api/Program.cs
new file mode 100644
index 0000000..31385d9
--- /dev/null
+++ b/services/acl/Acl.Api/Program.cs
@@ -0,0 +1,31 @@
+using Acl.Application;
+using Acl.Infrastructure;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.Services.AddSingleton();
+builder.Services.AddSingleton(sp => sp.GetRequiredService()
+ .GetSection("Acl:Defaults").Get()
+ ?? throw new InvalidOperationException("Missing configuration section 'Acl:Defaults'"));
+builder.Services.AddSingleton(sp => sp.GetRequiredService()
+ .GetSection("Acl:OpenZaak").Get()
+ ?? throw new InvalidOperationException("Missing configuration section 'Acl:OpenZaak'"));
+builder.Services.AddHttpClient();
+builder.Services.AddScoped();
+
+var app = builder.Build();
+
+app.MapGet("/health", () => "Healthy");
+
+// The ACL's single operation, exposed as a service endpoint.
+app.MapPost("/zaken", async (OpenZaakRequest body, AclService acl, CancellationToken ct) =>
+{
+ var zaakUrl = await acl.OpenZaakAsync(new DomainRegistration(body.Bsn), ct);
+ return Results.Ok(new { zaakUrl = zaakUrl.ToString() });
+});
+
+app.Run();
+
+public sealed record OpenZaakRequest(string Bsn);
+
+public partial class Program;
diff --git a/services/acl/Acl.Api/Properties/launchSettings.json b/services/acl/Acl.Api/Properties/launchSettings.json
new file mode 100644
index 0000000..59ed1e7
--- /dev/null
+++ b/services/acl/Acl.Api/Properties/launchSettings.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:5041",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:7260;http://localhost:5041",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/services/acl/Acl.Api/appsettings.Development.json b/services/acl/Acl.Api/appsettings.Development.json
new file mode 100644
index 0000000..0c208ae
--- /dev/null
+++ b/services/acl/Acl.Api/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/services/acl/Acl.Api/appsettings.json b/services/acl/Acl.Api/appsettings.json
new file mode 100644
index 0000000..10f68b8
--- /dev/null
+++ b/services/acl/Acl.Api/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/services/acl/Acl.Application/Acl.Application.csproj b/services/acl/Acl.Application/Acl.Application.csproj
new file mode 100644
index 0000000..b760144
--- /dev/null
+++ b/services/acl/Acl.Application/Acl.Application.csproj
@@ -0,0 +1,9 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
diff --git a/services/acl/Acl.Application/AclDefaults.cs b/services/acl/Acl.Application/AclDefaults.cs
new file mode 100644
index 0000000..232d3a4
--- /dev/null
+++ b/services/acl/Acl.Application/AclDefaults.cs
@@ -0,0 +1,10 @@
+namespace Acl.Application;
+
+/// Configured ZGW defaults the ACL fills in (ADR-0003).
+public sealed class AclDefaults
+{
+ public required string Bronorganisatie { get; init; }
+ public required string VerantwoordelijkeOrganisatie { get; init; }
+ public required string Vertrouwelijkheidaanduiding { get; init; }
+ public required Uri ZaaktypeUrl { get; init; }
+}
diff --git a/services/acl/Acl.Application/AclService.cs b/services/acl/Acl.Application/AclService.cs
new file mode 100644
index 0000000..effb09d
--- /dev/null
+++ b/services/acl/Acl.Application/AclService.cs
@@ -0,0 +1,9 @@
+namespace Acl.Application;
+
+/// The ACL's single operation: open a zaak from a domain payload,
+/// default-filling the ZGW-mandatory fields (ADR-0003).
+public sealed class AclService(IZaakGateway gateway, AclDefaults defaults, IClock clock)
+{
+ public Task OpenZaakAsync(DomainRegistration registration, CancellationToken ct = default)
+ => throw new NotImplementedException();
+}
diff --git a/services/acl/Acl.Application/DomainRegistration.cs b/services/acl/Acl.Application/DomainRegistration.cs
new file mode 100644
index 0000000..de985f0
--- /dev/null
+++ b/services/acl/Acl.Application/DomainRegistration.cs
@@ -0,0 +1,4 @@
+namespace Acl.Application;
+
+/// Domain-language payload handed to the ACL. No ZGW concepts here.
+public sealed record DomainRegistration(string Bsn);
diff --git a/services/acl/Acl.Application/IClock.cs b/services/acl/Acl.Application/IClock.cs
new file mode 100644
index 0000000..bd93e9c
--- /dev/null
+++ b/services/acl/Acl.Application/IClock.cs
@@ -0,0 +1,7 @@
+namespace Acl.Application;
+
+/// Abstracts "today" so startdatum default-fill is deterministic in tests.
+public interface IClock
+{
+ DateOnly Today { get; }
+}
diff --git a/services/acl/Acl.Application/IZaakGateway.cs b/services/acl/Acl.Application/IZaakGateway.cs
new file mode 100644
index 0000000..1962488
--- /dev/null
+++ b/services/acl/Acl.Application/IZaakGateway.cs
@@ -0,0 +1,8 @@
+namespace Acl.Application;
+
+/// Port to the ZGW Zaken API. Implemented in Infrastructure — the only
+/// code that talks to OpenZaak (ADR-0001).
+public interface IZaakGateway
+{
+ Task OpenZaakAsync(ZaakRequest request, CancellationToken ct = default);
+}
diff --git a/services/acl/Acl.Application/ZaakRequest.cs b/services/acl/Acl.Application/ZaakRequest.cs
new file mode 100644
index 0000000..875d162
--- /dev/null
+++ b/services/acl/Acl.Application/ZaakRequest.cs
@@ -0,0 +1,9 @@
+namespace Acl.Application;
+
+/// The fully default-filled zaak the gateway will create in OpenZaak.
+public sealed record ZaakRequest(
+ string Bronorganisatie,
+ string VerantwoordelijkeOrganisatie,
+ string Vertrouwelijkheidaanduiding,
+ Uri Zaaktype,
+ DateOnly Startdatum);
diff --git a/services/acl/Acl.Infrastructure/Acl.Infrastructure.csproj b/services/acl/Acl.Infrastructure/Acl.Infrastructure.csproj
new file mode 100644
index 0000000..6321050
--- /dev/null
+++ b/services/acl/Acl.Infrastructure/Acl.Infrastructure.csproj
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
diff --git a/services/acl/Acl.Infrastructure/OpenZaakGateway.cs b/services/acl/Acl.Infrastructure/OpenZaakGateway.cs
new file mode 100644
index 0000000..3332a1e
--- /dev/null
+++ b/services/acl/Acl.Infrastructure/OpenZaakGateway.cs
@@ -0,0 +1,10 @@
+using Acl.Application;
+
+namespace Acl.Infrastructure;
+
+/// The only code that talks to OpenZaak's Zaken API (ADR-0001).
+public sealed class OpenZaakGateway(HttpClient http, OpenZaakOptions options) : IZaakGateway
+{
+ public Task OpenZaakAsync(ZaakRequest request, CancellationToken ct = default)
+ => throw new NotImplementedException();
+}
diff --git a/services/acl/Acl.Infrastructure/OpenZaakOptions.cs b/services/acl/Acl.Infrastructure/OpenZaakOptions.cs
new file mode 100644
index 0000000..66f9dc6
--- /dev/null
+++ b/services/acl/Acl.Infrastructure/OpenZaakOptions.cs
@@ -0,0 +1,9 @@
+namespace Acl.Infrastructure;
+
+/// Connection + credential config for OpenZaak's ZGW APIs.
+public sealed class OpenZaakOptions
+{
+ public required Uri BaseUrl { get; init; }
+ public required string ClientId { get; init; }
+ public required string Secret { get; init; }
+}
diff --git a/services/acl/Acl.Infrastructure/SystemClock.cs b/services/acl/Acl.Infrastructure/SystemClock.cs
new file mode 100644
index 0000000..47939d3
--- /dev/null
+++ b/services/acl/Acl.Infrastructure/SystemClock.cs
@@ -0,0 +1,8 @@
+using Acl.Application;
+
+namespace Acl.Infrastructure;
+
+public sealed class SystemClock : IClock
+{
+ public DateOnly Today => DateOnly.FromDateTime(DateTime.UtcNow);
+}
diff --git a/services/acl/Acl.Infrastructure/ZgwToken.cs b/services/acl/Acl.Infrastructure/ZgwToken.cs
new file mode 100644
index 0000000..a784fd5
--- /dev/null
+++ b/services/acl/Acl.Infrastructure/ZgwToken.cs
@@ -0,0 +1,29 @@
+using System.Security.Cryptography;
+using System.Text;
+using System.Text.Json;
+
+namespace Acl.Infrastructure;
+
+/// Mints a ZGW (vng-api-common) JWT: HS256 over the standard claims.
+internal static class ZgwToken
+{
+ public static string Mint(string clientId, string secret)
+ {
+ var header = B64Url(JsonSerializer.SerializeToUtf8Bytes(new { alg = "HS256", typ = "JWT" }));
+ var payload = B64Url(JsonSerializer.SerializeToUtf8Bytes(new
+ {
+ iss = clientId,
+ iat = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
+ client_id = clientId,
+ user_id = "acl",
+ user_representation = "acl",
+ }));
+ var signingInput = $"{header}.{payload}";
+ using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
+ var signature = B64Url(hmac.ComputeHash(Encoding.UTF8.GetBytes(signingInput)));
+ return $"{signingInput}.{signature}";
+ }
+
+ private static string B64Url(byte[] bytes) =>
+ Convert.ToBase64String(bytes).TrimEnd('=').Replace('+', '-').Replace('/', '_');
+}
diff --git a/services/acl/Acl.Tests/Acl.Tests.csproj b/services/acl/Acl.Tests/Acl.Tests.csproj
new file mode 100644
index 0000000..22e2588
--- /dev/null
+++ b/services/acl/Acl.Tests/Acl.Tests.csproj
@@ -0,0 +1,26 @@
+
+
+
+ net10.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/services/acl/Acl.Tests/AclServiceTests.cs b/services/acl/Acl.Tests/AclServiceTests.cs
new file mode 100644
index 0000000..e1348e1
--- /dev/null
+++ b/services/acl/Acl.Tests/AclServiceTests.cs
@@ -0,0 +1,47 @@
+using Acl.Application;
+
+namespace Acl.Tests;
+
+public class AclServiceTests
+{
+ private sealed class FakeGateway : IZaakGateway
+ {
+ public ZaakRequest? Captured;
+ public Uri Result { get; } = new("http://openzaak/zaken/api/v1/zaken/abc");
+
+ public Task OpenZaakAsync(ZaakRequest request, CancellationToken ct = default)
+ {
+ Captured = request;
+ return Task.FromResult(Result);
+ }
+ }
+
+ private sealed class FixedClock(DateOnly today) : IClock
+ {
+ public DateOnly Today { get; } = today;
+ }
+
+ [Fact]
+ public async Task Opening_a_zaak_default_fills_zgw_fields_and_returns_the_zaak_url()
+ {
+ var gateway = new FakeGateway();
+ var defaults = new AclDefaults
+ {
+ Bronorganisatie = "517439943",
+ VerantwoordelijkeOrganisatie = "517439943",
+ Vertrouwelijkheidaanduiding = "openbaar",
+ ZaaktypeUrl = new("http://openzaak/catalogi/api/v1/zaaktypen/big"),
+ };
+ var service = new AclService(gateway, defaults, new FixedClock(new DateOnly(2026, 6, 4)));
+
+ var url = await service.OpenZaakAsync(new DomainRegistration("123456782"));
+
+ Assert.Equal(gateway.Result, url);
+ var req = Assert.IsType(gateway.Captured);
+ Assert.Equal("517439943", req.Bronorganisatie);
+ Assert.Equal("517439943", req.VerantwoordelijkeOrganisatie);
+ Assert.Equal("openbaar", req.Vertrouwelijkheidaanduiding);
+ Assert.Equal(defaults.ZaaktypeUrl, req.Zaaktype);
+ Assert.Equal(new DateOnly(2026, 6, 4), req.Startdatum);
+ }
+}
diff --git a/services/acl/Acl.Tests/OpenZaakGatewayTests.cs b/services/acl/Acl.Tests/OpenZaakGatewayTests.cs
new file mode 100644
index 0000000..b6aa129
--- /dev/null
+++ b/services/acl/Acl.Tests/OpenZaakGatewayTests.cs
@@ -0,0 +1,51 @@
+using System.Net;
+using System.Net.Http.Json;
+using Acl.Application;
+using Acl.Infrastructure;
+
+namespace Acl.Tests;
+
+public class OpenZaakGatewayTests
+{
+ private sealed class StubHandler(Func> onSend)
+ : HttpMessageHandler
+ {
+ protected override Task SendAsync(HttpRequestMessage request, CancellationToken ct)
+ => onSend(request);
+ }
+
+ [Fact]
+ public async Task Posts_zaak_to_openzaak_with_bearer_and_default_fields_and_returns_url()
+ {
+ HttpRequestMessage? seen = null;
+ string? body = null;
+ var handler = new StubHandler(async req =>
+ {
+ seen = req;
+ body = await req.Content!.ReadAsStringAsync();
+ return new HttpResponseMessage(HttpStatusCode.Created)
+ {
+ Content = JsonContent.Create(new { url = "http://openzaak/zaken/api/v1/zaken/xyz" }),
+ };
+ });
+ var gateway = new OpenZaakGateway(
+ new HttpClient(handler),
+ new OpenZaakOptions { BaseUrl = new("http://openzaak"), ClientId = "cid", Secret = "sec" });
+ var request = new ZaakRequest(
+ "517439943", "517439943", "openbaar",
+ new("http://openzaak/catalogi/api/v1/zaaktypen/big"), new DateOnly(2026, 6, 4));
+
+ var url = await gateway.OpenZaakAsync(request);
+
+ Assert.Equal("http://openzaak/zaken/api/v1/zaken/xyz", url.ToString());
+ Assert.Equal(HttpMethod.Post, seen!.Method);
+ Assert.Equal("http://openzaak/zaken/api/v1/zaken", seen.RequestUri!.ToString());
+ Assert.Equal("Bearer", seen.Headers.Authorization!.Scheme);
+ Assert.False(string.IsNullOrWhiteSpace(seen.Headers.Authorization.Parameter));
+ Assert.Contains("\"bronorganisatie\":\"517439943\"", body);
+ Assert.Contains("\"verantwoordelijkeOrganisatie\":\"517439943\"", body);
+ Assert.Contains("\"vertrouwelijkheidaanduiding\":\"openbaar\"", body);
+ Assert.Contains("\"startdatum\":\"2026-06-04\"", body);
+ Assert.Contains("\"zaaktype\":\"http://openzaak/catalogi/api/v1/zaaktypen/big\"", body);
+ }
+}
diff --git a/services/acl/Acl.slnx b/services/acl/Acl.slnx
new file mode 100644
index 0000000..d33e693
--- /dev/null
+++ b/services/acl/Acl.slnx
@@ -0,0 +1,6 @@
+
+
+
+
+
+