Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,9 @@ emission, full 2FA spectrum, GDPR self-service.
- **Observability built-in** — OpenTelemetry metrics + traces,
Prometheus scrape endpoint, custom IdP meter, in-app live
activity feed.
- **Recovery CLI** — break-glass admin path (bootstrap-admin,
reset-2fa, magic-link, rebuild-projections) when the UI can't
- **Recovery CLI** — shell-authorized first installation plus
break-glass admin paths (`install-link`, `bootstrap-admin`,
`reset-2fa`, `magic-link`, `rebuild-projections`) when the UI can't
help you.

## Quick links
Expand Down Expand Up @@ -97,10 +98,11 @@ Postgres container, what the first boot actually does, reaching tenant
realms at `*.localhost`, the recovery CLI, demo seed data, tests and
Playwright.

For the first admin you need the recovery CLI — that guide covers it, and
[First-time setup](./docs/getting-started/first-time-setup.md) has the
decision tree for the other bootstrap routes (invite mode, provisioning
further realms).
On an empty database, issue a short-lived installation URL with
`recover install-link`. The browser form (or the same API from CI) creates the
first ordinary realm, marks it as the Control Plane, and creates its first
`realm:admin`. [First-time setup](./docs/getting-started/first-time-setup.md)
covers the complete interactive and automated flow.

## Contributing

Expand Down
2 changes: 2 additions & 0 deletions docs/admin/applications.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ or with system invariants:

## Creating an app

![Create application dialog](/screenshots/admin-anwendung-modal.png)

Click **Create** in the list view.

1. Pick a slug — kebab-case, memorable: `acme`, `billing`,
Expand Down
2 changes: 0 additions & 2 deletions docs/admin/change-requests.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ Administration → **Change Requests**.
Some compliance regimes require that user profile changes are reviewed — particularly email-address changes, which are an account-takeover vector. The approval flow inserts a human gate between "user wants to change" and "change is live".
:::

![Change request list](/screenshots/admin-change-requests-inbox.png)

## The list

Columns: *Last changed*, *User*, *Type*, *Fields* (which fields the request touches), *Status*. By default only open requests show; tick **Also show completed** to include approved/rejected ones too. There's free-text search, but no separate Status/User/date-range filters — open a row to see the proposed old → new values for each field.
Expand Down
2 changes: 1 addition & 1 deletion docs/admin/groups.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Groups are the **organisational layer** in Modgud. They serve two distinct purpo

A user can be a member of any number of groups; a group can be a member of another group (transitive resolution).

![Groups list](/screenshots/admin-gruppen-liste.png)
![Create group dialog](/screenshots/admin-gruppe-modal.png)

## Why groups?

Expand Down
2 changes: 2 additions & 0 deletions docs/admin/invite-codes.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ Double-click a row to open its **details** (adds who/when it was redeemed, once

## Minting codes

![Mint invite codes dialog](/screenshots/admin-einladungscode-modal.png)

Click **Mint codes** (top-right of the grid, or the empty-state call to action) to open the mint dialog:

1. **App** — which Application these codes belong to. Pre-filled from the header's current App selection if one is active. Codes are single-use and permanently bound to this app.
Expand Down
2 changes: 1 addition & 1 deletion docs/admin/login-providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ three types are wired up:
Future types — **LDAP**, **Kerberos** — are reserved at the API level and
will surface in the picker once the backend handlers ship.

![Login providers list](/screenshots/admin-login-provider.png)
![Create login provider dialog](/screenshots/admin-login-provider-modal.png)

## The Internal provider

Expand Down
4 changes: 2 additions & 2 deletions docs/admin/oauth-apis.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ An app can be both (e.g. a BFF pattern: user-login as a client, its
own API as an API).
:::

![OAuth APIs list](/screenshots/admin-oauth-apis.png)
![Create OAuth API dialog](/screenshots/admin-oauth-api-modal.png)

## When do I need an OAuth API registration?

Expand Down Expand Up @@ -152,7 +152,7 @@ immediately switch to the new app context.

**Audience (aud)** is immutable, so to make a near-identical
resource server, clone it. List → right-click → **Clone**. The Create
wizard opens pre-filled — display name, description, scopes, user claims,
create dialog opens pre-filled — display name, description, scopes, user claims,
the linked Application and its catalog subset are copied; only
**Audience (aud)** is blank.

Expand Down
2 changes: 1 addition & 1 deletion docs/admin/oauth-clients.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Examples:
- A CLI tool with the device-code flow
- A server-to-server job using client-credentials

![OAuth clients list](/screenshots/admin-oauth-clients.png)
![Create OAuth client dialog](/screenshots/admin-oauth-client-modal.png)

## Relationship to Applications

Expand Down
2 changes: 1 addition & 1 deletion docs/admin/oauth-scopes.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

**Scopes** define what permissions an OAuth client may request from the user — and which resources (APIs) the resulting token may target.

![OAuth scopes list](/screenshots/admin-oauth-scopes.png)
![Create OAuth scope dialog](/screenshots/admin-oauth-scope-modal.png)

## Standard scopes (seeded per realm)

Expand Down
4 changes: 2 additions & 2 deletions docs/admin/realm-provisioning.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,8 +234,8 @@ var secret = realm.SecretFor("acme-web");

## Caveat — using a provisioned realm for OAuth flows

Creating, updating and deleting realms is host-agnostic (the control-plane endpoints
live on the system realm). But **driving OAuth flows _against_ a provisioned realm is
Creating, updating and deleting realms uses the current Control-Plane host. But
**driving OAuth flows _against_ a provisioned realm is
host-routed**: Modgud resolves the tenant from the request's `Host` header
(`Realm.Domains`), and each realm's issuer is `https://{PrimaryDomain}`. So a token
request for a realm must arrive with that realm's host. For machine flows
Expand Down
43 changes: 19 additions & 24 deletions docs/admin/realms.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ how multi-tenant Modgud deployments separate customers / environments
- **Compliance isolation** (some customer data must not coexist in
the same DB)

Single-tenant deployments only need the system realm — provisioned
automatically on first start.
Single-tenant deployments only need the first realm created during
[first installation](../getting-started/first-time-setup).
:::

![Realm list](/screenshots/admin-realms-liste.png)
![Create realm dialog](/screenshots/admin-realm-modal.png)

## The Control-Plane realm

Expand All @@ -30,14 +30,10 @@ isn't seeded there). See
[Concepts: Control Plane / Data Plane](../concepts/control-plane) for
the full three-layer defence.

The Control-Plane flag is a **stored, transferable** field. The `system`
realm is stamped as the CP at first boot, but the role can be moved to any
The Control-Plane flag is a **stored, transferable** field. First installation
stamps the first ordinary realm as the CP; the role can later move to any
active realm (see [Transferring the control plane](#transferring-the-control-plane)).
There is always exactly one CP per deployment.

The system realm's default domains are `system.localhost`,
`localhost`, `127.0.0.1` — anything resolving to those lands on the
system realm.
There is exactly one CP after installation, and no slug has special meaning.

## Realm fields

Expand Down Expand Up @@ -154,17 +150,17 @@ transferring, or recover one afterwards via the
database and removes the realm record (Control-Plane only, irreversible) —
see [Declarative Realm Provisioning](./realm-provisioning) for the API surface.

## First-time setup of a fresh realm
## Inviting a realm administrator

The realm is provisioned together with a bootstrap-invite for the
Initial Admin (above). The invite recipient clicks the magic-link and
sets their password — that's the standard path for nearly every
realm.
A realm is complete and active as soon as realm creation finishes; it does not
need an administrator to be valid. When someone should manage it, use
**Invite realm admin** in the realm's context menu. The recipient clicks the
magic link and sets their password.

If something goes wrong:

- **Token lost or expired** — reopen the realm in the admin UI and
click **Resend invite**. Same recipient, fresh token.
- **Token lost or expired** — issue a new invitation. It automatically revokes
the previous open link.
- **No prior invite, no admin yet** (e.g. provisioned via a tool
that didn't issue one) — drop into the container and run
`dotnet Modgud.Api.dll recover bootstrap-admin --email <e> --realm <slug>`.
Expand All @@ -179,17 +175,16 @@ Modgud's `RealmMiddleware` resolves the realm from
`HttpContext.Request.Host`. Each request finds its realm by matching
the host against any realm's `Domains` list.

If a host doesn't match any realm: 404 (the request is for an
unrecognised tenant). For dev work without hosts-file edits, the
system realm's default `Domains` list includes `localhost` and
`127.0.0.1` — the single-realm fallback in `RealmCache` also catches
localhost variants when only one realm is active.
If a host doesn't match any realm, the request returns 404. Register every
hostname explicitly in the realm's Domains list. `*.localhost` resolves to
loopback on modern browsers and operating systems, but Modgud still needs the
exact host-to-realm mapping.

## Tips

::: tip Naming conventions
Realm slugs are baked into the tenant DB name and the default Domains
list (`<slug>.localhost`). Pick stable, customer-friendly slugs and
Realm slugs are baked into the tenant DB name. Pick stable,
customer-friendly slugs and
stick with them. Slug changes are not supported.
:::

Expand Down
4 changes: 1 addition & 3 deletions docs/admin/roles.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ An **application role** bundles permissions for exactly one app. A pure
catalog permissions and grants the bypass across every app in its own realm.
Users receive roles only through their [groups](./groups) — never directly.

![Roles list](/screenshots/admin-rollen-liste.png)

## The permission model

```
Expand Down Expand Up @@ -99,7 +97,7 @@ External apps (Acme-Tasks, Knowledge, …) bring their own resources, defined in

Administration → **Roles** → **Create**, or double-click an entry.

![Role detail](/screenshots/admin-rolle-detail.png)
![Create role dialog](/screenshots/admin-rolle-modal.png)

The modal has two tabs:

Expand Down
2 changes: 2 additions & 0 deletions docs/admin/service-accounts.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ A backend-for-frontend that both **brokers user login** (redeeming a native gran

## Creating a Service Account

![Create service account dialog](/screenshots/admin-service-account-modal.png)

1. Open `/admin/service-accounts` and click **Create**.
2. Fill in:
- **Account name** — lowercase letters, digits, dots, hyphens or underscores; 2-64 chars; starts with a letter or digit. This is the audit-log handle (`ci.build-agent`, `integrations.acme-tasks`, `nightly.sync`). Unique across the whole principal table — a Person and a ServiceAccount can't share an account name, because both can act as the login handle in different contexts.
Expand Down
2 changes: 1 addition & 1 deletion docs/admin/users.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Administration → **Users**.

![User list](/screenshots/admin-benutzer-liste.png)
![Create user dialog](/screenshots/admin-benutzer-modal.png)

## User list

Expand Down
32 changes: 13 additions & 19 deletions docs/concepts/control-plane.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# Control Plane / Data Plane

Modgud separates **cross-realm administration** (realm CRUD, the
first-run setup wizard) from **tenant self-service** (everything else)
Modgud separates **deployment-wide installation and cross-realm
administration** (first installation, realm CRUD) from **tenant self-service**
(everything else)
on three independent layers. A request that hits a Control-Plane endpoint
from a tenant host has to defeat all three to succeed — and they're
deliberately decoupled so a regression in one doesn't open the others.
Expand Down Expand Up @@ -159,24 +160,17 @@ recover one via the CLI.

## Hostname routing — DB is source of truth

The system realm is seeded with the localhost-style domains
`["system.localhost", "localhost", "127.0.0.1"]` so a fresh checkout
boots without any ENV setup. For a deployed installation, the
operator adds the public hostname via the Recovery CLI:
The first-installation form requires the first realm's domain and primary
domain. Additional hostnames are managed on the realm or with
`recover realm-add-domain`; there is no seeded hostname or special slug.

```bash
docker exec modgud dotnet Modgud.Api.dll \
recover realm-add-domain --slug system --domain auth.example.com
```
`IRealmCache` is invalidated when realm metadata changes. From the next request
onward, a matching Host header resolves to that realm. If it currently holds
`IsControlPlane`, `ControlPlaneGateMiddleware` exposes
`/api/admin/realms/*`; otherwise that surface remains 404.

The `IRealmCache` is invalidated immediately — no container restart
needed. From the next request onwards, `Host: auth.example.com`
resolves to the system realm and `ControlPlaneGateMiddleware` lets
`/api/admin/realms/*` through.

There's no separate ENV variable mirroring the hostname list. The
realm's own `Domains` field is the single source of truth — kept in
the DB next to the rest of the realm metadata.
There's no separate environment variable mirroring the hostname list. The
realm's own `Domains` field in `IGlobalStore` is the single source of truth.

## First-admin onboarding

Expand All @@ -193,7 +187,7 @@ docker exec <container> dotnet Modgud.Api.dll recover bootstrap-admin \
--email admin@example.com \
--username admin \
--password 'StrongPass1!' \
--realm system
--realm acme
```

Atomic seed of `ApplicationUser` (Identity-Password-Rules enforced —
Expand Down
12 changes: 6 additions & 6 deletions docs/concepts/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ Mapping to other systems:
|---|---|---|---|
| Realm | Realm | Tenant | Tenant (Directory) |

The **system realm** is the first realm, created automatically on first
boot. It starts as the **Control-Plane** realm — flagged
`IsControlPlane = true` — meaning only its users may create further
realms. Exactly one realm per deployment is the Control Plane.
The first realm is created explicitly during first installation. It starts as
the **Control-Plane** realm — flagged `IsControlPlane = true` — meaning only
its `realm:admin` users may create further realms. The flag can later move to
another active realm; no realm is special by slug.

The realm boundary is the **domain** (Host header), not the URL path.
Realm `acme` lives under `acme.example.com`, the system realm under
`system.example.com` or `localhost`.
Realm `acme` might live under `acme.example.com`; a local realm commonly uses
`auth.localhost` or `localhost`.

### Application

Expand Down
Loading
Loading