diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 9e37e47c3..8eb96417c 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -702,99 +702,112 @@ jobs:
find container-sbom-download -type f -name 'container-*.cdx.json' -exec cp -n {} dist/sboms/ \;
fi
- - name: Upload agent-console SBOM
- id: upload-sbom-agent-console
+ - name: Upload agenstra-frontend-agent-console SBOM
+ id: upload-sbom-agenstra-frontend-agent-console
uses: forepath/gh-upload-sbom@v2
continue-on-error: true
with:
serverhostname: dependencytrack.forepath.io
apikey: ${{ secrets.DEPENDENCY_TRACK_API_KEY }}
parent: '91161848-3734-4339-a504-bf08853348b1'
- projectname: 'agent-console'
+ projectname: 'agenstra-frontend-agent-console'
projectversion: ${{ needs.publish.outputs.new_release_version }}
autocreate: true
bomfilename: 'dist/sboms/agenstra-frontend-agent-console.cdx.json'
- - name: Upload agent-controller SBOM
- id: upload-sbom-agent-controller
+ - name: Upload agenstra-backend-agent-controller SBOM
+ id: upload-sbom-agenstra-backend-agent-controller
uses: forepath/gh-upload-sbom@v2
continue-on-error: true
with:
serverhostname: dependencytrack.forepath.io
apikey: ${{ secrets.DEPENDENCY_TRACK_API_KEY }}
parent: 'fab6501b-b798-4e62-91b9-4bb1c5c175c1'
- projectname: 'agent-controller'
+ projectname: 'agenstra-backend-agent-controller'
projectversion: ${{ needs.publish.outputs.new_release_version }}
autocreate: true
bomfilename: 'dist/sboms/agenstra-backend-agent-controller.cdx.json'
- - name: Upload agent-manager SBOM
- id: upload-sbom-agent-manager
+ - name: Upload agenstra-backend-agent-manager SBOM
+ id: upload-sbom-agenstra-backend-agent-manager
uses: forepath/gh-upload-sbom@v2
continue-on-error: true
with:
serverhostname: dependencytrack.forepath.io
apikey: ${{ secrets.DEPENDENCY_TRACK_API_KEY }}
parent: '7d399337-2c6e-408e-8f1f-5f0344af1d08'
- projectname: 'agent-manager'
+ projectname: 'agenstra-backend-agent-manager'
projectversion: ${{ needs.publish.outputs.new_release_version }}
autocreate: true
bomfilename: 'dist/sboms/agenstra-backend-agent-manager.cdx.json'
- - name: Upload billing-console SBOM
- id: upload-sbom-billing-console
+ - name: Upload decabill-frontend-billing-console SBOM
+ id: upload-sbom-decabill-frontend-billing-console
uses: forepath/gh-upload-sbom@v2
continue-on-error: true
with:
serverhostname: dependencytrack.forepath.io
apikey: ${{ secrets.DEPENDENCY_TRACK_API_KEY }}
parent: '6c94f662-28c5-4317-93aa-415bdbccc3ce'
- projectname: 'billing-console'
+ projectname: 'decabill-frontend-billing-console'
projectversion: ${{ needs.publish.outputs.new_release_version }}
autocreate: true
bomfilename: 'dist/sboms/decabill-frontend-billing-console.cdx.json'
- - name: Upload billing-manager SBOM
- id: upload-sbom-billing-manager
+ - name: Upload decabill-backend-billing-manager SBOM
+ id: upload-sbom-decabill-backend-billing-manager
uses: forepath/gh-upload-sbom@v2
continue-on-error: true
with:
serverhostname: dependencytrack.forepath.io
apikey: ${{ secrets.DEPENDENCY_TRACK_API_KEY }}
parent: '9cc09622-15b6-4dbf-a639-42288ffaa6a4'
- projectname: 'billing-manager'
+ projectname: 'decabill-backend-billing-manager'
projectversion: ${{ needs.publish.outputs.new_release_version }}
autocreate: true
bomfilename: 'dist/sboms/decabill-backend-billing-manager.cdx.json'
- - name: Upload desktop SBOM
- id: upload-sbom-desktop
+ - name: Upload agenstra-native-agent-console SBOM
+ id: upload-sbom-agenstra-native-agent-console
uses: forepath/gh-upload-sbom@v2
continue-on-error: true
with:
serverhostname: dependencytrack.forepath.io
apikey: ${{ secrets.DEPENDENCY_TRACK_API_KEY }}
parent: '3dd3b1cd-c07f-4355-baaf-e391de4293d2'
- projectname: 'desktop'
+ projectname: 'agenstra-native-agent-console'
projectversion: ${{ needs.publish.outputs.new_release_version }}
autocreate: true
bomfilename: 'dist/sboms/agenstra-native-agent-console.cdx.json'
- - name: Upload docs SBOM
- id: upload-sbom-docs
+ - name: Upload agenstra-frontend-docs SBOM
+ id: upload-sbom-agenstra-frontend-docs
uses: forepath/gh-upload-sbom@v2
continue-on-error: true
with:
serverhostname: dependencytrack.forepath.io
apikey: ${{ secrets.DEPENDENCY_TRACK_API_KEY }}
parent: '231546ff-a89a-487f-a6b0-6a1361d17a59'
- projectname: 'docs'
+ projectname: 'agenstra-frontend-docs'
projectversion: ${{ needs.publish.outputs.new_release_version }}
autocreate: true
bomfilename: 'dist/sboms/agenstra-frontend-docs.cdx.json'
- - name: Upload mcp-devkit SBOM
- id: upload-sbom-mcp-devkit
+ - name: Upload decabill-frontend-docs SBOM
+ id: upload-sbom-decabill-frontend-docs
+ uses: forepath/gh-upload-sbom@v2
+ continue-on-error: true
+ with:
+ serverhostname: dependencytrack.forepath.io
+ apikey: ${{ secrets.DEPENDENCY_TRACK_API_KEY }}
+ parent: '231546ff-a89a-487f-a6b0-6a1361d17a59'
+ projectname: 'decabill-frontend-docs'
+ projectversion: ${{ needs.publish.outputs.new_release_version }}
+ autocreate: true
+ bomfilename: 'dist/sboms/decabill-frontend-docs.cdx.json'
+
+ - name: Upload shared-mcp-devkit SBOM
+ id: upload-sbom-shared-mcp-devkit
uses: forepath/gh-upload-sbom@v2
continue-on-error: true
with:
@@ -806,8 +819,8 @@ jobs:
autocreate: true
bomfilename: 'dist/sboms/shared-mcp-devkit.cdx.json'
- - name: Upload mcp-proxy SBOM
- id: upload-sbom-mcp-proxy
+ - name: Upload shared-mcp-proxy SBOM
+ id: upload-sbom-shared-mcp-proxy
uses: forepath/gh-upload-sbom@v2
continue-on-error: true
with:
@@ -819,15 +832,15 @@ jobs:
autocreate: true
bomfilename: 'dist/sboms/shared-mcp-proxy.cdx.json'
- - name: Upload portal SBOM
- id: upload-sbom-portal
+ - name: Upload agenstra-frontend-landingpage SBOM
+ id: upload-sbom-agenstra-frontend-landingpage
uses: forepath/gh-upload-sbom@v2
continue-on-error: true
with:
serverhostname: dependencytrack.forepath.io
apikey: ${{ secrets.DEPENDENCY_TRACK_API_KEY }}
parent: 'bb6467b6-ebe9-4f90-91f9-2a785f95441e'
- projectname: 'portal'
+ projectname: 'agenstra-frontend-landingpage'
projectversion: ${{ needs.publish.outputs.new_release_version }}
autocreate: true
bomfilename: 'dist/sboms/agenstra-frontend-landingpage.cdx.json'
@@ -840,29 +853,31 @@ jobs:
echo ""
echo "| Project | Outcome |"
echo "|---------|---------|"
- echo "| agent-console | ${{ steps.upload-sbom-agent-console.outcome }} |"
- echo "| agent-controller | ${{ steps.upload-sbom-agent-controller.outcome }} |"
- echo "| agent-manager | ${{ steps.upload-sbom-agent-manager.outcome }} |"
- echo "| billing-console | ${{ steps.upload-sbom-billing-console.outcome }} |"
- echo "| billing-manager | ${{ steps.upload-sbom-billing-manager.outcome }} |"
- echo "| desktop | ${{ steps.upload-sbom-desktop.outcome }} |"
- echo "| docs | ${{ steps.upload-sbom-docs.outcome }} |"
- echo "| shared-mcp-devkit | ${{ steps.upload-sbom-mcp-devkit.outcome }} |"
- echo "| shared-mcp-proxy | ${{ steps.upload-sbom-mcp-proxy.outcome }} |"
- echo "| portal | ${{ steps.upload-sbom-portal.outcome }} |"
+ echo "| agenstra-frontend-agent-console | ${{ steps.upload-sbom-agenstra-frontend-agent-console.outcome }} |"
+ echo "| agenstra-backend-agent-controller | ${{ steps.upload-sbom-agenstra-backend-agent-controller.outcome }} |"
+ echo "| agenstra-backend-agent-manager | ${{ steps.upload-sbom-agenstra-backend-agent-manager.outcome }} |"
+ echo "| decabill-frontend-billing-console | ${{ steps.upload-sbom-decabill-frontend-billing-console.outcome }} |"
+ echo "| decabill-backend-billing-manager | ${{ steps.upload-sbom-decabill-backend-billing-manager.outcome }} |"
+ echo "| agenstra-native-agent-console | ${{ steps.upload-sbom-agenstra-native-agent-console.outcome }} |"
+ echo "| agenstra-frontend-docs | ${{ steps.upload-sbom-agenstra-frontend-docs.outcome }} |"
+ echo "| decabill-frontend-docs | ${{ steps.upload-sbom-decabill-frontend-docs.outcome }} |"
+ echo "| shared-mcp-devkit | ${{ steps.upload-sbom-shared-mcp-devkit.outcome }} |"
+ echo "| shared-mcp-proxy | ${{ steps.upload-sbom-shared-mcp-proxy.outcome }} |"
+ echo "| agenstra-frontend-landingpage | ${{ steps.upload-sbom-agenstra-frontend-landingpage.outcome }} |"
} >> "$GITHUB_STEP_SUMMARY"
failed=0
for outcome in \
- "${{ steps.upload-sbom-agent-console.outcome }}" \
- "${{ steps.upload-sbom-agent-controller.outcome }}" \
- "${{ steps.upload-sbom-agent-manager.outcome }}" \
- "${{ steps.upload-sbom-billing-console.outcome }}" \
- "${{ steps.upload-sbom-billing-manager.outcome }}" \
- "${{ steps.upload-sbom-desktop.outcome }}" \
- "${{ steps.upload-sbom-docs.outcome }}" \
- "${{ steps.upload-sbom-mcp-devkit.outcome }}" \
- "${{ steps.upload-sbom-mcp-proxy.outcome }}" \
- "${{ steps.upload-sbom-portal.outcome }}"
+ "${{ steps.upload-sbom-agenstra-frontend-agent-console.outcome }}" \
+ "${{ steps.upload-sbom-agenstra-backend-agent-controller.outcome }}" \
+ "${{ steps.upload-sbom-agenstra-backend-agent-manager.outcome }}" \
+ "${{ steps.upload-sbom-decabill-frontend-billing-console.outcome }}" \
+ "${{ steps.upload-sbom-decabill-backend-billing-manager.outcome }}" \
+ "${{ steps.upload-sbom-agenstra-native-agent-console.outcome }}" \
+ "${{ steps.upload-sbom-agenstra-frontend-docs.outcome }}" \
+ "${{ steps.upload-sbom-decabill-frontend-docs.outcome }}" \
+ "${{ steps.upload-sbom-shared-mcp-devkit.outcome }}" \
+ "${{ steps.upload-sbom-shared-mcp-proxy.outcome }}" \
+ "${{ steps.upload-sbom-agenstra-frontend-landingpage.outcome }}"
do
if [ "$outcome" = "failure" ]; then
failed=1
diff --git a/README.md b/README.md
index 7a5207caa..ce44e6c71 100644
--- a/README.md
+++ b/README.md
@@ -71,14 +71,14 @@ flowchart TB
AC -->|"HTTP REST API
WebSocket (Socket.IO)"| AM
```
-| Component | Nx project | Path | Description |
-| ------------------------ | ----------------------------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
-| Frontend agent console | `agenstra-frontend-agent-console` | [`apps/agenstra/frontend-agent-console`](./apps/agenstra/frontend-agent-console/) | Web-based IDE, chat, file management, tickets, knowledge |
-| Native agent console | `agenstra-native-agent-console` | [`apps/agenstra/native-agent-console`](./apps/agenstra/native-agent-console/) | Electron desktop shell around the agent console |
-| Backend agent controller | `agenstra-backend-agent-controller` | [`apps/agenstra/backend-agent-controller`](./apps/agenstra/backend-agent-controller/) | Control plane for clients, tickets, proxying, and statistics |
-| Backend agent manager | `agenstra-backend-agent-manager` | [`apps/agenstra/backend-agent-manager`](./apps/agenstra/backend-agent-manager/) | Agent lifecycle, Docker workloads, VNC, SSH, worker images |
-| Frontend docs | `agenstra-frontend-docs` | [`apps/agenstra/frontend-docs`](./apps/agenstra/frontend-docs/) | In-product documentation site |
-| Frontend landing page | `agenstra-frontend-landingpage` | [`apps/agenstra/frontend-landingpage`](./apps/agenstra/frontend-landingpage/) | Public Agenstra marketing and pricing site |
+| Component | Nx project | Path | Description |
+| ------------------------ | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
+| Frontend agent console | `agenstra-frontend-agent-console` | [`apps/agenstra/frontend-agent-console`](./apps/agenstra/frontend-agent-console/) | Web-based IDE, chat, file management, tickets, knowledge |
+| Native agent console | `agenstra-native-agent-console` | [`apps/agenstra/native-agent-console`](./apps/agenstra/native-agent-console/) | Electron desktop shell around the agent console |
+| Backend agent controller | `agenstra-backend-agent-controller` | [`apps/agenstra/backend-agent-controller`](./apps/agenstra/backend-agent-controller/) | Control plane for clients, tickets, proxying, and statistics |
+| Backend agent manager | `agenstra-backend-agent-manager` | [`apps/agenstra/backend-agent-manager`](./apps/agenstra/backend-agent-manager/) | Agent lifecycle, Docker workloads, VNC, SSH, worker images |
+| Frontend docs | `agenstra-frontend-docs` | [`apps/agenstra/frontend-docs`](./apps/agenstra/frontend-docs/) (patch over [`apps/shared/frontend-docs`](./apps/shared/frontend-docs/)) | In-product documentation site |
+| Frontend landing page | `agenstra-frontend-landingpage` | [`apps/agenstra/frontend-landingpage`](./apps/agenstra/frontend-landingpage/) | Public Agenstra marketing and pricing site |
Key Agenstra capabilities include centralized management of remote agent-manager instances, WebSocket-based agent chat, Monaco Editor file editing in containers, automated cloud server provisioning (Hetzner Cloud, DigitalOcean), Git operations from the browser, container monitoring and logs, VNC browser access, and CI/CD pipeline management from the console.
@@ -92,10 +92,11 @@ To get started with Agenstra:
Decabill is the ForePath billing product for subscriptions, invoicing, payment processing, and customer billing administration. Decabill apps and libraries are sublicensed under [BUSL-1.1](./apps/agenstra/backend-agent-controller/LICENSE) (same terms as the Agenstra agent controller).
-| Component | Nx project | Path | Description |
-| ------------------------ | ----------------------------------- | ------------------------------------------------------------------------------------- | -------------------------------------------- |
-| Backend billing manager | `decabill-backend-billing-manager` | [`apps/decabill/backend-billing-manager`](./apps/decabill/backend-billing-manager/) | Subscriptions, invoicing, Stripe integration |
-| Frontend billing console | `decabill-frontend-billing-console` | [`apps/decabill/frontend-billing-console`](./apps/decabill/frontend-billing-console/) | Admin and customer billing UI |
+| Component | Nx project | Path | Description |
+| ------------------------ | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- |
+| Backend billing manager | `decabill-backend-billing-manager` | [`apps/decabill/backend-billing-manager`](./apps/decabill/backend-billing-manager/) | Subscriptions, invoicing, Stripe integration |
+| Frontend billing console | `decabill-frontend-billing-console` | [`apps/decabill/frontend-billing-console`](./apps/decabill/frontend-billing-console/) | Admin and customer billing UI |
+| Frontend docs | `decabill-frontend-docs` | [`apps/decabill/frontend-docs`](./apps/decabill/frontend-docs/) (patch over [`apps/shared/frontend-docs`](./apps/shared/frontend-docs/)) | In-product documentation site |
### Shared platform
@@ -106,6 +107,7 @@ Shared applications and libraries are used by multiple ForePath products. They c
| Platform authentication | `shared-platform-authentication` | [`apps/shared/platform-authentication`](./apps/shared/platform-authentication/) | Keycloak Docker Compose for local and shared auth |
| MCP devkit | `shared-mcp-devkit` | [`apps/shared/mcp-devkit`](./apps/shared/mcp-devkit/) | Model Context Protocol server for workspace and Nx tooling |
| MCP proxy | `shared-mcp-proxy` | [`apps/shared/mcp-proxy`](./apps/shared/mcp-proxy/) | Proxy that routes MCP clients to the devkit |
+| Frontend docs (base) | `shared-frontend-docs` | [`apps/shared/frontend-docs`](./apps/shared/frontend-docs/) | Shared SSR docs shell used by brand patch apps |
Identity libraries under [`libs/domains/identity`](./libs/domains/identity/) provide Keycloak-backed authentication for NestJS backends and Angular frontends across Agenstra and future products.
@@ -129,6 +131,17 @@ Identity libraries under [`libs/domains/identity`](./libs/domains/identity/) pro
Feature guides (client management, agent management, server provisioning, WebSocket communication, file management, version control, web IDE, chat, VNC access) live under [`docs/agenstra/features/`](./docs/agenstra/features/).
+### Decabill
+
+- [Getting Started Guide](./docs/decabill/getting-started.md)
+- [System Overview](./docs/decabill/architecture/system-overview.md)
+- [Docker Deployment](./docs/decabill/deployment/docker-deployment.md)
+- [Environment Configuration](./docs/decabill/deployment/environment-configuration.md)
+- [API Reference](./docs/decabill/api-reference/README.md)
+- [Security documentation](./docs/decabill/security/README.md)
+
+Feature guides (subscriptions, invoices, billing administration, multi-tenancy, payments, server provisioning) live under [`docs/decabill/features/`](./docs/decabill/features/).
+
## License
The repository default is the **MIT License**. See [LICENSE](./LICENSE) for the full text.
@@ -183,7 +196,9 @@ The following components are source-available. You may view the source code to u
- [`apps/agenstra/frontend-landingpage`](./apps/agenstra/frontend-landingpage/) ([LICENSE](./apps/agenstra/frontend-landingpage/LICENSE))
- [`libs/domains/agenstra/frontend/feature-landingpage`](./libs/domains/agenstra/frontend/feature-landingpage/) ([LICENSE](./libs/domains/agenstra/frontend/feature-landingpage/LICENSE))
- [`apps/agenstra/frontend-docs`](./apps/agenstra/frontend-docs/) ([LICENSE](./apps/agenstra/frontend-docs/LICENSE))
-- [`libs/domains/agenstra/frontend/feature-docs`](./libs/domains/agenstra/frontend/feature-docs/) ([LICENSE](./libs/domains/agenstra/frontend/feature-docs/LICENSE))
+- [`apps/decabill/frontend-docs`](./apps/decabill/frontend-docs/) ([LICENSE](./apps/decabill/frontend-docs/LICENSE))
+- [`apps/shared/frontend-docs`](./apps/shared/frontend-docs/) ([LICENSE](./apps/shared/frontend-docs/LICENSE))
+- [`libs/domains/shared/frontend/feature-docs`](./libs/domains/shared/frontend/feature-docs/) ([LICENSE](./libs/domains/shared/frontend/feature-docs/LICENSE))
- [`libs/domains/agenstra/frontend/data-access-portal`](./libs/domains/agenstra/frontend/data-access-portal/) ([LICENSE](./libs/domains/agenstra/frontend/data-access-portal/LICENSE))
## Contributing
diff --git a/SECURITY.md b/SECURITY.md
index 65f5b1d70..0c163e028 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -1,5 +1,7 @@
# Security Policy
+This policy applies to the **ForePath One** monorepo, which contains multiple products. Product-specific security documentation lives in each product docs tree. Use the links in **Security Resources** below for detailed registers, SBOM paths, and hardening notes.
+
## Supported Versions
We provide security updates for the following versions of this framework:
@@ -11,7 +13,10 @@ We provide security updates for the following versions of this framework:
| 0.x.x | No |
| Earlier major lines | No |
-Security updates are intended for supported **2.x.x** releases. Full disclosure and CRA-oriented context: **[Supported versions and security updates](./docs/agenstra/security/vulnerability-reporting-and-artifacts.md#supported-versions-and-security-updates)**.
+Security updates are intended for supported **2.x.x** releases. Full disclosure and CRA-oriented context:
+
+- **Agenstra:** [Supported versions and security updates](./docs/agenstra/security/vulnerability-reporting-and-artifacts.md#supported-versions-and-security-updates)
+- **Decabill:** [Supported versions and security updates](./docs/decabill/security/vulnerability-reporting-and-artifacts.md#supported-versions-and-security-updates)
## Reporting a Vulnerability
@@ -24,7 +29,7 @@ We take security seriously and appreciate your help in keeping this framework an
Instead, please report security vulnerabilities to our security team:
- **Email**: soc@forepath.io
-- **Subject**: `[SECURITY] Framework Vulnerability Report`
+- **Subject**: `[SECURITY] Framework Vulnerability Report` (you may add the product name, for example `Agenstra` or `Decabill`, in the body)
- **Response Time**: We aim to respond within 48 hours
### What to Include in Your Report
@@ -60,7 +65,7 @@ We believe in recognizing security researchers who help keep this framework secu
### For Developers
- **Keep Dependencies Updated** - Regularly update all dependencies
-- **Follow Security Guidelines** - Adhere to the project’s code quality and security practices
+- **Follow Security Guidelines** - Adhere to the project's code quality and security practices
- **Use Secure Coding Practices** - Follow secure coding principles
- **Regular Security Audits** - Perform regular security audits of your code
@@ -92,44 +97,52 @@ This framework includes several built-in security features:
## Documented security deviations (accepted risks)
-The product intentionally departs from stricter baselines in a few places. Each item below is **accepted** with compensating measures and a **review cadence**. Expanded register entries (BSI / ISMS-style fields, operator summaries, and withdrawal paths) live in **[docs/agenstra/security/accepted-risks.md](./docs/agenstra/security/accepted-risks.md)**. Additional threat context and backlog items may appear in [`thread-analysis.md`](./thread-analysis.md) (internal analysis note).
+The products intentionally depart from stricter baselines in a few places. Each item below is **accepted** with compensating measures and a **review cadence**. Expanded register entries live in the product security docs linked below.
+
+### Agenstra (register AR-001 through AR-005)
+
+Full register: **[docs/agenstra/security/accepted-risks.md](./docs/agenstra/security/accepted-risks.md)**
+
+| ID | Area | Summary |
+| ---------- | -------------------------- | --------------------------------------------------------------------------- |
+| **AR-001** | Desktop app | No OS-trusted code signing; no in-app auto-update (checksum manifests) |
+| **AR-002** | Web frontends | CSP allows `unsafe-inline` / `unsafe-eval` for Monaco (report-only default) |
+| **AR-003** | Backend auth resolution | `AUTHENTICATION_METHOD` optional; implicit keycloak when no API key set |
+| **AR-004** | Desktop window open policy | Electron `setWindowOpenHandler` allows new windows |
+| **AR-005** | Trivy gate | Unfixed CVEs do not fail CI (`ignore-unfixed: true`) |
-| ID | Area | What we accept | Mitigations (short) | Next review |
-| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
-| **AR-001** | **Provisioning SSH** (cloud-init templates) | **`PermitRootLogin yes`** and **root** `authorized_keys` installed via provisioning scripts (`libs/domains/decabill/backend/feature-billing-manager/src/lib/utils/cloud-init/agent-controller.utils.ts`, `agent-manager.utils.ts`) | SSH **key-based** access; **password authentication disabled** in generated `sshd` config; deployers should restrict network access, rotate keys, and monitor instances | **2027-05-06**, or sooner if cloud-init/SSH templates change materially |
-| **AR-002** | **Desktop app** (`agenstra-native-agent-console`) | **No OS-trusted code signing** and **no in-app auto-update** in the Electron Forge pipeline (`apps/agenstra/native-agent-console/forge.config.js`) | Release artifacts include **`SHA256SUMS`** and **`integrity-manifest.json`** produced by [`tools/release-integrity`](./tools/release-integrity/README.md); CI/release pipelines **generate and verify** these manifests. Users should verify checksums after download. The web browser remains the primary client; the native build is a secondary channel. | **2027-05-06**, or sooner if desktop becomes the primary distribution path |
-| **AR-003** | **Web frontends** (`frontend-*`) | **Content Security Policy** allows **`'unsafe-inline'`** and **`'unsafe-eval'`** so **Monaco Editor** and related tooling work; policy is sent as **`Content-Security-Policy-Report-Only`** by default (violations are reported, not blocked) | Set **`CSP_ENFORCE=true`** only in environments where compatibility is validated. Implementation: `libs/domains/shared/frontend/util-express-server/src/lib/security-headers.ts`. Hardening path: stricter CSP with a validated Monaco/worker/nonce strategy. | **2027-05-06**, or sooner if CSP middleware changes materially |
-| **AR-004** | **Backend authentication mode resolution** (`getAuthenticationMethod` in `libs/domains/identity/backend/util-auth/src/lib/hybrid-auth.guard.ts`) | We do **not** require **`AUTHENTICATION_METHOD`** to always be set. When it is unset: if **`STATIC_API_KEY`** is set → **api-key** mode; otherwise → **keycloak** (OIDC / **Keycloak** integration with the deployer’s IdP). **Protected routes are not anonymous**—Keycloak- or users-mode guards still enforce authentication per configuration. | **Default `keycloak`** favors the most integrated, enterprise-typical option (customer IdP). For **api-key** or **users** deployments, set **`AUTHENTICATION_METHOD`** explicitly and treat **`STATIC_API_KEY`** as a high-value secret (rotation, least exposure). | **2027-05-06**, or sooner if hybrid auth resolution changes materially |
-| **AR-005** | **Desktop window open policy** (`agenstra-native-agent-console`) | **`setWindowOpenHandler`** in `apps/agenstra/native-agent-console/src/main.ts` uses **`action: 'allow'`** so `window.open` / `target=_blank` can open new Electron windows with inherited `webPreferences`. | Compared with a full browser, phishing and popup abuse risk is **lower**: there is **no address bar (omnibox)** and **users cannot install browser extensions/plugins**. **Sandbox** and **contextIsolation** remain enabled. Revisit if the shell gains untrusted browsing or URL-entry UX. | **2027-05-06**, or sooner if main-process window policy changes materially |
-| **AR-006** | **Trivy vulnerability gate** ([`trivy.yaml`](./trivy.yaml)) | **Unfixed CVEs do not fail** CI or local Trivy hooks (`vulnerability.ignore-unfixed: true`). Only **CRITICAL** findings **with a published Fixed Version** fail the gate. | SARIF and workflow artifacts still surface unfixed issues for review; SBOMs and Dependency Track on release add visibility. Use [`.trivyignore`](./.trivyignore) for fixable CVEs that cannot be applied yet—not to waive unfixed findings. See **[AR-006](./docs/agenstra/security/accepted-risks.md#ar-006--ci--local-trivy-unfixed-vulnerabilities-not-gated)**. | **2027-05-06**, or sooner if `trivy.yaml` severity or ignore policy changes materially |
-| **AR-007** | **Billing multi-tenant API key** (`backend-billing-manager`, `TenantUserGuard`) | With **`STATIC_API_KEY`** and **without** **`STATIC_API_KEY_TENANT_ID`**, one deployment API key grants **admin access to every tenant** allowed by **`TENANTS`**, selected per request via **`X-Tenant`**. **Accepted** because there is a single shared automation key. | Set **`STATIC_API_KEY_TENANT_ID`** to bind the key to one tenant; prefer **keycloak** / **users** for the billing console; rotate and protect **`STATIC_API_KEY`**. User/session auth remains per-tenant. See **[AR-007](./docs/agenstra/security/accepted-risks.md#ar-007--billing-multi-tenant-api-key-scope-static_api_key_tenant_id-unset)**. | **2027-05-06**, or sooner if billing tenant or API-key policy changes materially |
+### Decabill (register DR-001 through DR-005)
-**Hardening paths (if an acceptance is withdrawn):**
+Full register: **[docs/decabill/security/accepted-risks.md](./docs/decabill/security/accepted-risks.md)**
-- **AR-001**: Prefer a non-root admin user, **`PermitRootLogin no`**, least-privilege `sudo`, and cloud-init-native `ssh_authorized_keys` where possible; reduce secrets in user-data.
-- **AR-002**: Add OS-trusted signing and/or Electron auto-update when native distribution requirements justify the operational cost.
-- **AR-003**: Tighten CSP after automated and manual verification so core UI (including Monaco) still functions.
-- **AR-004**: Require **`AUTHENTICATION_METHOD`** in all environments if auditors or policy demand fully explicit configuration, or add startup validation that fails when **`STATIC_API_KEY`** is set without an explicit mode.
-- **AR-005**: Tighten **`setWindowOpenHandler`** (e.g. URL allowlist or **`action: 'deny'`**) if the product starts loading untrusted origins or adds browser-like navigation.
-- **AR-006**: Fail on unfixed CRITICAL (and optionally HIGH) findings if auditors require zero tolerance regardless of vendor fix availability.
-- **AR-007**: Require **`STATIC_API_KEY_TENANT_ID`** when multiple tenants use API key auth, or disable API key on multi-tenant billing, or introduce per-tenant API keys.
+| ID | Area | Summary |
+| ---------- | ---------------------------- | ------------------------------------------------------------------- |
+| **DR-001** | Provisioning SSH | Cloud-init may enable root SSH with authorized_keys |
+| **DR-002** | Billing multi-tenant API key | Shared `STATIC_API_KEY` can access all tenants when tenant id unset |
+| **DR-003** | Web frontends | CSP allows `unsafe-inline` / `unsafe-eval` (report-only default) |
+| **DR-004** | Backend auth resolution | Same implicit auth mode resolution as shared identity stack |
+| **DR-005** | Trivy gate | Unfixed CVEs do not fail CI (monorepo-wide `trivy.yaml`) |
## Security Resources
## Software Bill of Materials (SBOM)
-We publish CycloneDX SBOM files for each release (Nx service SBOMs and Trivy container image SBOMs).
+We publish CycloneDX SBOM files for each release (Nx service SBOMs and Trivy container image SBOMs). Each product publishes to its own object-store bucket under the same key layout.
- **Path**: `releases//sboms/`
- **Example**: `releases/2.0.0/sboms/`
-- **How to find your version**: Check the release version in [Downloads](https://downloads.agenstra.com/), then replace `` in the path above.
-Details: **[Software Bill of Materials (SBOM)](./docs/agenstra/security/vulnerability-reporting-and-artifacts.md#software-bill-of-materials-sbom)**.
+| Product | Downloads | SBOM documentation |
+| -------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
+| Agenstra | [downloads.agenstra.com](https://downloads.agenstra.com/) | [Agenstra SBOM](./docs/agenstra/security/vulnerability-reporting-and-artifacts.md#software-bill-of-materials-sbom) |
+| Decabill | [downloads.decabill.com](https://downloads.decabill.com/) | [Decabill SBOM](./docs/decabill/security/vulnerability-reporting-and-artifacts.md#software-bill-of-materials-sbom) |
### Documentation
-- [Project overview and docs](./docs/agenstra/README.md) - Architecture, deployment, and setup
-- [Security documentation](./docs/agenstra/security/README.md) - CRA- and BSI-oriented transparency, accepted-risk register, hardening, SBOM, disclosure, and CI scanning (Trivy)
+- [Agenstra documentation](./docs/agenstra/README.md) - Architecture, deployment, and setup
+- [Agenstra security documentation](./docs/agenstra/security/README.md) - CRA/BSI transparency, accepted risks, hardening, SBOM, disclosure, CI scanning
+- [Decabill documentation](./docs/decabill/README.md) - Billing product guides
+- [Decabill security documentation](./docs/decabill/security/README.md) - Decabill accepted risks, SBOM, and hardening
### External Resources
diff --git a/apps/agenstra/frontend-billing-console/project.json b/apps/agenstra/frontend-billing-console/project.json
index dbc839cc2..a83935696 100644
--- a/apps/agenstra/frontend-billing-console/project.json
+++ b/apps/agenstra/frontend-billing-console/project.json
@@ -221,7 +221,8 @@
"cache": false,
"executor": "nx:run-commands",
"options": {
- "command": "docker rm -f agenstra-billing-console-server 2>/dev/null || true; docker run -d --name agenstra-billing-console-server --pull never -p ${PORT:-4500}:${PORT:-4500} -e HOST=${HOST:-0.0.0.0} -e PORT=${PORT:-4500} -e NODE_ENV=${NODE_ENV:-production} -e DEFAULT_LOCALE=${DEFAULT_LOCALE:-en} -e CSP_ENFORCE=${CSP_ENFORCE:-true} -e CSP_CONNECT_SRC_EXTRA=${CSP_CONNECT_SRC_EXTRA:-http://host.docker.internal:3200} ghcr.io/forepath/agenstra-billing-console-server:latest"
+ "cwd": "apps/decabill/frontend-billing-console",
+ "command": "BILLING_CONSOLE_SERVER_IMAGE=ghcr.io/forepath/agenstra-billing-console-server:latest BILLING_CONSOLE_SERVER_CONTAINER_NAME=agenstra-billing-console-server docker compose up -d --force-recreate --remove-orphans"
}
},
"sbom": {
diff --git a/apps/agenstra/frontend-docs/project.json b/apps/agenstra/frontend-docs/project.json
index 4e60f5672..9737ade82 100644
--- a/apps/agenstra/frontend-docs/project.json
+++ b/apps/agenstra/frontend-docs/project.json
@@ -3,11 +3,11 @@
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"projectType": "application",
"prefix": "app",
- "sourceRoot": "apps/agenstra/frontend-docs/src",
+ "sourceRoot": "apps/shared/frontend-docs/src",
"i18n": {
"sourceLocale": "en",
"locales": {
- "de": "apps/agenstra/frontend-docs/src/i18n/messages.de.xlf"
+ "de": "apps/shared/frontend-docs/src/i18n/messages.de.xlf"
}
},
"tags": ["domain:agenstra", "type:app", "scope:frontend"],
@@ -19,7 +19,7 @@
"{workspaceRoot}/dist/apps/agenstra/frontend-docs-temp/public/docs"
],
"options": {
- "command": "mkdir -p dist/apps/agenstra/frontend-docs-temp/assets/docs dist/apps/agenstra/frontend-docs-temp/public/docs dist/tools/docs && npx tsc tools/docs/generate-docs.ts --outDir dist/tools/docs --module esnext --moduleResolution node --target es2020 --esModuleInterop --skipLibCheck --resolveJsonModule && node dist/tools/docs/generate-docs.js",
+ "command": "mkdir -p dist/apps/agenstra/frontend-docs-temp/assets/docs dist/apps/agenstra/frontend-docs-temp/public/docs dist/tools/docs && npx tsc tools/docs/generate-docs.ts --outDir dist/tools/docs --module esnext --moduleResolution node --target es2020 --esModuleInterop --skipLibCheck --resolveJsonModule && node dist/tools/docs/generate-docs.js --contentRoot=agenstra --outputTemp=dist/apps/agenstra/frontend-docs-temp",
"cwd": "{workspaceRoot}",
"cache": false
}
@@ -31,14 +31,15 @@
"options": {
"outputPath": "dist/apps/agenstra/frontend-docs",
"index": "apps/agenstra/frontend-docs/src/index.html",
- "browser": "apps/agenstra/frontend-docs/src/main.ts",
- "polyfills": [
- "zone.js",
- "apps/agenstra/frontend-docs/src/polyfills.ts"
- ],
- "tsConfig": "apps/agenstra/frontend-docs/tsconfig.app.json",
+ "browser": "apps/shared/frontend-docs/src/main.ts",
+ "polyfills": ["zone.js", "apps/shared/frontend-docs/src/polyfills.ts"],
+ "tsConfig": "apps/shared/frontend-docs/tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
+ {
+ "glob": "**/*",
+ "input": "apps/shared/frontend-docs/public"
+ },
{
"glob": "**/*",
"input": "apps/agenstra/frontend-docs/public"
@@ -71,9 +72,10 @@
],
"styles": [
"node_modules/cookieconsent/build/cookieconsent.min.css",
- "apps/agenstra/frontend-docs/src/styles.scss"
+ "apps/agenstra/frontend-docs/src/styles.scss",
+ "libs/domains/shared/frontend/util-cookie-consent/src/lib/cookie-consent.scss"
],
- "server": "apps/agenstra/frontend-docs/src/main.server.ts",
+ "server": "apps/shared/frontend-docs/src/main.server.ts",
"ssr": {
"entry": "apps/agenstra/frontend-docs/src/server.ts"
},
@@ -157,14 +159,14 @@
"executor": "@angular-devkit/build-angular:extract-i18n",
"options": {
"buildTarget": "agenstra-frontend-docs:build",
- "outputPath": "apps/agenstra/frontend-docs/src/i18n"
+ "outputPath": "apps/shared/frontend-docs/src/i18n"
}
},
"extract-i18n": {
"executor": "nx:run-commands",
"options": {
"command": "node ../../../remove-context-groups.cjs src/i18n/messages.xlf",
- "cwd": "apps/agenstra/frontend-docs"
+ "cwd": "apps/shared/frontend-docs"
},
"dependsOn": ["extract-i18n-angular"]
},
@@ -175,8 +177,8 @@
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
- "jestConfig": "apps/agenstra/frontend-docs/jest.config.ts",
- "tsConfig": "apps/agenstra/frontend-docs/tsconfig.spec.json"
+ "jestConfig": "apps/shared/frontend-docs/jest.config.ts",
+ "tsConfig": "apps/shared/frontend-docs/tsconfig.spec.json"
}
},
"serve-static": {
@@ -195,7 +197,7 @@
"{workspaceRoot}/dist/apps/agenstra/frontend-docs-delegating-server"
],
"options": {
- "command": "mkdir -p dist/apps/agenstra/frontend-docs-delegating-server/server && npx tsc apps/agenstra/frontend-docs/src/delegating-server.ts --outDir dist/apps/agenstra/frontend-docs-delegating-server/server --target es2022 --module esnext --moduleResolution node --allowSyntheticDefaultImports --esModuleInterop --skipLibCheck --declaration false --sourceMap false --removeComments true",
+ "command": "mkdir -p dist/apps/agenstra/frontend-docs-delegating-server/server && npx tsc apps/shared/frontend-docs/src/delegating-server.ts --outDir dist/apps/agenstra/frontend-docs-delegating-server/server --target es2022 --module esnext --moduleResolution node --allowSyntheticDefaultImports --esModuleInterop --skipLibCheck --declaration false --sourceMap false --removeComments true",
"cwd": "."
}
},
@@ -212,7 +214,7 @@
"executor": "@nx-tools/nx-container:build",
"options": {
"engine": "docker",
- "file": "apps/agenstra/frontend-docs/Dockerfile.server",
+ "file": "apps/shared/frontend-docs/Dockerfile.server",
"context": "dist/apps/agenstra/frontend-docs/server"
},
"configurations": {
@@ -235,8 +237,8 @@
"cache": false,
"executor": "nx:run-commands",
"options": {
- "cwd": "apps/agenstra/frontend-docs",
- "command": "docker compose up -d --force-recreate --remove-orphans"
+ "cwd": "apps/shared/frontend-docs",
+ "command": "DOCS_SERVER_IMAGE=ghcr.io/forepath/agenstra-docs-server:latest DOCS_SERVER_CONTAINER_NAME=agenstra-docs-server docker compose up -d --force-recreate --remove-orphans"
}
},
"sbom": {
diff --git a/apps/agenstra/frontend-docs/src/server.ts b/apps/agenstra/frontend-docs/src/server.ts
index 4cf9eb430..00ee0940e 100644
--- a/apps/agenstra/frontend-docs/src/server.ts
+++ b/apps/agenstra/frontend-docs/src/server.ts
@@ -1,62 +1,12 @@
-import { dirname, join, resolve } from 'node:path';
+import { isMainModule } from '@angular/ssr/node';
+// eslint-disable-next-line @nx/enforce-module-boundaries
+import bootstrap from '@forepath/shared/frontend/util-docs-bootstrap';
+import { createDocsServer } from '@forepath/shared/frontend/util-express-server/docs-server';
import { fileURLToPath } from 'node:url';
-import { APP_BASE_HREF } from '@angular/common';
-import { CommonEngine, isMainModule } from '@angular/ssr/node';
-import {
- buildSsrAllowedHosts,
- createSecurityHeadersMiddleware,
- registerRuntimeConfigEndpoint,
-} from '@forepath/shared/frontend/util-express-server';
-import express from 'express';
+const app = createDocsServer(['agenstra.com'], bootstrap);
-import bootstrap from './main.server';
-
-const serverDistFolder = dirname(fileURLToPath(import.meta.url));
-const browserDistFolder = resolve(serverDistFolder, '../browser');
-const indexHtml = join(serverDistFolder, 'index.server.html');
-const app = express();
-const commonEngine = new CommonEngine({
- allowedHosts: buildSsrAllowedHosts(['agenstra.com']),
-});
-
-app.use(createSecurityHeadersMiddleware());
-registerRuntimeConfigEndpoint(app);
-
-/**
- * Serve static files from /browser
- */
-app.get(
- '**',
- express.static(browserDistFolder, {
- maxAge: '1y',
- index: 'index.html',
- }),
-);
-
-/**
- * Handle all other requests by rendering the Angular application.
- */
-app.get('**', (req, res, next) => {
- const { protocol, originalUrl, baseUrl, headers } = req;
-
- commonEngine
- .render({
- bootstrap,
- documentFilePath: indexHtml,
- url: `${protocol}://${headers.host}${originalUrl}`,
- publicPath: browserDistFolder,
- providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }],
- })
- .then((html) => res.send(html))
- .catch((err) => next(err));
-});
-
-/**
- * Start the server if this module is the main entry point.
- * The server listens on the port defined by the `PORT` environment variable, or defaults to 4000.
- */
-if (isMainModule(import.meta.url)) {
+if (isMainModule(fileURLToPath(import.meta.url))) {
const port = parseInt(process.env['PORT'] || '4000', 10);
app.listen(port, '0.0.0.0', () => {
diff --git a/apps/agenstra/frontend-docs/src/styles.scss b/apps/agenstra/frontend-docs/src/styles.scss
index b2ba6d3f6..a283b479a 100644
--- a/apps/agenstra/frontend-docs/src/styles.scss
+++ b/apps/agenstra/frontend-docs/src/styles.scss
@@ -117,4 +117,3 @@ $agenstra-badge-text-bg-colors: primary, secondary, success, info, warning, dang
@import 'bootstrap/scss/bootstrap-grid';
@import 'bootstrap-icons/font/bootstrap-icons';
@import '@fontsource-variable/plus-jakarta-sans/wght.css';
-@import 'libs/domains/shared/frontend/util-cookie-consent/src/lib/cookie-consent.scss';
diff --git a/apps/decabill/frontend-billing-console/docker-compose.yaml b/apps/decabill/frontend-billing-console/docker-compose.yaml
index bcca7c152..0f52ad11f 100644
--- a/apps/decabill/frontend-billing-console/docker-compose.yaml
+++ b/apps/decabill/frontend-billing-console/docker-compose.yaml
@@ -1,10 +1,9 @@
services:
frontend-billing-console-server:
- image: ghcr.io/forepath/decabill-billing-console-server:latest
+ image: ${BILLING_CONSOLE_SERVER_IMAGE:-ghcr.io/forepath/decabill-billing-console-server:latest}
pull_policy: never
- container_name: billing-console-server
+ container_name: ${BILLING_CONSOLE_SERVER_CONTAINER_NAME:-billing-console-server}
environment:
- # Frontend server configuration
HOST: ${HOST:-0.0.0.0}
PORT: ${PORT:-4500}
NODE_ENV: ${NODE_ENV:-production}
diff --git a/apps/decabill/frontend-billing-console/project.json b/apps/decabill/frontend-billing-console/project.json
index 27d3f7159..47573c974 100644
--- a/apps/decabill/frontend-billing-console/project.json
+++ b/apps/decabill/frontend-billing-console/project.json
@@ -218,7 +218,7 @@
"executor": "nx:run-commands",
"options": {
"cwd": "apps/decabill/frontend-billing-console",
- "command": "docker compose up -d --force-recreate --remove-orphans"
+ "command": "BILLING_CONSOLE_SERVER_IMAGE=ghcr.io/forepath/decabill-billing-console-server:latest BILLING_CONSOLE_SERVER_CONTAINER_NAME=decabill-billing-console-server docker compose up -d --force-recreate --remove-orphans"
}
},
"sbom": {
diff --git a/apps/decabill/frontend-docs/.eslintrc.json b/apps/decabill/frontend-docs/.eslintrc.json
new file mode 100644
index 000000000..78770429d
--- /dev/null
+++ b/apps/decabill/frontend-docs/.eslintrc.json
@@ -0,0 +1,36 @@
+{
+ "extends": ["../../../.eslintrc.json", "../../../.eslintrc.json"],
+ "ignorePatterns": ["!**/*"],
+ "overrides": [
+ {
+ "files": ["*.ts"],
+ "extends": [
+ "plugin:@nx/angular",
+ "plugin:@angular-eslint/template/process-inline-templates"
+ ],
+ "rules": {
+ "@angular-eslint/directive-selector": [
+ "error",
+ {
+ "type": "attribute",
+ "prefix": "app",
+ "style": "camelCase"
+ }
+ ],
+ "@angular-eslint/component-selector": [
+ "error",
+ {
+ "type": "element",
+ "prefix": "app",
+ "style": "kebab-case"
+ }
+ ]
+ }
+ },
+ {
+ "files": ["*.html"],
+ "extends": ["plugin:@nx/angular-template"],
+ "rules": {}
+ }
+ ]
+}
diff --git a/apps/decabill/frontend-docs/LICENSE b/apps/decabill/frontend-docs/LICENSE
new file mode 100644
index 000000000..f5ad2d389
--- /dev/null
+++ b/apps/decabill/frontend-docs/LICENSE
@@ -0,0 +1,107 @@
+Licensor: IPvX UG (haftungsbeschränkt)
+
+Licensed Work: TSEF
+ The Licensed Work is Copyright © 2025 IPvX UG (haftungsbeschränkt)
+
+Additional Use Grant: You may use the Licensed Work in production as long as
+ your Total Finances do not exceed EUR €2,000,000 for the
+ most recent 12-month period, provided that IPvX UG (haftungsbeschränkt)
+ will not be liable to you in any way, including for any
+ damages, including general, special, incidental or
+ consequential damages, arising out of such use.
+
+ References to: "Total Finances" mean the largest of your
+ aggregate gross revenues, entire budget, and/or funding
+ (no matter the source); "you" and "your" include (without
+ limitation) any individual or entity agreeing to these
+ terms and any affiliates of such individual or entity; and
+ "production" mean any use other than (i) development of
+ (including evaluation of the Licensed Work), debugging, or
+ testing your offerings, or (ii) making the Licensed Work
+ available standalone in unmodified object code form.
+
+Change Date: Three years from release date
+
+Change License: GNU Affero Public License (AGPL) v3
+
+For information about alternative licensing arrangements, please visit
+https://agenstra.com.
+
+--------------------------------------------------------------------------------
+
+Business Source License 1.1
+
+Terms
+
+The Licensor hereby grants you the right to copy, modify, create derivative
+works, redistribute, and make non-production use of the Licensed Work. The
+Licensor may make an Additional Use Grant, above, permitting limited production
+use.
+
+Effective on the Change Date, or the fourth anniversary of the first publicly
+available distribution of a specific version of the Licensed Work under this
+License, whichever comes first, the Licensor hereby grants you rights under the
+terms of the Change License, and the rights granted in the paragraph above
+terminate.
+
+If your use of the Licensed Work does not comply with the requirements currently
+in effect as described in this License, you must purchase a commercial license
+from the Licensor, its affiliated entities, or authorized resellers, or you must
+refrain from using the Licensed Work.
+
+All copies of the original and modified Licensed Work, and derivative works of
+the Licensed Work, are subject to this License. This License applies separately
+for each version of the Licensed Work and the Change Date may vary for each
+version of the Licensed Work released by Licensor.
+
+You must conspicuously display this License on each original or modified copy of
+the Licensed Work. If you receive the Licensed Work in original or modified form
+from a third party, the terms and conditions set forth in this License apply to
+your use of that work.
+
+Any use of the Licensed Work in violation of this License will automatically
+terminate your rights under this License for the current and all other versions
+of the Licensed Work.
+
+This License does not grant you any right in any trademark or logo of Licensor
+or its affiliates (provided that you may use a trademark or logo of Licensor as
+expressly required by this License).
+
+TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON AN
+"AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, EXPRESS
+OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND TITLE.
+
+MariaDB hereby grants you permission to use this License’s text to license your
+works, and to refer to it using the trademark "Business Source License", as long
+as you comply with the Covenants of Licensor below.
+
+Covenants of Licensor
+
+In consideration of the right to use this License’s text and the "Business
+Source License" name and trademark, Licensor covenants to MariaDB, and to all
+other recipients of the licensed work to be provided by Licensor:
+
+1. To specify as the Change License the GPL Version 2.0 or any later version,
+ or a license that is compatible with GPL Version 2.0 or a later version,
+ where "compatible" means that software provided under the Change License can
+ be included in a program with software provided under GPL Version 2.0 or a
+ later version. Licensor may specify additional Change Licenses without
+ limitation.
+
+2. To either: (a) specify an additional grant of rights to use that does not
+ impose any additional restriction on the right granted in this License, as
+ the Additional Use Grant; or (b) insert the text "None".
+
+3. To specify a Change Date.
+
+4. Not to modify this License in any other way.
+
+Notice
+
+The Business Source License (this document, or the "License") is not an Open
+Source license. However, the Licensed Work will eventually be made available
+under an Open Source License, as stated in this License.
+
+License text copyright © 2023 MariaDB plc, All Rights Reserved.
+"Business Source License" is a trademark of MariaDB plc.
diff --git a/apps/decabill/frontend-docs/project.json b/apps/decabill/frontend-docs/project.json
new file mode 100644
index 000000000..9cf85907e
--- /dev/null
+++ b/apps/decabill/frontend-docs/project.json
@@ -0,0 +1,239 @@
+{
+ "name": "decabill-frontend-docs",
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "application",
+ "prefix": "app",
+ "sourceRoot": "apps/shared/frontend-docs/src",
+ "i18n": {
+ "sourceLocale": "en",
+ "locales": {
+ "de": "apps/shared/frontend-docs/src/i18n/messages.de.xlf"
+ }
+ },
+ "tags": ["domain:decabill", "type:app", "scope:frontend"],
+ "targets": {
+ "generate-docs": {
+ "executor": "nx:run-commands",
+ "outputs": [
+ "{workspaceRoot}/dist/apps/decabill/frontend-docs-temp/assets/docs",
+ "{workspaceRoot}/dist/apps/decabill/frontend-docs-temp/public/docs"
+ ],
+ "options": {
+ "command": "mkdir -p dist/apps/decabill/frontend-docs-temp/assets/docs dist/apps/decabill/frontend-docs-temp/public/docs dist/tools/docs && npx tsc tools/docs/generate-docs.ts --outDir dist/tools/docs --module esnext --moduleResolution node --target es2020 --esModuleInterop --skipLibCheck --resolveJsonModule && node dist/tools/docs/generate-docs.js --contentRoot=decabill --outputTemp=dist/apps/decabill/frontend-docs-temp",
+ "cwd": "{workspaceRoot}",
+ "cache": false
+ }
+ },
+ "build": {
+ "executor": "@angular-devkit/build-angular:application",
+ "dependsOn": ["generate-docs"],
+ "outputs": ["{options.outputPath}"],
+ "options": {
+ "outputPath": "dist/apps/decabill/frontend-docs",
+ "index": "apps/decabill/frontend-docs/src/index.html",
+ "browser": "apps/shared/frontend-docs/src/main.ts",
+ "polyfills": ["zone.js", "apps/shared/frontend-docs/src/polyfills.ts"],
+ "tsConfig": "apps/shared/frontend-docs/tsconfig.app.json",
+ "inlineStyleLanguage": "scss",
+ "assets": [
+ {
+ "glob": "**/*",
+ "input": "apps/shared/frontend-docs/public"
+ },
+ {
+ "glob": "**/*",
+ "input": "apps/decabill/frontend-docs/public"
+ },
+ {
+ "glob": "**/*.json",
+ "input": "dist/apps/decabill/frontend-docs-temp/assets",
+ "output": "assets"
+ },
+ {
+ "glob": "**/*.md",
+ "input": "docs/decabill",
+ "output": "docs/decabill"
+ },
+ {
+ "glob": "**/*.yaml",
+ "input": "libs/domains/decabill/backend/feature-billing-manager/spec",
+ "output": "spec/billing-manager"
+ }
+ ],
+ "styles": [
+ "node_modules/cookieconsent/build/cookieconsent.min.css",
+ "apps/decabill/frontend-docs/src/styles.scss",
+ "libs/domains/shared/frontend/util-cookie-consent/src/lib/cookie-consent.scss"
+ ],
+ "server": "apps/shared/frontend-docs/src/main.server.ts",
+ "ssr": {
+ "entry": "apps/decabill/frontend-docs/src/server.ts"
+ },
+ "prerender": true,
+ "scripts": [
+ "node_modules/cookieconsent/build/cookieconsent.min.js",
+ "node_modules/@popperjs/core/dist/umd/popper.min.js",
+ "node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"
+ ],
+ "localize": true,
+ "i18nMissingTranslation": "warning",
+ "security": {
+ "allowedHosts": [
+ "localhost",
+ "127.0.0.1",
+ "decabill.com",
+ "*.decabill.com"
+ ]
+ }
+ },
+ "configurations": {
+ "production": {
+ "budgets": [
+ {
+ "type": "initial",
+ "maximumWarning": "500kb",
+ "maximumError": "5mb"
+ },
+ {
+ "type": "anyComponentStyle",
+ "maximumWarning": "4kb",
+ "maximumError": "8kb"
+ }
+ ],
+ "outputHashing": "all",
+ "fileReplacements": [
+ {
+ "replace": "libs/domains/shared/frontend/util-configuration/src/lib/environment.ts",
+ "with": "libs/domains/shared/frontend/util-configuration/src/lib/environment.decabill.production.ts"
+ }
+ ]
+ },
+ "development": {
+ "optimization": false,
+ "extractLicenses": false,
+ "sourceMap": true,
+ "fileReplacements": [
+ {
+ "replace": "libs/domains/shared/frontend/util-configuration/src/lib/environment.ts",
+ "with": "libs/domains/shared/frontend/util-configuration/src/lib/environment.decabill.ts"
+ }
+ ]
+ },
+ "en": {
+ "localize": ["en"]
+ },
+ "de": {
+ "localize": ["de"]
+ }
+ },
+ "defaultConfiguration": "production"
+ },
+ "serve": {
+ "continuous": true,
+ "executor": "@angular-devkit/build-angular:dev-server",
+ "options": {
+ "hmr": false,
+ "port": 4401
+ },
+ "configurations": {
+ "production": {
+ "buildTarget": "decabill-frontend-docs:build:production,en"
+ },
+ "development": {
+ "buildTarget": "decabill-frontend-docs:build:development,en"
+ }
+ },
+ "defaultConfiguration": "development"
+ },
+ "extract-i18n-angular": {
+ "executor": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "buildTarget": "decabill-frontend-docs:build",
+ "outputPath": "apps/shared/frontend-docs/src/i18n"
+ }
+ },
+ "extract-i18n": {
+ "executor": "nx:run-commands",
+ "options": {
+ "command": "node ../../../remove-context-groups.cjs src/i18n/messages.xlf",
+ "cwd": "apps/shared/frontend-docs"
+ },
+ "dependsOn": ["extract-i18n-angular"]
+ },
+ "lint": {
+ "executor": "@nx/eslint:lint"
+ },
+ "test": {
+ "executor": "@nx/jest:jest",
+ "outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
+ "options": {
+ "jestConfig": "apps/shared/frontend-docs/jest.config.ts",
+ "tsConfig": "apps/shared/frontend-docs/tsconfig.spec.json"
+ }
+ },
+ "serve-static": {
+ "continuous": true,
+ "executor": "@nx/web:file-server",
+ "options": {
+ "buildTarget": "decabill-frontend-docs:build",
+ "port": 4401,
+ "staticFilePath": "dist/apps/decabill/frontend-docs/browser",
+ "spa": true
+ }
+ },
+ "build-delegating-server": {
+ "executor": "nx:run-commands",
+ "outputs": [
+ "{workspaceRoot}/dist/apps/decabill/frontend-docs-delegating-server"
+ ],
+ "options": {
+ "command": "mkdir -p dist/apps/decabill/frontend-docs-delegating-server/server && npx tsc apps/shared/frontend-docs/src/delegating-server.ts --outDir dist/apps/decabill/frontend-docs-delegating-server/server --target es2022 --module esnext --moduleResolution node --allowSyntheticDefaultImports --esModuleInterop --skipLibCheck --declaration false --sourceMap false --removeComments true",
+ "cwd": "."
+ }
+ },
+ "postbuild": {
+ "executor": "nx:run-commands",
+ "options": {
+ "command": "cp dist/apps/decabill/frontend-docs-delegating-server/server/delegating-server.js dist/apps/decabill/frontend-docs/server/server.mjs && cp -r dist/apps/decabill/frontend-docs/browser dist/apps/decabill/frontend-docs/server/browser"
+ },
+ "dependsOn": ["build", "build-delegating-server"]
+ },
+ "server-container-image": {
+ "dependsOn": ["postbuild"],
+ "cache": false,
+ "executor": "@nx-tools/nx-container:build",
+ "options": {
+ "engine": "docker",
+ "file": "apps/shared/frontend-docs/Dockerfile.server",
+ "context": "dist/apps/decabill/frontend-docs/server"
+ },
+ "configurations": {
+ "test": {
+ "load": true,
+ "tags": ["ghcr.io/forepath/decabill-docs-server:latest"]
+ },
+ "release": {
+ "push": true,
+ "tags": [
+ "ghcr.io/forepath/decabill-docs-server:latest",
+ "ghcr.io/forepath/decabill-docs-server:$VERSION"
+ ]
+ }
+ },
+ "defaultConfiguration": "test"
+ },
+ "start-containers": {
+ "dependsOn": ["server-container-image"],
+ "cache": false,
+ "executor": "nx:run-commands",
+ "options": {
+ "cwd": "apps/shared/frontend-docs",
+ "command": "DOCS_SERVER_IMAGE=ghcr.io/forepath/decabill-docs-server:latest DOCS_SERVER_CONTAINER_NAME=decabill-docs-server docker compose up -d --force-recreate --remove-orphans"
+ }
+ },
+ "sbom": {
+ "executor": "@forepath/sbom:sbom",
+ "dependsOn": ["sbom:build", "build"]
+ }
+ }
+}
diff --git a/apps/decabill/frontend-docs/public/assets/images/favicon.svg b/apps/decabill/frontend-docs/public/assets/images/favicon.svg
new file mode 100644
index 000000000..6ac31b983
--- /dev/null
+++ b/apps/decabill/frontend-docs/public/assets/images/favicon.svg
@@ -0,0 +1,7 @@
+
+
+
diff --git a/apps/decabill/frontend-docs/public/assets/images/icon_colorful.svg b/apps/decabill/frontend-docs/public/assets/images/icon_colorful.svg
new file mode 100644
index 000000000..b63a7f869
--- /dev/null
+++ b/apps/decabill/frontend-docs/public/assets/images/icon_colorful.svg
@@ -0,0 +1,18 @@
+
+
+
diff --git a/apps/decabill/frontend-docs/public/assets/images/icon_light.svg b/apps/decabill/frontend-docs/public/assets/images/icon_light.svg
new file mode 100644
index 000000000..b7378e52c
--- /dev/null
+++ b/apps/decabill/frontend-docs/public/assets/images/icon_light.svg
@@ -0,0 +1,18 @@
+
+
+
diff --git a/apps/decabill/frontend-docs/public/assets/images/og-preview.png b/apps/decabill/frontend-docs/public/assets/images/og-preview.png
new file mode 100644
index 000000000..a7de23df9
Binary files /dev/null and b/apps/decabill/frontend-docs/public/assets/images/og-preview.png differ
diff --git a/apps/decabill/frontend-docs/public/favicon.ico b/apps/decabill/frontend-docs/public/favicon.ico
new file mode 100644
index 000000000..eb9ed8d8d
Binary files /dev/null and b/apps/decabill/frontend-docs/public/favicon.ico differ
diff --git a/apps/decabill/frontend-docs/src/index.html b/apps/decabill/frontend-docs/src/index.html
new file mode 100644
index 000000000..b377c00f7
--- /dev/null
+++ b/apps/decabill/frontend-docs/src/index.html
@@ -0,0 +1,37 @@
+
+
+
+
+ Decabill
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/decabill/frontend-docs/src/server.ts b/apps/decabill/frontend-docs/src/server.ts
new file mode 100644
index 000000000..76083071e
--- /dev/null
+++ b/apps/decabill/frontend-docs/src/server.ts
@@ -0,0 +1,17 @@
+import { isMainModule } from '@angular/ssr/node';
+// eslint-disable-next-line @nx/enforce-module-boundaries
+import bootstrap from '@forepath/shared/frontend/util-docs-bootstrap';
+import { createDocsServer } from '@forepath/shared/frontend/util-express-server/docs-server';
+import { fileURLToPath } from 'node:url';
+
+const app = createDocsServer(['decabill.com'], bootstrap);
+
+if (isMainModule(fileURLToPath(import.meta.url))) {
+ const port = parseInt(process.env['PORT'] || '4000', 10);
+
+ app.listen(port, '0.0.0.0', () => {
+ console.log(`Node Express server listening on http://localhost:${port}`);
+ });
+}
+
+export default app;
diff --git a/apps/decabill/frontend-docs/src/styles.scss b/apps/decabill/frontend-docs/src/styles.scss
new file mode 100644
index 000000000..3ab21fbd6
--- /dev/null
+++ b/apps/decabill/frontend-docs/src/styles.scss
@@ -0,0 +1,209 @@
+// Decabill design system (Bootstrap 5 overrides)
+//
+// Stack (see node_modules/bootstrap/scss/bootstrap.scss):
+// functions → variables → variables-dark → maps → … → root
+// - _variables.scss: palette, $theme-colors, light-mode emphasis/subtle/body/border
+// - _variables-dark.scss: [data-bs-theme="dark"] counterparts ($body-*-dark, etc.)
+// - _maps.scss: $theme-colors-text(|-dark), *-bg-subtle*, *-border-subtle* → CSS vars in _root.scss
+@import 'bootstrap/scss/functions';
+
+$primary: #32a852;
+$secondary: #4d6a55;
+$tertiary: #4fa186;
+$success: #287736;
+$warning: #d18552;
+$danger: #e04248;
+$info: #3f8c92;
+// mix($a, $b, $w): higher $w = more of $a. Small $w with $primary first = subtle green wash.
+$light: mix($primary, #f8f8f5, 3.5%);
+$dark: #0f0f11;
+
+$code-color: mix($primary, #2a2d31, 28%);
+
+// Light-mode surfaces: only tint the pale end of the scale. $gray-300–$gray-900 stay as-is so
+// _variables-dark.scss (body text, secondary bg, borders) is unchanged.
+$gray-100: mix($primary, #f3f4f6, 4%);
+$gray-200: mix($primary, #e5e7eb, 3%);
+$gray-300: #d1d5db;
+$gray-400: #9ca3af;
+$gray-500: #6b7280;
+$gray-600: #4b5563;
+$gray-700: #2a2d31;
+$gray-800: #18191d;
+$gray-900: #0f0f11;
+
+// Dark mode maps $light-text-emphasis-dark to $gray-100; pin neutral so it does not pick up the tint above.
+$light-text-emphasis-dark: #f3f4f6;
+
+$border-color: rgba(15, 15, 17, 0.1);
+$border-color-dark: rgba(255, 255, 255, 0.08);
+
+$border-color-translucent: rgba(15, 15, 17, 0.06);
+$border-color-translucent-dark: rgba(255, 255, 255, 0.06);
+
+$body-bg: mix($primary, #f5f4f9, 5.5%);
+$body-color: #3c3f45;
+$body-emphasis-color: #24262c;
+
+$body-secondary-bg: mix($primary, #ebe9f0, 4%);
+// Tertiary must share $body-bg’s hue; a separate neutral (#f1f0f5 + low primary) reads cool/greenish beside the canvas.
+$body-tertiary-bg: mix($primary, #ebe9f0, 4%);
+
+// ~28% primary in the blend (was inverted: 72% on first arg made links very loud).
+$link-color: mix($primary, $gray-700, 28%);
+
+$primary-text-emphasis: #1f7833;
+$secondary-text-emphasis: #3c503e;
+$success-text-emphasis: #205627;
+$info-text-emphasis: #2d5c5f;
+$warning-text-emphasis: #8c5d38;
+$danger-text-emphasis: #a32a31;
+$light-text-emphasis: $gray-600;
+$dark-text-emphasis: $gray-700;
+
+$primary-bg-subtle: #e6f5ea;
+$secondary-bg-subtle: #eaedeb;
+$success-bg-subtle: #e5efe7;
+$info-bg-subtle: #e8f1f2;
+$warning-bg-subtle: #f8ebe3;
+$danger-bg-subtle: #fceaeb;
+$light-bg-subtle: mix($gray-100, $body-bg, 55%);
+$dark-bg-subtle: #d6d8dc;
+
+$primary-border-subtle: #cce9d4;
+$secondary-border-subtle: #d6ddd8;
+$success-border-subtle: #c7dccb;
+$info-border-subtle: #cde1e3;
+$warning-border-subtle: #f1dacb;
+$danger-border-subtle: #f7ccce;
+$light-border-subtle: $gray-200;
+$dark-border-subtle: #b9bcc4;
+
+$font-family-base: 'Plus Jakarta Sans Variable', sans-serif;
+$font-size-base: 0.875rem;
+
+$badge-font-size: 0.75rem;
+$badge-font-weight: 400;
+$badge-padding-y: 0.25rem;
+$badge-padding-x: 0.5rem;
+$badge-border-radius: 0.375rem;
+$badge-line-height: 1.5;
+
+@mixin agenstra-badge-text-bg-tint($color-name) {
+ .badge {
+ &.bg-#{$color-name},
+ &.text-bg-#{$color-name} {
+ background-color: color-mix(in srgb, var(--bs-#{$color-name}) 24%, var(--bs-tertiary-bg)) !important;
+ color: var(--bs-#{$color-name}-text-emphasis) !important;
+ line-height: $badge-line-height;
+ }
+ }
+}
+$agenstra-badge-text-bg-colors: primary, secondary, success, info, warning, danger, light, dark;
+@each $color-name in $agenstra-badge-text-bg-colors {
+ @include agenstra-badge-text-bg-tint($color-name);
+}
+
+@import 'bootstrap/scss/bootstrap';
+
+// Extra --bs-* variables not generated by Bootstrap (used by portal components)
+:root {
+ --bs-primary-dark: #339b4c;
+ --bs-success-dark: #205e2b;
+ --bs-body-bg-alt: #f2f6f6;
+ --bs-dark-alt: #262828;
+ --bs-scrollbar-thumb: rgba(var(--bs-body-color-rgb), 0.28);
+ --bs-scrollbar-thumb-hover: rgba(var(--bs-body-color-rgb), 0.45);
+ --bs-scrollbar-thumb-active: rgba(var(--bs-body-color-rgb), 0.55);
+ --bs-scrollbar-track: transparent;
+}
+
+@import 'bootstrap/scss/bootstrap-grid';
+@import 'bootstrap-icons/font/bootstrap-icons';
+@import '@fontsource-variable/plus-jakarta-sans/wght.css';
+
+// Global scrollbar: thumb tints follow --bs-body-color-rgb, so light/dark switch with [data-bs-theme].
+// Firefox only — Chrome 121+ ignores ::-webkit-scrollbar-* when scrollbar-width is set on the same element.
+@supports not selector(::-webkit-scrollbar) {
+ * {
+ scrollbar-width: thin;
+ scrollbar-color: var(--bs-scrollbar-thumb) var(--bs-scrollbar-track);
+ }
+}
+
+*::-webkit-scrollbar {
+ width: 0.5rem;
+ height: 0.5rem;
+ -webkit-appearance: none;
+}
+
+*::-webkit-scrollbar-button,
+*::-webkit-scrollbar-button:single-button,
+*::-webkit-scrollbar-button:double-button,
+*::-webkit-scrollbar-button:vertical:start:decrement,
+*::-webkit-scrollbar-button:vertical:end:increment,
+*::-webkit-scrollbar-button:horizontal:start:decrement,
+*::-webkit-scrollbar-button:horizontal:end:increment {
+ display: none;
+ width: 0;
+ height: 0;
+}
+
+*::-webkit-scrollbar-track {
+ background: var(--bs-scrollbar-track);
+}
+
+*::-webkit-scrollbar-thumb {
+ background-color: var(--bs-scrollbar-thumb);
+ border: 2px solid transparent;
+ border-radius: var(--bs-border-radius);
+ background-clip: padding-box;
+}
+
+*::-webkit-scrollbar-thumb:hover {
+ background-color: var(--bs-scrollbar-thumb-hover);
+}
+
+*::-webkit-scrollbar-thumb:active {
+ background-color: var(--bs-scrollbar-thumb-active);
+}
+
+*::-webkit-scrollbar-corner {
+ background: var(--bs-scrollbar-track);
+}
+
+.status-badge {
+ width: 1.125rem;
+ height: 1.125rem;
+ border-radius: 0.625rem;
+ font-size: 0.75rem;
+ color: var(--bs-white);
+}
+
+.info-badge {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 0.75rem;
+
+ .info-badge-chevron {
+ font-size: 0.5rem;
+ top: -0.125rem;
+ position: relative;
+ }
+
+ .info-badge-text {
+ border-radius: 0.28125rem;
+ }
+}
+
+.list-group-item.active {
+ .status-badge {
+ background-color: var(--bs-white) !important;
+ color: var(--bs-primary) !important;
+ }
+
+ .info-badge {
+ background-color: #2b7b4d !important;
+ }
+}
diff --git a/apps/forepath/frontend-billing-console/project.json b/apps/forepath/frontend-billing-console/project.json
index 7231ea3d0..109a47507 100644
--- a/apps/forepath/frontend-billing-console/project.json
+++ b/apps/forepath/frontend-billing-console/project.json
@@ -221,7 +221,8 @@
"cache": false,
"executor": "nx:run-commands",
"options": {
- "command": "docker rm -f forepath-billing-console-server 2>/dev/null || true; docker run -d --name forepath-billing-console-server --pull never -p ${PORT:-4500}:${PORT:-4500} -e HOST=${HOST:-0.0.0.0} -e PORT=${PORT:-4500} -e NODE_ENV=${NODE_ENV:-production} -e DEFAULT_LOCALE=${DEFAULT_LOCALE:-en} -e CSP_ENFORCE=${CSP_ENFORCE:-true} -e CSP_CONNECT_SRC_EXTRA=${CSP_CONNECT_SRC_EXTRA:-http://host.docker.internal:3200} ghcr.io/forepath/forepath-billing-console-server:latest"
+ "cwd": "apps/decabill/frontend-billing-console",
+ "command": "BILLING_CONSOLE_SERVER_IMAGE=ghcr.io/forepath/forepath-billing-console-server:latest BILLING_CONSOLE_SERVER_CONTAINER_NAME=forepath-billing-console-server docker compose up -d --force-recreate --remove-orphans"
}
},
"sbom": {
diff --git a/apps/forepath/frontend-landingpage/src/delegating-server.ts b/apps/forepath/frontend-landingpage/src/delegating-server.ts
index 38960cd4d..f8df17681 100644
--- a/apps/forepath/frontend-landingpage/src/delegating-server.ts
+++ b/apps/forepath/frontend-landingpage/src/delegating-server.ts
@@ -1,6 +1,6 @@
+import { createReadStream, existsSync, statSync } from 'node:fs';
import { createServer, IncomingMessage, ServerResponse } from 'node:http';
-import { readFileSync, existsSync, createReadStream, statSync } from 'node:fs';
-import { join, dirname, extname } from 'node:path';
+import { dirname, extname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
@@ -30,6 +30,13 @@ async function loadLocaleServers() {
}
function getLocaleFromRequest(req: IncomingMessage): string {
+ const url = new URL(req.url || '', `http://${req.headers.host}`);
+ const pathSegments = url.pathname.split('/').filter(Boolean);
+
+ if (pathSegments.length > 0 && AVAILABLE_LOCALES.includes(pathSegments[0])) {
+ return pathSegments[0];
+ }
+
const acceptLanguage = req.headers['accept-language'];
if (acceptLanguage) {
@@ -40,13 +47,6 @@ function getLocaleFromRequest(req: IncomingMessage): string {
}
}
- const url = new URL(req.url || '', `http://${req.headers.host}`);
- const pathSegments = url.pathname.split('/').filter(Boolean);
-
- if (pathSegments.length > 0 && AVAILABLE_LOCALES.includes(pathSegments[0])) {
- return pathSegments[0];
- }
-
return DEFAULT_LOCALE;
}
diff --git a/apps/shared/frontend-docs/.eslintrc.json b/apps/shared/frontend-docs/.eslintrc.json
new file mode 100644
index 000000000..78770429d
--- /dev/null
+++ b/apps/shared/frontend-docs/.eslintrc.json
@@ -0,0 +1,36 @@
+{
+ "extends": ["../../../.eslintrc.json", "../../../.eslintrc.json"],
+ "ignorePatterns": ["!**/*"],
+ "overrides": [
+ {
+ "files": ["*.ts"],
+ "extends": [
+ "plugin:@nx/angular",
+ "plugin:@angular-eslint/template/process-inline-templates"
+ ],
+ "rules": {
+ "@angular-eslint/directive-selector": [
+ "error",
+ {
+ "type": "attribute",
+ "prefix": "app",
+ "style": "camelCase"
+ }
+ ],
+ "@angular-eslint/component-selector": [
+ "error",
+ {
+ "type": "element",
+ "prefix": "app",
+ "style": "kebab-case"
+ }
+ ]
+ }
+ },
+ {
+ "files": ["*.html"],
+ "extends": ["plugin:@nx/angular-template"],
+ "rules": {}
+ }
+ ]
+}
diff --git a/apps/agenstra/frontend-docs/Dockerfile.server b/apps/shared/frontend-docs/Dockerfile.server
similarity index 100%
rename from apps/agenstra/frontend-docs/Dockerfile.server
rename to apps/shared/frontend-docs/Dockerfile.server
diff --git a/libs/domains/agenstra/frontend/feature-docs/LICENSE b/apps/shared/frontend-docs/LICENSE
similarity index 100%
rename from libs/domains/agenstra/frontend/feature-docs/LICENSE
rename to apps/shared/frontend-docs/LICENSE
diff --git a/apps/agenstra/frontend-docs/docker-compose.yaml b/apps/shared/frontend-docs/docker-compose.yaml
similarity index 67%
rename from apps/agenstra/frontend-docs/docker-compose.yaml
rename to apps/shared/frontend-docs/docker-compose.yaml
index da012e4dc..41f5cedbe 100644
--- a/apps/agenstra/frontend-docs/docker-compose.yaml
+++ b/apps/shared/frontend-docs/docker-compose.yaml
@@ -1,14 +1,14 @@
services:
frontend-docs-server:
- image: ghcr.io/forepath/agenstra-docs-server:latest
+ image: ${DOCS_SERVER_IMAGE:-ghcr.io/forepath/agenstra-docs-server:latest}
pull_policy: never
- container_name: docs-server
+ container_name: ${DOCS_SERVER_CONTAINER_NAME:-docs-server}
environment:
- # Frontend server configuration
HOST: ${HOST:-0.0.0.0}
PORT: ${PORT:-4200}
NODE_ENV: ${NODE_ENV:-production}
DEFAULT_LOCALE: ${DEFAULT_LOCALE:-en}
+ CSP_ENFORCE: ${CSP_ENFORCE:-true}
ports:
- '${PORT:-4200}:${PORT:-4200}'
networks:
diff --git a/apps/agenstra/frontend-docs/jest.config.ts b/apps/shared/frontend-docs/jest.config.ts
similarity index 84%
rename from apps/agenstra/frontend-docs/jest.config.ts
rename to apps/shared/frontend-docs/jest.config.ts
index d064e60bc..c262939dc 100644
--- a/apps/agenstra/frontend-docs/jest.config.ts
+++ b/apps/shared/frontend-docs/jest.config.ts
@@ -1,8 +1,8 @@
export default {
- displayName: 'agenstra-frontend-docs',
+ displayName: 'shared-frontend-docs',
preset: '../../../jest.preset.cjs',
setupFilesAfterEnv: ['/src/test-setup.ts'],
- coverageDirectory: '../../../coverage/apps/agenstra/frontend-docs',
+ coverageDirectory: '../../../coverage/apps/shared/frontend-docs',
transform: {
'^.+\\.(ts|mjs|js|html)$': [
'jest-preset-angular',
diff --git a/apps/shared/frontend-docs/project.json b/apps/shared/frontend-docs/project.json
new file mode 100644
index 000000000..7fb8b8cb1
--- /dev/null
+++ b/apps/shared/frontend-docs/project.json
@@ -0,0 +1,27 @@
+{
+ "name": "shared-frontend-docs",
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "application",
+ "prefix": "app",
+ "sourceRoot": "apps/shared/frontend-docs/src",
+ "i18n": {
+ "sourceLocale": "en",
+ "locales": {
+ "de": "apps/shared/frontend-docs/src/i18n/messages.de.xlf"
+ }
+ },
+ "tags": ["domain:shared", "type:app", "scope:frontend"],
+ "targets": {
+ "lint": {
+ "executor": "@nx/eslint:lint"
+ },
+ "test": {
+ "executor": "@nx/jest:jest",
+ "outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
+ "options": {
+ "jestConfig": "apps/shared/frontend-docs/jest.config.ts",
+ "tsConfig": "apps/shared/frontend-docs/tsconfig.spec.json"
+ }
+ }
+ }
+}
diff --git a/apps/shared/frontend-docs/public/assets/images/favicon.svg b/apps/shared/frontend-docs/public/assets/images/favicon.svg
new file mode 100644
index 000000000..d6da75d2f
--- /dev/null
+++ b/apps/shared/frontend-docs/public/assets/images/favicon.svg
@@ -0,0 +1,17 @@
+
+
+
diff --git a/apps/shared/frontend-docs/public/assets/images/icon_colorful.svg b/apps/shared/frontend-docs/public/assets/images/icon_colorful.svg
new file mode 100644
index 000000000..4ba784e52
--- /dev/null
+++ b/apps/shared/frontend-docs/public/assets/images/icon_colorful.svg
@@ -0,0 +1,14 @@
+
+
+
diff --git a/apps/shared/frontend-docs/public/assets/images/icon_light.svg b/apps/shared/frontend-docs/public/assets/images/icon_light.svg
new file mode 100644
index 000000000..bb376d5ff
--- /dev/null
+++ b/apps/shared/frontend-docs/public/assets/images/icon_light.svg
@@ -0,0 +1,14 @@
+
+
+
diff --git a/apps/shared/frontend-docs/public/assets/images/og-preview.png b/apps/shared/frontend-docs/public/assets/images/og-preview.png
new file mode 100644
index 000000000..0d56f3bfb
Binary files /dev/null and b/apps/shared/frontend-docs/public/assets/images/og-preview.png differ
diff --git a/apps/agenstra/frontend-docs/public/favicon.ico b/apps/shared/frontend-docs/public/favicon.ico
similarity index 100%
rename from apps/agenstra/frontend-docs/public/favicon.ico
rename to apps/shared/frontend-docs/public/favicon.ico
diff --git a/apps/agenstra/frontend-docs/src/app/app.component.html b/apps/shared/frontend-docs/src/app/app.component.html
similarity index 100%
rename from apps/agenstra/frontend-docs/src/app/app.component.html
rename to apps/shared/frontend-docs/src/app/app.component.html
diff --git a/apps/agenstra/frontend-docs/src/app/app.component.spec.ts b/apps/shared/frontend-docs/src/app/app.component.spec.ts
similarity index 100%
rename from apps/agenstra/frontend-docs/src/app/app.component.spec.ts
rename to apps/shared/frontend-docs/src/app/app.component.spec.ts
diff --git a/apps/agenstra/frontend-docs/src/app/app.component.ts b/apps/shared/frontend-docs/src/app/app.component.ts
similarity index 100%
rename from apps/agenstra/frontend-docs/src/app/app.component.ts
rename to apps/shared/frontend-docs/src/app/app.component.ts
diff --git a/apps/agenstra/frontend-docs/src/app/app.config.server.ts b/apps/shared/frontend-docs/src/app/app.config.server.ts
similarity index 100%
rename from apps/agenstra/frontend-docs/src/app/app.config.server.ts
rename to apps/shared/frontend-docs/src/app/app.config.server.ts
diff --git a/apps/agenstra/frontend-docs/src/app/app.config.ts b/apps/shared/frontend-docs/src/app/app.config.ts
similarity index 100%
rename from apps/agenstra/frontend-docs/src/app/app.config.ts
rename to apps/shared/frontend-docs/src/app/app.config.ts
diff --git a/apps/agenstra/frontend-docs/src/app/app.routes.ts b/apps/shared/frontend-docs/src/app/app.routes.ts
similarity index 64%
rename from apps/agenstra/frontend-docs/src/app/app.routes.ts
rename to apps/shared/frontend-docs/src/app/app.routes.ts
index cfb869b3c..299ff8d5e 100644
--- a/apps/agenstra/frontend-docs/src/app/app.routes.ts
+++ b/apps/shared/frontend-docs/src/app/app.routes.ts
@@ -1,5 +1,5 @@
import { Route } from '@angular/router';
-import { docsRoutes } from '@forepath/agenstra/frontend/feature-docs';
+import { docsRoutes } from '@forepath/shared/frontend/feature-docs';
export const appRoutes: Route[] = [
{
diff --git a/apps/agenstra/frontend-docs/src/delegating-server.ts b/apps/shared/frontend-docs/src/delegating-server.ts
similarity index 100%
rename from apps/agenstra/frontend-docs/src/delegating-server.ts
rename to apps/shared/frontend-docs/src/delegating-server.ts
index 6159c51fa..c030512ea 100644
--- a/apps/agenstra/frontend-docs/src/delegating-server.ts
+++ b/apps/shared/frontend-docs/src/delegating-server.ts
@@ -29,6 +29,13 @@ async function loadLocaleServers() {
}
function getLocaleFromRequest(req: IncomingMessage): string {
+ const url = new URL(req.url || '', `http://${req.headers.host}`);
+ const pathSegments = url.pathname.split('/').filter(Boolean);
+
+ if (pathSegments.length > 0 && AVAILABLE_LOCALES.includes(pathSegments[0])) {
+ return pathSegments[0];
+ }
+
const acceptLanguage = req.headers['accept-language'];
if (acceptLanguage) {
@@ -39,13 +46,6 @@ function getLocaleFromRequest(req: IncomingMessage): string {
}
}
- const url = new URL(req.url || '', `http://${req.headers.host}`);
- const pathSegments = url.pathname.split('/').filter(Boolean);
-
- if (pathSegments.length > 0 && AVAILABLE_LOCALES.includes(pathSegments[0])) {
- return pathSegments[0];
- }
-
return DEFAULT_LOCALE;
}
diff --git a/apps/agenstra/frontend-docs/src/i18n/messages.de.xlf b/apps/shared/frontend-docs/src/i18n/messages.de.xlf
similarity index 79%
rename from apps/agenstra/frontend-docs/src/i18n/messages.de.xlf
rename to apps/shared/frontend-docs/src/i18n/messages.de.xlf
index 65fe37d1d..96d608c9f 100644
--- a/apps/agenstra/frontend-docs/src/i18n/messages.de.xlf
+++ b/apps/shared/frontend-docs/src/i18n/messages.de.xlf
@@ -66,6 +66,18 @@
Agenstra, AI agents, agent management, distributed systems, AI agent infrastructure, agent platform, AI agent console, container management, WebSocket agents, Docker agents
Agenstra, KI-Agents, Agent-Verwaltung, verteilte Systeme, KI-Agent-Infrastruktur, Agent-Plattform, KI-Agent-Konsole, Container-Verwaltung, WebSocket-Agents, Docker-Agents
+
+ Official Decabill documentation: install, deploy, secure, and operate billing APIs, subscriptions, payment processors, and the billing console for platform teams.
+ Offizielle Decabill-Dokumentation: Installation, Deployment, Sicherheit und Betrieb von Billing-APIs, Abonnements, Zahlungsanbietern und der Billing-Konsole für Plattformteams.
+
+
+ Decabill, billing, subscriptions, payment processing, invoicing, Stripe, billing API, billing console, multi-tenant billing
+ Decabill, Billing, Abonnements, Zahlungsabwicklung, Rechnungsstellung, Stripe, Billing-API, Billing-Konsole, Mandantenfähiges Billing
+
+
+ Documentation
+ Dokumentation
+
Search Documentation
Dokumentation durchsuchen
@@ -90,6 +102,14 @@
Search Agenstra docs for setup guides, API references, security hardening, agent configuration, deployment patterns, and troubleshooting.
Durchsuchen Sie die Agenstra-Dokumentation nach Setup-Anleitungen, API-Referenzen, Security-Hardening, Agent-Konfiguration, Deployment-Mustern und Troubleshooting.
+
+ Search Decabill docs for setup guides, API references, security hardening, billing configuration, deployment patterns, and troubleshooting.
+ Durchsuchen Sie die Decabill-Dokumentation nach Setup-Anleitungen, API-Referenzen, Security-Hardening, Billing-Konfiguration, Deployment-Mustern und Troubleshooting.
+
+
+ Search Documentation
+ Dokumentation durchsuchen
+
Found result for ""
Ergebnis für "" gefunden
diff --git a/apps/agenstra/frontend-docs/src/i18n/messages.xlf b/apps/shared/frontend-docs/src/i18n/messages.xlf
similarity index 81%
rename from apps/agenstra/frontend-docs/src/i18n/messages.xlf
rename to apps/shared/frontend-docs/src/i18n/messages.xlf
index 43ccd9048..ddaf983e4 100644
--- a/apps/agenstra/frontend-docs/src/i18n/messages.xlf
+++ b/apps/shared/frontend-docs/src/i18n/messages.xlf
@@ -41,15 +41,21 @@
Loading...
-
- Documentation :: Agenstra
-
Official Agenstra documentation: install, deploy, secure, and operate agent hosts, workspaces, tickets, APIs, and integrations for platform teams.
Agenstra, AI agents, agent management, distributed systems, AI agent infrastructure, agent platform, AI agent console, container management, WebSocket agents, Docker agents
+
+ Official Decabill documentation: install, deploy, secure, and operate billing APIs, subscriptions, payment processors, and the billing console for platform teams.
+
+
+ Decabill, billing, subscriptions, payment processing, invoicing, Stripe, billing API, billing console, multi-tenant billing
+
+
+ Documentation
+
Search Documentation
@@ -62,12 +68,15 @@
Enter a search query above to find documentation.
-
- Search Documentation :: Agenstra
+
+ Search Documentation
Search Agenstra docs for setup guides, API references, security hardening, agent configuration, deployment patterns, and troubleshooting.
+
+ Search Decabill docs for setup guides, API references, security hardening, billing configuration, deployment patterns, and troubleshooting.
+
Found result for ""
diff --git a/apps/shared/frontend-docs/src/index.html b/apps/shared/frontend-docs/src/index.html
new file mode 100644
index 000000000..dcec4cbd4
--- /dev/null
+++ b/apps/shared/frontend-docs/src/index.html
@@ -0,0 +1,47 @@
+
+
+
+
+ Agenstra
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/agenstra/frontend-docs/src/main.server.ts b/apps/shared/frontend-docs/src/main.server.ts
similarity index 100%
rename from apps/agenstra/frontend-docs/src/main.server.ts
rename to apps/shared/frontend-docs/src/main.server.ts
diff --git a/apps/agenstra/frontend-docs/src/main.ts b/apps/shared/frontend-docs/src/main.ts
similarity index 100%
rename from apps/agenstra/frontend-docs/src/main.ts
rename to apps/shared/frontend-docs/src/main.ts
diff --git a/apps/agenstra/frontend-docs/src/polyfills.ts b/apps/shared/frontend-docs/src/polyfills.ts
similarity index 100%
rename from apps/agenstra/frontend-docs/src/polyfills.ts
rename to apps/shared/frontend-docs/src/polyfills.ts
diff --git a/apps/agenstra/frontend-docs/src/server.mjs b/apps/shared/frontend-docs/src/server.mjs
similarity index 100%
rename from apps/agenstra/frontend-docs/src/server.mjs
rename to apps/shared/frontend-docs/src/server.mjs
diff --git a/apps/shared/frontend-docs/src/server.ts b/apps/shared/frontend-docs/src/server.ts
new file mode 100644
index 000000000..c073616df
--- /dev/null
+++ b/apps/shared/frontend-docs/src/server.ts
@@ -0,0 +1,17 @@
+import { isMainModule } from '@angular/ssr/node';
+import { createDocsServer } from '@forepath/shared/frontend/util-express-server/docs-server';
+import { fileURLToPath } from 'node:url';
+
+import bootstrap from './main.server';
+
+const app = createDocsServer(['agenstra.com'], bootstrap);
+
+if (isMainModule(fileURLToPath(import.meta.url))) {
+ const port = parseInt(process.env['PORT'] || '4000', 10);
+
+ app.listen(port, '0.0.0.0', () => {
+ console.log(`Node Express server listening on http://localhost:${port}`);
+ });
+}
+
+export default app;
diff --git a/apps/shared/frontend-docs/src/styles.scss b/apps/shared/frontend-docs/src/styles.scss
new file mode 100644
index 000000000..a283b479a
--- /dev/null
+++ b/apps/shared/frontend-docs/src/styles.scss
@@ -0,0 +1,119 @@
+// Agenstra design system (Bootstrap 5 overrides)
+//
+// Stack (see node_modules/bootstrap/scss/bootstrap.scss):
+// functions → variables → variables-dark → maps → … → root
+// - _variables.scss: palette, $theme-colors, light-mode emphasis/subtle/body/border
+// - _variables-dark.scss: [data-bs-theme="dark"] counterparts ($body-*-dark, etc.)
+// - _maps.scss: $theme-colors-text(|-dark), *-bg-subtle*, *-border-subtle* → CSS vars in _root.scss
+@import 'bootstrap/scss/functions';
+
+$primary: #7a3fff;
+$secondary: #4a5568;
+$tertiary: #6c9cb8;
+$success: #21808d;
+$warning: #e68161;
+$danger: #ff5459;
+$info: #3b82c4;
+// mix($a, $b, $w): higher $w = more of $a. Small $w with $primary first = subtle violet wash.
+$light: mix($primary, #f8f8f5, 3.5%);
+$dark: #0f0f11;
+
+$code-color: mix($primary, #2a2d31, 28%);
+
+// Light-mode surfaces: only tint the pale end of the scale. $gray-300–$gray-900 stay as-is so
+// _variables-dark.scss (body text, secondary bg, borders) is unchanged.
+$gray-100: mix($primary, #f3f4f6, 4%);
+$gray-200: mix($primary, #e5e7eb, 3%);
+$gray-300: #d1d5db;
+$gray-400: #9ca3af;
+$gray-500: #6b7280;
+$gray-600: #4b5563;
+$gray-700: #2a2d31;
+$gray-800: #18191d;
+$gray-900: #0f0f11;
+
+// Dark mode maps $light-text-emphasis-dark to $gray-100; pin neutral so it does not pick up the tint above.
+$light-text-emphasis-dark: #f3f4f6;
+
+$border-color: rgba(15, 15, 17, 0.1);
+$border-color-dark: rgba(255, 255, 255, 0.08);
+
+$border-color-translucent: rgba(15, 15, 17, 0.06);
+$border-color-translucent-dark: rgba(255, 255, 255, 0.06);
+
+$body-bg: mix($primary, #f5f4f9, 5.5%);
+$body-color: #3c3f45;
+$body-emphasis-color: #24262c;
+
+$body-secondary-bg: mix($primary, #ebe9f0, 4%);
+// Tertiary must share $body-bg’s hue; a separate neutral (#f1f0f5 + low primary) reads cool/greenish beside the canvas.
+$body-tertiary-bg: mix($primary, #ebe9f0, 4%);
+
+// ~28% primary in the blend (was inverted: 72% on first arg made links very loud).
+$link-color: mix($primary, $gray-700, 28%);
+
+$primary-text-emphasis: #4b2d9e;
+$secondary-text-emphasis: #3a404c;
+$success-text-emphasis: #1a5c66;
+$info-text-emphasis: #2a5580;
+$warning-text-emphasis: #9a5a42;
+$danger-text-emphasis: #b9363c;
+$light-text-emphasis: $gray-600;
+$dark-text-emphasis: $gray-700;
+
+$primary-bg-subtle: #ece8f6;
+$secondary-bg-subtle: #e8eaee;
+$success-bg-subtle: #e3f1f3;
+$info-bg-subtle: #e7f0f8;
+$warning-bg-subtle: #f4ebe6;
+$danger-bg-subtle: #fcebec;
+$light-bg-subtle: mix($gray-100, $body-bg, 55%);
+$dark-bg-subtle: #d6d8dc;
+
+$primary-border-subtle: #d9cff0;
+$secondary-border-subtle: #d5d8df;
+$success-border-subtle: #c5dde1;
+$info-border-subtle: #cddfea;
+$warning-border-subtle: #e8d5cc;
+$danger-border-subtle: #f5d0d2;
+$light-border-subtle: $gray-200;
+$dark-border-subtle: #b9bcc4;
+
+$font-family-base: 'Plus Jakarta Sans Variable', sans-serif;
+$font-size-base: 0.875rem;
+
+$badge-font-size: 0.75rem;
+$badge-font-weight: 400;
+$badge-padding-y: 0.25rem;
+$badge-padding-x: 0.5rem;
+$badge-border-radius: 0.375rem;
+$badge-line-height: 1.5;
+
+@mixin agenstra-badge-text-bg-tint($color-name) {
+ .badge {
+ &.bg-#{$color-name},
+ &.text-bg-#{$color-name} {
+ background-color: color-mix(in srgb, var(--bs-#{$color-name}) 24%, var(--bs-tertiary-bg)) !important;
+ color: var(--bs-#{$color-name}-text-emphasis) !important;
+ line-height: $badge-line-height;
+ }
+ }
+}
+$agenstra-badge-text-bg-colors: primary, secondary, success, info, warning, danger, light, dark;
+@each $color-name in $agenstra-badge-text-bg-colors {
+ @include agenstra-badge-text-bg-tint($color-name);
+}
+
+@import 'bootstrap/scss/bootstrap';
+
+// Extra --bs-* variables not generated by Bootstrap (used by portal components)
+:root {
+ --bs-primary-dark: #7c3aed;
+ --bs-success-dark: #1a6570;
+ --bs-body-bg-alt: #faf9fd;
+ --bs-dark-alt: #262828;
+}
+
+@import 'bootstrap/scss/bootstrap-grid';
+@import 'bootstrap-icons/font/bootstrap-icons';
+@import '@fontsource-variable/plus-jakarta-sans/wght.css';
diff --git a/apps/agenstra/frontend-docs/src/test-setup.ts b/apps/shared/frontend-docs/src/test-setup.ts
similarity index 100%
rename from apps/agenstra/frontend-docs/src/test-setup.ts
rename to apps/shared/frontend-docs/src/test-setup.ts
diff --git a/apps/agenstra/frontend-docs/tsconfig.app.json b/apps/shared/frontend-docs/tsconfig.app.json
similarity index 76%
rename from apps/agenstra/frontend-docs/tsconfig.app.json
rename to apps/shared/frontend-docs/tsconfig.app.json
index e7bea4241..531238766 100644
--- a/apps/agenstra/frontend-docs/tsconfig.app.json
+++ b/apps/shared/frontend-docs/tsconfig.app.json
@@ -10,7 +10,11 @@
"src/server.ts",
"src/polyfills.ts"
],
- "include": ["src/**/*.d.ts", "src/delegating-server.ts"],
+ "include": [
+ "src/**/*.d.ts",
+ "src/delegating-server.ts",
+ "src/create-docs-server.ts"
+ ],
"exclude": [
"jest.config.ts",
"src/test-setup.ts",
diff --git a/apps/agenstra/frontend-docs/tsconfig.editor.json b/apps/shared/frontend-docs/tsconfig.editor.json
similarity index 100%
rename from apps/agenstra/frontend-docs/tsconfig.editor.json
rename to apps/shared/frontend-docs/tsconfig.editor.json
diff --git a/apps/agenstra/frontend-docs/tsconfig.json b/apps/shared/frontend-docs/tsconfig.json
similarity index 100%
rename from apps/agenstra/frontend-docs/tsconfig.json
rename to apps/shared/frontend-docs/tsconfig.json
diff --git a/apps/agenstra/frontend-docs/tsconfig.spec.json b/apps/shared/frontend-docs/tsconfig.spec.json
similarity index 100%
rename from apps/agenstra/frontend-docs/tsconfig.spec.json
rename to apps/shared/frontend-docs/tsconfig.spec.json
diff --git a/docs/agenstra/README.md b/docs/agenstra/README.md
index cd7e75793..a1b50437a 100644
--- a/docs/agenstra/README.md
+++ b/docs/agenstra/README.md
@@ -119,25 +119,13 @@ New to Agenstra? Follow this learning path:
Agenstra follows a three-tier architecture:
-```
-┌─────────────────────┐
-│ Frontend Console │ Angular application with NgRx state management
-│ (Web-based IDE) │ Monaco Editor, Chat Interface, File Management
-└──────────┬──────────┘
- │ HTTP REST API
- │ WebSocket (Socket.IO)
- ▼
-┌─────────────────────┐
-│ Agent Controller │ Centralized control plane
-│ (Backend) │ Client management, event forwarding
-└──────────┬──────────┘
- │ HTTP REST API
- │ WebSocket (Socket.IO)
- ▼
-┌─────────────────────┐
-│ Agent Manager │ Agent lifecycle management
-│ (Backend) │ Container management, Docker integration
-└─────────────────────┘
+```mermaid
+graph TB
+ FE["Frontend Console
(Web-based IDE)
Angular, Monaco Editor, Chat, File Management"]
+ AC["Agent Controller
(Backend)
Client management, event forwarding"]
+ AM["Agent Manager
(Backend)
Container management, Docker integration"]
+ FE -->|"HTTP REST API
WebSocket (Socket.IO)"| AC
+ AC -->|"HTTP REST API
WebSocket (Socket.IO)"| AM
```
For detailed architecture information, see the [Architecture Documentation](./architecture/README.md).
diff --git a/docs/agenstra/api-reference/README.md b/docs/agenstra/api-reference/README.md
index db4300d39..6594ee7e2 100644
--- a/docs/agenstra/api-reference/README.md
+++ b/docs/agenstra/api-reference/README.md
@@ -75,27 +75,6 @@ The Agent Manager WebSocket gateway provides:
- Terminal session management (`createTerminal`, `terminalInput`, `terminalOutput`, `closeTerminal`)
- Container statistics broadcasting (`containerStats`; default every 15s on the manager, configurable via `CONTAINER_STATS_SCHEDULER_INTERVAL`)
-## Billing Manager HTTP API
-
-**OpenAPI Specification**: [openapi.yaml](/spec/billing-manager/openapi.yaml) (canonical source: `libs/domains/decabill/backend/feature-billing-manager/spec/openapi.yaml`)
-
-Admin manual invoices and customer billing profile CRUD: [Billing Administration](../features/billing-administration.md)
-
-## Billing Manager WebSocket
-
-The billing manager exposes a Socket.IO gateway for **dashboard server status** (provisioned subscription items), separate from the HTTP port. Connections use the same JWT / Keycloak handshake auth as HTTP; static API key clients are not given an end-user billing stream (aligned with REST).
-
-**AsyncAPI Specification**: [asyncapi.yaml](/spec/billing-manager/asyncapi.yaml)
-
-- **View in AsyncAPI Studio**: [Open in AsyncAPI Studio](https://studio.asyncapi.com/?url=https://docs.agenstra.com/spec/billing-manager/asyncapi.yaml)
-- **Download**: [asyncapi.yaml](/spec/billing-manager/asyncapi.yaml) (canonical source in-repo: `libs/domains/decabill/backend/feature-billing-manager/spec/asyncapi.yaml`)
-
-The billing status gateway provides:
-
-- `subscribeDashboardStatus` / `unsubscribeDashboardStatus` client commands
-- `dashboardStatusUpdate` unicast emissions with the same server-info fields as `GET .../server-info`
-- `error` events scoped to the initiating socket only
-
## Using the Specifications
### Swagger Editor
diff --git a/docs/agenstra/applications/frontend-agent-console.md b/docs/agenstra/applications/frontend-agent-console.md
index 4298d0b04..2ce71642d 100644
--- a/docs/agenstra/applications/frontend-agent-console.md
+++ b/docs/agenstra/applications/frontend-agent-console.md
@@ -218,7 +218,7 @@ See [Authentication](../features/authentication.md) for environment variables an
## Environment Configuration
-Configure the application via environment variables. The **Express** runtime (`/config` proxy, CSP, and related variables) is shared with **agenstra-frontend-billing-console**, **agenstra-frontend-landingpage**, and **agenstra-frontend-docs**; see [Environment configuration](../deployment/environment-configuration.md) for the full list and billing-manager provisioning defaults.
+Configure the application via environment variables. The **Express** runtime (`/config` proxy, CSP, and related variables) is shared with **agenstra-frontend-landingpage** and **agenstra-frontend-docs** (patch apps over the shared docs base). See [Environment configuration](../deployment/environment-configuration.md) for the full list.
### Runtime Configuration (Docker Containers)
diff --git a/docs/agenstra/deployment/background-jobs.md b/docs/agenstra/deployment/background-jobs.md
index af558a4ce..cd6b4aeab 100644
--- a/docs/agenstra/deployment/background-jobs.md
+++ b/docs/agenstra/deployment/background-jobs.md
@@ -1,6 +1,6 @@
# Background jobs (BullMQ)
-Background work for **backend agent controller** and **backend billing manager** runs through **Redis + BullMQ** instead of in-process `setInterval` loops in the API container.
+Background work for **backend agent controller** runs through **Redis + BullMQ** instead of in-process `setInterval` loops in the API container.
## Architecture
@@ -21,12 +21,11 @@ Each backend stack has its own **Redis** service in Docker Compose. Workers and
Workers and schedulers assume the API has already applied schema migrations. Running workers before the API in a fresh environment can cause query errors until migrations complete.
-Job registration (queue names, repeatable intervals, job names) lives in one file per app:
+Job registration (queue names, repeatable intervals, job names) lives in:
- `apps/agenstra/backend-agent-controller/src/queue/job-registry.ts`
-- `apps/decabill/backend-billing-manager/src/queue/job-registry.ts`
-Coordinators fan out **unit jobs** (one subscription, one ticket, one import config, etc.). BullMQ `jobId` values prevent duplicate active work for the same entity. Custom job IDs use `.` separators and only allowed characters (alphanumeric, `.`, `-`, `_`, `~`) — e.g. `coordinator.filter-rules-sync`, `billing.subscription.`. Colons and slashes are not valid.
+Coordinators fan out **unit jobs** (one ticket, one import config, etc.). BullMQ `jobId` values prevent duplicate active work for the same entity. Custom job IDs use `.` separators and only allowed characters (alphanumeric, `.`, `-`, `_`, `~`), for example `coordinator.filter-rules-sync`. Colons and slashes are not valid.
## Redis and queue environment variables
@@ -36,7 +35,7 @@ Coordinators fan out **unit jobs** (one subscription, one ticket, one import con
| `REDIS_PORT` | Redis port | `6379` |
| `REDIS_PASSWORD` | Optional password | empty |
| `REDIS_DB` | Redis database index | `0` |
-| `REDIS_KEY_PREFIX` | Key namespace | `agenstra-controller` or `agenstra-billing` |
+| `REDIS_KEY_PREFIX` | Key namespace | `agenstra-controller` |
| `QUEUE_ROLE` | `api`, `scheduler`, `worker`, or `all` | `all` (local), `api` in API container |
| `QUEUE_WORKER_CONCURRENCY` | Default worker concurrency | `5` |
| `QUEUE_BULL_BOARD_ENABLED` | Mount Bull Board UI on API / `all` only | `true` on API in compose; `false` on worker/scheduler |
@@ -51,15 +50,13 @@ Existing `*_SCHEDULER_INTERVAL*` variables now control **coordinator repeat** in
Each backend `docker-compose.yaml` defines:
- `redis`
-- `backend-*` (API, `QUEUE_ROLE=api`)
-- `backend-*-scheduler` (`QUEUE_ROLE=scheduler`)
-- `backend-*-worker` (`QUEUE_ROLE=worker`)
-
-Billing Redis is published on host port **6380** by default so it can run alongside controller Redis (**6379**).
+- `backend-agent-controller` (API, `QUEUE_ROLE=api`)
+- `backend-agent-controller-scheduler` (`QUEUE_ROLE=scheduler`)
+- `backend-agent-controller-worker` (`QUEUE_ROLE=worker`)
## Bull Board
-When enabled on the API container (`QUEUE_BULL_BOARD_ENABLED=true`, default in compose), Bull Board is served at **`QUEUE_BULL_BOARD_PATH`** (default **`/admin/queues`**) on the API port (controller **3100**, billing **3200**). That path is excluded from the Nest global `/api` prefix, so use `http://localhost:3100/admin/queues`, not `/api/admin/queues`.
+When enabled on the API container (`QUEUE_BULL_BOARD_ENABLED=true`, default in compose), Bull Board is served at **`QUEUE_BULL_BOARD_PATH`** (default **`/admin/queues`**) on the API port (controller **3100**). That path is excluded from the Nest global `/api` prefix, so use `http://localhost:3100/admin/queues`, not `/api/admin/queues`.
Bull Board uses **HTTP Basic authentication** (`QUEUE_BULL_BOARD_USERNAME` / `QUEUE_BULL_BOARD_PASSWORD`). Local compose defaults to `admin` / `bullmq`; override in production. Startup fails in production if the board is enabled without a password.
diff --git a/docs/agenstra/deployment/docker-deployment.md b/docs/agenstra/deployment/docker-deployment.md
index 5ebcdcbac..2e95f4bb3 100644
--- a/docs/agenstra/deployment/docker-deployment.md
+++ b/docs/agenstra/deployment/docker-deployment.md
@@ -147,7 +147,7 @@ When `CONFIG` is set, the frontend server also supports the following optional h
- `CONFIG_JSON_MAX_DEPTH` - Max JSON traversal depth for key counting (default: `12`)
- `CONFIG_JSON_MAX_KEYS` - Max total JSON keys (default: `512`)
-Frontend Express servers (agent console, billing console, portal, docs) also support:
+Frontend Express servers (agent console, portal, docs) also support:
- `CSP_ENFORCE` - Set to `true` to enforce Content Security Policy (sends `Content-Security-Policy`), otherwise report-only (`Content-Security-Policy-Report-Only`).
- `CSP_DEFAULT_SRC_EXTRA` - Extra origins appended to `default-src` (same URL list rules as `CSP_CONNECT_SRC_EXTRA`).
@@ -159,8 +159,6 @@ Frontend Express servers (agent console, billing console, portal, docs) also sup
- `CSP_WORKER_SRC_EXTRA`, `CSP_STYLE_SRC_EXTRA`, `CSP_IMG_SRC_EXTRA`, `CSP_FONT_SRC_EXTRA` - Same pattern for `worker-src`, `style-src`, `img-src`, and `font-src` respectively.
- `CSP_FRAME_ANCESTORS` - Optional full override of CSP `frame-ancestors` (default `'none'`). See [Environment configuration](./environment-configuration.md).
-Billing manager–generated agent-controller cloud-init sets `CONFIG_ALLOWED_HOSTS` to the instance FQDN and `CSP_ENFORCE` to `true` by default for the frontend container. See [Environment configuration](./environment-configuration.md).
-
## Running Containers
### Using Docker Compose (Recommended)
diff --git a/docs/agenstra/deployment/environment-configuration.md b/docs/agenstra/deployment/environment-configuration.md
index f965a6beb..9370c1b56 100644
--- a/docs/agenstra/deployment/environment-configuration.md
+++ b/docs/agenstra/deployment/environment-configuration.md
@@ -195,32 +195,9 @@ Optional runtime extensions for agents, CI/CD pipelines, and chat filters. See [
- `DYNAMIC_PROVIDER_PLUGIN_PATH` - Absolute plugin root for post-build loading (unset by default; use `/var/lib/forepath/provider-plugins` with the compose volume when enabling plugins).
- `DYNAMIC_PROVIDER_PLUGIN_INSTALL` - Comma-separated `npm install` targets into the plugin path at container startup.
-## Backend Billing Manager
-
-### Multi-tenancy
-
-Billing data and users are partitioned by **`tenant_id`**. HTTP clients send **`X-Tenant`**; the billing console and landing page attach it via `environment.billing.tenantId` (defaults to `default`).
-
-| Variable | Description |
-| -------------------------- | ------------------------------------------------------------------------------------------------------------------ |
-| `TENANTS` | Comma-separated tenant ids allowed for **`X-Tenant`** (always includes `default`). Unset → only `default`. |
-| `STATIC_API_KEY_TENANT_ID` | Optional. When set with **`STATIC_API_KEY`** auth, API key requests are accepted only when **`X-Tenant`** matches. |
-| `BILLING_FRONTEND_URL` | Billing console base URL for the `default` tenant (Stripe return redirects). |
-| `TENANT_FRONTEND_URLS` | Per-tenant console URLs: `tenantId=https://…` pairs, comma-separated. |
-
-**API key scope (accepted risk [AR-007](../security/accepted-risks.md#ar-007--billing-multi-tenant-api-key-scope-static_api_key_tenant_id-unset)):** With **`STATIC_API_KEY`** and **without** **`STATIC_API_KEY_TENANT_ID`**, one deployment key grants **admin access to every tenant** in **`TENANTS`**, selected per request via **`X-Tenant`**. This is **intentional** (single shared automation credential). Set **`STATIC_API_KEY_TENANT_ID`** to bind the key to one tenant, or use **keycloak** / **users** for interactive multi-tenant console access.
-
-See also [Billing Administration](../features/billing-administration.md) and the billing sections in this document.
-
## Frontend applications (Express SSR)
-The Angular apps **agenstra-frontend-agent-console**, **agenstra-frontend-billing-console**, **agenstra-frontend-landingpage**, and **agenstra-frontend-docs** use the same Express layer for `GET /config` (runtime JSON proxy) and security headers. The variables below are written with the agent console in mind; they apply to all four apps unless an app-specific doc says otherwise.
-
-### Billing manager (provisioned agent controller)
-
-When the billing manager generates cloud-init for a product that includes the agent-controller frontend container, it sets **`CONFIG_ALLOWED_HOSTS`** to the instance **FQDN** (so production `CONFIG` fetches stay allowlisted) and **`CSP_ENFORCE`** to **`true`** by default. Override the CSP default only if your provisioning pipeline sets `frontend.cspEnforce` in the cloud-init config.
-
-It also sets client workspace SSRF env vars on **`backend-agent-controller`**: **`CLIENT_ENDPOINT_TLS_REJECT_UNAUTHORIZED`** (default **`true`**), **`CLIENT_ENDPOINT_ALLOW_INSECURE_HTTP`** (default **`false`**), and **`CLIENT_ENDPOINT_ALLOWED_HOSTS`** (default **`*`** so tenants may register arbitrary agent-manager hostnames while other SSRF layers still apply). DNS rebinding checks follow runtime rules (on by default); use **`CLIENT_ENDPOINT_ALLOW_INTERNAL_HOST`** in non-provisioned setups when you need the same bypass as **`CONFIG_ALLOW_INTERNAL_HOST`** (billing does not set it by default). Optional **`clientEndpointAllowedHosts`** / **`security.clientEndpointAllowedHosts`** in **`requestedConfig`** **merge** the instance FQDN with listed hosts (FQDN first); a single **`*`** entry keeps allow-all. Optional **`security.clientEndpointAllowInsecureHttp`** and **`security.clientEndpointTlsRejectUnauthorized`** map to the same `CLIENT_ENDPOINT_*` variables.
+The Angular apps **agenstra-frontend-agent-console**, **agenstra-frontend-landingpage**, and **agenstra-frontend-docs** use the same Express layer for `GET /config` (runtime JSON proxy) and security headers. The variables below are written with the agent console in mind. They apply to all listed apps unless an app-specific doc says otherwise.
### Runtime Configuration
@@ -273,23 +250,23 @@ When `CONFIG` is set, the frontend server fetches and validates the remote JSON
## Redis and BullMQ (background jobs)
-Used by **backend agent controller** and **backend billing manager**. See [Background jobs](./background-jobs.md).
-
-| Variable | Description | Default |
-| --------------------------- | -------------------------------------- | ------------------------------------------ |
-| `REDIS_HOST` | Redis host | `localhost` (compose: `redis`) |
-| `REDIS_PORT` | Redis port | `6379` |
-| `REDIS_PASSWORD` | Optional password | empty |
-| `REDIS_DB` | Redis DB index | `0` |
-| `REDIS_KEY_PREFIX` | Key prefix | `agenstra-controller` / `agenstra-billing` |
-| `QUEUE_ROLE` | `api`, `scheduler`, `worker`, or `all` | `all` locally; `api` for API container |
-| `QUEUE_WORKER_CONCURRENCY` | Worker concurrency | `5` |
-| `QUEUE_BULL_BOARD_ENABLED` | Enable Bull Board | `true` in dev for `all`/`scheduler` |
-| `QUEUE_BULL_BOARD_PATH` | Bull Board path | `/admin/queues` |
-| `QUEUE_BULL_BOARD_USERNAME` | Bull Board HTTP Basic user | `admin` |
-| `QUEUE_BULL_BOARD_PASSWORD` | Bull Board HTTP Basic password | required; `bullmq` in local compose |
-
-Scheduler interval variables (e.g. `BILLING_SCHEDULER_INTERVAL`, `AUTONOMOUS_TICKET_SCHEDULER_INTERVAL_MS`) configure **coordinator** repeat intervals in BullMQ.
+Used by **backend agent controller**. See [Background jobs](./background-jobs.md).
+
+| Variable | Description | Default |
+| --------------------------- | -------------------------------------- | -------------------------------------- |
+| `REDIS_HOST` | Redis host | `localhost` (compose: `redis`) |
+| `REDIS_PORT` | Redis port | `6379` |
+| `REDIS_PASSWORD` | Optional password | empty |
+| `REDIS_DB` | Redis DB index | `0` |
+| `REDIS_KEY_PREFIX` | Key prefix | `agenstra-controller` |
+| `QUEUE_ROLE` | `api`, `scheduler`, `worker`, or `all` | `all` locally; `api` for API container |
+| `QUEUE_WORKER_CONCURRENCY` | Worker concurrency | `5` |
+| `QUEUE_BULL_BOARD_ENABLED` | Enable Bull Board | `true` in dev for `all`/`scheduler` |
+| `QUEUE_BULL_BOARD_PATH` | Bull Board path | `/admin/queues` |
+| `QUEUE_BULL_BOARD_USERNAME` | Bull Board HTTP Basic user | `admin` |
+| `QUEUE_BULL_BOARD_PASSWORD` | Bull Board HTTP Basic password | required; `bullmq` in local compose |
+
+Scheduler interval variables (for example `AUTONOMOUS_TICKET_SCHEDULER_INTERVAL_MS`) configure **coordinator** repeat intervals in BullMQ.
## Environment-Specific Defaults
diff --git a/docs/agenstra/deployment/production-checklist.md b/docs/agenstra/deployment/production-checklist.md
index 0332d8f91..6b10894f8 100644
--- a/docs/agenstra/deployment/production-checklist.md
+++ b/docs/agenstra/deployment/production-checklist.md
@@ -53,7 +53,7 @@ Comprehensive checklist for deploying Agenstra to production.
### Authentication
- Use strong API keys or Keycloak with proper configuration
-- Consider setting **`AUTHENTICATION_METHOD`** explicitly in production if your policy requires fully unambiguous mode selection (see **[Accepted risks — AR-004](../security/accepted-risks.md)**)
+- Consider setting **`AUTHENTICATION_METHOD`** explicitly in production if your policy requires fully unambiguous mode selection (see **[Accepted risks - AR-003](../security/accepted-risks.md)**)
- Enable token expiration and refresh
- Implement proper session management
- Use HTTPS for all API communications
diff --git a/docs/agenstra/features/authentication.md b/docs/agenstra/features/authentication.md
index dbc6a23d2..f8c4e3535 100644
--- a/docs/agenstra/features/authentication.md
+++ b/docs/agenstra/features/authentication.md
@@ -321,7 +321,7 @@ See **[Client Management](./client-management.md#per-client-permissions)** for d
## Related Documentation
- **[Environment Configuration](../deployment/environment-configuration.md)** - Environment variable reference
-- **[Security — Accepted risks](../security/accepted-risks.md)** - **AR-004** (implicit authentication method resolution when `AUTHENTICATION_METHOD` is unset)
+- **[Security - Accepted risks](../security/accepted-risks.md)** - **AR-003** (implicit authentication method resolution when `AUTHENTICATION_METHOD` is unset)
- **[Security — Operational hardening](../security/operational-hardening.md)** - Backend authentication resolution behavior
- **[Client Management](./client-management.md)** - Per-client permissions and user management
- **[Backend Agent Controller Application](../applications/backend-agent-controller.md)** - Backend authentication implementation
diff --git a/docs/agenstra/features/billing-administration.md b/docs/agenstra/features/billing-administration.md
deleted file mode 100644
index 67facd71e..000000000
--- a/docs/agenstra/features/billing-administration.md
+++ /dev/null
@@ -1,47 +0,0 @@
-# Billing Administration
-
-Admin-only features in the billing console for manual invoice management and customer billing profile CRUD.
-
-See also: [API Reference](../api-reference/README.md#billing-manager-http-api) for the published OpenAPI and AsyncAPI specifications.
-
-## Access control
-
-All endpoints under `/admin/billing/*` require admin role (`@KeycloakRoles(ADMIN)` + `@UsersRoles(ADMIN)`). Frontend routes use `authGuard` + `billingAdminGuard`.
-
-**Multi-tenancy:** Admin and user routes are scoped by **`X-Tenant`** and the user’s **`tenant_id`**. API key auth with **`STATIC_API_KEY`** and without **`STATIC_API_KEY_TENANT_ID`** can administer **all** configured tenants (accepted risk **[AR-007](../security/accepted-risks.md#ar-007--billing-multi-tenant-api-key-scope-static_api_key_tenant_id-unset)**).
-
-## Manual invoice administration
-
-**Immutability:** Only invoices in `draft` status can be edited or deleted. Once issued (`issued`, `paid`, `partially_paid`, `overdue`, or `void`), line items and amounts are immutable. Admins can still void unpaid issued invoices or mark payment status manually.
-
-**Workflow:**
-
-1. `POST /admin/billing/invoices/manual` — create draft with user, optional subscription, custom line items
-2. `POST /admin/billing/invoices/{id}` — update draft line items
-3. `POST /admin/billing/invoices/{id}/issue` — issue draft (requires complete customer profile)
-4. `DELETE /admin/billing/invoices/{id}` — delete draft only
-
-The workflow steps above mirror the manual invoice administration sequence (create draft → update → issue or delete).
-
-**Frontend:** `/administration/billing` in the billing console — split layout with dashboard cards and charts on the left, invoice list (batch-loaded, client-side search, list-group style) on the right.
-
-## Customer billing profiles (admin)
-
-Customer billing data is stored in `billing_customer_profiles` (one profile per user).
-
-| Method | Path | Purpose |
-| ------ | --------------------------------------- | ------------------------------------------------------ |
-| GET | `/admin/billing/customer-profiles` | Paginated list |
-| GET | `/admin/billing/customer-profiles/{id}` | Full profile detail |
-| POST | `/admin/billing/customer-profiles` | Create for user |
-| POST | `/admin/billing/customer-profiles/{id}` | Update |
-| DELETE | `/admin/billing/customer-profiles/{id}` | Delete (blocked if user has invoices or subscriptions) |
-
-Self-service `GET/POST /customer-profile` remains for end users.
-
-**Frontend:** `/administration/customer-profiles` in the billing console.
-
-## Related admin pages
-
-- **Billing dashboard** (`/administration/billing`) — KPIs, charts, bill-now (unchanged)
-- **Users** (`/users`) — shared identity user manager (unchanged)
diff --git a/docs/agenstra/features/dynamic-provider-plugins.md b/docs/agenstra/features/dynamic-provider-plugins.md
index 1396bf7ce..8437758d8 100644
--- a/docs/agenstra/features/dynamic-provider-plugins.md
+++ b/docs/agenstra/features/dynamic-provider-plugins.md
@@ -2,7 +2,7 @@
Agenstra backends can register **extra provider implementations at runtime** from comma-separated `DYNAMIC_*` environment variables. The same mechanism supports **baked-in** packages (present in the app image deploy graph) and **post-build** plugins (mounted or installed into a container path without rebuilding the image).
-This page covers **Agenstra** backends only: **agent controller** and **agent manager**. Decabill billing backends use the same loader for payment processors and billing UI metadata; see [Billing Administration](./billing-administration.md) and the [API Reference](../api-reference/README.md#billing-manager-http-api).
+This page covers **Agenstra** backends only: **agent controller** and **agent manager**. Payment processor and billing UI plugins are documented in the Decabill product documentation site.
## Overview
diff --git a/docs/agenstra/features/usage-statistics.md b/docs/agenstra/features/usage-statistics.md
index c9ea4259a..1b3474ec4 100644
--- a/docs/agenstra/features/usage-statistics.md
+++ b/docs/agenstra/features/usage-statistics.md
@@ -29,7 +29,7 @@ Global equivalents exist under `/api/statistics/...` for cross-workspace views w
| Concern | Source | Typical use |
| ------------------------ | ------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
-| **Usage statistics** | Agent controller HTTP API | Dashboards, billing-adjacent usage, compliance reporting |
+| **Usage statistics** | Agent controller HTTP API | Dashboards, operational usage, compliance reporting |
| **Container statistics** | Agent manager WebSocket (`containerStats` forwarded via controller) | Live health of a running agent container (default poll every 15s on the manager; see `CONTAINER_STATS_SCHEDULER_INTERVAL`) |
## Related documentation
diff --git a/docs/agenstra/features/websocket-communication.md b/docs/agenstra/features/websocket-communication.md
index 57d2e788a..e05be1e97 100644
--- a/docs/agenstra/features/websocket-communication.md
+++ b/docs/agenstra/features/websocket-communication.md
@@ -37,12 +37,6 @@ The agent console opens a dedicated Socket.IO connection to **`status`** (derive
See `libs/domains/agenstra/backend/feature-agent-controller/spec/asyncapi.yaml` and `libs/domains/agenstra/frontend/data-access-agent-console/docs/notifications-state.mmd`.
-### Billing manager (dashboard status)
-
-The billing console can open a second Socket.IO connection to the **billing-manager** status gateway (default namespace `/billing`, separate TCP port from REST). Handshake auth matches HTTP (`Bearer` JWT for users or Keycloak). **Static API key** auth does not receive a user-scoped billing stream; `subscribeDashboardStatus` is rejected with an `error` event, consistent with REST returning "User not authenticated" for API-key-only requests.
-
-The server selects subscriptions **only** from the authenticated user’s data on every poll tick and emits `dashboardStatusUpdate` **only** to that socket (no rooms). See `libs/domains/decabill/backend/feature-billing-manager/spec/asyncapi.yaml`.
-
## Connection Flow
### Frontend to Controller
diff --git a/docs/agenstra/security/README.md b/docs/agenstra/security/README.md
index 4b41394b1..4c3cf481b 100644
--- a/docs/agenstra/security/README.md
+++ b/docs/agenstra/security/README.md
@@ -16,7 +16,7 @@ How public documentation relates to **CRA** (Regulation (EU) 2024/2847) and **BS
### [Accepted risks (register)](./accepted-risks.md)
-Register **AR-001** through **AR-007**: provisioning SSH posture, native desktop signing and update posture, frontend CSP, backend authentication method resolution, Electron window-open policy, Trivy unfixed-CVE gating, and billing multi-tenant API key scope. Includes acceptance dates, review cadence, mitigations, and withdrawal paths.
+Register **AR-001** through **AR-005**: native desktop signing and update posture, frontend CSP, backend authentication method resolution, Electron window-open policy, and Trivy unfixed-CVE gating. Includes acceptance dates, review cadence, mitigations, and withdrawal paths.
### [Container image security](./container-images.md)
@@ -32,7 +32,7 @@ Responsible disclosure (contact and process), CycloneDX **SBOM** location, and *
### [CI security scanning (Trivy)](./ci-security-scanning.md)
-Automated **Trivy** scans on pull requests (filesystem, IaC/config, container images); SARIF upload to GitHub Security; CRITICAL fail gate (fixable issues only — see **[AR-006](./accepted-risks.md#ar-006--ci--local-trivy-unfixed-vulnerabilities-not-gated)**). Pre-commit runs filesystem/config scans locally.
+Automated **Trivy** scans on pull requests (filesystem, IaC/config, container images); SARIF upload to GitHub Security; CRITICAL fail gate (fixable issues only; see **[AR-005](./accepted-risks.md#ar-005---ci--local-trivy-unfixed-vulnerabilities-not-gated)**). Pre-commit runs filesystem/config scans locally.
## Configuration reference
diff --git a/docs/agenstra/security/accepted-risks.md b/docs/agenstra/security/accepted-risks.md
index 4b71172ba..7ad56b9cf 100644
--- a/docs/agenstra/security/accepted-risks.md
+++ b/docs/agenstra/security/accepted-risks.md
@@ -2,173 +2,127 @@
This register records **explicit risk acceptance** for product and deployment constraints that deviate from stricter security baselines. It supports **BSI / ISMS-style** traceability and **CRA-oriented** technical documentation (risk treatment and transparency). A compact summary table may also be published at the repository root in `SECURITY.md` for hosts that surface that file. For vulnerability reporting, SBOM paths, and desktop checksum verification, see **[Vulnerability reporting and artifacts](./vulnerability-reporting-and-artifacts.md)**.
-**Review cadence:** entries use acceptance **2026-05-06** and next review **2027-05-06** unless a row states otherwise; trigger an early review if the relevant templates, packaging, CSP integration, authentication resolution, billing multi-tenancy, or Electron shell policy change materially.
+**Review cadence:** entries use acceptance **2026-05-06** and next review **2027-05-06** unless a row states otherwise. Trigger an early review if the relevant templates, packaging, CSP integration, authentication resolution, or Electron shell policy change materially.
---
-## AR-001 — Provisioning SSH (cloud-init templates)
-
-| Field | Recorded value |
-| ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| **ID** | AR-001 |
-| **Area** | Provisioning SSH in cloud-init templates |
-| **Configuration** | **`PermitRootLogin yes`** and **root** `authorized_keys` installed via provisioning scripts (`libs/domains/decabill/backend/feature-billing-manager/src/lib/utils/cloud-init/agent-controller.utils.ts`, `agent-manager.utils.ts`) |
-| **Residual risk** | Compromise of the provisioning SSH private key or leakage of user-data/metadata can yield **root** on affected instances. |
-| **Mitigations in scope of this repo (templates)** | SSH **key-based** access; **password authentication disabled** in generated `sshd` configuration. |
-| **Compensating controls (deployer / org)** | Restrict network access (security groups, allowlisted IPs, bastion), rotate keys, monitor instances, minimize secrets in user-data. |
-| **Risk owner** | Maintaining party for this repository and product security documentation (Forepath). |
-| **Acceptor** | Repository maintainer (acceptance recorded in project documentation). |
-| **Acceptance date** | **2026-05-06** |
-| **Next review date** | **2027-05-06** |
-| **Rationale (business / technical)** | First-boot automation prioritizes operational simplicity for provisioned hosts. Non-root SSH and **`PermitRootLogin no`** remain the documented hardening path when constraints allow. |
+## AR-001 - Desktop app: no OS-trusted code signing / no in-app auto-update
+
+| Field | Recorded value |
+| -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **ID** | AR-001 |
+| **Area** | **`native-agent-console`** (Electron Forge pipeline, `apps/agenstra/native-agent-console/forge.config.js`) |
+| **Configuration** | **No** OS-trusted **code signing** and **no** **in-app auto-update** channel in the documented release pipeline. |
+| **Residual risk** | Users rely on **manual checksum verification** and a trusted download channel rather than OS trust stores or automatic security updates from the application. |
+| **Mitigations in scope of this repo** | Release artifacts include **`SHA256SUMS`** and **`integrity-manifest.json`** produced by the **`release-integrity`** Nx project (`tools/release-integrity` in the repository). CI and release pipelines generate and verify these manifests. |
+| **Compensating controls (user / org)** | Verify checksums after download. Prefer the **web browser** as the primary client. Treat **`STATIC_API_KEY`** and other secrets per **[Environment configuration](../deployment/environment-configuration.md)**. |
+| **Risk owner** | Maintaining party for this repository and product security documentation (Forepath). |
+| **Acceptor** | Repository maintainer (acceptance recorded in project documentation). |
+| **Acceptance date** | **2026-05-06** |
+| **Next review date** | **2027-05-06** |
+| **Rationale (business / technical)** | The product is **primarily** used via the **web browser**. The native build is a **secondary** distribution channel. |
#### Operator summary (AR-001)
-Some deployment flows generate cloud-init that configures **SSH for `root`** and installs **`authorized_keys` under `/root/.ssh/`**. This is a **known, documented** property. Mitigations in templates are **key-only** SSH and **disabled password authentication**. Deployers should add network controls, bastions, key rotation, and—where possible—non-root administration with **`PermitRootLogin no`**.
-
----
-
-## AR-002 — Desktop app: no OS-trusted code signing / no in-app auto-update
-
-| Field | Recorded value |
-| -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| **ID** | AR-002 |
-| **Area** | **`native-agent-console`** (Electron Forge pipeline, `apps/agenstra/native-agent-console/forge.config.js`) |
-| **Configuration** | **No** OS-trusted **code signing** and **no** **in-app auto-update** channel in the documented release pipeline. |
-| **Residual risk** | Users rely on **manual checksum verification** and a trusted download channel rather than OS trust stores or automatic security updates from the application. |
-| **Mitigations in scope of this repo** | Release artifacts include **`SHA256SUMS`** and **`integrity-manifest.json`** produced by the **`release-integrity`** Nx project (`tools/release-integrity` in the repository); CI/release pipelines generate and verify these manifests. |
-| **Compensating controls (user / org)** | Verify checksums after download; prefer the **web browser** as the primary client; treat **`STATIC_API_KEY`** and other secrets per **[Environment configuration](../deployment/environment-configuration.md)**. |
-| **Risk owner** | Maintaining party for this repository and product security documentation (Forepath). |
-| **Acceptor** | Repository maintainer (acceptance recorded in project documentation). |
-| **Acceptance date** | **2026-05-06** |
-| **Next review date** | **2027-05-06** |
-| **Rationale (business / technical)** | The product is **primarily** used via the **web browser**; the native build is a **secondary** distribution channel. |
-
-#### Operator summary (AR-002)
-
**OS-trusted code signing** and **in-app auto-update** are **not** provided. Verify artifacts using **`SHA256SUMS`** / **`integrity-manifest.json`**. Details: **[Desktop release integrity](./vulnerability-reporting-and-artifacts.md#desktop-release-integrity)**.
---
-## AR-003 — Web frontends: CSP `unsafe-inline` / `unsafe-eval` (Monaco)
+## AR-002 - Web frontends: CSP `unsafe-inline` / `unsafe-eval` (Monaco)
| Field | Recorded value |
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
-| **ID** | AR-003 |
+| **ID** | AR-002 |
| **Area** | **`frontend-*`** Express servers |
-| **Configuration** | **Content Security Policy** allows **`'unsafe-inline'`** and **`'unsafe-eval'`** so **Monaco Editor** and related tooling work; policy is sent as **`Content-Security-Policy-Report-Only`** by default (violations reported, not blocked). Implementation: `libs/domains/shared/frontend/util-express-server/src/lib/security-headers.ts`. |
-| **Residual risk** | XSS impact can be greater than under a strict CSP; **report-only** does not block violations. |
-| **Mitigations in scope of this repo** | Set **`CSP_ENFORCE=true`** only in environments where compatibility is validated; plan stricter CSP with a validated Monaco/worker/nonce strategy. |
+| **Configuration** | **Content Security Policy** allows **`'unsafe-inline'`** and **`'unsafe-eval'`** so **Monaco Editor** and related tooling work. Policy is sent as **`Content-Security-Policy-Report-Only`** by default (violations reported, not blocked). Implementation: `libs/domains/shared/frontend/util-express-server/src/lib/security-headers.ts`. |
+| **Residual risk** | XSS impact can be greater than under a strict CSP. **Report-only** does not block violations. |
+| **Mitigations in scope of this repo** | Set **`CSP_ENFORCE=true`** only in environments where compatibility is validated. Plan stricter CSP with a validated Monaco, worker, or nonce strategy. |
| **Compensating controls (deployer)** | Enforce HTTPS, restrict **CORS**, keep dependencies patched, monitor reports if CSP reporting is configured. |
| **Risk owner** | Maintaining party for this repository and product security documentation (Forepath). |
| **Acceptor** | Repository maintainer (acceptance recorded in project documentation). |
| **Acceptance date** | **2026-05-06** |
| **Next review date** | **2027-05-06** |
-| **Rationale (business / technical)** | Monaco is **core** to the console; tightening without a validated strategy risks **breaking** the product. Enforcement is **opt-in** after verification. |
+| **Rationale (business / technical)** | Monaco is **core** to the console. Tightening without a validated strategy risks **breaking** the product. Enforcement is **opt-in** after verification. |
-#### Operator summary (AR-003)
+#### Operator summary (AR-002)
-By default, CSP is **report-only**. Use **`CSP_ENFORCE=true`** only after verification. See **[Operational hardening — Content Security Policy](./operational-hardening.md#content-security-policy-frontend-express)** and **[Environment configuration](../deployment/environment-configuration.md)**.
+By default, CSP is **report-only**. Use **`CSP_ENFORCE=true`** only after verification. See **[Operational hardening - Content Security Policy](./operational-hardening.md#content-security-policy-frontend-express)** and **[Environment configuration](../deployment/environment-configuration.md)**.
---
-## AR-004 — Backend authentication method resolution
-
-| Field | Recorded value |
-| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| **ID** | AR-004 |
-| **Area** | **`getAuthenticationMethod`** in `libs/domains/identity/backend/util-auth/src/lib/hybrid-auth.guard.ts` |
-| **Configuration** | **`AUTHENTICATION_METHOD`** is **not** required to be set. When unset: if **`STATIC_API_KEY`** is set → **api-key** mode; otherwise → **keycloak** (OIDC / Keycloak integration with the deployer’s IdP). **Protected routes are not anonymous**—Keycloak- or users-mode guards still enforce authentication per configuration. |
-| **Residual risk** | Deployments may **implicitly** run in **keycloak** mode without a single obvious env flag, which can surprise operators who expect an explicit mode switch. **`STATIC_API_KEY`** remains a **high-value secret** in **api-key** mode. |
-| **Mitigations in scope of this repo** | Documented resolution order; **api-key** mode requires **`STATIC_API_KEY`** and validates the header; **keycloak** / **users** paths delegate to their guards. |
-| **Compensating controls (deployer)** | For **api-key** or **users** deployments, set **`AUTHENTICATION_METHOD`** explicitly; treat **`STATIC_API_KEY`** with rotation and least exposure; prefer **keycloak** with the customer IdP for integrated enterprise setups. |
-| **Risk owner** | Maintaining party for this repository and product security documentation (Forepath). |
-| **Acceptor** | Repository maintainer (acceptance recorded in project documentation). |
-| **Acceptance date** | **2026-05-06** |
-| **Next review date** | **2027-05-06** |
-| **Rationale (business / technical)** | Defaulting to **keycloak** when no API key is configured favors the **enterprise-typical** integrated IdP path while preserving backward compatibility for **`STATIC_API_KEY`**. |
+## AR-003 - Backend authentication method resolution
+
+| Field | Recorded value |
+| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| **ID** | AR-003 |
+| **Area** | **`getAuthenticationMethod`** in `libs/domains/identity/backend/util-auth/src/lib/hybrid-auth.guard.ts` |
+| **Configuration** | **`AUTHENTICATION_METHOD`** is **not** required to be set. When unset: if **`STATIC_API_KEY`** is set, **api-key** mode is used; otherwise **keycloak** (OIDC / Keycloak integration with the deployer's IdP). **Protected routes are not anonymous**. Keycloak or users-mode guards still enforce authentication per configuration. |
+| **Residual risk** | Deployments may **implicitly** run in **keycloak** mode without a single obvious env flag, which can surprise operators who expect an explicit mode switch. **`STATIC_API_KEY`** remains a **high-value secret** in **api-key** mode. |
+| **Mitigations in scope of this repo** | Documented resolution order. **api-key** mode requires **`STATIC_API_KEY`** and validates the header. **keycloak** / **users** paths delegate to their guards. |
+| **Compensating controls (deployer)** | For **api-key** or **users** deployments, set **`AUTHENTICATION_METHOD`** explicitly. Treat **`STATIC_API_KEY`** with rotation and least exposure. Prefer **keycloak** with the customer IdP for integrated enterprise setups. |
+| **Risk owner** | Maintaining party for this repository and product security documentation (Forepath). |
+| **Acceptor** | Repository maintainer (acceptance recorded in project documentation). |
+| **Acceptance date** | **2026-05-06** |
+| **Next review date** | **2027-05-06** |
+| **Rationale (business / technical)** | Defaulting to **keycloak** when no API key is configured favors the **enterprise-typical** integrated IdP path while preserving backward compatibility for **`STATIC_API_KEY`**. |
-#### Operator summary (AR-004)
+#### Operator summary (AR-003)
-Set **`AUTHENTICATION_METHOD`** explicitly if your policy requires **fully explicit** configuration. Never expose **`STATIC_API_KEY`**. See **[Authentication](../features/authentication.md)** and **[Operational hardening — Authentication mode](./operational-hardening.md#authentication-mode-backends)**.
+Set **`AUTHENTICATION_METHOD`** explicitly if your policy requires **fully explicit** configuration. Never expose **`STATIC_API_KEY`**. See **[Authentication](../features/authentication.md)** and **[Operational hardening - Authentication mode](./operational-hardening.md#authentication-mode-backends)**.
---
-## AR-005 — Desktop window open policy (`native-agent-console`)
+## AR-004 - Desktop window open policy (`native-agent-console`)
| Field | Recorded value |
| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| **ID** | AR-005 |
+| **ID** | AR-004 |
| **Area** | **`setWindowOpenHandler`** in `apps/agenstra/native-agent-console/src/main.ts` |
| **Configuration** | Handler uses **`action: 'allow'`** so `window.open` / `target=_blank` can open new Electron windows with inherited `webPreferences`. |
| **Residual risk** | Pop-up or multi-window flows could be abused in **phishing- or distraction-style** attacks compared with a stricter **deny** or allowlist policy. |
-| **Mitigations in scope of this repo** | **Sandbox** and **contextIsolation** remain enabled; there is **no address bar (omnibox)** and **users cannot install browser extensions** in this shell, which limits some browser-class attacks. |
-| **Compensating controls (deployer)** | Prefer the **web client** for untrusted content; revisit policy if the shell gains **untrusted browsing** or **URL-entry** UX. |
+| **Mitigations in scope of this repo** | **Sandbox** and **contextIsolation** remain enabled. There is **no address bar (omnibox)** and **users cannot install browser extensions** in this shell, which limits some browser-class attacks. |
+| **Compensating controls (deployer)** | Prefer the **web client** for untrusted content. Revisit policy if the shell gains **untrusted browsing** or **URL-entry** UX. |
| **Risk owner** | Maintaining party for this repository and product security documentation (Forepath). |
| **Acceptor** | Repository maintainer (acceptance recorded in project documentation). |
| **Acceptance date** | **2026-05-06** |
| **Next review date** | **2027-05-06** |
-| **Rationale (business / technical)** | Allowing new windows supports legitimate product flows (e.g. external documentation) while the **secondary** desktop channel keeps a reduced attack surface versus a full browser. |
+| **Rationale (business / technical)** | Allowing new windows supports legitimate product flows (for example external documentation) while the **secondary** desktop channel keeps a reduced attack surface versus a full browser. |
-#### Operator summary (AR-005)
+#### Operator summary (AR-004)
New windows are **allowed** by design. Risk is **lower** than in a general-purpose browser because of the **non-browser** shell, but operators should treat the desktop app as **not** a general browsing environment for untrusted sites.
---
-## AR-006 — CI / local Trivy: unfixed vulnerabilities not gated
-
-| Field | Recorded value |
-| ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| **ID** | AR-006 |
-| **Area** | **Trivy** vulnerability scanning (`trivy.yaml` at repository root, pull-request CI, pre-commit, local image scans—not the release workflow) |
-| **Configuration** | **`vulnerability.ignore-unfixed: true`** — findings **without a Fixed Version** (no vendor/upstream fix published yet) are **excluded from the fail gate**. Only **CRITICAL** severities with an available fix fail CI and local hooks (see [`ci-security-scanning.md`](./ci-security-scanning.md)). |
-| **Residual risk** | Known **CRITICAL** issues may remain in dependencies, base images, or OS packages until upstream publishes a fix; they are visible in SARIF/report output but do not block merge or release. |
-| **Mitigations in scope of this repo** | Trivy still scans for **vuln**, **secret**, and **misconfig**; **HIGH** and below are report-only; per-CVE exceptions use `.trivyignore` with traceability. SBOM publication and Dependency Track on release provide ongoing visibility. |
-| **Compensating controls (deployer)** | Monitor GitHub Security / SARIF artifacts; patch when fixes ship; track accepted CVEs in `.trivyignore` only when a fix exists but cannot be applied yet (not for permanently unfixed issues). |
-| **Risk owner** | Maintaining party for this repository and product security documentation (Forepath). |
-| **Acceptor** | Repository maintainer (acceptance recorded in project documentation). |
-| **Acceptance date** | **2026-05-16** |
-| **Next review date** | **2027-05-06** |
-| **Rationale (business / technical)** | Blocking on unfixed CVEs creates **false failures** with no remediation path and delays delivery without reducing exploitable risk. Gating on **fixable CRITICAL** issues keeps CI actionable while acknowledging vendor lag. |
-
-#### Operator summary (AR-006)
-
-**Unfixed vulnerabilities are acceptable for pipeline gating** — Trivy will not fail because a CVE has an empty **Fixed Version**. Address **CRITICAL** findings that have a published fix; track anything else via SARIF and release SBOMs. Do not add unfixed CVEs to `.trivyignore` solely to silence the gate (they are already ignored). See **[CI security scanning](./ci-security-scanning.md)**.
-
----
-
-## AR-007 — Billing multi-tenant API key scope (`STATIC_API_KEY_TENANT_ID` unset)
+## AR-005 - CI / local Trivy: unfixed vulnerabilities not gated
+
+| Field | Recorded value |
+| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| **ID** | AR-005 |
+| **Area** | **Trivy** vulnerability scanning (`trivy.yaml` at repository root, pull-request CI, pre-commit, local image scans, not the release workflow) |
+| **Configuration** | **`vulnerability.ignore-unfixed: true`**. Findings **without a Fixed Version** (no vendor or upstream fix published yet) are **excluded from the fail gate**. Only **CRITICAL** severities with an available fix fail CI and local hooks (see [`ci-security-scanning.md`](./ci-security-scanning.md)). |
+| **Residual risk** | Known **CRITICAL** issues may remain in dependencies, base images, or OS packages until upstream publishes a fix. They are visible in SARIF and report output but do not block merge or release. |
+| **Mitigations in scope of this repo** | Trivy still scans for **vuln**, **secret**, and **misconfig**. **HIGH** and below are report-only. Per-CVE exceptions use `.trivyignore` with traceability. SBOM publication and Dependency Track on release provide ongoing visibility. |
+| **Compensating controls (deployer)** | Monitor GitHub Security and SARIF artifacts. Patch when fixes ship. Track accepted CVEs in `.trivyignore` only when a fix exists but cannot be applied yet (not for permanently unfixed issues). |
+| **Risk owner** | Maintaining party for this repository and product security documentation (Forepath). |
+| **Acceptor** | Repository maintainer (acceptance recorded in project documentation). |
+| **Acceptance date** | **2026-05-16** |
+| **Next review date** | **2027-05-06** |
+| **Rationale (business / technical)** | Blocking on unfixed CVEs creates **false failures** with no remediation path and delays delivery without reducing exploitable risk. Gating on **fixable CRITICAL** issues keeps CI actionable while acknowledging vendor lag. |
-| Field | Recorded value |
-| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| **ID** | AR-007 |
-| **Area** | **Billing manager** multi-tenancy with **`AUTHENTICATION_METHOD=api-key`** (or inferred api-key when **`STATIC_API_KEY`** is set). Implementation: `TenantUserGuard` in `libs/domains/decabill/backend/feature-billing-manager/src/lib/guards/tenant-user.guard.ts`; admin bypass via `ensureAdmin()` in `billing-access.utils.ts`. |
-| **Configuration** | A **single** deployment-wide **`STATIC_API_KEY`**. **`STATIC_API_KEY_TENANT_ID`** is **optional**. When unset, a valid API key is accepted for **any** tenant id allowed by **`TENANTS`** / **`X-Tenant`** (each request selects the tenant via header). API key auth is treated as **admin** for billing admin routes. |
-| **Residual risk** | Anyone who possesses **`STATIC_API_KEY`** can read and mutate **all** configured tenants’ billing data by changing **`X-Tenant`**, not only one tenant. This is **cross-tenant admin access** from a single shared secret. |
-| **Mitigations in scope of this repo** | **`STATIC_API_KEY_TENANT_ID`** optionally binds API key auth to one tenant (must match **`X-Tenant`**). User/session auth (**keycloak** / **users**) enforces per-user **`tenant_id`** regardless of this setting. WebSocket dashboard status does **not** stream data to API key clients. |
-| **Compensating controls (deployer)** | Treat **`STATIC_API_KEY`** as a **high-value** secret (rotation, least exposure, no client-side use). Prefer **keycloak** or **users** for the billing console in multi-tenant production. Set **`STATIC_API_KEY_TENANT_ID`** when automation must use API key against **one** tenant only. Use separate billing deployments or keys per tenant if policy forbids shared cross-tenant automation credentials. |
-| **Risk owner** | Maintaining party for this repository and product security documentation (Forepath). |
-| **Acceptor** | Repository maintainer (acceptance recorded in project documentation). |
-| **Acceptance date** | **2026-06-19** |
-| **Next review date** | **2027-05-06** |
-| **Rationale (business / technical)** | Billing deployments use **one** static API key for automation and operator scripts. Requiring a separate key per tenant would multiply secret management without a current product requirement. **Explicit acceptance:** shared key + optional tenant header is **intentional**; operators who need single-tenant binding use **`STATIC_API_KEY_TENANT_ID`**. |
-
-#### Operator summary (AR-007)
+#### Operator summary (AR-005)
-With **`STATIC_API_KEY`** and **without** **`STATIC_API_KEY_TENANT_ID`**, the key is **not** limited to one tenant: it grants **admin-level access to every tenant** allowed by **`TENANTS`**, selected per request via **`X-Tenant`**. That behavior is **accepted** because there is only one deployment key. To restrict API key use to a single tenant, set **`STATIC_API_KEY_TENANT_ID`**. Interactive users (Keycloak/JWT) remain scoped to their own tenant. See **[Environment configuration — Billing manager multi-tenancy](../deployment/environment-configuration.md#backend-billing-manager)** and **[Billing Administration](../features/billing-administration.md)**.
+**Unfixed vulnerabilities are acceptable for pipeline gating**. Trivy will not fail because a CVE has an empty **Fixed Version**. Address **CRITICAL** findings that have a published fix. Track anything else via SARIF and release SBOMs. Do not add unfixed CVEs to `.trivyignore` solely to silence the gate (they are already ignored). See **[CI security scanning](./ci-security-scanning.md)**.
---
## Hardening paths (if an acceptance is withdrawn)
-- **AR-001:** Prefer a non-root admin user, **`PermitRootLogin no`**, least-privilege `sudo`, and cloud-init-native `ssh_authorized_keys` where possible; reduce secrets in user-data.
-- **AR-002:** Add OS-trusted signing and/or Electron auto-update when native distribution requirements justify the operational cost.
-- **AR-003:** Tighten CSP after automated and manual verification so core UI (including Monaco) still functions.
-- **AR-004:** Require **`AUTHENTICATION_METHOD`** in all environments if auditors demand explicit configuration, or add startup validation that fails when **`STATIC_API_KEY`** is set without an explicit mode.
-- **AR-005:** Tighten **`setWindowOpenHandler`** (for example URL allowlist or **`action: 'deny'`**) if the product loads untrusted origins or adds browser-like navigation.
-- **AR-006:** Set **`vulnerability.ignore-unfixed: false`** (and optionally add **HIGH** to `severity`) if policy requires failing on all CRITICAL findings regardless of fix availability; expect more `.trivyignore` churn until dependencies catch up.
-- **AR-007:** Require **`STATIC_API_KEY_TENANT_ID`** whenever **`STATIC_API_KEY`** is set and **`TENANTS`** lists more than one id; or reject API key auth on billing when multiple tenants are configured; or issue per-tenant API keys (product change).
+- **AR-001:** Add OS-trusted signing and/or Electron auto-update when native distribution requirements justify the operational cost.
+- **AR-002:** Tighten CSP after automated and manual verification so core UI (including Monaco) still functions.
+- **AR-003:** Require **`AUTHENTICATION_METHOD`** in all environments if auditors demand explicit configuration, or add startup validation that fails when **`STATIC_API_KEY`** is set without an explicit mode.
+- **AR-004:** Tighten **`setWindowOpenHandler`** (for example URL allowlist or **`action: 'deny'`**) if the product loads untrusted origins or adds browser-like navigation.
+- **AR-005:** Set **`vulnerability.ignore-unfixed: false`** (and optionally add **HIGH** to `severity`) if policy requires failing on all CRITICAL findings regardless of fix availability. Expect more `.trivyignore` churn until dependencies catch up.
---
diff --git a/docs/agenstra/security/ci-security-scanning.md b/docs/agenstra/security/ci-security-scanning.md
index c36881d7e..cb4a01018 100644
--- a/docs/agenstra/security/ci-security-scanning.md
+++ b/docs/agenstra/security/ci-security-scanning.md
@@ -26,7 +26,7 @@ The `.github/workflows/release.yml` workflow does **not** run Trivy vulnerabilit
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Fail pipeline** | CRITICAL |
| **Report only** | HIGH, MEDIUM, LOW (visible in SARIF when uploaded) |
-| **Unfixed CVEs** | Ignored (`vulnerability.ignore-unfixed: true`) — findings without a Fixed Version do not fail the gate; see **[AR-006](./accepted-risks.md#ar-006--ci--local-trivy-unfixed-vulnerabilities-not-gated)** |
+| **Unfixed CVEs** | Ignored (`vulnerability.ignore-unfixed: true`). Findings without a Fixed Version do not fail the gate. See **[AR-005](./accepted-risks.md#ar-005---ci--local-trivy-unfixed-vulnerabilities-not-gated)** |
## Viewing results
@@ -41,7 +41,7 @@ SARIF categories include `trivy-fs`, `trivy-config`, and `trivy-images-*` on pul
2. **Documented ignore** — open a PR that adds the CVE to `.trivyignore`, reference an **[accepted-risk](./accepted-risks.md)** entry (or document a false positive), and note a **review/expiry date** in the PR description.
3. **Do not** weaken `trivy.yaml` for one-off exceptions.
-See **[Accepted risks](./accepted-risks.md)** for deliberate product-level deviations (separate from CVE ignores), including **[AR-006](./accepted-risks.md#ar-006--ci--local-trivy-unfixed-vulnerabilities-not-gated)** (unfixed vulnerabilities are not pipeline blockers).
+See **[Accepted risks](./accepted-risks.md)** for deliberate product-level deviations (separate from CVE ignores), including **[AR-005](./accepted-risks.md#ar-005---ci--local-trivy-unfixed-vulnerabilities-not-gated)** (unfixed vulnerabilities are not pipeline blockers).
## Local reproduction
diff --git a/docs/agenstra/security/compliance-and-standards.md b/docs/agenstra/security/compliance-and-standards.md
index 3b59bef4c..173d24f7d 100644
--- a/docs/agenstra/security/compliance-and-standards.md
+++ b/docs/agenstra/security/compliance-and-standards.md
@@ -49,10 +49,9 @@ Formal IT-Grundschutz certification or **ISMS** certification requires **organiz
Understanding where data and credentials move supports both CRA-style technical documentation and organizational risk analysis:
1. **Browser / Electron** to **Express frontend** to **backend APIs** (`/api`).
-2. **Browser** to **Agent Controller WebSocket** to **remote agent-manager WebSocket** (`/agents`) using **client-stored** credentials toward the remote host (not the end-user’s controller JWT merged into HTTP proxy headers for those paths).
+2. **Browser** to **Agent Controller WebSocket** to **remote agent-manager WebSocket** (`/agents`) using **client-stored** credentials toward the remote host (not the end-user's controller JWT merged into HTTP proxy headers for those paths).
3. **Controller** to **customer `client.endpoint`** (SSRF and misconfiguration risk; mitigated by allowlists, TLS policy, DNS checks).
-4. **Provisioning** to **cloud APIs and SSH** to new hosts (see **AR-001** in **[Accepted risks](./accepted-risks.md)**).
-5. **Agent Manager** to **Docker / containers** (execution and file operations; non-root `agenstra` user, bind mounts under `/opt/agents`, restricted `sudo`).
+4. **Agent Manager** to **Docker / containers** (execution and file operations; non-root `agenstra` user, bind mounts under `/opt/agents`, restricted `sudo`).
Detail: **[Container image security](./container-images.md)**, **[Operational hardening](./operational-hardening.md)**.
diff --git a/docs/agenstra/security/container-images.md b/docs/agenstra/security/container-images.md
index 43b979a46..c8b03e8fd 100644
--- a/docs/agenstra/security/container-images.md
+++ b/docs/agenstra/security/container-images.md
@@ -6,10 +6,10 @@ For image build targets and registry names, see **[Backend Agent Manager](../app
## Runtime users
-| Image family | User | Default UID/GID | Notes |
-| --------------------------------------------------------------------------------------------- | ---------- | --------------- | --------------------------------------- |
-| Manager/controller **API**, **worker**, **VNC**, **SSH**, **agi** (OpenClaw), billing **API** | `agenstra` | **10001** | `ARG APP_UID` / `APP_GID` at build time |
-| Frontend **server** images (agent console, billing console, portal, docs) | `node` | **1000** | Alpine-based SSR images |
+| Image family | User | Default UID/GID | Notes |
+| ---------------------------------------------------------------------------- | ---------- | --------------- | --------------------------------------- |
+| Manager/controller **API**, **worker**, **VNC**, **SSH**, **agi** (OpenClaw) | `agenstra` | **10001** | `ARG APP_UID` / `APP_GID` at build time |
+| Frontend **server** images (agent console, portal, docs) | `node` | **1000** | Alpine-based SSR images |
Processes do **not** run as root after container start. The optional SSH image still starts **`sshd`** via a single allowed `sudo` invocation in the entrypoint.
diff --git a/docs/agenstra/security/operational-hardening.md b/docs/agenstra/security/operational-hardening.md
index 538e22c65..a03569902 100644
--- a/docs/agenstra/security/operational-hardening.md
+++ b/docs/agenstra/security/operational-hardening.md
@@ -30,30 +30,18 @@ Resolution is implemented in **`getAuthenticationMethod`** (`libs/domains/identi
**`api-key`** without **`STATIC_API_KEY`** fails at runtime with an error. **Keycloak** and **users** modes rely on their respective guards after **`HybridAuthGuard`**. Health endpoints **`/api/health`** and **`/health`** remain unauthenticated by design.
-**Operator note:** Set **`AUTHENTICATION_METHOD`** explicitly if your security policy requires unambiguous configuration. Implicit **keycloak** when neither an explicit mode nor **`STATIC_API_KEY`** is set is an **accepted risk**; see **AR-004** in **[Accepted risks](./accepted-risks.md)**.
+**Operator note:** Set **`AUTHENTICATION_METHOD`** explicitly if your security policy requires unambiguous configuration. Implicit **keycloak** when neither an explicit mode nor **`STATIC_API_KEY`** is set is an **accepted risk**. See **AR-003** in **[Accepted risks](./accepted-risks.md)**.
-## Billing manager — multi-tenancy
-
-| Control | Purpose |
-| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------ |
-| **`X-Tenant` header** | Selects tenant context on HTTP and billing WebSocket handshakes. Validated against **`TENANTS`**; unknown ids → **400**. |
-| **`TenantUserGuard`** | Ensures authenticated users’ **`tenant_id`** matches the request tenant. |
-| **`STATIC_API_KEY_TENANT_ID`** | Optional bind of API key auth to one tenant. |
-
-**Accepted risk [AR-007](./accepted-risks.md#ar-007--billing-multi-tenant-api-key-scope-static_api_key_tenant_id-unset):** With **`STATIC_API_KEY`** and **without** **`STATIC_API_KEY_TENANT_ID`**, one deployment API key grants **admin access to every tenant** in **`TENANTS`** (tenant chosen per request via **`X-Tenant`**). This is **intentional** for a single shared automation key. Interactive **keycloak** / **users** sessions remain limited to the user’s tenant.
-
-Code: `libs/domains/decabill/backend/feature-billing-manager/src/lib/guards/tenant-user.guard.ts`, `libs/domains/shared/backend/util-http-context/src/lib/tenant-id.middleware.ts`.
-
-## Agent Controller — remote client endpoints (SSRF)
+## Agent Controller - remote client endpoints (SSRF)
Customer-configured **`client.endpoint`** values drive HTTP and WebSocket traffic from the controller to remote agent-managers.
-| Control | Purpose |
-| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
-| **`CLIENT_ENDPOINT_ALLOWED_HOSTS`** | Comma-separated hostname allowlist. The literal **`*`** explicitly allows **any host**. **Required in production** — the controller **exits** on startup if unset. |
-| **`CLIENT_ENDPOINT_ALLOW_INSECURE_HTTP`** | Set to `true` only if `http:` endpoints must be allowed (discouraged). |
-| **`CLIENT_ENDPOINT_TLS_REJECT_UNAUTHORIZED`** | Defaults to TLS verification on. **`false` is forbidden in production.** |
-| **`CLIENT_ENDPOINT_SKIP_DNS_CHECK`** | Skips DNS resolution defense (private/loopback rebinding). Use only in controlled test scenarios. |
+| Control | Purpose |
+| --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **`CLIENT_ENDPOINT_ALLOWED_HOSTS`** | Comma-separated hostname allowlist. The literal **`*`** explicitly allows **any host**. **Required in production**. The controller **exits** on startup if unset. |
+| **`CLIENT_ENDPOINT_ALLOW_INSECURE_HTTP`** | Set to `true` only if `http:` endpoints must be allowed (discouraged). |
+| **`CLIENT_ENDPOINT_TLS_REJECT_UNAUTHORIZED`** | Defaults to TLS verification on. **`false` is forbidden in production.** |
+| **`CLIENT_ENDPOINT_SKIP_DNS_CHECK`** | Skips DNS resolution defense (private/loopback rebinding). Use only in controlled test scenarios. |
Using **`*`** for **`CLIENT_ENDPOINT_ALLOWED_HOSTS`** intentionally **widens** the reachable host set. Prefer **narrow** hostnames when feasible; combine with egress controls and monitoring.
@@ -61,7 +49,7 @@ DNS validation resolves the endpoint hostname and rejects addresses in private/l
Code: `libs/domains/agenstra/backend/feature-agent-controller/src/lib/utils/client-endpoint-security.ts`.
-## HTTP proxy to remote agent-manager — headers
+## HTTP proxy to remote agent-manager - headers
Outbound proxied HTTP requests **drop** caller-supplied credential-like headers (`Authorization`, cookies, `x-api-key`, and similar) and attach only the **service-computed** `Authorization` for the **client entity** (stored API key or token). This avoids forwarding the **portal user’s** JWT on HTTP proxy paths.
@@ -89,14 +77,14 @@ When **`CONFIG`** points to a remote JSON URL, Express servers validate fetches
**`CONFIG_ALLOWED_HOSTS`** supports **`*`** to explicitly allow **any host**. That choice increases risk if **`CONFIG`** points to an attacker-controlled origin; prefer explicit host allowlists in production.
-See **[Environment configuration — Frontend (all `frontend-*` apps)](../deployment/environment-configuration.md)** for variable names.
+See **[Environment configuration - Frontend (all `frontend-*` apps)](../deployment/environment-configuration.md)** for variable names.
## Content Security Policy (frontend Express)
- CSP includes **`'unsafe-inline'`** and **`'unsafe-eval'`** for Monaco and tooling; default delivery is **`Content-Security-Policy-Report-Only`**.
- Set **`CSP_ENFORCE=true`** only after verifying the application still works.
-Accepted risk: **AR-003** in **[Accepted risks](./accepted-risks.md)**.
+Accepted risk: **AR-002** in **[Accepted risks](./accepted-risks.md)**.
## WebSocket CORS (Agent Controller)
@@ -107,17 +95,17 @@ Accepted risk: **AR-003** in **[Accepted risks](./accepted-risks.md)**.
Browser-originated **state-changing** requests can be restricted by origin allowlist middleware on backends (see `origin-allowlist.middleware.ts` in identity util-auth). Configure per deployment expectations.
-## Electron shell — new windows
+## Electron shell - new windows
-**`native-agent-console`** may open new windows for `window.open` / `target=_blank` with **allow** semantics. See **AR-005** in **[Accepted risks](./accepted-risks.md)**.
+**`native-agent-console`** may open new windows for `window.open` / `target=_blank` with **allow** semantics. See **AR-004** in **[Accepted risks](./accepted-risks.md)**.
## Related documentation
-- **[Accepted risks](./accepted-risks.md)** — AR-001 through AR-005
+- **[Accepted risks](./accepted-risks.md)** - AR-001 through AR-005
- **[Environment configuration](../deployment/environment-configuration.md)**
- **[Production checklist](../deployment/production-checklist.md)**
-- **[Backend Agent Controller application](../applications/backend-agent-controller.md)** — WebSocket and ports
-- **[Vulnerability reporting and artifacts](./vulnerability-reporting-and-artifacts.md)** — Disclosure and response commitments
+- **[Backend Agent Controller application](../applications/backend-agent-controller.md)** - WebSocket and ports
+- **[Vulnerability reporting and artifacts](./vulnerability-reporting-and-artifacts.md)** - Disclosure and response commitments
---
diff --git a/docs/agenstra/security/vulnerability-reporting-and-artifacts.md b/docs/agenstra/security/vulnerability-reporting-and-artifacts.md
index 8d8640199..68d5acf2f 100644
--- a/docs/agenstra/security/vulnerability-reporting-and-artifacts.md
+++ b/docs/agenstra/security/vulnerability-reporting-and-artifacts.md
@@ -76,30 +76,27 @@ See **[CI security scanning (Trivy)](./ci-security-scanning.md)** for workflows,
## Software Bill of Materials (SBOM)
-CycloneDX SBOM files are published for each release. **Agenstra** and **Decabill** each publish to a dedicated object-store bucket; the object key layout is the same for both domains.
+CycloneDX SBOM files are published for each release to the **Agenstra** object-store bucket.
| Field | Value |
| ---------------- | --------------------------- |
| **Path pattern** | `releases//sboms/` |
| **Example** | `releases/2.0.0/sboms/` |
-**Buckets and credentials (production `production` environment secrets):**
+**Bucket and credentials (production `production` environment secrets):**
Shared R2 credentials: `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_REGION`
| Domain | Product-specific secrets |
| -------- | -------------------------------------------------------- |
| Agenstra | `AGENSTRA_AWS_BUCKET`, `AGENSTRA_CLOUDFLARE_R2_ENDPOINT` |
-| Decabill | `DECABILL_AWS_BUCKET`, `DECABILL_CLOUDFLARE_R2_ENDPOINT` |
-**Decabill SBOM files** — `decabill-*.cdx.json`, `container-decabill-*.cdx.json`, and legacy `container-agenstra-billing-*.cdx.json` container SBOMs.
-
-**Agenstra SBOM files** — all other `*.cdx.json` files under the release SBOM set (agenstra services, shared MCP tools, and agenstra container images).
+**Agenstra SBOM files** include `agenstra-*.cdx.json`, `shared-*.cdx.json`, and `container-agenstra-*.cdx.json` container image SBOMs for images built in the release pipeline.
**Sources:**
-- **Service SBOMs** — Nx `sbom` target (`dist/sboms/.cdx.json`)
-- **Container image SBOMs** — Trivy CycloneDX (`dist/sboms/container-.cdx.json`) for images built in the release pipeline
+- **Service SBOMs** - Nx `sbom` target (`dist/sboms/.cdx.json`)
+- **Container image SBOMs** - Trivy CycloneDX (`dist/sboms/container-.cdx.json`) for images built in the release pipeline
Pull requests upload service and container SBOM files (separate CycloneDX files per image) as the **`sbom-artifacts`** workflow artifact. Releases upload each container image SBOM to Dependency-Track with `forepath/gh-upload-sbom@v2` (one upload per file, matrix job). Set the Dependency-Track **parent project UUID** for all container image projects in `.github/workflows/release.yml` (`prepare-container-image-sbom-uploads` → `env.CONTAINER_IMAGE_SBOM_PARENT`).
@@ -109,7 +106,7 @@ Resolve `` from your deployment or from [Downloads](https://downloads.a
## Desktop release integrity
-Published desktop installers and archives for **agenstra-native-agent-console** include **SHA-256 checksum manifests** so you can confirm files were not corrupted or swapped in transit. This supports **integrity verification** in depth; it does **not** by itself prove **publisher identity** (see **AR-002** in **[Accepted risks](./accepted-risks.md)**).
+Published desktop installers and archives for **agenstra-native-agent-console** include **SHA-256 checksum manifests** so you can confirm files were not corrupted or swapped in transit. This supports **integrity verification** in depth. It does **not** by itself prove **publisher identity** (see **AR-001** in **[Accepted risks](./accepted-risks.md)**).
### What is published
@@ -149,7 +146,7 @@ shasum -a 256 -c SHA256SUMS
## Related documentation
-- **[Accepted risks](./accepted-risks.md)** — AR-002 (desktop signing and update posture)
+- **[Accepted risks](./accepted-risks.md)** - AR-001 (desktop signing and update posture)
- **[Operational hardening](./operational-hardening.md)**
- **[Compliance and standards](./compliance-and-standards.md)**
- **[Deployment — Production checklist](../deployment/production-checklist.md)**
diff --git a/docs/decabill/README.md b/docs/decabill/README.md
new file mode 100644
index 000000000..61d3df9fb
--- /dev/null
+++ b/docs/decabill/README.md
@@ -0,0 +1,86 @@
+# Decabill Documentation
+
+Welcome to the documentation for **Decabill**, the ForePath billing product for subscriptions, invoicing, payment processing, and customer billing administration.
+
+## What is Decabill?
+
+Decabill lets operators and customers manage billing in one place:
+
+- **Subscriptions and service plans** with configurable providers and pricing
+- **Invoicing and payments** including Stripe checkout flows
+- **Customer self-service** for profiles, invoices, and subscription lifecycle
+- **Administration** for manual invoices, customer billing profiles, and operational dashboards
+- **Multi-tenant deployments** with tenant-scoped data and configurable frontends
+- **Server provisioning** for bundled product stacks via cloud-init when service plans include infrastructure
+
+## Documentation Structure
+
+### [Getting Started](./getting-started.md)
+
+Prerequisites, installation, and your first login to the billing console.
+
+### [Architecture](./architecture/README.md)
+
+- [System Overview](./architecture/system-overview.md)
+- [Components](./architecture/components.md)
+- [Data Flow](./architecture/data-flow.md)
+
+### [Applications](./applications/README.md)
+
+- [Frontend Billing Console](./applications/frontend-billing-console.md)
+- [Backend Billing Manager](./applications/backend-billing-manager.md)
+
+### [Features](./features/README.md)
+
+Product capabilities including subscriptions, invoices, administration, multi-tenancy, payments, and real-time dashboard status.
+
+### [Deployment](./deployment/README.md)
+
+- [Local Development](./deployment/local-development.md)
+- [Docker Deployment](./deployment/docker-deployment.md)
+- [Environment Configuration](./deployment/environment-configuration.md)
+- [Production Checklist](./deployment/production-checklist.md)
+- [Background Jobs](./deployment/background-jobs.md)
+
+### [Security](./security/README.md)
+
+Compliance-oriented transparency, accepted-risk register, vulnerability reporting, SBOM artifacts, and CI scanning.
+
+### [API Reference](./api-reference/README.md)
+
+Billing Manager HTTP OpenAPI and WebSocket AsyncAPI specifications.
+
+### [Troubleshooting](./troubleshooting/README.md)
+
+- [Common Issues](./troubleshooting/common-issues.md)
+- [Debugging Guide](./troubleshooting/debugging-guide.md)
+
+## Quick Start
+
+New to Decabill? Follow this path:
+
+1. **[Getting Started](./getting-started.md)** for local setup
+2. **[System Overview](./architecture/system-overview.md)** for architecture
+3. **[Multi-tenancy](./features/multi-tenancy.md)** if you run more than one tenant
+4. **[Environment Configuration](./deployment/environment-configuration.md)** before production
+
+## System Architecture
+
+Decabill follows a two-tier architecture:
+
+```mermaid
+graph TB
+ FE["Frontend Billing Console
(Customer + Admin UI)
Angular SSR + Express"]
+ BM["Backend Billing Manager
(API, jobs, provisioning)
NestJS + PostgreSQL + Redis"]
+ FE -->|"HTTP REST + WebSocket"| BM
+```
+
+## External Resources
+
+- [NestJS Documentation](https://docs.nestjs.com/)
+- [Angular Documentation](https://angular.dev/)
+- [Stripe Documentation](https://stripe.com/docs)
+
+---
+
+_For repository-wide security contact and supported versions, see the root `SECURITY.md` file in the GitHub repository._
diff --git a/docs/decabill/api-reference/README.md b/docs/decabill/api-reference/README.md
new file mode 100644
index 000000000..e63570382
--- /dev/null
+++ b/docs/decabill/api-reference/README.md
@@ -0,0 +1,117 @@
+# API Reference
+
+Complete API specifications for the Decabill billing manager. Specifications are published as OpenAPI 3.1.0 (HTTP REST) and AsyncAPI 3.0.0 (dashboard WebSocket gateway).
+
+## Billing Manager HTTP API
+
+The billing manager exposes all billing, subscription, invoice, catalog, and admin operations over HTTP on port **3200** with global prefix **`/api`**.
+
+### OpenAPI Specification
+
+**Specification file**: [openapi.yaml](/spec/billing-manager/openapi.yaml)
+
+- **View in Swagger Editor**: [Open in Swagger Editor](https://editor.swagger.io/?url=https://docs.decabill.com/spec/billing-manager/openapi.yaml)
+- **Download**: [openapi.yaml](/spec/billing-manager/openapi.yaml)
+
+Canonical source in the monorepo: `libs/domains/decabill/backend/feature-billing-manager/spec/openapi.yaml`
+
+The HTTP API includes:
+
+- **Public offerings** - Unauthenticated plan listings for marketing pages
+- **Service catalog** - Service types and service plans (admin)
+- **Subscriptions and backorders** - Order, cancel, resume, retry, and availability
+- **Customer profile** - Self-service billing metadata
+- **Invoices and open positions** - Issue, preview, download, void, pay, and billing-day accumulation
+- **Admin billing** - Manual invoices, customer profiles, statistics, audit logs, bill-now
+- **Authentication and users** - Login, register, and user management when `AUTHENTICATION_METHOD=users`
+- **Stripe webhook** - Signed payment event handling
+- **Configuration** - `GET /config` for operator-visible settings
+
+### Authentication
+
+Unless documented as public, operations require authentication:
+
+- **Bearer JWT** when using built-in users
+- **Bearer Keycloak access token** when using Keycloak
+- **Bearer or ApiKey static key** when using `AUTHENTICATION_METHOD=api-key`
+
+Send **`X-Tenant`** on every request for multi-tenant deployments. Invalid tenant ids return **400**.
+
+See **[Authentication](../features/authentication.md)**.
+
+### Admin manual invoices and customer profiles
+
+Admin CRUD for manual invoices and customer billing profiles is documented in the OpenAPI **`/admin/billing/*`** paths.
+
+Product-oriented guide: **[Billing Administration](../features/billing-administration.md)**
+
+## Billing Manager WebSocket Gateway
+
+The billing manager runs a Socket.IO server on port **8082** (default), namespace **`billing`**, separate from the HTTP listener.
+
+Static API key authentication is **not** sufficient for the dashboard stream. Connections require an end-user JWT or Keycloak identity, matching REST billing rules.
+
+### AsyncAPI Specification
+
+**Specification file**: [asyncapi.yaml](/spec/billing-manager/asyncapi.yaml)
+
+- **View in AsyncAPI Studio**: [Open in AsyncAPI Studio](https://studio.asyncapi.com/?url=https://docs.decabill.com/spec/billing-manager/asyncapi.yaml)
+- **Download**: [asyncapi.yaml](/spec/billing-manager/asyncapi.yaml)
+
+Canonical source in the monorepo: `libs/domains/decabill/backend/feature-billing-manager/spec/asyncapi.yaml`
+
+The status gateway provides:
+
+| Direction | Event | Description |
+| ---------------- | ---------------------------- | ------------------------------------------------------------------ |
+| Client to server | `subscribeDashboardStatus` | Start polling provisioned server status for the authenticated user |
+| Client to server | `unsubscribeDashboardStatus` | Stop polling for this socket |
+| Server to client | `dashboardStatusUpdate` | Periodic status payload (same shape as REST server-info) |
+| Server to client | `error` | Application errors scoped to the initiating socket |
+
+Pass **`X-Tenant`** in handshake metadata (`auth.tenantId` in browser clients, `extraHeaders` in Node clients).
+
+See **[Real-time Status](../features/real-time-status.md)**.
+
+## Using the Specifications
+
+### Swagger Editor
+
+[Swagger Editor](https://editor.swagger.io/) helps you:
+
+- Browse endpoints and schemas interactively
+- Validate request and response models
+- Export client stubs or documentation
+
+Load the spec via the docs.decabill.com URL above or the local `/spec/billing-manager/openapi.yaml` path served by the docs site build.
+
+### AsyncAPI Studio
+
+[AsyncAPI Studio](https://studio.asyncapi.com/) helps you:
+
+- Visualize dashboard socket message flows
+- Inspect payload schemas for `dashboardStatusUpdate`
+- Validate the AsyncAPI document
+
+## Generated Client Package
+
+The repository generates a TypeScript Axios client from the OpenAPI spec:
+
+```bash
+nx run decabill-backend-billing-manager:generate-client
+```
+
+Published npm package (GitHub Packages): **`@forepath/decabill-billing-manager-client`**
+
+Configure `@forepath` scope in `.npmrc` to install from GitHub Packages. Clients are regenerated on release to stay aligned with the spec.
+
+## Related Documentation
+
+- **[Backend Billing Manager](../applications/backend-billing-manager.md)** - Ports, queue roles, and deployment
+- **[Frontend Billing Console](../applications/frontend-billing-console.md)** - How the UI calls the API
+- **[Architecture Data Flow](../architecture/data-flow.md)** - HTTP, WebSocket, Stripe, and provisioning sequences
+- **[Features Overview](../features/README.md)** - Product capability index
+
+---
+
+_For operational deployment variables, see **[Environment Configuration](../deployment/environment-configuration.md)**._
diff --git a/docs/decabill/applications/README.md b/docs/decabill/applications/README.md
new file mode 100644
index 000000000..198b50910
--- /dev/null
+++ b/docs/decabill/applications/README.md
@@ -0,0 +1,107 @@
+# Applications Documentation
+
+This section provides detailed documentation for each application in the Decabill product.
+
+## Overview
+
+Decabill consists of two primary applications:
+
+1. **Frontend Billing Console** - Angular SSR customer and admin UI
+2. **Backend Billing Manager** - NestJS API, WebSocket gateway, background jobs, and integrations
+
+Both applications live under `apps/decabill/` and delegate feature logic to domain libraries in `libs/domains/decabill/`.
+
+## Applications
+
+### [Frontend Billing Console](./frontend-billing-console.md)
+
+Web application for subscription self-service, invoicing, payments, and billing administration.
+
+**Key features**:
+
+- Localized Angular UI with Express SSR in production
+- Dashboard with live server status (WebSocket) and power actions
+- Customer routes for plans, invoices, and profile
+- Admin routes for service catalog, manual billing, and customer profiles
+- Identity integration (login, register, user management)
+
+**Default port**: **4500**
+
+**Docker image**: `ghcr.io/forepath/decabill-billing-console-server:latest`
+
+### [Backend Billing Manager](./backend-billing-manager.md)
+
+Backend service for all billing business logic, persistence, and async processing.
+
+**Key features**:
+
+- HTTP REST API (OpenAPI)
+- Socket.IO dashboard status gateway (AsyncAPI)
+- PostgreSQL with automatic migrations on API startup
+- BullMQ schedulers and workers (split roles in compose)
+- Stripe checkout and webhooks
+- Cloud provisioning and backorder retry
+
+**Default ports**: HTTP **3200**, WebSocket **8082**
+
+**Docker image**: `ghcr.io/forepath/decabill-billing-api:latest`
+
+## Application Relationships
+
+```mermaid
+graph TB
+ FE[Frontend Billing Console]
+ BM[Backend Billing Manager]
+ PG[(PostgreSQL)]
+ RD[(Redis)]
+
+ FE -->|HTTP /api| BM
+ FE -->|WS /billing| BM
+ BM --> PG
+ BM --> RD
+```
+
+The console never talks to Stripe or cloud providers directly. All privileged operations flow through the billing manager.
+
+## Communication Patterns
+
+| Channel | Direction | Purpose |
+| --------------- | ------------------- | ------------------------------------------- |
+| HTTP REST | Console to Manager | CRUD, checkout initiation, admin operations |
+| WebSocket | Console to Manager | Dashboard server status stream |
+| BullMQ | Internal to Manager | Schedulers, workers, repeatable jobs |
+| Stripe webhooks | Stripe to Manager | Payment completion events |
+| Provider APIs | Manager to cloud | Availability, provisioning, DNS |
+
+## Build and Run Commands
+
+From the repository root:
+
+```bash
+# Backend
+nx serve decabill-backend-billing-manager
+nx build decabill-backend-billing-manager
+nx run decabill-backend-billing-manager:api-container-image
+
+# Frontend
+nx serve decabill-frontend-billing-console
+nx build decabill-frontend-billing-console
+nx run decabill-frontend-billing-console:container-image
+```
+
+Docker Compose entry points:
+
+- `apps/decabill/backend-billing-manager/docker-compose.yaml`
+- `apps/decabill/frontend-billing-console/docker-compose.yaml`
+
+## Related Documentation
+
+- **[Getting Started](../getting-started.md)** - First-time setup
+- **[Architecture Overview](../architecture/system-overview.md)** - System design
+- **[Deployment Guide](../deployment/README.md)** - Local and container deployment
+- **[Features Documentation](../features/README.md)** - Product capabilities
+- **[API Reference](../api-reference/README.md)** - Specifications
+
+---
+
+_For library-level implementation details, see the README files under `libs/domains/decabill/`._
diff --git a/docs/decabill/applications/backend-billing-manager.md b/docs/decabill/applications/backend-billing-manager.md
new file mode 100644
index 000000000..ddb331062
--- /dev/null
+++ b/docs/decabill/applications/backend-billing-manager.md
@@ -0,0 +1,192 @@
+# Backend Billing Manager
+
+NestJS backend application for subscription billing, invoicing, payments, provisioning, and background processing.
+
+## Purpose
+
+The billing manager is the authoritative service for Decabill. It exposes HTTP and WebSocket interfaces, persists data in PostgreSQL, enqueues work on Redis-backed BullMQ queues, integrates with Stripe, and optionally provisions cloud infrastructure when service plans require it.
+
+The app module in `apps/decabill/backend-billing-manager` bootstraps shared queue role logic, runs migrations when acting as API, and imports the domain **`BillingManagerModule`** from `@forepath/decabill/backend`.
+
+## Features
+
+This application provides:
+
+- **HTTP REST API** - Subscriptions, invoices, catalog, customer profile, admin billing, public offerings
+- **WebSocket gateway** - Dashboard server status on namespace **`billing`**
+- **Background jobs** - Billing cycles, expiration, reminders, overdue handling, backorder retry, SSH stack updates
+- **Stripe integration** - Checkout sessions and signed webhooks
+- **Invoice PDFs** - ZUGFeRD-style generation and filesystem storage
+- **Multi-tenancy** - Tenant allowlist and row-level isolation
+- **Authentication** - API key, Keycloak, or built-in users (JWT)
+- **Dynamic plugins** - Optional payment processors and billing UI metadata
+- **Email** - Invoice and reminder delivery via SMTP
+- **Rate limiting and CORS** - Production-safe HTTP defaults
+
+## Architecture
+
+Built with:
+
+- **NestJS** - Modules, controllers, guards, and gateways
+- **TypeORM** - Entities and migrations (app migrations plus identity migrations)
+- **BullMQ** - Queue name **`billing`** with coordinator and unit jobs
+- **Socket.IO** - Separate WebSocket listener on `WEBSOCKET_PORT`
+- **PostgreSQL** - Primary datastore
+- **Redis** - BullMQ connection
+
+Domain logic, OpenAPI source, and AsyncAPI source live in `libs/domains/decabill/backend/feature-billing-manager`.
+
+## Ports and Network Surfaces
+
+| Variable | Default | Description |
+| --------------------- | --------- | ------------------------------- |
+| `PORT` | **3200** | HTTP API (global prefix `/api`) |
+| `WEBSOCKET_PORT` | **8082** | Socket.IO server |
+| `WEBSOCKET_NAMESPACE` | `billing` | Dashboard status namespace |
+| `HOST` | `0.0.0.0` | Bind address |
+
+Health and monitoring endpoints are provided through shared backend utilities where enabled.
+
+## Queue Roles
+
+The same container image runs different responsibilities based on `QUEUE_ROLE`:
+
+### `api`
+
+- Serves HTTP REST and WebSocket
+- Runs TypeORM migrations on startup
+- May expose Bull Board at `QUEUE_BULL_BOARD_PATH` (default `/admin/queues`)
+- Does not process BullMQ unit jobs or register repeatable coordinators
+
+### `worker`
+
+- Consumes BullMQ jobs from the **`billing`** queue
+- Processes unit jobs enqueued by coordinators (billing, expiration, invoices, reminders, backorder retry, subscription item update, admin bill-now)
+- Respects `QUEUE_WORKER_CONCURRENCY` (default **5**)
+
+### `scheduler`
+
+- On startup, registers repeatable **coordinator** jobs (subscription billing, expiration, invoice overdue, open-position invoice, renewal reminder, subscription item update, backorder retry)
+- Does not serve HTTP or execute unit work directly
+
+### `all`
+
+- Combines API, scheduler, and worker for local development
+- Enables Bull Board by default when not explicitly disabled
+
+Docker Compose runs three containers (`backend-billing-manager`, `backend-billing-manager-worker`, `backend-billing-manager-scheduler`) sharing **`decabill-billing-api`**.
+
+See **[Background Jobs](../deployment/background-jobs.md)** for job names and intervals.
+
+## Docker Image
+
+**Image**: `ghcr.io/forepath/decabill-billing-api:latest`
+
+**Dockerfile**: `apps/decabill/backend-billing-manager/Dockerfile.api`
+
+Build locally:
+
+```bash
+nx run decabill-backend-billing-manager:api-container-image
+```
+
+Start the full stack:
+
+```bash
+cd apps/decabill/backend-billing-manager
+docker compose up -d
+```
+
+Compose services: `postgres`, `redis`, `backend-billing-manager`, `backend-billing-manager-worker`, `backend-billing-manager-scheduler`, `mailhog`.
+
+Volumes include `invoice_pdf_data` mounted at `/data/invoices` and optional `./provider-plugins` for dynamic providers.
+
+## Authentication
+
+Configure one method via `AUTHENTICATION_METHOD`:
+
+| Method | Key variables | Console pairing |
+| ---------- | ------------------------------ | ---------------------------------------------- |
+| `api-key` | `STATIC_API_KEY` | Automation; no dashboard WebSocket user stream |
+| `keycloak` | `KEYCLOAK_*` | OAuth login in console |
+| `users` | `JWT_SECRET`, `DISABLE_SIGNUP` | Built-in register and login |
+
+Optional `STATIC_API_KEY_TENANT_ID` binds API key auth to one tenant.
+
+See **[Authentication](../features/authentication.md)**.
+
+## Major API Areas
+
+Full paths and schemas are in **[API Reference](../api-reference/README.md)** and `/spec/billing-manager/openapi.yaml`.
+
+| Area | Example paths | Notes |
+| ---------------- | ------------------------------------ | ----------------------------------------------- |
+| Public offerings | `GET /public/service-plan-offerings` | Unauthenticated marketing data |
+| Catalog | `/service-types`, `/service-plans` | Admin CRUD |
+| Subscriptions | `/subscriptions`, `/backorders` | Order, cancel, resume |
+| Invoices | `/invoices`, open positions | PDF download, void, pay |
+| Customer | `/customer-profile` | Required before ordering |
+| Admin billing | `/admin/billing/*` | Manual invoices, profiles, statistics, bill-now |
+| Payments | `/invoices/{id}/pay`, Stripe webhook | Checkout redirect |
+| Availability | `/availability/check` | Provider capacity |
+
+Send `X-Tenant` on every request when using multi-tenancy.
+
+## WebSocket Gateway
+
+AsyncAPI documents the **`billing`** namespace:
+
+- Client: `subscribeDashboardStatus`, `unsubscribeDashboardStatus`
+- Server: `dashboardStatusUpdate`, `error`
+
+Requires the same user JWT or Keycloak session as interactive REST calls. Connect to `http://:8082/billing` with authorization in the handshake.
+
+Spec: `/spec/billing-manager/asyncapi.yaml`
+
+## Stripe and Provisioning
+
+- **Stripe** - `STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET`, checkout return URLs, `BILLING_DEFAULT_PAYMENT_PROCESSOR`
+- **Hetzner / DigitalOcean** - `HETZNER_API_TOKEN`, `DIGITALOCEAN_API_TOKEN`
+- **DNS** - Optional Cloudflare integration (`CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ZONE_ID`, `DNS_BASE_DOMAIN`)
+
+See **[Payment Processing](../features/payment-processing.md)** and **[Server Provisioning](../features/server-provisioning.md)**.
+
+## Development Commands
+
+```bash
+# Run locally (set QUEUE_ROLE=all in .env)
+nx serve decabill-backend-billing-manager
+
+# Build
+nx build decabill-backend-billing-manager
+
+# Tests
+nx test decabill-backend-billing-manager
+
+# Generate TypeScript client from OpenAPI
+nx run decabill-backend-billing-manager:generate-client
+```
+
+Published client package: `@forepath/decabill-billing-manager-client` (generated from the same OpenAPI spec).
+
+## Bull Board (Local)
+
+When enabled on an API or `all` process:
+
+- URL: `http://localhost:3200/admin/queues`
+- Default credentials: `admin` / `bullmq` (override with `QUEUE_BULL_BOARD_USERNAME` and `QUEUE_BULL_BOARD_PASSWORD`)
+
+Disable in production unless tightly access-controlled.
+
+## Related Documentation
+
+- **[Frontend Billing Console](./frontend-billing-console.md)** - UI routes and SSR
+- **[Architecture Components](../architecture/components.md)** - Infrastructure dependencies
+- **[Data Flow](../architecture/data-flow.md)** - Sequence diagrams
+- **[Environment Configuration](../deployment/environment-configuration.md)** - Complete env list
+- **[Billing Administration](../features/billing-administration.md)** - Admin features
+- **[API Reference](../api-reference/README.md)** - OpenAPI and AsyncAPI
+
+---
+
+_Canonical OpenAPI source: `libs/domains/decabill/backend/feature-billing-manager/spec/openapi.yaml`_
diff --git a/docs/decabill/applications/frontend-billing-console.md b/docs/decabill/applications/frontend-billing-console.md
new file mode 100644
index 000000000..92760d075
--- /dev/null
+++ b/docs/decabill/applications/frontend-billing-console.md
@@ -0,0 +1,181 @@
+# Frontend Billing Console
+
+Angular web application with localized builds and an Express SSR server for customer self-service and billing administration.
+
+## Purpose
+
+The billing console is the primary user interface for Decabill. It connects to the billing manager REST API and dashboard WebSocket gateway. It does not embed business rules for invoicing or provisioning; those remain server-side.
+
+Customers use it to manage subscriptions, pay invoices, and view provisioned server status. Administrators manage service types, service plans, manual invoices, customer billing profiles, and users.
+
+## Features
+
+This application provides:
+
+- **Overview dashboard** - Subscription cards, server status, start/stop/restart actions
+- **Plans** - Browse and order service plans, manage subscription lifecycle
+- **Invoices** - List, detail, download, and Stripe checkout redirect
+- **Customer profile** - Billing metadata required before ordering
+- **Administration** - Service types, service plans, billing KPIs, manual invoices, billing profiles (admin only)
+- **Identity UI** - Login, registration, password reset, email confirmation, user management
+- **Real-time status** - Socket.IO subscription on the overview page
+- **Localization** - English and German builds with locale-prefixed SSR paths
+- **Runtime configuration** - Express `/config` endpoint for deployed environments
+
+## Architecture
+
+Built with:
+
+- **Angular** - Components, routing, and i18n
+- **NgRx** - State, effects, and facades from `@forepath/decabill/frontend/data-access-billing-console`
+- **Express** - SSR static file server (`src/server.ts`)
+- **Bootstrap 5** - Layout and components
+- **Socket.IO client** - Dashboard status namespace
+- **Identity bundle** - `@forepath/identity/frontend` for auth routes and guards
+
+Feature components live in `@forepath/decabill/frontend/feature-billing-console` and are wired through `billingConsoleRoutes`.
+
+## Routes Overview
+
+All routes render inside `BillingConsoleContainerComponent` unless noted. Paths below omit the optional locale prefix (`/en`, `/de`) that the Express server injects in SSR mode.
+
+### Default and customer routes
+
+| Path | Guard | Component | Description |
+| ---------------- | ----------- | ------------- | ---------------------------------------- |
+| `/` | none | redirect | Redirects to `dashboard` |
+| `/dashboard` | `authGuard` | Overview | Subscription overview and server control |
+| `/subscriptions` | `authGuard` | Subscriptions | Plans list and ordering |
+| `/invoices` | `authGuard` | Invoices | Invoice list, payment, and detail |
+
+Stripe return URLs typically land on `/invoices?payment=success` or `?payment=cancel`.
+
+### Identity routes (from `identityAuthRoutes`)
+
+| Path | Guard | Description |
+| -------------------------------------- | ----------------------------------- | ---------------------------------- |
+| `/login` | `loginGuard` | Email/password or Keycloak entry |
+| `/register` | `signupDisabledGuard`, `loginGuard` | Self-service registration |
+| `/request-password-reset` | `loginGuard` | Request reset code |
+| `/request-password-reset-confirmation` | `loginGuard` | Confirmation message |
+| `/reset-password` | `loginGuard` | Submit reset code and new password |
+| `/confirm-email` | `loginGuard` | Email confirmation code |
+| `/users` | `authGuard`, `adminGuard` | Admin user management |
+
+### Administration routes (`billingAdminGuard` plus `authGuard`)
+
+| Path | Component | Description |
+| ----------------------------------- | ------------------------- | ------------------------------------ |
+| `/administration/service-types` | ServiceTypesPage | Provider and service type catalog |
+| `/administration/service-plans` | ServicePlansPage | Priced plans and ordering highlights |
+| `/administration/billing` | AdminBillingPage | KPIs, bill-now, open/overdue lists |
+| `/administration/customer-profiles` | AdminCustomerProfilesPage | Customer billing profile CRUD |
+
+Unknown paths redirect to the shell root (`**` to ``).
+
+## Express SSR Server
+
+Production and Docker deployments run the compiled Express server from `src/server.ts`.
+
+### Responsibilities
+
+- Security headers via `createSecurityHeadersMiddleware()`
+- Runtime config registration via `registerRuntimeConfigEndpoint(app)` (`/config`)
+- Locale detection from URL prefix, `Accept-Language`, or `DEFAULT_LOCALE`
+- Static serving of per-locale Angular browser bundles
+- SPA fallback: non-file routes receive `index.html` for client routing
+- Monaco-related middleware for assets when bundled
+
+### Environment variables (server)
+
+| Variable | Default | Purpose |
+| ----------------------- | ---------------------------------------------------- | ------------------------------------------ |
+| `HOST` | `0.0.0.0` | Bind address |
+| `PORT` | `4200` in generic server; **4500** in Docker compose | Listen port |
+| `DEFAULT_LOCALE` | `en` | Fallback locale |
+| `CSP_ENFORCE` | `true` in compose | Content-Security-Policy enforcement |
+| `CSP_CONNECT_SRC_EXTRA` | API origin | Allow API and WebSocket in CSP connect-src |
+
+Local `nx serve` uses the Angular dev server on port **4500** without Express unless you build and run `serve-server`.
+
+## Configuration
+
+### Build-time environment
+
+Development builds replace `environment.ts` with `environment.decabill.ts`:
+
+```typescript
+billing: {
+ restApiUrl: 'http://localhost:3200/api',
+ frontendUrl: 'http://localhost:4500',
+ websocketUrl: 'http://localhost:8082/billing',
+ tenantId: 'decabill',
+},
+authentication: {
+ type: 'users',
+ disableSignup: false,
+},
+```
+
+Production uses `environment.decabill.production.ts`. Align `authentication.type` with backend `AUTHENTICATION_METHOD`.
+
+### Docker image
+
+**Image**: `ghcr.io/forepath/decabill-billing-console-server:latest`
+
+Build locally:
+
+```bash
+nx run decabill-frontend-billing-console:container-image
+```
+
+Compose file: `apps/decabill/frontend-billing-console/docker-compose.yaml`
+
+## NgRx State Slices
+
+The route providers register facades and reducers for:
+
+- `subscriptions`, `subscriptionServerInfo`, `servicePlans`, `serviceTypes`
+- `invoices`, `customerProfile`, `backorders`, `availability`
+- `adminBilling`, `adminInvoiceManager`, `adminCustomerProfiles`
+- `billingDashboardSocket` (WebSocket lifecycle and status payloads)
+
+Effects call the billing manager HTTP client and socket service; see `billing-console.routes.ts` for the full effect list.
+
+## Development Commands
+
+```bash
+# Dev server (port 4500)
+nx serve decabill-frontend-billing-console
+
+# Production build (localized)
+nx build decabill-frontend-billing-console
+
+# SSR server after build
+nx run decabill-frontend-billing-console:serve-server
+
+# Unit tests
+nx test decabill-frontend-billing-console
+```
+
+## Docker Compose
+
+```bash
+cd apps/decabill/frontend-billing-console
+docker compose up -d
+```
+
+Ensure `CSP_CONNECT_SRC_EXTRA` includes the browser-reachable billing manager origin (for example `http://host.docker.internal:3200` on Docker Desktop).
+
+## Related Documentation
+
+- **[Backend Billing Manager](./backend-billing-manager.md)** - API and WebSocket endpoints
+- **[Authentication](../features/authentication.md)** - Login methods
+- **[Dashboard and Server Control](../features/dashboard-and-server-control.md)** - Overview behavior
+- **[Real-time Status](../features/real-time-status.md)** - WebSocket events
+- **[Getting Started](../getting-started.md)** - Local setup
+- **[Docker Deployment](../deployment/docker-deployment.md)** - Container deployment
+
+---
+
+_For HTTP request schemas, see **[API Reference](../api-reference/README.md)**._
diff --git a/docs/decabill/architecture/README.md b/docs/decabill/architecture/README.md
new file mode 100644
index 000000000..59532d480
--- /dev/null
+++ b/docs/decabill/architecture/README.md
@@ -0,0 +1,138 @@
+# Architecture Documentation
+
+This section covers the architectural principles, patterns, and structural decisions that guide the Decabill billing product. Understanding these concepts helps you deploy, integrate, and operate subscriptions, invoicing, and optional infrastructure provisioning.
+
+## Overview
+
+Decabill follows a **two-tier architecture** that separates customer and admin UI from billing backend services:
+
+- **Frontend Billing Console** for self-service and administration
+- **Backend Billing Manager** for HTTP API, background jobs, payments, and provisioning
+
+The stack is built on:
+
+- **Domain-driven modules** in `@forepath/decabill/backend` and `@forepath/decabill/frontend`
+- **RESTful HTTP APIs** for synchronous billing operations
+- **Socket.IO** for dashboard server status streaming
+- **PostgreSQL** for persistent billing and identity data
+- **Redis and BullMQ** for schedulers, workers, and repeatable jobs
+- **Stripe** (and optional dynamic payment plugins) for checkout and webhooks
+
+## Documentation Structure
+
+### [System Overview](./system-overview.md)
+
+High-level architecture and component relationships:
+
+- Two-tier console and manager layout
+- Communication patterns between browser, API, and data stores
+- Visual architecture diagrams
+
+### [Components](./components.md)
+
+Detailed breakdown of runtime components:
+
+- Frontend billing console (Angular SSR and Express)
+- Backend billing manager (NestJS, queue roles, gateways)
+- PostgreSQL, Redis, Stripe, and external cloud providers
+
+### [Data Flow](./data-flow.md)
+
+Communication patterns and end-to-end flows:
+
+- HTTP REST for CRUD, checkout initiation, and admin operations
+- WebSocket dashboard status polling
+- Stripe redirect and webhook reconciliation
+- Subscription provisioning and backorder retry
+
+## Key Architectural Concepts
+
+### Two-Tier Architecture
+
+1. **Presentation tier** - Angular billing console with NgRx state, localized SSR, and Express static hosting
+2. **Application tier** - NestJS billing manager with TypeORM, BullMQ workers, and provider integrations
+
+There is no separate billing controller. The console talks directly to the billing manager API and WebSocket gateway.
+
+### Queue Process Roles
+
+The billing manager binary runs in one of four **QUEUE_ROLE** modes:
+
+| Role | HTTP API | Migrations | Repeatable schedulers | BullMQ workers | Bull Board |
+| ----------- | -------- | ---------- | --------------------- | -------------- | ---------------- |
+| `api` | Yes | Yes | No | No | Optional |
+| `worker` | No | No | No | Yes | No |
+| `scheduler` | No | No | Yes | No | No |
+| `all` | Yes | Yes | Yes | Yes | Optional (local) |
+
+Production Docker Compose splits `api`, `worker`, and `scheduler` into separate containers sharing one image (`decabill-billing-api`).
+
+### Authentication and Multi-Tenancy
+
+- HTTP and WebSocket accept JWT (users), Keycloak tokens, or static API key depending on `AUTHENTICATION_METHOD`
+- Optional `X-Tenant` header scopes data per tenant (`TENANTS` allowlist)
+- Dashboard WebSocket requires an end-user billing identity (API key alone is insufficient)
+
+See **[Authentication](../features/authentication.md)** and **[Multi-tenancy](../features/multi-tenancy.md)**.
+
+### State Management
+
+- **Frontend** - NgRx facades and effects for subscriptions, invoices, admin billing, and dashboard socket state
+- **Backend** - PostgreSQL as source of truth; Redis for job queues; in-memory socket subscription timers for dashboard polling
+
+## Related Documentation
+
+### Getting Started
+
+- **[Getting Started](../getting-started.md)** - Local setup and first login
+
+### Features
+
+- **[Subscriptions](../features/subscriptions.md)** - Order and lifecycle
+- **[Invoices](../features/invoices.md)** - Open positions, PDFs, and payment
+- **[Payment Processing](../features/payment-processing.md)** - Stripe checkout and webhooks
+- **[Server Provisioning](../features/server-provisioning.md)** - Cloud-init stacks for eligible plans
+- **[Real-time Status](../features/real-time-status.md)** - Dashboard WebSocket behavior
+
+### Deployment
+
+- **[Local Development](../deployment/local-development.md)** - Nx and compose setup
+- **[Docker Deployment](../deployment/docker-deployment.md)** - Container images and services
+- **[Background Jobs](../deployment/background-jobs.md)** - BullMQ coordinators and units
+- **[Environment Configuration](../deployment/environment-configuration.md)** - Full env reference
+
+### Applications
+
+- **[Frontend Billing Console](../applications/frontend-billing-console.md)**
+- **[Backend Billing Manager](../applications/backend-billing-manager.md)**
+
+## Architecture Principles
+
+### Scalability
+
+- Horizontally scale **worker** containers for job throughput
+- Run a single **scheduler** per Redis namespace to register repeatable coordinators
+- Scale **api** containers behind a load balancer; WebSocket port may require sticky sessions or a dedicated gateway
+
+### Maintainability
+
+- Feature logic lives in `libs/domains/decabill/backend/feature-billing-manager` and frontend libraries consumed by the apps
+- OpenAPI and AsyncAPI specs are the contract for HTTP and dashboard socket events
+
+### Security
+
+- Server-side validation on all billing inputs
+- Encryption at rest for sensitive subscription and backorder snapshots (`ENCRYPTION_KEY`)
+- Rate limiting and CORS on HTTP; CSP on the SSR console
+
+See **[Security](../security/README.md)** for compliance-oriented documentation.
+
+### Reliability
+
+- Coordinator and unit job pattern prevents duplicate heavy work across tenants
+- Backorder retry and subscription billing schedulers recover from transient provider failures
+- Stripe webhooks are handled idempotently with tenant metadata
+
+---
+
+_For API contracts, see **[API Reference](../api-reference/README.md)**._
diff --git a/docs/decabill/architecture/components.md b/docs/decabill/architecture/components.md
new file mode 100644
index 000000000..0433716d3
--- /dev/null
+++ b/docs/decabill/architecture/components.md
@@ -0,0 +1,191 @@
+# Components
+
+This document describes the major runtime components in Decabill, their responsibilities, dependencies, and default ports.
+
+## Backend Billing Manager
+
+**Location**: `apps/decabill/backend-billing-manager`
+
+**Purpose**: NestJS application hosting the billing HTTP API, dashboard WebSocket gateway, TypeORM persistence, BullMQ integration, Stripe payments, and cloud provisioning.
+
+**Docker image**: `ghcr.io/forepath/decabill-billing-api:latest`
+
+**Library implementation**: `libs/domains/decabill/backend/feature-billing-manager` (imported as `BillingManagerModule` and related providers)
+
+### Key responsibilities
+
+- REST controllers for subscriptions, invoices, service catalog, customer profile, admin billing, and public offerings
+- Socket.IO **billing** namespace for dashboard server status
+- TypeORM entities and migrations (billing tables plus identity migrations bundled at startup)
+- BullMQ queue **`billing`** with coordinator and unit jobs (see job registry in the app)
+- Stripe checkout session creation and webhook endpoint processing
+- Provider integrations (Hetzner Cloud, DigitalOcean) for availability checks and provisioning
+- Invoice PDF generation (ZUGFeRD-style HTML template) and filesystem storage
+- Optional dynamic payment processor and billing UI metadata plugins
+
+### Dependencies
+
+- PostgreSQL 16 (primary data store)
+- Redis 7 (BullMQ backing store)
+- SMTP or Mailhog (transactional email)
+- Stripe API (when payment processing is enabled)
+- Cloud provider API tokens (when plans include infrastructure)
+
+### Ports and endpoints
+
+| Surface | Default | Notes |
+| ---------- | --------------- | -------------------------------------------- |
+| HTTP API | **3200** | Global prefix `/api` |
+| WebSocket | **8082** | Namespace `/billing` (`WEBSOCKET_NAMESPACE`) |
+| Bull Board | `/admin/queues` | Optional on API or `all` role |
+
+### Queue roles (same image, different `QUEUE_ROLE`)
+
+- **`api`** - Serves HTTP and WebSocket; runs migrations; may expose Bull Board
+- **`worker`** - Consumes BullMQ unit jobs (billing, expiration, reminders, backorder retry, SSH updates, bill-now units)
+- **`scheduler`** - Registers repeatable coordinator jobs on startup
+- **`all`** - Combines all roles for local development
+
+**Documentation**: [Backend Billing Manager Application](../applications/backend-billing-manager.md)
+
+## Frontend Billing Console
+
+**Location**: `apps/decabill/frontend-billing-console`
+
+**Purpose**: Angular application with localized builds and an Express SSR server for production hosting.
+
+**Docker image**: `ghcr.io/forepath/decabill-billing-console-server:latest`
+
+**Feature library**: `libs/domains/decabill/frontend/feature-billing-console`
+
+**Data access**: `libs/domains/decabill/frontend/data-access-billing-console` (NgRx)
+
+### Key responsibilities
+
+- Routed UI for dashboard, subscriptions, invoices, and admin catalog or billing pages
+- Identity auth UI from `@forepath/identity/frontend` (login, register, users)
+- HTTP client to billing manager REST API with tenant and auth interceptors
+- Socket.IO client connecting to `WEBSOCKET_URL` (default `http://localhost:8082/billing`)
+- Cookie consent, Bootstrap layout, and ApexCharts where used in admin views
+
+### Dependencies
+
+- Billing manager HTTP and WebSocket endpoints (configured at build or runtime)
+- Identity configuration aligned with backend `AUTHENTICATION_METHOD`
+
+### Ports
+
+| Mode | Default | Notes |
+| -------------------- | -------- | ----------------------------------- |
+| `nx serve` | **4500** | Angular dev server |
+| Express SSR / Docker | **4500** | Serves `browser/{locale}` bundles |
+| `serve-static` | **4500** | File server for built SPA (non-SSR) |
+
+**Documentation**: [Frontend Billing Console Application](../applications/frontend-billing-console.md)
+
+## PostgreSQL
+
+**Role**: System of record for tenants, users (identity), subscriptions, subscription items, invoices, open positions, backorders, service catalog, customer billing profiles, and audit-oriented admin data.
+
+**Compose service**: `postgres` in `apps/decabill/backend-billing-manager/docker-compose.yaml`
+
+**Notable concerns**:
+
+- Migrations run when `QUEUE_ROLE` is `api` or `all`
+- Tenant scoping via `tenant_id` columns on billing entities
+- Encrypted columns for provider config snapshots and SSH private keys when `ENCRYPTION_KEY` is set
+
+## Redis
+
+**Role**: BullMQ connection, job metadata, and repeatable coordinator schedules.
+
+**Compose service**: `redis` with AOF persistence
+
+**Configuration**:
+
+- `REDIS_HOST`, `REDIS_PORT`, `REDIS_PASSWORD`, `REDIS_DB`
+- `REDIS_KEY_PREFIX` (default `decabill-billing`) isolates keys when sharing a Redis instance
+- Host port **6380** maps to container **6379** in the default compose file to avoid clashing with other stacks
+
+## Stripe
+
+**Role**: Default payment processor for invoice checkout.
+
+**Integration points**:
+
+- `STRIPE_SECRET_KEY` for server-side Checkout Session creation
+- `STRIPE_WEBHOOK_SECRET` for signed webhook verification
+- `STRIPE_CHECKOUT_SUCCESS_URL` and `STRIPE_CHECKOUT_CANCEL_URL` (overridable per tenant via `TENANT_FRONTEND_URLS`)
+- Customer ids stored on billing profiles after first payment
+
+**Alternatives**: Additional processors may load via `DYNAMIC_PAYMENT_PROCESSORS`. See **[Dynamic Provider Plugins](../features/dynamic-provider-plugins.md)**.
+
+## Mailhog (Local Only)
+
+**Role**: Captures outbound SMTP from the billing manager during local compose runs.
+
+**Ports** (default compose): SMTP **1026**, UI **8026**
+
+Replace with production SMTP settings (`SMTP_*`, `EMAIL_FROM`) in real deployments.
+
+## Cloud Providers
+
+Built-in provisioning providers:
+
+- **Hetzner Cloud** (`HETZNER_API_TOKEN`)
+- **DigitalOcean** (`DIGITALOCEAN_API_TOKEN`)
+
+Used for availability checks, server creation, DNS (Cloudflare optional), and subscription item server info snapshots. Provisioned stacks may include Docker Compose bundles deployed via cloud-init. See **[Server Provisioning](../features/server-provisioning.md)**.
+
+## Component Dependencies
+
+```mermaid
+graph TB
+ subgraph "Frontend"
+ APP[frontend-billing-console]
+ FEAT[feature-billing-console]
+ DATA[data-access-billing-console]
+ end
+
+ subgraph "Backend"
+ API[backend-billing-manager]
+ LIB[feature-billing-manager]
+ end
+
+ subgraph "Infrastructure"
+ PG[(PostgreSQL)]
+ RD[(Redis)]
+ ST[Stripe]
+ end
+
+ APP --> FEAT
+ APP --> DATA
+ FEAT --> DATA
+ API --> LIB
+ DATA -.->|HTTP + WS| API
+ API --> PG
+ API --> RD
+ API --> ST
+```
+
+## External Dependencies
+
+| Dependency | Purpose |
+| ---------------------------------- | -------------------------- |
+| [NestJS](https://docs.nestjs.com/) | Backend framework |
+| [Angular](https://angular.dev/) | Console UI |
+| [Socket.IO](https://socket.io/) | Dashboard status transport |
+| [BullMQ](https://docs.bullmq.io/) | Background jobs |
+| [Stripe](https://stripe.com/docs) | Payments |
+| [TypeORM](https://typeorm.io/) | Database access |
+
+## Related Documentation
+
+- **[System Overview](./system-overview.md)** - Architecture summary
+- **[Data Flow](./data-flow.md)** - Request and event flows
+- **[Background Jobs](../deployment/background-jobs.md)** - Queue job catalog
+- **[API Reference](../api-reference/README.md)** - HTTP and WebSocket contracts
+
+---
+
+_For deployment topology, see **[Docker Deployment](../deployment/docker-deployment.md)**._
diff --git a/docs/decabill/architecture/data-flow.md b/docs/decabill/architecture/data-flow.md
new file mode 100644
index 000000000..6868af413
--- /dev/null
+++ b/docs/decabill/architecture/data-flow.md
@@ -0,0 +1,256 @@
+# Data Flow
+
+This document describes communication patterns and end-to-end data flows across the Decabill billing console and billing manager.
+
+## HTTP REST API Flow
+
+### Authenticated read (example: list subscriptions)
+
+```mermaid
+sequenceDiagram
+ participant B as Browser
+ participant C as Billing Console
+ participant A as Billing Manager API
+ participant DB as PostgreSQL
+
+ B->>C: Navigate to /subscriptions
+ C->>A: GET /api/subscriptions (Bearer JWT, X-Tenant)
+ A->>A: Validate auth and tenant
+ A->>DB: Query subscriptions for user
+ DB-->>A: Rows
+ A-->>C: JSON array
+ C->>C: NgRx loadSubscriptionsSuccess
+ C-->>B: Render plans list
+```
+
+Admin routes under `/api/admin/billing/*` follow the same pattern with role checks. API key auth bypasses user identity but grants admin REST access when configured.
+
+### Subscription order with availability check
+
+```mermaid
+sequenceDiagram
+ participant U as User
+ participant C as Billing Console
+ participant A as Billing Manager API
+ participant P as Cloud Provider API
+ participant DB as PostgreSQL
+
+ U->>C: Select plan and submit order
+ C->>A: POST /api/availability/check
+ A->>P: Provider capacity query
+ P-->>A: Available / unavailable
+ A-->>C: Availability result
+ alt Available
+ C->>A: POST /api/subscriptions
+ A->>DB: Create subscription and items
+ A->>P: Provision server (if plan requires)
+ P-->>A: Server reference
+ A->>DB: Update item status and snapshot
+ A-->>C: Subscription created
+ else Unavailable with autoBackorder
+ C->>A: POST /api/subscriptions (autoBackorder)
+ A->>DB: Create backorder row
+ A-->>C: Backorder queued
+ end
+ C-->>U: Confirmation or backorder message
+```
+
+See **[Subscriptions](../features/subscriptions.md)** and **[Backorders](../features/backorders.md)**.
+
+## WebSocket Dashboard Flow
+
+The overview page connects to the billing namespace and subscribes to periodic server status for provisioned subscription items owned by the logged-in user.
+
+```mermaid
+sequenceDiagram
+ participant C as Billing Console
+ participant W as Status Gateway :8082/billing
+ participant S as Domain Services
+ participant DB as PostgreSQL
+ participant H as Provisioned Host
+
+ C->>W: Connect (auth handshake, X-Tenant)
+ W->>W: Validate JWT / Keycloak user
+ C->>W: subscribeDashboardStatus
+ loop Each poll interval
+ W->>S: Resolve user subscriptions
+ S->>DB: Load active items
+ S->>H: SSH or provider status (as configured)
+ H-->>S: Running / stopped / metrics
+ S-->>W: Server info DTOs
+ W-->>C: dashboardStatusUpdate (unicast)
+ end
+ C->>W: unsubscribeDashboardStatus (on leave)
+```
+
+Poll interval is clamped between 10s and 120s. Default server interval comes from `STATUS_POLL_INTERVAL` (15000 ms).
+
+REST fallback: `GET /api/subscriptions/.../server-info` when WebSocket is disabled or unavailable. See **[Dashboard and Server Control](../features/dashboard-and-server-control.md)**.
+
+## Stripe Redirect and Webhook Flow
+
+Checkout is initiated over HTTP. Payment state is finalized asynchronously via Stripe webhooks.
+
+```mermaid
+sequenceDiagram
+ participant U as User
+ participant C as Billing Console
+ participant A as Billing Manager API
+ participant ST as Stripe
+ participant DB as PostgreSQL
+
+ U->>C: Pay invoice
+ C->>A: POST /api/invoices/{id}/pay
+ A->>DB: Load invoice and profile
+ A->>ST: Create Checkout Session
+ ST-->>A: session.url
+ A-->>C: Redirect URL
+ C->>ST: Browser redirect to Checkout
+ U->>ST: Complete or cancel payment
+ ST->>U: Redirect to success or cancel URL
+ Note over ST,A: Async webhook
+ ST->>A: POST /api/stripe/webhook (signed)
+ A->>A: Verify signature and tenant metadata
+ A->>DB: Mark invoice paid / record failure
+ U->>C: Land on /invoices?payment=success
+ C->>A: GET /api/invoices (refresh)
+ A-->>C: Updated payment state
+```
+
+Tenant-specific return URLs resolve from `TENANT_FRONTEND_URLS` or `BILLING_FRONTEND_URL`. See **[Payment Processing](../features/payment-processing.md)** and **[Invoices](../features/invoices.md)**.
+
+## Open Position and Billing Day Flow
+
+Recurring charges accumulate as open positions until the user's billing day scheduler creates a consolidated invoice.
+
+```mermaid
+sequenceDiagram
+ participant SCH as Scheduler role
+ participant Q as BullMQ billing queue
+ participant W as Worker role
+ participant DB as PostgreSQL
+
+ SCH->>Q: Enqueue subscription-billing.coordinator (repeatable)
+ Q->>W: coordinator job
+ W->>DB: Find due subscription billing work
+ W->>Q: Enqueue subscription-billing.unit per item
+ Q->>W: unit jobs
+ W->>DB: Create open position rows
+
+ SCH->>Q: Enqueue open-position-invoice.coordinator
+ Q->>W: coordinator
+ W->>DB: Users whose billing day is today
+ W->>Q: open-position-invoice.unit per user
+ Q->>W: unit
+ W->>DB: Create invoice from open positions
+ W->>DB: Generate PDF metadata
+```
+
+Admin **bill-now** follows a similar coordinator and unit pattern outside the normal schedule. See **[Billing Administration](../features/billing-administration.md)**.
+
+## Server Provisioning Flow
+
+When a service plan includes infrastructure, the manager provisions a cloud server and records connection details on the subscription item.
+
+```mermaid
+sequenceDiagram
+ participant A as Billing Manager
+ participant P as Cloud Provider
+ participant S as New Server
+ participant DNS as Cloudflare (optional)
+ participant DB as PostgreSQL
+
+ A->>P: Create server with cloud-init
+ P->>S: Boot instance
+ S->>S: cloud-init installs Docker stack
+ S->>S: Start postgres, API, console behind nginx
+ P-->>A: Server id and IP
+ A->>DNS: Create A record (when configured)
+ A->>DB: Store server info snapshot on item
+ A->>DB: Reserve hostname if applicable
+```
+
+Later, the **subscription-item-update** scheduler SSHes to the host and runs `docker compose up -d --pull=always` to refresh bundled stacks. See **[Server Provisioning](../features/server-provisioning.md)**.
+
+## Backorder Retry Flow
+
+```mermaid
+sequenceDiagram
+ participant SCH as Scheduler
+ participant W as Worker
+ participant A as Billing Manager
+ participant P as Provider
+ participant DB as PostgreSQL
+
+ SCH->>W: backorder-retry.coordinator
+ W->>DB: Load pending or retrying backorders
+ loop Each backorder
+ W->>A: Retry availability and order logic
+ A->>P: Capacity check
+ alt Capacity available
+ A->>DB: Promote to subscription
+ else Still unavailable
+ A->>DB: Increment retry, keep queued
+ end
+ end
+```
+
+Users may also trigger manual retry or cancel from the console when exposed in UI effects.
+
+## Admin Manual Invoice Flow
+
+```mermaid
+sequenceDiagram
+ participant Admin as Admin User
+ participant C as Billing Console
+ participant A as Billing Manager API
+ participant DB as PostgreSQL
+
+ Admin->>C: Create draft manual invoice
+ C->>A: POST /api/admin/billing/invoices
+ A->>DB: Insert draft
+ Admin->>C: Edit lines and issue
+ C->>A: POST .../issue
+ A->>DB: Finalize invoice, PDF path
+ A-->>C: Issued invoice
+```
+
+Void, mark paid, and mark unpaid operations update invoice state without Stripe when paid offline. See **[Billing Administration](../features/billing-administration.md)**.
+
+## Multi-Tenant Request Flow
+
+Every HTTP and WebSocket call carries tenant context:
+
+1. Client sends `X-Tenant` (HTTP header or socket auth metadata)
+2. API validates against `TENANTS` allowlist
+3. Services scope queries with `tenant_id`
+4. Stripe webhooks recover tenant id from Checkout Session metadata
+
+See **[Multi-tenancy](../features/multi-tenancy.md)**.
+
+## State Management Flow (NgRx)
+
+```mermaid
+graph TB
+ A[Component action] --> B[Facade]
+ B --> C[Effect]
+ C --> D[Billing HTTP or Socket service]
+ D --> E[Success or failure action]
+ E --> F[Reducer]
+ F --> G[Store]
+ G --> H[Selector]
+ H --> I[Component template]
+```
+
+Dashboard socket effects (`connectBillingDashboardSocket$`, `billingDashboardSocketApplicationErrorFallback$`) bridge Socket.IO events into the `billingDashboardSocket` slice used by the overview page.
+
+## Related Documentation
+
+- **[System Overview](./system-overview.md)** - Tier architecture
+- **[Components](./components.md)** - Runtime components
+- **[Real-time Status](../features/real-time-status.md)** - WebSocket contract details
+- **[API Reference](../api-reference/README.md)** - OpenAPI and AsyncAPI specs
+
+---
+
+_For queue job names and intervals, see **[Background Jobs](../deployment/background-jobs.md)**._
diff --git a/docs/decabill/architecture/system-overview.md b/docs/decabill/architecture/system-overview.md
new file mode 100644
index 000000000..7924b85de
--- /dev/null
+++ b/docs/decabill/architecture/system-overview.md
@@ -0,0 +1,160 @@
+# System Overview
+
+This document provides a high-level overview of the Decabill system architecture, component relationships, and communication patterns.
+
+## Two-Tier Architecture
+
+Decabill separates the billing console from the billing manager. All product logic for subscriptions, invoices, payments, and provisioning runs in the manager. The console renders UI, holds client state, and calls the manager over HTTP and WebSocket.
+
+```mermaid
+graph TB
+ subgraph "Presentation Tier"
+ FE[Frontend Billing Console
Angular + NgRx + Express SSR]
+ end
+
+ subgraph "Application Tier"
+ BM[Backend Billing Manager
NestJS API + Socket.IO]
+ PG[(PostgreSQL)]
+ RD[(Redis / BullMQ)]
+ ST[Stripe]
+ CP[Cloud Providers
Hetzner / DigitalOcean]
+ end
+
+ FE -->|HTTP REST /api| BM
+ FE -->|WebSocket /billing| BM
+ BM --> PG
+ BM --> RD
+ BM --> ST
+ BM --> CP
+```
+
+### Tier Responsibilities
+
+#### Presentation Tier (Frontend Billing Console)
+
+- Customer and admin UI for dashboard, plans, invoices, and administration
+- Identity routes (login, register, password reset, user management)
+- NgRx data access and effects for billing API calls
+- Socket.IO client for live server status on the overview dashboard
+- Express SSR server for localized static assets, CSP, and runtime config
+
+#### Application Tier (Backend Billing Manager)
+
+- HTTP REST API under `/api` (OpenAPI documented)
+- WebSocket gateway on port **8082**, namespace **`billing`**
+- PostgreSQL persistence with TypeORM migrations on API startup
+- BullMQ schedulers and workers for billing cycles, reminders, backorders, and provisioning updates
+- Stripe checkout session creation and webhook handling
+- Optional cloud server provisioning via provider APIs and cloud-init
+
+## Component Relationships
+
+```mermaid
+graph LR
+ subgraph "Billing Console"
+ UI[Feature Components]
+ NGX[NgRx Store]
+ HTTP[HTTP Client]
+ WS[Socket.IO Client]
+ end
+
+ subgraph "Billing Manager"
+ CTRL[REST Controllers]
+ SVC[Domain Services]
+ GW[Status Gateway]
+ Q[Queue Processors]
+ REPO[TypeORM Repositories]
+ end
+
+ UI --> NGX
+ NGX --> HTTP
+ NGX --> WS
+ HTTP --> CTRL
+ WS --> GW
+ CTRL --> SVC
+ GW --> SVC
+ SVC --> REPO
+ SVC --> Q
+ Q --> REPO
+```
+
+## Communication Patterns
+
+### HTTP REST API
+
+The console and external integrators call the billing manager synchronously for CRUD, checkout initiation, availability checks, and admin operations. All authenticated routes expect a bearer token, Keycloak token, or API key depending on deployment configuration. Tenant scope travels in the `X-Tenant` header.
+
+Typical namespaces in the API include:
+
+- Public plan offerings (unauthenticated marketing endpoints)
+- Service types and service plans (admin catalog)
+- Subscriptions, backorders, and customer profile
+- Invoices, open positions, and payment initiation
+- Admin billing, manual invoices, statistics, and audit
+
+See **[API Reference](../api-reference/README.md)**.
+
+### WebSocket Dashboard Status
+
+The billing manager exposes a Socket.IO namespace separate from the HTTP port. Authenticated users subscribe with `subscribeDashboardStatus`. The server polls provisioned subscription items on an interval and emits `dashboardStatusUpdate` events scoped to the connecting socket only.
+
+Static API key clients cannot use this stream because there is no end-user subscription scope. See **[Real-time Status](../features/real-time-status.md)**.
+
+### Background Processing
+
+Repeatable coordinator jobs enqueue unit jobs on the **`billing`** BullMQ queue. Workers process subscription billing, expiration, invoice overdue marking, open-position invoicing, renewal reminders, subscription item updates (SSH/docker compose pull), backorder retries, and admin bill-now operations.
+
+Schedulers register coordinators; workers execute units; the API process serves HTTP and optionally Bull Board.
+
+## Deployment Topology
+
+### Local Development
+
+A single process with `QUEUE_ROLE=all` or Docker Compose with three manager containers plus Postgres, Redis, and Mailhog.
+
+### Production
+
+- One or more **api** replicas behind TLS termination
+- Dedicated **worker** replicas sized for job volume
+- A single **scheduler** replica per Redis key prefix
+- Managed PostgreSQL and Redis
+- SSR billing console container (`decabill-billing-console-server`) with CSP connect-src allowing the public API origin
+
+```mermaid
+graph TB
+ U[Users / Browsers]
+ CDN[TLS / Reverse Proxy]
+ CON[decabill-billing-console-server]
+ API[decabill-billing-api api role]
+ W1[decabill-billing-api worker]
+ SCH[decabill-billing-api scheduler]
+ PG[(PostgreSQL)]
+ RD[(Redis)]
+
+ U --> CDN
+ CDN --> CON
+ CDN --> API
+ CON -->|API + WS| API
+ API --> PG
+ API --> RD
+ W1 --> PG
+ W1 --> RD
+ SCH --> RD
+```
+
+## Data Boundaries
+
+- **Tenant isolation** - Rows carry `tenant_id`; requests with invalid or missing tenant ids are rejected when not allowed
+- **User scope** - Customers see their subscriptions and invoices; admins access `/admin/billing/*` routes and administration UI
+- **Provider secrets** - Cloud API tokens and SSH keys are stored encrypted; Stripe secrets remain server-side only
+
+## Related Documentation
+
+- **[Components](./components.md)** - Detailed component breakdown
+- **[Data Flow](./data-flow.md)** - Sequence diagrams for major flows
+- **[Applications](../applications/README.md)** - Per-application reference
+- **[Getting Started](../getting-started.md)** - Run the stack locally
+
+---
+
+_For environment variables and ports, see **[Environment Configuration](../deployment/environment-configuration.md)**._
diff --git a/docs/decabill/deployment/README.md b/docs/decabill/deployment/README.md
new file mode 100644
index 000000000..3edf504a8
--- /dev/null
+++ b/docs/decabill/deployment/README.md
@@ -0,0 +1,133 @@
+# Deployment Documentation
+
+This section provides deployment guides and configuration information for Decabill.
+
+## Overview
+
+Decabill can be deployed in several ways:
+
+- **Local Development** - For development and testing on your machine
+- **Docker Deployment** - Containerized deployment using Docker Compose
+- **Production Deployment** - Production-ready deployment with security and performance considerations
+
+## Deployment Guides
+
+### [Local Development](./local-development.md)
+
+Setting up Decabill for local development:
+
+- Prerequisites and installation
+- Local database and Redis setup
+- Running the billing manager and frontends locally
+- Development workflow and testing
+
+### [Docker Deployment](./docker-deployment.md)
+
+Containerized deployment using Docker:
+
+- Docker Compose setup for billing manager, billing console, and docs
+- Container configuration and image hardening
+- Volume management for invoice PDFs and provider plugins
+- Network configuration and multi-container orchestration
+
+### [Environment Configuration](./environment-configuration.md)
+
+Complete environment variables reference:
+
+- Billing manager multi-tenancy
+- Stripe and payment processor configuration
+- Frontend Express variables for billing console and docs
+- Redis and BullMQ queue settings
+
+### [Production Checklist](./production-checklist.md)
+
+Production deployment guide:
+
+- Pre-deployment checklist
+- Security considerations
+- Performance optimization
+- Monitoring and backup strategies
+
+### [Background Jobs](./background-jobs.md)
+
+BullMQ background processing for the billing manager:
+
+- Queue roles (API, scheduler, worker)
+- Job registry and coordinator schedules
+- Redis host port 6380 and Bull Board on port 3200
+
+## Deployment Architecture
+
+```mermaid
+graph TB
+ subgraph "Frontend"
+ BC[Billing Console
Angular SSR + Express]
+ DOCS[Docs
Angular SSR + Express]
+ end
+
+ subgraph "Backend"
+ BM[Billing Manager
NestJS API]
+ SCH[Scheduler]
+ WRK[Worker]
+ end
+
+ subgraph "Data"
+ DB[(PostgreSQL)]
+ RD[(Redis)]
+ end
+
+ subgraph "External"
+ STRIPE[Stripe]
+ CLOUD[Cloud Providers
Hetzner / DigitalOcean]
+ end
+
+ BC -->|HTTP REST + WebSocket| BM
+ DOCS -->|Static / SSR| BC
+ BM --> DB
+ BM --> RD
+ SCH --> RD
+ WRK --> RD
+ WRK --> DB
+ BM --> STRIPE
+ WRK --> CLOUD
+```
+
+## Quick Start
+
+### Docker Compose (Recommended)
+
+```bash
+# Billing manager (API, worker, scheduler, Postgres, Redis, Mailhog)
+cd apps/decabill/backend-billing-manager
+docker compose up -d
+
+# Billing console frontend
+cd ../frontend-billing-console
+docker compose up -d
+
+# Docs frontend (optional)
+cd ../frontend-docs
+docker compose up -d
+```
+
+### Local Development
+
+```bash
+# Install dependencies (repository root)
+npm install
+
+# Start billing manager
+nx serve decabill-backend-billing-manager
+
+# Start billing console
+nx serve decabill-frontend-billing-console
+```
+
+## Related Documentation
+
+- **[Security](../security/README.md)** - Accepted risks, hardening, SBOM, and disclosure
+- **[Troubleshooting](../troubleshooting/README.md)** - Common issues and debugging
+
+---
+
+_For detailed deployment information, see the individual deployment guides._
diff --git a/docs/decabill/deployment/background-jobs.md b/docs/decabill/deployment/background-jobs.md
new file mode 100644
index 000000000..f7f941f92
--- /dev/null
+++ b/docs/decabill/deployment/background-jobs.md
@@ -0,0 +1,125 @@
+# Background Jobs (BullMQ)
+
+Background work for **backend billing manager** runs through **Redis + BullMQ** instead of in-process `setInterval` loops in the API container.
+
+## Architecture
+
+| Role | `QUEUE_ROLE` | HTTP API | Registers repeatable coordinators | Processes unit jobs |
+| ------------------------------------ | ------------ | -------- | --------------------------------- | ------------------- |
+| API (default in compose API service) | `api` | Yes | No | No |
+| Scheduler | `scheduler` | No | Yes | No |
+| Worker | `worker` | No | No | Yes |
+| Local all-in-one | `all` | Yes | Yes | Yes |
+
+The billing stack has its own **Redis** service in Docker Compose. Workers and schedulers use the **same environment variables** as the API (database, Stripe, scheduler intervals, provisioning tokens, etc.). **Database migrations** run only on containers with `QUEUE_ROLE=api` or `QUEUE_ROLE=all`.
+
+### Startup order
+
+1. Start **Redis** and **Postgres** (and Mailhog or production SMTP).
+2. Start the **API** container (`QUEUE_ROLE=api`) and wait until it is healthy so migrations have run.
+3. Start **scheduler** and **worker** containers (`depends_on` with `service_healthy` on the API service in the provided compose file).
+
+Workers and schedulers assume the API has already applied schema migrations. Running workers before the API in a fresh environment can cause query errors until migrations complete.
+
+## Job Registry
+
+Job registration (queue names, repeatable intervals, job names) lives in:
+
+`apps/decabill/backend-billing-manager/src/queue/job-registry.ts`
+
+### Queue
+
+- **Queue name:** `billing` (`BILLING_QUEUE_NAME`)
+
+### Coordinator jobs (repeatable)
+
+Registered by the scheduler from `getBillingRepeatableJobs()`:
+
+| Coordinator job name | Env interval variable | Default interval |
+| -------------------------------------- | ------------------------------------------ | ---------------- |
+| `subscription-billing.coordinator` | `BILLING_SCHEDULER_INTERVAL` | 60s |
+| `subscription-expiration.coordinator` | `EXPIRATION_SCHEDULER_INTERVAL` | 60s |
+| `invoice-overdue.coordinator` | `INVOICE_OVERDUE_SCHEDULER_INTERVAL` | 24h |
+| `open-position-invoice.coordinator` | `OPEN_POSITION_INVOICE_SCHEDULER_INTERVAL` | 24h |
+| `renewal-reminder.coordinator` | `REMINDER_SCHEDULER_INTERVAL` | 1h |
+| `subscription-item-update.coordinator` | `SUBSCRIPTION_UPDATE_SCHEDULER_INTERVAL` | 24h |
+| `backorder-retry.coordinator` | `BACKORDER_RETRY_INTERVAL_MS` | 60s |
+
+Coordinator job IDs use dot separators (for example `coordinator.subscription-billing`) via `buildCoordinatorJobId`.
+
+### Unit jobs (worker-processed)
+
+Coordinators fan out unit jobs such as:
+
+- `subscription-billing.unit`
+- `subscription-expiration.unit`
+- `invoice-overdue.unit`
+- `open-position-invoice.unit`
+- `renewal-reminder.unit`
+- `subscription-item-update.unit`
+- `backorder-retry.unit`
+- Admin bill-now coordinator and unit jobs (`AdminBillNowJobName`)
+
+BullMQ `jobId` values prevent duplicate active work for the same entity. Custom job IDs use `.` separators and only allowed characters (alphanumeric, `.`, `-`, `_`, `~`).
+
+## Redis and Queue Environment Variables
+
+| Variable | Purpose | Default (local compose) |
+| --------------------------- | --------------------------------------- | ------------------------- |
+| `REDIS_HOST` | Redis hostname | `redis` in compose |
+| `REDIS_PORT` | Redis port inside network | `6379` |
+| `REDIS_HOST_PORT` | Host port mapping in compose | `6380` |
+| `REDIS_PASSWORD` | Optional password | empty |
+| `REDIS_DB` | Redis database index | `0` |
+| `REDIS_KEY_PREFIX` | Key namespace | `decabill-billing` |
+| `QUEUE_ROLE` | `api`, `scheduler`, `worker`, or `all` | `api` in API container |
+| `QUEUE_WORKER_CONCURRENCY` | Default worker concurrency | `5` |
+| `QUEUE_BULL_BOARD_ENABLED` | Mount Bull Board UI on API / `all` only | `true` on API in compose |
+| `QUEUE_BULL_BOARD_PATH` | Bull Board route | `/admin/queues` |
+| `QUEUE_BULL_BOARD_USERNAME` | HTTP Basic username | `admin` |
+| `QUEUE_BULL_BOARD_PASSWORD` | HTTP Basic password (required) | `bullmq` in local compose |
+
+Scheduler interval variables (`BILLING_SCHEDULER_INTERVAL`, `EXPIRATION_SCHEDULER_INTERVAL`, etc.) control **coordinator repeat** intervals in BullMQ.
+
+## Docker Compose
+
+`apps/decabill/backend-billing-manager/docker-compose.yaml` defines:
+
+- `redis` (host port **6380** by default)
+- `backend-billing-manager` (API, `QUEUE_ROLE=api`, ports **3200** and **8082**)
+- `backend-billing-manager-scheduler` (`QUEUE_ROLE=scheduler`)
+- `backend-billing-manager-worker` (`QUEUE_ROLE=worker`)
+
+Billing Redis is published on host port **6380** so it can run alongside other Redis instances on **6379**.
+
+## Bull Board
+
+When enabled on the API container (`QUEUE_BULL_BOARD_ENABLED=true`, default in local compose):
+
+- URL: **`http://localhost:3200/admin/queues`** (billing API port **3200**)
+- Path is **not** under the Nest global `/api` prefix
+- HTTP Basic authentication: `QUEUE_BULL_BOARD_USERNAME` / `QUEUE_BULL_BOARD_PASSWORD`
+- Local compose defaults: `admin` / `bullmq`; override in production
+
+Startup fails in production if the board is enabled without a password.
+
+Completed and failed jobs are **not auto-removed** (`removeOnComplete: false`, `removeOnFail: false`) so run history remains visible. Treat the **last three runs** and **48 hours** as minimum retention before manual cleanup.
+
+Bull Board routes bypass the API origin allowlist and HybridAuthGuard so dashboard actions (retry, delete, clean) work with browser Basic auth.
+
+Worker and scheduler containers set `QUEUE_BULL_BOARD_ENABLED=false` so they do not start an HTTP server solely for Bull Board.
+
+## Tenant Context in Jobs
+
+Unit jobs resolve `tenant_id` from job payload data so multi-tenant billing runs stay scoped. See `resolve-billing-job-tenant-id.ts` in the billing manager queue module.
+
+## Related Documentation
+
+- **[Environment Configuration](./environment-configuration.md)** - Redis and scheduler variables
+- **[Local Development](./local-development.md)** - `QUEUE_ROLE=all` locally
+- **[Docker Deployment](./docker-deployment.md)** - Compose services
+- **[Multi-tenancy](../features/multi-tenancy.md)** - `X-Tenant` and `TENANTS`
+
+---
+
+_For production queue hardening, see [Production Checklist](./production-checklist.md)._
diff --git a/docs/decabill/deployment/docker-deployment.md b/docs/decabill/deployment/docker-deployment.md
new file mode 100644
index 000000000..fad057cbf
--- /dev/null
+++ b/docs/decabill/deployment/docker-deployment.md
@@ -0,0 +1,183 @@
+# Docker Deployment
+
+Containerized deployment guide for Decabill using Docker and Docker Compose.
+
+## Overview
+
+Docker deployment provides:
+
+- Isolated environments for billing API, workers, and frontends
+- Consistent dependencies (Postgres, Redis, Mailhog)
+- Repeatable production-like stacks on developer machines
+
+## Prerequisites
+
+- Docker 20.10 or higher
+- Docker Compose 2.0 or higher
+
+## Docker Compose Setup
+
+### Backend Billing Manager
+
+```bash
+cd apps/decabill/backend-billing-manager
+docker compose up -d
+```
+
+The `docker-compose.yaml` includes:
+
+- **postgres** - PostgreSQL 16
+- **redis** - Redis 7 with persistence; host port **6380** maps to container **6379**
+- **backend-billing-manager** - API (`QUEUE_ROLE=api`, port **3200**, WebSocket **8082**)
+- **backend-billing-manager-scheduler** - Coordinator registration (`QUEUE_ROLE=scheduler`)
+- **backend-billing-manager-worker** - Job processing (`QUEUE_ROLE=worker`)
+- **mailhog** - Local SMTP capture for invoice and reminder emails
+
+Volumes:
+
+- `postgres_data` - Database files
+- `redis_data` - Redis AOF data
+- `invoice_pdf_data` - Invoice PDF storage at `/data/invoices`
+- `./provider-plugins` - Optional dynamic provider plugins mount
+
+Image: `ghcr.io/forepath/decabill-billing-api:latest`
+
+### Frontend Billing Console
+
+```bash
+cd apps/decabill/frontend-billing-console
+docker compose up -d
+```
+
+Image: `ghcr.io/forepath/decabill-billing-console-server:latest`
+
+Default port **4500**. Compose sets `CSP_CONNECT_SRC_EXTRA=http://host.docker.internal:3200` so the browser can reach the billing API from the containerized console.
+
+### Frontend Docs
+
+```bash
+cd apps/decabill/frontend-docs
+docker compose up -d
+```
+
+Image: `ghcr.io/forepath/decabill-docs-server:latest`
+
+Default port **4200**.
+
+## Container Configuration
+
+### Environment Variables
+
+Configure billing manager containers via `.env` or `docker-compose.yaml`. The shared anchor `x-backend-billing-manager-environment` lists all supported variables. See **[Environment Configuration](./environment-configuration.md)**.
+
+Example API service snippet:
+
+```yaml
+services:
+ backend-billing-manager:
+ environment:
+ - DB_HOST=postgres
+ - REDIS_HOST=redis
+ - STATIC_API_KEY=your-api-key
+ - CORS_ORIGIN=https://billing.example.com
+ - QUEUE_ROLE=api
+ - QUEUE_BULL_BOARD_ENABLED=true
+ - QUEUE_BULL_BOARD_PASSWORD=strong-password
+```
+
+### Frontend Container Configuration
+
+Frontend server images support runtime configuration via the `CONFIG` environment variable:
+
+```yaml
+services:
+ frontend-billing-console-server:
+ environment:
+ - CONFIG=https://config.example.com/decabill-billing-config.json
+ - CONFIG_ALLOWED_HOSTS=config.example.com
+ - CSP_ENFORCE=true
+ - CSP_CONNECT_SRC_EXTRA=https://api.billing.example.com
+ - PORT=4500
+```
+
+When `CONFIG` is set, the frontend server also supports hardening variables documented in **[Environment Configuration](./environment-configuration.md)** (`CONFIG_ALLOWED_HOSTS`, `CONFIG_FETCH_TIMEOUT_MS`, and related settings).
+
+Billing console and docs Express servers support CSP variables:
+
+- `CSP_ENFORCE` - Enforce CSP when `true`; otherwise report-only
+- `CSP_CONNECT_SRC_EXTRA` - Extra `connect-src` origins (required in production for plain HTTP APIs)
+- `CSP_SCRIPT_SRC_EXTRA`, `CSP_STYLE_SRC_EXTRA`, `CSP_IMG_SRC_EXTRA`, `CSP_FONT_SRC_EXTRA`, `CSP_WORKER_SRC_EXTRA`
+- `CSP_FRAME_ANCESTORS` - Optional full override for `frame-ancestors`
+
+## Building Containers
+
+```bash
+# Billing API image
+nx docker:api decabill-backend-billing-manager
+
+# Billing console server image
+nx docker:server decabill-frontend-billing-console
+
+# Docs server image
+nx docker:server decabill-frontend-docs
+```
+
+## Container Security (Images)
+
+First-party Decabill images follow a common hardening baseline:
+
+- **Non-root**: Billing API runs as `agenstra` (UID/GID **10001** by default). Frontend server images run as `node` (**1000**).
+- **Secrets at runtime**: Database, Stripe, encryption keys, and API keys are supplied at deploy time, not baked into images.
+- **No Docker socket**: The billing manager does not mount `/var/run/docker.sock` (unlike agent orchestration stacks).
+
+See **[Container image security](../security/container-images.md)**.
+
+## Running Containers
+
+### Using Docker Compose
+
+```bash
+docker compose up -d
+docker compose logs -f
+docker compose down
+docker compose down -v # removes volumes
+```
+
+### Startup Order (Billing Manager)
+
+1. Postgres and Redis become healthy
+2. API container (`QUEUE_ROLE=api`) starts and runs migrations
+3. Worker and scheduler start after API health check passes
+
+Workers and schedulers assume schema migrations have already been applied.
+
+## Health Checks
+
+The billing API image health check calls `http://localhost:3200/api/health`. Compose `depends_on` with `service_healthy` enforces ordering for worker and scheduler services.
+
+## Bull Board
+
+On the API container with `QUEUE_BULL_BOARD_ENABLED=true`:
+
+- URL: `http://localhost:3200/admin/queues` (not under `/api`)
+- HTTP Basic auth: `QUEUE_BULL_BOARD_USERNAME` / `QUEUE_BULL_BOARD_PASSWORD`
+
+See **[Background Jobs](./background-jobs.md)**.
+
+## Logging
+
+```bash
+docker compose logs -f backend-billing-manager
+docker compose logs --tail=100 backend-billing-manager-worker
+```
+
+## Related Documentation
+
+- **[Local Development](./local-development.md)** - Local setup
+- **[Production Checklist](./production-checklist.md)** - Production deployment
+- **[Environment Configuration](./environment-configuration.md)** - Environment variables
+- **[Background Jobs](./background-jobs.md)** - Queue roles and Redis
+
+---
+
+_For production deployment, see the [Production Checklist](./production-checklist.md)._
diff --git a/docs/decabill/deployment/environment-configuration.md b/docs/decabill/deployment/environment-configuration.md
new file mode 100644
index 000000000..7e9b1577d
--- /dev/null
+++ b/docs/decabill/deployment/environment-configuration.md
@@ -0,0 +1,235 @@
+# Environment Configuration
+
+Complete reference for environment variables used in Decabill.
+
+## Backend Billing Manager
+
+### Application Configuration
+
+| Variable | Description | Default |
+| ----------------------- | ---------------------------------------- | -------------- |
+| `HOST` | HTTP bind address | `0.0.0.0` |
+| `PORT` | HTTP API port | `3200` |
+| `WEBSOCKET_PORT` | WebSocket gateway port | `8082` |
+| `WEBSOCKET_NAMESPACE` | Socket.IO namespace | `billing` |
+| `WEBSOCKET_CORS_ORIGIN` | WebSocket CORS origins (comma-separated) | `*` in compose |
+| `NODE_ENV` | `development` or `production` | `development` |
+
+### Database Configuration
+
+| Variable | Description | Default |
+| ------------- | ----------------- | ----------- |
+| `DB_HOST` | Database host | `localhost` |
+| `DB_PORT` | Database port | `5432` |
+| `DB_USERNAME` | Database username | `postgres` |
+| `DB_PASSWORD` | Database password | `postgres` |
+| `DB_DATABASE` | Database name | `postgres` |
+
+### Authentication
+
+| Variable | Description |
+| -------------------------- | ---------------------------------------------------------------------------- |
+| `AUTHENTICATION_METHOD` | Explicit: `api-key`, `keycloak`, or `users`. If unset, inferred (see DR-004) |
+| `STATIC_API_KEY` | Static API key when using api-key mode |
+| `STATIC_API_KEY_TENANT_ID` | Optional tenant bind for API key auth (see DR-002) |
+| `DISABLE_SIGNUP` | When `true`, disables self-registration for users mode |
+| `JWT_SECRET` | Required for users mode |
+| `KEYCLOAK_*` | Keycloak URL, realm, client id/secret, token validation |
+
+### Multi-tenancy
+
+Billing data and users are partitioned by **`tenant_id`**. HTTP clients send **`X-Tenant`**; the billing console attaches it via `environment.billing.tenantId` (defaults to `default`).
+
+| Variable | Description |
+| -------------------------- | -------------------------------------------------------------------------------------------------------------- |
+| `TENANTS` | Comma-separated tenant ids allowed for **`X-Tenant`** (always includes `default`). Unset means only `default`. |
+| `STATIC_API_KEY_TENANT_ID` | When set with **`STATIC_API_KEY`**, API key requests are accepted only when **`X-Tenant`** matches. |
+| `BILLING_FRONTEND_URL` | Billing console base URL for the `default` tenant (Stripe return redirects). |
+| `TENANT_FRONTEND_URLS` | Per-tenant console URLs: `tenantId=https://…` pairs, comma-separated. |
+
+**API key scope (accepted risk [DR-002](../security/accepted-risks.md#dr-002--billing-multi-tenant-api-key-scope-static_api_key_tenant_id-unset)):** With **`STATIC_API_KEY`** and **without** **`STATIC_API_KEY_TENANT_ID`**, one deployment key grants **admin access to every tenant** in **`TENANTS`**, selected per request via **`X-Tenant`**. Set **`STATIC_API_KEY_TENANT_ID`** to bind the key to one tenant, or use **keycloak** / **users** for interactive multi-tenant console access.
+
+### CORS and Rate Limiting
+
+| Variable | Description |
+| -------------------- | ------------------------------------------------------------------- |
+| `CORS_ORIGIN` | Allowed CORS origins (comma-separated). **Required in production.** |
+| `RATE_LIMIT_ENABLED` | Enable rate limiting (default `true` in production) |
+| `RATE_LIMIT_TTL` | Window in seconds (default `60`) |
+| `RATE_LIMIT_LIMIT` | Max requests per window (default `100`) |
+
+### Encryption and Issuer Details
+
+| Variable | Description |
+| ---------------------------------- | ------------------------------------------------------ |
+| `ENCRYPTION_KEY` | Encrypts sensitive stored data (API tokens, snapshots) |
+| `BILLING_ISSUER_*` | Legal entity on invoices (name, VAT, address, bank) |
+| `BILLING_TAX_RATE_STANDARD` | Default standard tax rate (default `19`) |
+| `BILLING_TAX_RATE_REDUCED` | Reduced tax rate (default `7`) |
+| `BILLING_INVOICE_PDF_STORAGE_PATH` | PDF output path (default `/data/invoices`) |
+| `BILLING_SKIP_FILE_CACHE` | Skip PDF file cache when `true` |
+
+### Stripe and Payment Processors
+
+| Variable | Description |
+| ----------------------------------- | ------------------------------------------------------- |
+| `BILLING_DEFAULT_PAYMENT_PROCESSOR` | Default processor (default `stripe`) |
+| `STRIPE_SECRET_KEY` | Stripe secret API key |
+| `STRIPE_WEBHOOK_SECRET` | Stripe webhook signing secret |
+| `STRIPE_CHECKOUT_SUCCESS_URL` | Redirect after successful checkout |
+| `STRIPE_CHECKOUT_CANCEL_URL` | Redirect after cancelled checkout |
+| `DYNAMIC_PAYMENT_PROCESSORS` | Comma-separated extra payment processor packages |
+| `DYNAMIC_BILLING_PROVIDER_METADATA` | Extra billing provider metadata packages |
+| `DYNAMIC_PROVIDERS_FAIL_FAST` | Abort startup if critical dynamic provider fails |
+| `DYNAMIC_PROVIDER_PLUGIN_PATH` | Plugin root (e.g. `/var/lib/forepath/provider-plugins`) |
+| `DYNAMIC_PROVIDER_PLUGIN_INSTALL` | `npm install` targets at container startup |
+
+### Email (SMTP)
+
+| Variable | Description | Local default |
+| --------------- | ---------------- | ------------------- |
+| `SMTP_HOST` | SMTP server host | `mailhog` |
+| `SMTP_PORT` | SMTP port | `1025` |
+| `SMTP_USER` | SMTP username | empty |
+| `SMTP_PASSWORD` | SMTP password | empty |
+| `EMAIL_FROM` | From address | `noreply@localhost` |
+
+### Provisioning and DNS
+
+| Variable | Description |
+| ------------------------ | ------------------------------------- |
+| `HETZNER_API_TOKEN` | Hetzner Cloud API token |
+| `DIGITALOCEAN_API_TOKEN` | DigitalOcean API token |
+| `CLOUDFLARE_API_TOKEN` | Cloudflare API token |
+| `CLOUDFLARE_ZONE_ID` | Cloudflare zone for DNS records |
+| `DNS_BASE_DOMAIN` | Base domain for provisioned hostnames |
+
+Provisioning SSH posture is documented under accepted risk **[DR-001](../security/accepted-risks.md#dr-001--provisioning-ssh-cloud-init-templates)**.
+
+### Scheduler Intervals (BullMQ Coordinators)
+
+These variables control repeatable **coordinator** intervals in milliseconds:
+
+| Variable | Default | Purpose |
+| ------------------------------------------ | ---------- | -------------------------------- |
+| `BILLING_SCHEDULER_INTERVAL` | `60000` | Subscription billing coordinator |
+| `BILLING_SCHEDULER_BATCH_SIZE` | `100` | Batch size for billing runs |
+| `EXPIRATION_SCHEDULER_INTERVAL` | `60000` | Subscription expiration |
+| `EXPIRATION_SCHEDULER_BATCH_SIZE` | `100` | Expiration batch size |
+| `REMINDER_SCHEDULER_INTERVAL` | `3600000` | Renewal reminders |
+| `REMINDER_SCHEDULER_BATCH_SIZE` | `100` | Reminder batch size |
+| `REMINDER_DAYS` | `3` | Days before renewal to remind |
+| `BACKORDER_RETRY_INTERVAL_MS` | `60000` | Backorder retry coordinator |
+| `BACKORDER_RETRY_BATCH_SIZE` | `100` | Backorder batch size |
+| `INVOICE_OVERDUE_SCHEDULER_INTERVAL` | `86400000` | Invoice overdue coordinator |
+| `INVOICE_OVERDUE_SCHEDULER_BATCH_SIZE` | `100` | Overdue batch size |
+| `OPEN_POSITION_INVOICE_SCHEDULER_INTERVAL` | `86400000` | Open position invoicing |
+| `SUBSCRIPTION_UPDATE_SCHEDULER_INTERVAL` | `86400000` | Subscription item updates |
+| `STATUS_POLL_INTERVAL` | `15000` | Dashboard status polling |
+
+See **[Background Jobs](./background-jobs.md)** for queue roles and job names.
+
+## Frontend Applications (Express SSR)
+
+**decabill-frontend-billing-console** and **decabill-frontend-docs** use the shared Express layer for `GET /config` and security headers.
+
+### Runtime Configuration (`CONFIG`)
+
+| Variable | Description |
+| ---------------------------- | -------------------------------------------------------------------------------- |
+| `CONFIG` | URL to remote JSON merged at runtime via `/config` |
+| `CONFIG_ALLOWED_HOSTS` | Hostname allowlist for `CONFIG`. **Required in production** when `CONFIG` is set |
+| `CONFIG_ALLOW_INSECURE_HTTP` | Allow `http://` CONFIG URLs in production when `true` (default `false`) |
+| `CONFIG_ALLOW_INTERNAL_HOST` | Allow private/loopback CONFIG targets when `true` (default `false`) |
+| `CONFIG_FETCH_TIMEOUT_MS` | Fetch timeout (default `10000`) |
+| `CONFIG_FETCH_MAX_BYTES` | Max response size (default `262144`) |
+| `CONFIG_JSON_MAX_DEPTH` | Max JSON depth (default `12`) |
+| `CONFIG_JSON_MAX_KEYS` | Max JSON keys (default `512`) |
+
+### Content Security Policy (Express)
+
+| Variable | Description |
+| ----------------------- | ------------------------------------------------------------------------- |
+| `CSP_ENFORCE` | Enforcing CSP when `true`; report-only otherwise (see DR-003) |
+| `CSP_CONNECT_SRC_EXTRA` | Extra `connect-src` origins (billing console often needs billing API URL) |
+| `CSP_SCRIPT_SRC_EXTRA` | Extra `script-src` origins |
+| `CSP_STYLE_SRC_EXTRA` | Extra `style-src` origins |
+| `CSP_IMG_SRC_EXTRA` | Extra `img-src` origins |
+| `CSP_FONT_SRC_EXTRA` | Extra `font-src` origins |
+| `CSP_WORKER_SRC_EXTRA` | Extra `worker-src` origins |
+| `CSP_DEFAULT_SRC_EXTRA` | Extra `default-src` origins |
+| `CSP_BASE_URI_EXTRA` | Extra `base-uri` origins |
+| `CSP_FRAME_ANCESTORS` | Full override for `frame-ancestors` |
+
+Default `connect-src` allows `'self'`, `https:`, and `wss:`. In production, plain `http:` / `ws:` require explicit origins via `CSP_CONNECT_SRC_EXTRA`.
+
+Billing console compose default: `CSP_CONNECT_SRC_EXTRA=http://host.docker.internal:3200`.
+
+### Billing Console Server
+
+| Variable | Description | Default |
+| ---------------- | ------------------------ | ---------------------- |
+| `PORT` | HTTP port | `4500` (console image) |
+| `HOST` | Bind address | `0.0.0.0` |
+| `DEFAULT_LOCALE` | Default locale | `en` |
+| `API_URL` | Build-time API URL | See app config |
+| `WEBSOCKET_URL` | Build-time WebSocket URL | See app config |
+
+### Docs Server
+
+| Variable | Description | Default |
+| ---------------- | -------------- | ----------------- |
+| `PORT` | HTTP port | `4200` |
+| `HOST` | Bind address | `0.0.0.0` |
+| `DEFAULT_LOCALE` | Default locale | `en` |
+| `CSP_ENFORCE` | Enforce CSP | `true` in compose |
+
+## Redis and BullMQ (Background Jobs)
+
+Used by **backend billing manager only**. See **[Background Jobs](./background-jobs.md)**.
+
+| Variable | Description | Default |
+| --------------------------- | -------------------------------------- | ------------------------------------- |
+| `REDIS_HOST` | Redis host | `localhost` (compose: `redis`) |
+| `REDIS_PORT` | Redis port inside container/network | `6379` |
+| `REDIS_HOST_PORT` | Host port published by compose | `6380` |
+| `REDIS_PASSWORD` | Optional password | empty |
+| `REDIS_DB` | Redis DB index | `0` |
+| `REDIS_KEY_PREFIX` | Key prefix | `decabill-billing` |
+| `QUEUE_ROLE` | `api`, `scheduler`, `worker`, or `all` | `all` locally; `api` in API container |
+| `QUEUE_WORKER_CONCURRENCY` | Worker concurrency | `5` |
+| `QUEUE_BULL_BOARD_ENABLED` | Enable Bull Board on API / `all` | `true` in dev API compose |
+| `QUEUE_BULL_BOARD_PATH` | Bull Board path | `/admin/queues` |
+| `QUEUE_BULL_BOARD_USERNAME` | Bull Board HTTP Basic user | `admin` |
+| `QUEUE_BULL_BOARD_PASSWORD` | Bull Board HTTP Basic password | required in production |
+
+## Environment-Specific Defaults
+
+### Development
+
+- `NODE_ENV=development`
+- `CORS_ORIGIN=*` (all origins allowed)
+- `RATE_LIMIT_ENABLED=false`
+- `QUEUE_ROLE=all` for single-process local runs
+
+### Production
+
+- `NODE_ENV=production`
+- `CORS_ORIGIN` **required**
+- `RATE_LIMIT_ENABLED=true`
+- `ENCRYPTION_KEY` **required** for encrypted fields
+- Strong `STATIC_API_KEY` or Keycloak/users auth
+- `QUEUE_BULL_BOARD_PASSWORD` **required** when Bull Board is enabled
+- `CONFIG_ALLOWED_HOSTS` when `CONFIG` is set on frontends
+
+## Related Documentation
+
+- **[Local Development](./local-development.md)** - Local setup
+- **[Docker Deployment](./docker-deployment.md)** - Containerized deployment
+- **[Production Checklist](./production-checklist.md)** - Production deployment
+- **[Background Jobs](./background-jobs.md)** - BullMQ roles and coordinators
+- **[Accepted risks](../security/accepted-risks.md)** - DR-001, DR-002, DR-003, DR-004
+
+---
+
+_For feature-specific details, see [Features](../features/README.md)._
diff --git a/docs/decabill/deployment/local-development.md b/docs/decabill/deployment/local-development.md
new file mode 100644
index 000000000..88fe51e4d
--- /dev/null
+++ b/docs/decabill/deployment/local-development.md
@@ -0,0 +1,235 @@
+# Local Development
+
+Setting up Decabill for local development and testing.
+
+## Prerequisites
+
+Before you begin, ensure you have:
+
+- **Node.js** 24.14.1 or higher
+- **PostgreSQL** (running locally or in Docker)
+- **Redis** (running locally or in Docker; billing compose uses host port **6380** by default)
+- **Git** (optional, for repository checkout)
+- **Keycloak** (optional; API key or users authentication work for local dev)
+
+## Installation
+
+### Clone Repository
+
+```bash
+git clone https://github.com/forepath/one.git
+cd one
+```
+
+### Install Dependencies
+
+```bash
+npm install
+```
+
+## Database and Redis Setup
+
+### Using Docker (Recommended)
+
+```bash
+# PostgreSQL for billing manager
+docker run -d \
+ --name decabill-postgres \
+ -e POSTGRES_USER=postgres \
+ -e POSTGRES_PASSWORD=postgres \
+ -e POSTGRES_DB=postgres \
+ -p 5432:5432 \
+ postgres:16-alpine
+
+# Redis for BullMQ (host port 6380 to avoid clashing with other stacks)
+docker run -d \
+ --name decabill-redis \
+ -p 6380:6379 \
+ redis:7-alpine
+```
+
+### Using the Billing Manager Compose Stack
+
+The billing manager `docker-compose.yaml` starts Postgres, Redis (published on **6380**), Mailhog, API, worker, and scheduler together. Copy the example env file and adjust:
+
+```bash
+cd apps/decabill/backend-billing-manager
+cp .start-containers.env.example .env
+docker compose up -d
+```
+
+## Configuration
+
+### Backend Billing Manager
+
+Create a `.env` file in `apps/decabill/backend-billing-manager` (or use `.start-containers.env.example` as a template):
+
+```bash
+# Database
+DB_HOST=localhost
+DB_PORT=5432
+DB_USERNAME=postgres
+DB_PASSWORD=postgres
+DB_DATABASE=postgres
+
+# Redis (when not using compose network)
+REDIS_HOST=localhost
+REDIS_PORT=6380
+REDIS_KEY_PREFIX=decabill-billing
+
+# Queue (local all-in-one)
+QUEUE_ROLE=all
+QUEUE_BULL_BOARD_ENABLED=true
+QUEUE_BULL_BOARD_PASSWORD=bullmq
+
+# Authentication (choose one)
+AUTHENTICATION_METHOD=api-key
+STATIC_API_KEY=dev-api-key-123
+# Optional: bind API key to one tenant (see accepted risk DR-002)
+# STATIC_API_KEY_TENANT_ID=default
+
+# Ports
+PORT=3200
+WEBSOCKET_PORT=8082
+
+# Multi-tenancy
+TENANTS=default
+BILLING_FRONTEND_URL=http://localhost:4500
+
+# CORS (for development)
+CORS_ORIGIN=*
+
+# Rate limiting (disabled for development)
+RATE_LIMIT_ENABLED=false
+
+# Encryption (required for production; set for local if testing encrypted fields)
+ENCRYPTION_KEY=
+
+# Stripe (optional for local payment flows)
+STRIPE_SECRET_KEY=
+STRIPE_WEBHOOK_SECRET=
+STRIPE_CHECKOUT_SUCCESS_URL=http://localhost:4500/invoices?payment=success
+STRIPE_CHECKOUT_CANCEL_URL=http://localhost:4500/invoices?payment=cancel
+
+# Provisioning (optional)
+HETZNER_API_TOKEN=
+DIGITALOCEAN_API_TOKEN=
+```
+
+See **[Environment Configuration](./environment-configuration.md)** for the full variable list.
+
+### Frontend Billing Console
+
+The billing console reads API URLs from build-time environment and optional runtime `CONFIG`. For local `nx serve`, configure in `apps/decabill/frontend-billing-console` or see **[Getting Started](../getting-started.md)**.
+
+Typical local values:
+
+```bash
+API_URL=http://localhost:3200
+WEBSOCKET_URL=http://localhost:8082
+```
+
+Express server variables (when running the SSR server locally):
+
+```bash
+PORT=4500
+CSP_ENFORCE=false
+CSP_CONNECT_SRC_EXTRA=http://localhost:3200
+```
+
+### Frontend Docs
+
+```bash
+PORT=4200
+CSP_ENFORCE=false
+```
+
+## Running Applications
+
+### Start Backend
+
+```bash
+# Terminal 1: Billing manager (QUEUE_ROLE=all for local jobs)
+cd apps/decabill/backend-billing-manager
+nx serve decabill-backend-billing-manager
+```
+
+### Start Frontends
+
+```bash
+# Terminal 2: Billing console
+cd apps/decabill/frontend-billing-console
+nx serve decabill-frontend-billing-console
+
+# Terminal 3: Docs (optional)
+cd apps/decabill/frontend-docs
+nx serve decabill-frontend-docs
+```
+
+## Development Workflow
+
+### Making Changes
+
+1. Make code changes
+2. Applications auto-reload (hot reload where configured)
+3. Test in the browser
+4. Run tests: `nx test `
+
+### Running Tests
+
+```bash
+# Billing manager
+nx test decabill-backend-billing-manager
+
+# Billing console
+nx test decabill-frontend-billing-console
+
+# Run with coverage
+nx test decabill-backend-billing-manager --coverage
+```
+
+### Building
+
+```bash
+nx build decabill-backend-billing-manager
+nx build decabill-frontend-billing-console
+nx build decabill-frontend-docs
+```
+
+## Bull Board (Local)
+
+When `QUEUE_ROLE=all` or `api` with `QUEUE_BULL_BOARD_ENABLED=true`:
+
+- URL: `http://localhost:3200/admin/queues`
+- Default credentials: `admin` / `bullmq` (override via env)
+
+See **[Background Jobs](./background-jobs.md)**.
+
+## Troubleshooting
+
+### Database Connection Issues
+
+- Verify PostgreSQL is running: `docker ps` or `pg_isready`
+- Check credentials in `.env`
+- Ensure migrations ran (API container with `QUEUE_ROLE=api` or `all`)
+
+### Redis Connection Issues
+
+- Confirm Redis is reachable on the configured host and port (**6380** on host when using compose defaults)
+- Check `REDIS_HOST` and `REDIS_PORT`
+
+### Port Conflicts
+
+- Billing API: **3200**, WebSocket: **8082**, console: **4500**, docs: **4200**, Redis host port: **6380**
+- Change ports in `.env` if needed: `lsof -i :3200`
+
+## Related Documentation
+
+- **[Docker Deployment](./docker-deployment.md)** - Containerized deployment
+- **[Production Checklist](./production-checklist.md)** - Production deployment
+- **[Environment Configuration](./environment-configuration.md)** - Environment variables
+- **[Common Issues](../troubleshooting/common-issues.md)** - Problem solving
+
+---
+
+_For production deployment, see the [Production Checklist](./production-checklist.md)._
diff --git a/docs/decabill/deployment/production-checklist.md b/docs/decabill/deployment/production-checklist.md
new file mode 100644
index 000000000..f47b448f5
--- /dev/null
+++ b/docs/decabill/deployment/production-checklist.md
@@ -0,0 +1,184 @@
+# Production Deployment Checklist
+
+Comprehensive checklist for deploying Decabill to production.
+
+## Pre-Deployment Checklist
+
+### Environment Configuration
+
+- [ ] `NODE_ENV=production` on all applications
+- [ ] `CORS_ORIGIN` configured with production billing console and API origins
+- [ ] `RATE_LIMIT_ENABLED=true` (or leave unset; defaults to `true` in production)
+- [ ] `RATE_LIMIT_LIMIT` appropriate for expected traffic
+- [ ] `STATIC_API_KEY` or Keycloak/users credentials configured securely
+- [ ] `ENCRYPTION_KEY` set to a strong random value (required for encrypted subscription data)
+- [ ] Database credentials are not defaults
+- [ ] `TENANTS` lists only intended tenant ids
+- [ ] `STATIC_API_KEY_TENANT_ID` set if API key must not span tenants (see **[DR-002](../security/accepted-risks.md#dr-002--billing-multi-tenant-api-key-scope-static_api_key_tenant_id-unset)**)
+- [ ] `BILLING_FRONTEND_URL` and `TENANT_FRONTEND_URLS` match live console URLs
+- [ ] Stripe keys and webhook secret configured for production mode
+- [ ] `STRIPE_CHECKOUT_SUCCESS_URL` and `STRIPE_CHECKOUT_CANCEL_URL` use HTTPS console URLs
+
+### Redis and Background Jobs
+
+- [ ] Redis reachable from API, worker, and scheduler with correct `REDIS_KEY_PREFIX`
+- [ ] `QUEUE_ROLE=api` on API container only; separate worker and scheduler containers
+- [ ] API container healthy before workers start (migrations applied)
+- [ ] `QUEUE_BULL_BOARD_PASSWORD` set to a strong value if Bull Board is enabled
+- [ ] Bull Board disabled or network-restricted in production if not needed
+
+### Frontend (Billing Console and Docs)
+
+- [ ] `CSP_ENFORCE=true` only after verifying console and docs work under enforced CSP
+- [ ] `CSP_CONNECT_SRC_EXTRA` includes production billing API origin (HTTPS)
+- [ ] `CONFIG_ALLOWED_HOSTS` set when using runtime `CONFIG`
+- [ ] HTTPS termination configured at load balancer or ingress
+
+### Security
+
+- [ ] All default passwords changed (database, Redis if password-protected, Bull Board)
+- [ ] API keys are strong and stored in a secrets manager
+- [ ] HTTPS/WSS enabled for all browser and API traffic
+- [ ] CORS restricted to specific origins
+- [ ] Rate limiting enabled
+- [ ] Database connections use SSL/TLS where supported
+- [ ] Invoice PDF volume backed up and access-controlled
+- [ ] Provisioning SSH and cloud API tokens restricted (see **[DR-001](../security/accepted-risks.md#dr-001--provisioning-ssh-cloud-init-templates)**)
+
+### Database
+
+- [ ] PostgreSQL configured with production credentials
+- [ ] Automated backups configured and restore tested
+- [ ] Connection pooling tuned for workload
+- [ ] Migrations tested on staging
+
+### Infrastructure
+
+- [ ] Container images from trusted registry tags (not `latest` in production unless policy allows)
+- [ ] Resource limits set on API, worker, scheduler, and Redis
+- [ ] Centralized logging configured
+- [ ] Health checks and uptime monitoring on `/api/health`
+- [ ] Mailhog replaced with production SMTP
+
+## Security Considerations
+
+### Authentication
+
+- Use Keycloak or users mode for interactive multi-tenant console access in production
+- Set **`AUTHENTICATION_METHOD`** explicitly if policy requires unambiguous mode selection (see **[DR-004](../security/accepted-risks.md#dr-004--backend-authentication-method-resolution)**)
+- Rotate `STATIC_API_KEY` on a defined schedule if used for automation
+- Never expose API keys in frontend bundles or public `CONFIG` JSON
+
+### Multi-Tenancy
+
+- Review **`TENANTS`** and **`X-Tenant`** handling before go-live
+- Prefer per-tenant console URLs via `TENANT_FRONTEND_URLS` for branded tenants
+- Understand shared API key scope documented in **[DR-002](../security/accepted-risks.md#dr-002--billing-multi-tenant-api-key-scope-static_api_key_tenant_id-unset)**
+
+### Network Security
+
+- Restrict ingress to required ports (console, API, WebSocket)
+- Firewall Bull Board path (`/admin/queues`) to operations networks only
+- Enable WAF or rate limiting at edge where appropriate
+
+### Data Protection
+
+- Encrypt sensitive fields via `ENCRYPTION_KEY`
+- Protect invoice PDF storage path
+- Restrict database and Redis network access to application subnets
+
+## Performance Optimization
+
+### Database
+
+- Index frequently queried billing tables (subscriptions, invoices, users by `tenant_id`)
+- Monitor slow queries on scheduler-driven batch operations
+- Size Postgres for peak invoice generation windows
+
+### Application
+
+- Run workers with `QUEUE_WORKER_CONCURRENCY` tuned to CPU and external API rate limits
+- Scale worker replicas horizontally (multiple worker containers, same Redis)
+- Keep scheduler as a single logical coordinator per deployment (one scheduler container)
+
+### Redis
+
+- Persist Redis with AOF or RDB per your recovery requirements
+- Monitor memory usage for BullMQ job history (jobs are not auto-removed)
+
+## Monitoring Setup
+
+### Application Monitoring
+
+- Track API latency on billing and admin endpoints
+- Monitor WebSocket connection count on billing dashboard namespace
+- Alert on job failure rates in Bull Board or exported queue metrics
+- Track Stripe webhook processing errors
+
+### Infrastructure Monitoring
+
+- Container CPU and memory for API, worker, scheduler
+- Postgres connections and disk usage
+- Redis memory and persistence health
+- Invoice PDF volume disk usage
+
+### Logging
+
+- Centralize structured logs from all queue roles
+- Correlate logs with `X-Correlation-Id` / `X-Request-Id`
+- Do not log Stripe secrets, API keys, or full PII
+
+## Backup Strategies
+
+### Database Backups
+
+- Automated Postgres backups with point-in-time recovery if required
+- Test restore before production cutover
+- Include tenant-scoped data verification after restore
+
+### Invoice PDF Backups
+
+- Backup `BILLING_INVOICE_PDF_STORAGE_PATH` volume or object storage mirror
+- Align retention with legal and tax requirements
+
+### Configuration Backups
+
+- Version control deployment manifests and non-secret config
+- Store secrets in vault; document rotation procedures
+
+## Deployment Process
+
+1. **Pre-deployment**
+ - Run full test suite and security scans
+ - Review **[Accepted risks](../security/accepted-risks.md)** for operator obligations
+ - Validate environment on staging with production-like `TENANTS` and Stripe test mode
+
+2. **Deployment**
+ - Deploy Postgres and Redis
+ - Deploy API; wait for healthy `/api/health`
+ - Deploy scheduler and worker
+ - Deploy frontends
+ - Register Stripe webhook endpoint for production URL
+
+3. **Post-deployment**
+ - Verify health endpoints
+ - Create test subscription and invoice in staging tenant
+ - Confirm coordinator jobs appear in Bull Board
+ - Monitor logs for migration or Redis connection errors
+
+## Rollback Plan
+
+- Keep previous image tags available in registry
+- Document database migration rollback constraints (billing migrations may be forward-only)
+- Practice rollback on staging with volume snapshots
+
+## Related Documentation
+
+- **[Docker Deployment](./docker-deployment.md)** - Containerized deployment
+- **[Environment Configuration](./environment-configuration.md)** - Environment variables
+- **[Background Jobs](./background-jobs.md)** - Queue startup order
+- **[Troubleshooting](../troubleshooting/README.md)** - Problem-solving guides
+
+---
+
+_For detailed deployment steps, see [Docker Deployment](./docker-deployment.md)._
diff --git a/docs/decabill/features/README.md b/docs/decabill/features/README.md
new file mode 100644
index 000000000..a309d9f52
--- /dev/null
+++ b/docs/decabill/features/README.md
@@ -0,0 +1,217 @@
+# Features Documentation
+
+This section provides comprehensive documentation for all features in the Decabill billing product.
+
+## Overview
+
+Decabill provides a complete set of capabilities for subscription billing, invoicing, payments, and optional infrastructure provisioning:
+
+- **Authentication** - Keycloak OAuth2/OIDC, built-in users with JWT, or static API key
+- **Multi-tenancy** - Tenant-scoped data with `X-Tenant` header and configurable tenant frontends
+- **Subscriptions** - Order, cancel, and resume service plans with optional cloud provisioning
+- **Invoices** - ZUGFeRD PDFs, open positions, billing-day accumulation, and Stripe checkout
+- **Service Types and Plans** - Admin-managed catalog with provider schemas and pricing
+- **Billing Administration** - Manual invoices, customer profiles, KPIs, and bill-now
+- **Customer Profiles** - Self-service and admin billing metadata required for ordering
+- **Dashboard and Server Control** - Overview of subscriptions with start, stop, and restart actions
+- **Real-time Status** - WebSocket dashboard stream for provisioned server status
+- **Backorders** - Queue and retry when provider capacity is unavailable
+- **Payment Processing** - Stripe checkout and webhook-driven payment state
+- **Dynamic Provider Plugins** - Extend payment processors and billing UI metadata at runtime
+- **Server Provisioning** - Cloud-init deployment of bundled product stacks for eligible plans
+
+## Features
+
+### [Authentication](./authentication.md)
+
+Multiple authentication methods with configurable user registration. Supports API key, Keycloak OAuth2/OIDC, and built-in users with JWT.
+
+**Key Capabilities**:
+
+- Static API key for automation and single-operator deployments
+- Keycloak OAuth2/OIDC for enterprise SSO
+- Built-in user registration with email confirmation
+- Password reset with 6-character alphanumeric codes
+- Admin user management and optional signup disable
+
+### [Multi-tenancy](./multi-tenancy.md)
+
+Isolate billing data per tenant while sharing one billing manager deployment. Same email can register separately in each tenant.
+
+**Key Capabilities**:
+
+- `X-Tenant` header on HTTP and WebSocket requests
+- `TENANTS` environment allowlist
+- Per-tenant Stripe return URLs via `TENANT_FRONTEND_URLS`
+- Optional `STATIC_API_KEY_TENANT_ID` to bind API key auth to one tenant
+
+### [Subscriptions](./subscriptions.md)
+
+Order service plans, manage lifecycle (cancel, resume), and provision cloud instances when the plan includes infrastructure.
+
+**Key Capabilities**:
+
+- Subscription creation with availability checks and provider config validation
+- Cancel and resume with effective dates
+- Subscription items with provisioning status and hostname reservation
+- Usage records for usage-based pricing
+
+### [Invoices](./invoices.md)
+
+Issue, preview, download, void, and pay invoices. Open positions accumulate until each user's billing day.
+
+**Key Capabilities**:
+
+- ZUGFeRD-style PDFs with EN 16931 XML embedded
+- Open positions and billing-day scheduler
+- Stripe checkout initiation and webhook reconciliation
+- Admin manual invoice draft, edit, and issue workflow
+
+### [Service Types and Plans](./service-types-and-plans.md)
+
+Admin-managed catalog of service types, provisioning providers, and priced service plans.
+
+**Key Capabilities**:
+
+- Provider registry with config schemas and server type pricing
+- Public unauthenticated plan offerings for marketing pages
+- Customer geography selection when the provider schema supports it
+- Pricing preview before order
+
+### [Billing Administration](./billing-administration.md)
+
+Admin-only features for manual invoices, customer billing profiles, operational dashboards, and bill-now.
+
+**Key Capabilities**:
+
+- Draft, edit, issue, and void manual invoices
+- Customer billing profile CRUD
+- Billing summary, statistics, and open or overdue invoice lists
+- Bill-now to force invoice generation outside the scheduler
+
+### [Customer Profiles](./customer-profiles.md)
+
+Billing metadata required before subscription orders and for compliant invoice issuance.
+
+**Key Capabilities**:
+
+- Self-service `GET/POST /customer-profile`
+- Admin CRUD under `/admin/billing/customer-profiles`
+- Stripe customer ID stored on profile when payments are initiated
+- Completeness validation before `POST /subscriptions`
+
+### [Dashboard and Server Control](./dashboard-and-server-control.md)
+
+Customer overview of active subscriptions with live server status and power actions.
+
+**Key Capabilities**:
+
+- Overview page with subscription cards and server info
+- Start, stop, and restart provisioned servers
+- REST fallback when WebSocket is not configured
+- Links to invoices and subscription detail
+
+### [Real-time Status](./real-time-status.md)
+
+Socket.IO dashboard status stream for provisioned subscription items.
+
+**Key Capabilities**:
+
+- `subscribeDashboardStatus` with configurable poll interval
+- User-scoped subscription selection on every tick
+- JWT or Keycloak handshake auth (API key rejected)
+- `dashboardStatusUpdate` events mirroring REST server-info shape
+
+### [Backorders](./backorders.md)
+
+Queue subscription requests when provider capacity is unavailable and retry automatically or on demand.
+
+**Key Capabilities**:
+
+- Automatic backorder creation when ordering with `autoBackorder`
+- Scheduled retry processor for pending and retrying backorders
+- Manual retry and cancel via API
+- Encrypted requested config snapshot at rest
+
+### [Payment Processing](./payment-processing.md)
+
+Stripe checkout sessions and webhook-driven payment reconciliation.
+
+**Key Capabilities**:
+
+- `POST .../pay` initiates Stripe Checkout
+- Tenant-aware success and cancel redirect URLs
+- Idempotent Stripe webhook handling
+- Default processor configurable via `BILLING_DEFAULT_PAYMENT_PROCESSOR`
+
+### [Dynamic Provider Plugins](./dynamic-provider-plugins.md)
+
+Extend billing backends with extra payment processors and billing UI provider metadata without forking the image.
+
+**Key Capabilities**:
+
+- `DYNAMIC_PAYMENT_PROCESSORS` for payment backends
+- `DYNAMIC_BILLING_PROVIDER_METADATA` for admin UI registry entries
+- Baked-in or post-build plugin loading via shared dynamic provider registry
+- Critical registry fail-fast in production
+
+### [Server Provisioning](./server-provisioning.md)
+
+Automated cloud server provisioning via cloud-init when service plans include infrastructure.
+
+**Key Capabilities**:
+
+- Hetzner Cloud and DigitalOcean built-in providers
+- Docker stack with PostgreSQL, backend API, and frontend console behind Nginx
+- Let's Encrypt TLS with DNS A record creation
+- SSH-based subscription item update scheduler
+
+## Feature Relationships
+
+```mermaid
+graph TB
+ AUTH[Authentication]
+ MT[Multi-tenancy]
+ ST[Service Types and Plans]
+ SUB[Subscriptions]
+ CP[Customer Profiles]
+ INV[Invoices]
+ PP[Payment Processing]
+ BA[Billing Administration]
+ BO[Backorders]
+ SP[Server Provisioning]
+ DASH[Dashboard and Server Control]
+ RT[Real-time Status]
+ DP[Dynamic Provider Plugins]
+
+ AUTH --> MT
+ MT --> SUB
+ MT --> INV
+ MT --> BA
+ ST --> SUB
+ CP --> SUB
+ CP --> INV
+ SUB --> SP
+ SUB --> BO
+ SUB --> DASH
+ SP --> DASH
+ DASH --> RT
+ INV --> PP
+ BA --> INV
+ BA --> CP
+ DP --> PP
+ DP --> ST
+ SUB --> INV
+```
+
+## Related Documentation
+
+- **[Getting Started](../getting-started.md)** - Quick start guide
+- **[Architecture](../architecture/README.md)** - System architecture
+- **[Applications](../applications/README.md)** - Application documentation
+- **[Deployment](../deployment/README.md)** - Deployment guides
+- **[API Reference](../api-reference/README.md)** - OpenAPI and AsyncAPI specifications
+
+---
+
+_For detailed information about each feature, see the individual feature documentation pages._
diff --git a/docs/decabill/features/authentication.md b/docs/decabill/features/authentication.md
new file mode 100644
index 000000000..ff8a0a749
--- /dev/null
+++ b/docs/decabill/features/authentication.md
@@ -0,0 +1,227 @@
+# Authentication
+
+Authentication system supporting multiple methods with configurable user registration for the billing console and billing manager API.
+
+## Overview
+
+Decabill supports three authentication methods:
+
+- **API Key Authentication** - Static API key for automation and operator scripts
+- **Keycloak Authentication** - OAuth2/OIDC via Keycloak
+- **Users Authentication** - Built-in user registration with JWT
+
+Each method is configured via environment variables on the billing manager. The billing console runtime config must match the backend method.
+
+## Authentication Methods
+
+### API Key Authentication
+
+Simple authentication using a static API key. Suitable for automation, CI, and single-operator deployments.
+
+**Configuration**:
+
+```bash
+AUTHENTICATION_METHOD=api-key
+STATIC_API_KEY=your-secure-api-key-here
+```
+
+When `STATIC_API_KEY` is set and `AUTHENTICATION_METHOD` is unset, the backend may infer api-key mode. See [Security - Operational hardening](../security/operational-hardening.md) for resolution behavior.
+
+**Features**:
+
+- All requests require `Authorization: Bearer ` or `Authorization: ApiKey ` header
+- API key authentication grants admin rights on billing admin routes
+- No interactive user identity; WebSocket dashboard status is rejected (see [Real-time Status](./real-time-status.md))
+- Combine with [Multi-tenancy](./multi-tenancy.md) and optional `STATIC_API_KEY_TENANT_ID`
+
+### Keycloak Authentication
+
+Enterprise-grade authentication using Keycloak OAuth2/OIDC.
+
+**Configuration**:
+
+```bash
+AUTHENTICATION_METHOD=keycloak
+KEYCLOAK_AUTH_SERVER_URL=http://localhost:8380
+KEYCLOAK_REALM=decabill
+KEYCLOAK_CLIENT_ID=billing-manager
+KEYCLOAK_CLIENT_SECRET=your-client-secret
+```
+
+**Features**:
+
+- OAuth2/OIDC authentication flow in the billing console
+- Users are synced to the local `users` table
+- First synced user gets admin role, subsequent users get user role
+- Integration with existing identity providers and MFA via Keycloak
+- Per-user `tenant_id` enforced by [Multi-tenancy](./multi-tenancy.md)
+
+### Users Authentication
+
+Built-in user registration and authentication with JWT tokens.
+
+**Configuration**:
+
+```bash
+AUTHENTICATION_METHOD=users
+JWT_SECRET=your-jwt-secret-key
+DISABLE_SIGNUP=false
+```
+
+**Features**:
+
+- User registration with email and password
+- Email confirmation with 6-character alphanumeric codes
+- Password reset functionality
+- JWT-based authentication (7-day expiry)
+- First registered user gets admin role
+- Admin user management (CRUD, lock, unlock)
+- Optional signup disable for controlled onboarding
+
+## Users Authentication Flow
+
+### Registration
+
+1. User registers with email and password
+2. System checks if signup is enabled (`DISABLE_SIGNUP`)
+3. If signup is disabled, registration returns 503 Service Unavailable
+4. If enabled, user account is created in the current tenant (from `X-Tenant`):
+ - First user in the tenant: auto-confirmed and assigned admin role
+ - Subsequent users: receive confirmation code via email
+
+### Email Confirmation
+
+1. User receives confirmation code via email
+2. User submits email and code on the confirmation page
+3. System validates code and confirms email
+4. User can log in
+
+### Login
+
+1. User enters email and password
+2. System validates credentials and tenant scope
+3. System checks email confirmation and account lock state
+4. JWT token is issued and stored client-side
+5. Token is included in subsequent HTTP and WebSocket requests
+
+### Password Reset
+
+1. User requests password reset with email
+2. System sends 6-character alphanumeric reset code via email
+3. User submits email, code, and new password
+4. System validates code and updates password
+
+## Disabling Signup
+
+When `DISABLE_SIGNUP=true`:
+
+- `POST /auth/register` returns 503 with message "Signup is disabled"
+- Admin user creation via `POST /users` remains available
+- Billing console hides "Create an account" and redirects `/register` to login
+
+Frontend runtime config should set `authentication.disableSignup` to match the backend.
+
+## User Roles
+
+### Admin Role
+
+- Full access to billing admin routes under `/admin/billing/*`
+- User management (create, read, update, delete, lock, unlock)
+- Service type and service plan administration
+- Manual invoice and customer profile administration
+
+### User Role
+
+- Standard customer access: subscriptions, invoices, customer profile
+- Cannot access admin routes
+- Can change own password and update own profile
+
+## Security Features
+
+### Password Security
+
+- Passwords hashed with bcrypt
+- Minimum password length enforced
+- Password confirmation required on registration
+
+### Token Security
+
+- JWT tokens expire after 7 days
+- Each request verifies the user still exists and is not locked
+- Keycloak mode applies the same lock check against the synced local user row
+- SPA HTTP interceptor dispatches logout on 401 with session-ending messages
+
+### Rate Limiting
+
+- Authentication endpoints are rate-limited
+- Prevents brute force attacks
+
+## API Endpoints
+
+### Authentication Endpoints (Public)
+
+- `POST /auth/login` - Login with email and password
+- `POST /auth/register` - Register new user (503 when signup disabled)
+- `POST /auth/confirm-email` - Confirm email with code
+- `POST /auth/request-password-reset` - Request password reset
+- `POST /auth/reset-password` - Reset password with code
+- `POST /auth/change-password` - Change password (authenticated)
+
+### User Management Endpoints (Admin Only)
+
+- `GET /users` - List users
+- `POST /users` - Create user
+- `GET /users/{id}` - Get user
+- `POST /users/{id}` - Update user
+- `DELETE /users/{id}` - Delete user
+- `POST /users/{id}/lock` - Lock user account
+- `POST /users/{id}/unlock` - Unlock user account
+
+See [Billing Manager OpenAPI](/spec/billing-manager/openapi.yaml) for request and response schemas.
+
+## Authentication Flow Diagram
+
+```mermaid
+flowchart TB
+ subgraph AUTH["Authentication Methods"]
+ direction TB
+ AUTH_METHOD["AUTHENTICATION_METHOD env"]
+ AUTH_METHOD --> API_KEY["api-key"]
+ AUTH_METHOD --> KEYCLOAK["keycloak"]
+ AUTH_METHOD --> USERS["users"]
+ end
+
+ subgraph API_KEY_FLOW["API Key Flow"]
+ API_KEY --> AK1["STATIC_API_KEY required"]
+ AK1 --> AK2["Authorization: Bearer or ApiKey header"]
+ AK2 --> AK3["Admin rights on billing admin routes"]
+ AK2 --> AK4["No WebSocket dashboard user stream"]
+ end
+
+ subgraph KEYCLOAK_FLOW["Keycloak Flow"]
+ KEYCLOAK --> KC1["Keycloak OAuth2 / OIDC"]
+ KC1 --> KC2["User synced to users table"]
+ KC2 --> KC3["First user = admin, rest = user"]
+ KC2 --> KC4["tenant_id enforced per request"]
+ end
+
+ subgraph USERS_FLOW["Users Flow"]
+ USERS --> UF1["JWT-based auth"]
+ UF1 --> UF2["Register / Login / Confirm Email"]
+ UF2 --> UF3["DISABLE_SIGNUP: register returns 503"]
+ UF2 --> UF4["First user in tenant = admin"]
+ UF4 --> UF5["Admin CRUD and lock/unlock"]
+ end
+```
+
+## Related Documentation
+
+- **[Multi-tenancy](./multi-tenancy.md)** - Tenant header and API key scope
+- **[Environment Configuration](../deployment/environment-configuration.md)** - Environment variable reference
+- **[Security - Accepted risks](../security/accepted-risks.md)** - Authentication and tenant scope entries
+- **[Backend Billing Manager](../applications/backend-billing-manager.md)** - Backend authentication implementation
+- **[Frontend Billing Console](../applications/frontend-billing-console.md)** - Frontend authentication UI
+
+---
+
+_For detailed API specifications, see [Billing Manager OpenAPI](/spec/billing-manager/openapi.yaml)._
diff --git a/docs/decabill/features/backorders.md b/docs/decabill/features/backorders.md
new file mode 100644
index 000000000..cd27d4d2f
--- /dev/null
+++ b/docs/decabill/features/backorders.md
@@ -0,0 +1,96 @@
+# Backorders
+
+Queue subscription requests when cloud provider capacity is unavailable and retry automatically or on demand.
+
+## Overview
+
+When a user orders a plan with provisioning and capacity is unavailable (or provisioning fails with backorder enabled), Decabill creates a **backorder** record. A background processor and manual retry API attempt fulfillment when capacity returns.
+
+Requested configuration is stored as an encrypted snapshot at rest (AES-256-GCM).
+
+## Backorder Statuses
+
+| Status | Description |
+| ----------- | ------------------------------------- |
+| `pending` | Waiting for retry |
+| `retrying` | Last attempt failed; will retry later |
+| `fulfilled` | Successfully provisioned |
+| `canceled` | User or operator canceled |
+
+## Creating Backorders
+
+### At Subscription Order
+
+Pass `autoBackorder: true` on `POST /subscriptions`:
+
+- If availability check fails, a backorder is created instead of failing silently
+- If provisioning throws after subscription item creation, a backorder captures the failure with `providerErrors`
+
+Without `autoBackorder`, unavailable capacity returns an error to the client.
+
+### Snapshot Contents
+
+Each backorder stores:
+
+- `userId`, `serviceTypeId`, `planId`
+- `requestedConfigSnapshot` (encrypted)
+- `providerErrors` and optional `failureReason`
+- `preferredAlternatives` when suggested by availability service
+- `retryAfter` for scheduled backoff
+
+## Retry Processing
+
+A scheduled job processes pending and retrying backorders:
+
+```mermaid
+sequenceDiagram
+ participant Job as Backorder Retry Job
+ participant BO as BackorderService
+ participant Avail as AvailabilityService
+ participant Prov as ProvisioningService
+ participant DB as PostgreSQL
+
+ Job->>DB: SELECT pending/retrying backorders
+ loop Each backorder
+ Job->>BO: retry(backorderId)
+ BO->>Avail: checkAvailability
+ alt Available
+ BO->>DB: CREATE subscription + item
+ BO->>Prov: provision
+ BO->>DB: UPDATE backorder fulfilled
+ else Not available
+ BO->>DB: UPDATE status retrying
+ end
+ end
+```
+
+Fulfillment follows the same hostname reservation, provisioning, and DNS A record steps as immediate orders. See [Server Provisioning](./server-provisioning.md).
+
+## User API
+
+| Method | Path | Purpose |
+| ------ | ------------------------- | ---------------------- |
+| GET | `/backorders` | List user's backorders |
+| POST | `/backorders/{id}/retry` | Manual retry |
+| POST | `/backorders/{id}/cancel` | Cancel backorder |
+
+Manual retry runs the same availability and provisioning path as the scheduler.
+
+## Customer Geography on Retry
+
+When the original plan allowed customer location selection, the encrypted snapshot preserves geography overrides. Retry merges the snapshot with current plan defaults before provisioning.
+
+## UI
+
+The billing console lists backorders on the customer subscriptions flow with status, plan name, and retry or cancel actions where permitted.
+
+## Related Documentation
+
+- **[Subscriptions](./subscriptions.md)** - Order flow and `autoBackorder`
+- **[Service Types and Plans](./service-types-and-plans.md)** - Availability endpoints
+- **[Server Provisioning](./server-provisioning.md)** - Provisioning on fulfillment
+- **[Billing Manager OpenAPI](/spec/billing-manager/openapi.yaml)** - Backorder schemas
+
+---
+
+_Backorders prevent lost orders during provider capacity shortages and automate fulfillment when resources become available._
diff --git a/docs/decabill/features/billing-administration.md b/docs/decabill/features/billing-administration.md
new file mode 100644
index 000000000..fde2c9366
--- /dev/null
+++ b/docs/decabill/features/billing-administration.md
@@ -0,0 +1,107 @@
+# Billing Administration
+
+Admin-only features in the billing console for manual invoice management, customer billing profile CRUD, operational dashboards, and bill-now.
+
+See also: [API Reference](../api-reference/README.md) for the published OpenAPI and AsyncAPI specifications.
+
+## Access Control
+
+All endpoints under `/admin/billing/*` require admin role (`@KeycloakRoles(ADMIN)` + `@UsersRoles(ADMIN)`). Frontend routes use `authGuard` + `billingAdminGuard`.
+
+**Multi-tenancy:** Admin and user routes are scoped by **`X-Tenant`** and the user's **`tenant_id`**. API key auth with **`STATIC_API_KEY`** and without **`STATIC_API_KEY_TENANT_ID`** can administer **all** configured tenants (accepted risk **[DR-002](../security/accepted-risks.md#dr-002--billing-multi-tenant-api-key-scope-static_api_key_tenant_id-unset)**).
+
+## Billing Dashboard
+
+**Frontend route:** `/administration/billing`
+
+Split layout with dashboard cards and charts on the left, invoice list on the right.
+
+### KPIs and Statistics
+
+| Method | Path | Purpose |
+| ------ | -------------------------------------- | ---------------------------- |
+| GET | `/admin/billing/summary` | High-level billing summary |
+| GET | `/admin/billing/statistics/summary` | Aggregated statistics |
+| GET | `/admin/billing/statistics/by-product` | Breakdown by product or plan |
+
+### Invoice Lists
+
+| Method | Path | Purpose |
+| ------ | -------------------------------------- | ------------------------- |
+| GET | `/admin/billing/invoices` | Paginated invoice list |
+| GET | `/admin/billing/invoices/open-overdue` | Open and overdue invoices |
+
+The invoice list supports batch loading with client-side search in list-group style.
+
+### Bill Now
+
+`POST /admin/billing/bill-now` triggers immediate invoice generation for selected users, bypassing the billing-day scheduler when operators need on-demand billing.
+
+## Manual Invoice Administration
+
+**Immutability:** Only invoices in `draft` status can be edited or deleted. Once issued (`issued`, `paid`, `partially_paid`, `overdue`, or `void`), line items and amounts are immutable. Admins can still void unpaid issued invoices or mark payment status manually.
+
+**Workflow:**
+
+1. `POST /admin/billing/invoices/manual` - Create draft with user, optional subscription, custom line items
+2. `POST /admin/billing/invoices/{invoiceRefId}` - Update draft line items
+3. `POST /admin/billing/invoices/{invoiceRefId}/issue` - Issue draft (requires complete customer profile)
+4. `DELETE /admin/billing/invoices/{invoiceRefId}` - Delete draft only
+
+Additional admin actions on issued invoices:
+
+- `POST /admin/billing/invoices/{invoiceRefId}/void` - Void invoice
+- `POST /admin/billing/invoices/{invoiceRefId}/mark-paid` - Mark paid manually
+- `POST /admin/billing/invoices/{invoiceRefId}/mark-unpaid` - Revert paid status
+- `GET /admin/billing/invoices/{invoiceRefId}/audit-logs` - Audit trail
+- `GET /admin/billing/invoices/{invoiceRefId}/pdf` - Download PDF
+- `GET /admin/billing/invoices/{invoiceRefId}/void-document/pdf` - Void document PDF
+
+```mermaid
+sequenceDiagram
+ participant Admin
+ participant API as Billing Manager
+ participant DB as PostgreSQL
+
+ Admin->>API: POST /admin/billing/invoices/manual
+ API->>DB: Insert draft invoice
+ Admin->>API: POST /admin/billing/invoices/{id} (edit lines)
+ Admin->>API: POST /admin/billing/invoices/{id}/issue
+ API->>API: Validate customer profile complete
+ API->>DB: Issue invoice (immutable)
+```
+
+## Customer Billing Profiles (Admin)
+
+Customer billing data is stored in `billing_customer_profiles` (one profile per user).
+
+| Method | Path | Purpose |
+| ------ | --------------------------------------- | ------------------------------------------------------ |
+| GET | `/admin/billing/customer-profiles` | Paginated list |
+| GET | `/admin/billing/customer-profiles/{id}` | Full profile detail |
+| POST | `/admin/billing/customer-profiles` | Create for user |
+| POST | `/admin/billing/customer-profiles/{id}` | Update |
+| DELETE | `/admin/billing/customer-profiles/{id}` | Delete (blocked if user has invoices or subscriptions) |
+
+Self-service `GET/POST /customer-profile` remains for end users. See [Customer Profiles](./customer-profiles.md).
+
+**Frontend:** `/administration/customer-profiles` in the billing console.
+
+## Related Admin Pages
+
+- **Billing dashboard** (`/administration/billing`) - KPIs, charts, bill-now
+- **Customer profiles** (`/administration/customer-profiles`) - Admin CRUD
+- **Service types and plans** - Catalog administration in the billing console
+- **Users** (`/users`) - Shared identity user manager
+
+## Related Documentation
+
+- **[Invoices](./invoices.md)** - Status model and open positions
+- **[Customer Profiles](./customer-profiles.md)** - Profile fields and validation
+- **[Multi-tenancy](./multi-tenancy.md)** - Tenant scope and DR-002
+- **[Authentication](./authentication.md)** - Admin role requirements
+- **[Billing Manager OpenAPI](/spec/billing-manager/openapi.yaml)** - Full admin path schemas
+
+---
+
+_For manual invoice diagrams, see the billing manager feature module documentation._
diff --git a/docs/decabill/features/customer-profiles.md b/docs/decabill/features/customer-profiles.md
new file mode 100644
index 000000000..ac9bcbb3b
--- /dev/null
+++ b/docs/decabill/features/customer-profiles.md
@@ -0,0 +1,84 @@
+# Customer Profiles
+
+Billing metadata for invoice issuance and subscription ordering. One profile per user per tenant.
+
+## Overview
+
+Customer profiles store legal and contact information required for compliant invoices and Stripe customer records. Subscription creation rejects incomplete profiles with 400 Bad Request.
+
+Profiles are managed through self-service endpoints for customers and admin CRUD for operators.
+
+## Required Fields for Ordering
+
+Before `POST /subscriptions`, the backend validates:
+
+- First name
+- Last name
+- Email
+- Address line
+- City
+- Country
+
+Optional fields may include company name, VAT ID, postal code, and phone depending on deployment configuration and invoice issuer rules.
+
+## Self-Service
+
+| Method | Path | Purpose |
+| ------ | ------------------- | ------------------------------- |
+| GET | `/customer-profile` | Retrieve current user's profile |
+| POST | `/customer-profile` | Create or update profile |
+
+The billing console exposes a customer profile page for authenticated users to complete or update billing details before ordering.
+
+### Stripe Integration
+
+When the user initiates payment, the billing manager creates or updates a Stripe Customer and stores the Stripe customer id on the profile for subsequent checkout sessions.
+
+## Admin Management
+
+Admins manage profiles under `/admin/billing/customer-profiles`. See [Billing Administration](./billing-administration.md).
+
+Rules:
+
+- One profile per user
+- Delete is blocked when the user has existing invoices or subscriptions
+- Admin create is used when onboarding customers who cannot self-register
+
+**Frontend route:** `/administration/customer-profiles`
+
+## Validation Flow
+
+```mermaid
+flowchart TD
+ Order[POST /subscriptions] --> Check{Profile complete?}
+ Check -->|No| Reject[400 Bad Request]
+ Check -->|Yes| Avail[Availability check]
+ Avail --> Create[Create subscription]
+ Issue[POST .../issue manual invoice] --> CheckIssue{Profile complete?}
+ CheckIssue -->|No| RejectIssue[400 Bad Request]
+ CheckIssue -->|Yes| Issued[Issue invoice]
+```
+
+Manual invoice issuance uses the same completeness rules for the target user.
+
+## Data Storage
+
+Profiles are stored in `billing_customer_profiles` in PostgreSQL, scoped by tenant through the user's `tenant_id`.
+
+Sensitive fields follow standard application encryption and access controls. Stripe customer ids are stored for payment orchestration only.
+
+## User Billing Day
+
+The user's registration date (day of month, capped at 28) defaults as their **billing day** for open position accumulation. This is stored on the user record and is independent of the service plan's `billing_day_of_month`. See [Invoices](./invoices.md).
+
+## Related Documentation
+
+- **[Subscriptions](./subscriptions.md)** - Profile required at order time
+- **[Invoices](./invoices.md)** - Issuer and customer data on PDFs
+- **[Billing Administration](./billing-administration.md)** - Admin profile CRUD
+- **[Payment Processing](./payment-processing.md)** - Stripe customer linkage
+- **[Billing Manager OpenAPI](/spec/billing-manager/openapi.yaml)** - Profile DTO schemas
+
+---
+
+_Complete your profile in the billing console before placing your first subscription order._
diff --git a/docs/decabill/features/dashboard-and-server-control.md b/docs/decabill/features/dashboard-and-server-control.md
new file mode 100644
index 000000000..13c282d68
--- /dev/null
+++ b/docs/decabill/features/dashboard-and-server-control.md
@@ -0,0 +1,94 @@
+# Dashboard and Server Control
+
+Customer overview of subscriptions with live server status and power actions for provisioned infrastructure.
+
+## Overview
+
+The billing console **Overview** page (`/overview`) lists active subscriptions with provisioned items. For each item, users see hostname, FQDN, IP addresses, provider status, and action buttons to start, stop, or restart the cloud server.
+
+Status updates come from the [Real-time Status](./real-time-status.md) WebSocket when configured, or from REST polling via `GET .../server-info`.
+
+## Overview Page
+
+### Displayed Information
+
+Per subscription item with active provisioning:
+
+- Subscription and plan title
+- Provisioning status
+- Server name, public IP, hostname, and FQDN
+- Provider-reported power state (running, off, etc.)
+- Quick links to invoices and subscription detail
+
+### Server Action Buttons
+
+| Action | When shown | API |
+| ------- | -------------------------- | -------------------------- |
+| Start | Server is stoppable or off | `POST .../actions/start` |
+| Stop | Server is online | `POST .../actions/stop` |
+| Restart | Server is online | `POST .../actions/restart` |
+
+Buttons are disabled while an action is in progress. After a successful action, the UI refreshes server info from REST (or waits for the next WebSocket tick when the socket is connected).
+
+## Data Loading
+
+### With WebSocket
+
+When `billing.websocketUrl` is set in frontend runtime config:
+
+1. NgRx effects connect to the billing status gateway
+2. On connect, client emits `subscribeDashboardStatus`
+3. Server pushes `dashboardStatusUpdate` on each poll tick
+4. Overview binds to the merged subscription and server info state
+
+See [Real-time Status](./real-time-status.md).
+
+### Without WebSocket
+
+When WebSocket URL is not configured:
+
+1. Overview dispatches `loadOverviewServerInfo`
+2. Client fetches subscriptions and server info via REST for each item
+3. Server actions trigger immediate REST refresh after completion
+
+## Authorization
+
+Users see only their own subscriptions and items. Server control actions verify subscription ownership on every request. Admin users using the customer overview still see only their personal subscriptions unless using admin routes.
+
+API key authentication does not populate an end-user identity on the overview WebSocket path; use Keycloak or users auth for the dashboard stream.
+
+## Server Control Sequence
+
+```mermaid
+sequenceDiagram
+ participant User
+ participant UI as Billing Console
+ participant API as Billing Manager
+ participant Cloud as Cloud Provider
+
+ User->>UI: Click Restart
+ UI->>API: POST .../actions/restart
+ API->>Cloud: Restart server
+ Cloud-->>API: Success
+ API-->>UI: 200 success
+ UI->>API: GET .../server-info
+ API->>Cloud: Fetch status
+ API-->>UI: Updated server info
+```
+
+## Error Handling
+
+- Failed actions show error state in the overview card
+- Missing or failed server info displays a loading or error placeholder
+- Provisioning failures link to subscription detail and [Backorders](./backorders.md) when applicable
+
+## Related Documentation
+
+- **[Real-time Status](./real-time-status.md)** - WebSocket dashboard stream
+- **[Subscriptions](./subscriptions.md)** - Subscription items and server-info endpoint
+- **[Server Provisioning](./server-provisioning.md)** - What gets provisioned
+- **[Billing Manager OpenAPI](/spec/billing-manager/openapi.yaml)** - Action endpoint schemas
+
+---
+
+_Configure `billing.websocketUrl` in the billing console runtime config for live status without manual refresh._
diff --git a/docs/decabill/features/dynamic-provider-plugins.md b/docs/decabill/features/dynamic-provider-plugins.md
new file mode 100644
index 000000000..e9af1510c
--- /dev/null
+++ b/docs/decabill/features/dynamic-provider-plugins.md
@@ -0,0 +1,157 @@
+# Dynamic Provider Plugins
+
+Extend the billing manager with extra payment processors and billing UI provider metadata at runtime without forking the application image.
+
+## Overview
+
+Decabill uses the shared `@forepath/shared/backend/util-dynamic-provider-registry` loader. Provider packages can be **baked into** the billing manager deploy graph or **mounted post-build** into `DYNAMIC_PROVIDER_PLUGIN_PATH`.
+
+This page covers **Decabill billing manager** registries only.
+
+## Registries
+
+| Env var | Criticality | Registers |
+| ----------------------------------- | ----------- | ------------------------------------------------------ |
+| `DYNAMIC_PAYMENT_PROCESSORS` | critical | Payment processor implementations |
+| `DYNAMIC_BILLING_PROVIDER_METADATA` | optional | Admin UI provider metadata (`providerMetadata` export) |
+
+Shared tuning:
+
+| Variable | Purpose |
+| --------------------------------- | ------------------------------------------------------------- |
+| `DYNAMIC_PROVIDERS_FAIL_FAST` | When `true`, critical registries abort startup on load errors |
+| `DYNAMIC_PROVIDER_PLUGIN_PATH` | Absolute plugin root inside the container |
+| `DYNAMIC_PROVIDER_PLUGIN_INSTALL` | Comma-separated `npm install` targets at startup |
+
+**Production:** set `DYNAMIC_PROVIDERS_FAIL_FAST=true` when `DYNAMIC_PAYMENT_PROCESSORS` is non-empty.
+
+## Resolution Order
+
+For each `DYNAMIC_*` entry the loader:
+
+1. **Baked-in** - resolves the package from `/app/package.json` (image build graph)
+2. **Plugin path** - looks up the package by `package.json` name under `DYNAMIC_PROVIDER_PLUGIN_PATH`
+3. **Fail** - logs and skips, or aborts startup when critical and fail-fast is enabled
+
+Baked-in wins when the same package exists in both places.
+
+```mermaid
+flowchart TD
+ env[DYNAMIC_* entry] --> parse[parseProviderPackageSpec]
+ parse --> resolve[resolveProviderLoadTarget]
+ resolve --> baked{Baked into /app?}
+ baked -->|yes| loadBaked[loadProviderModule]
+ baked -->|no| plugin{Plugin path set?}
+ plugin -->|yes| index[Index by package.json name]
+ index --> loadPlugin[load from filesystem]
+ plugin -->|no| fail[Skip or fail-fast]
+ loadBaked --> register[registerDynamicProviders]
+ loadPlugin --> register
+```
+
+## Config Format
+
+```bash
+# alias=@package/specifier
+DYNAMIC_PAYMENT_PROCESSORS=acme=@forepath/decabill/backend/payment-acme
+
+# PascalCase alias selects named class export
+DYNAMIC_BILLING_PROVIDER_METADATA=AcmeMeta=@forepath/decabill/backend/billing-provider-acme
+
+# bare specifier
+DYNAMIC_PAYMENT_PROCESSORS=@forepath/decabill/backend/payment-acme
+
+# file: entry relative to plugin path
+DYNAMIC_PAYMENT_PROCESSORS=acme=file:payment-acme
+```
+
+Allowed package name prefixes: `@forepath/`, `@decabill/`. Do not combine `file:` with an `@forepath/` specifier on the same entry.
+
+## Plugin Package Contract
+
+External packages must export one of:
+
+1. **`createProvider`** (preferred) - `(moduleRef: ModuleRef) => T | Promise`
+2. **Named PascalCase class** - via entry alias or `package.json`:
+
+```json
+{
+ "forepath": {
+ "providerExport": "AcmePaymentProcessor"
+ }
+}
+```
+
+For billing UI metadata packages, export **`providerMetadata`** array compatible with the provider registry service.
+
+Declare Nest and host dependencies as **peerDependencies** resolved from `/app/node_modules`.
+
+## Payment Processors
+
+Processors implement the `PaymentProcessor` interface:
+
+- Register with `PaymentProcessorFactory` at module bootstrap
+- Handle checkout session creation and webhook processing for their provider
+- Expose a unique `type` string matching `BILLING_DEFAULT_PAYMENT_PROCESSOR`
+
+Built-in: `stripe` via `StripePaymentProcessor`. See [Payment Processing](./payment-processing.md).
+
+## Billing Provider Metadata
+
+`DYNAMIC_BILLING_PROVIDER_METADATA` adds entries to `GET /service-types/providers` for admin UI dropdowns and config schema rendering without implementing full provisioning in the same package.
+
+Built-in Hetzner and DigitalOcean providers register statically when API tokens are present.
+
+## Baked-in Plugins
+
+1. Add the provider package to the billing manager deploy graph
+2. Set the relevant `DYNAMIC_*` variable
+3. Rebuild the container image
+
+## Post-build Plugins
+
+1. Build the plugin to compiled JS with `package.json`
+2. Mount into `./provider-plugins/` (compose maps to `/var/lib/forepath/provider-plugins`) and/or set `DYNAMIC_PROVIDER_PLUGIN_INSTALL`
+3. Set `DYNAMIC_PROVIDER_PLUGIN_PATH=/var/lib/forepath/provider-plugins`
+4. Set `DYNAMIC_*` to reference package name or `file:` directory
+5. Restart the container
+
+Startup runs `install-provider-plugins.js` before `main.js` when the plugin path is set. Install failures fail container start.
+
+## Startup Error Policy
+
+| Registry criticality | `DYNAMIC_PROVIDERS_FAIL_FAST` | On load error |
+| -------------------- | ----------------------------- | ------------------ |
+| optional | any | Log and skip entry |
+| critical | unset / `false` | Log and skip entry |
+| critical | `true` | Abort startup |
+
+## Security
+
+- Package `name` in indexed `package.json` files must use allowlisted prefixes (`@forepath/`, `@decabill/`)
+- `file:` paths resolve under `DYNAMIC_PROVIDER_PLUGIN_PATH` only; traversal outside the root is rejected
+- Private registry installs require operator-supplied `.npmrc` or token mounts
+
+## Docker Compose Example
+
+```yaml
+environment:
+ DYNAMIC_PROVIDER_PLUGIN_PATH: /var/lib/forepath/provider-plugins
+ DYNAMIC_PROVIDER_PLUGIN_INSTALL: ${DYNAMIC_PROVIDER_PLUGIN_INSTALL:-}
+ DYNAMIC_PROVIDERS_FAIL_FAST: 'true'
+volumes:
+ - ./provider-plugins:/var/lib/forepath/provider-plugins
+```
+
+See [Docker Deployment](../deployment/docker-deployment.md).
+
+## Related Documentation
+
+- **[Payment Processing](./payment-processing.md)** - Stripe built-in processor
+- **[Service Types and Plans](./service-types-and-plans.md)** - Provider registry consumption
+- **[Environment Configuration](../deployment/environment-configuration.md)** - `DYNAMIC_*` reference
+- **[Backend Billing Manager](../applications/backend-billing-manager.md)** - Compose and env
+
+---
+
+_Implementation: `@forepath/shared/backend/util-dynamic-provider-registry`._
diff --git a/docs/decabill/features/invoices.md b/docs/decabill/features/invoices.md
new file mode 100644
index 000000000..18b4d6437
--- /dev/null
+++ b/docs/decabill/features/invoices.md
@@ -0,0 +1,128 @@
+# Invoices
+
+Invoice issuance, ZUGFeRD PDF generation, open position accumulation, and payment initiation for Decabill customers and administrators.
+
+## Overview
+
+Decabill stores invoices in PostgreSQL with immutable issued states. PDFs follow ZUGFeRD conventions with EN 16931 XML embedded. Customers pay via Stripe Checkout; admins can create manual invoices and adjust payment status.
+
+## Invoice Statuses
+
+| Status | Description |
+| ---------------- | ------------------------------------------- |
+| `draft` | Editable; admin manual invoices only |
+| `issued` | Finalized; line items and amounts immutable |
+| `paid` | Fully paid |
+| `partially_paid` | Partial payment recorded |
+| `overdue` | Past due date without full payment |
+| `void` | Voided; void document PDF available |
+
+**Immutability:** Only `draft` invoices can be edited or deleted. Issued invoices can be voided (when unpaid) or marked paid or unpaid by admins.
+
+## Open Positions and Billing Day
+
+Recurring and final subscription charges are recorded as **open positions** instead of creating an invoice immediately.
+
+A scheduler runs on each user's **billing day** (stored on the user record; default is registration day of month capped at 28). On that day, one accumulated invoice per user is created containing all unbilled open positions as line items.
+
+This is separate from the service plan's `billing_day_of_month`, which controls subscription period alignment.
+
+```mermaid
+sequenceDiagram
+ participant Sched as Billing Day Scheduler
+ participant API as InvoiceCreationService
+ participant DB as PostgreSQL
+
+ Sched->>DB: Find users whose billing day is today
+ loop Each user with open positions
+ Sched->>API: Create accumulated invoice
+ API->>DB: Insert invoice + line items from open positions
+ API->>DB: Mark positions as billed
+ end
+```
+
+Configure scheduler interval with `OPEN_POSITION_INVOICE_SCHEDULER_INTERVAL` (default daily).
+
+## Customer Invoice Access
+
+### Summary and Lists
+
+- `GET /invoices/summary` - Aggregated counts and amounts for the authenticated user
+- `GET /invoices/open-overdue` - Open and overdue invoices for the user
+
+### By Reference
+
+Invoices are addressed by stable `invoiceRefId`:
+
+- `GET /invoices/ref/{invoiceRefId}` - Invoice detail
+- `GET /invoices/ref/{invoiceRefId}/pdf` - Download ZUGFeRD PDF
+- `GET /invoices/ref/{invoiceRefId}/void-document/pdf` - Void document PDF when voided
+- `POST /invoices/ref/{invoiceRefId}/pay` - Initiate Stripe Checkout
+
+Subscription-scoped paths mirror the same operations under `/invoices/{subscriptionId}/ref/{invoiceRefId}`.
+
+### Void (Customer Context)
+
+Customers may void eligible invoices via subscription-scoped void endpoint where policy allows.
+
+## Admin Invoice Operations
+
+Admin routes under `/admin/billing/invoices`:
+
+- List all invoices with filters
+- Open and overdue lists across tenants (scoped by `X-Tenant`)
+- Manual invoice workflow (see [Billing Administration](./billing-administration.md))
+- Mark paid or unpaid
+- Audit logs per invoice
+- PDF and void document download
+
+**Bill now:** `POST /admin/billing/bill-now` forces invoice generation for selected users outside the scheduler.
+
+## PDF Generation
+
+PDFs are stored under `BILLING_INVOICE_PDF_STORAGE_PATH`. Issuer details come from environment:
+
+- `BILLING_ISSUER_*` (name, VAT ID, address, email, IBAN)
+- `BILLING_TAX_RATE_STANDARD` and `BILLING_TAX_RATE_REDUCED`
+
+## Usage on Invoices
+
+Usage records posted via `POST /usage/record` appear on invoices when pricing includes usage cost or unit counts.
+
+## Payment
+
+Customer payment flow:
+
+1. `POST .../pay` creates a Stripe Checkout Session
+2. User completes checkout on Stripe
+3. Stripe webhook updates invoice to paid (idempotent)
+
+See [Payment Processing](./payment-processing.md).
+
+## Manual Invoice Workflow (Admin)
+
+1. `POST /admin/billing/invoices/manual` - Create draft with user, optional subscription, custom line items
+2. `POST /admin/billing/invoices/{invoiceRefId}` - Update draft line items
+3. `POST /admin/billing/invoices/{invoiceRefId}/issue` - Issue draft (requires complete customer profile)
+4. `DELETE /admin/billing/invoices/{invoiceRefId}` - Delete draft only
+
+## API Endpoints Summary
+
+| Audience | Key paths |
+| -------- | -------------------------------------------------------------------------------------------------------------------------------------- |
+| Customer | `/invoices/summary`, `/invoices/ref/{invoiceRefId}`, `/invoices/ref/{invoiceRefId}/pdf`, `/invoices/ref/{invoiceRefId}/pay` |
+| Admin | `/admin/billing/invoices`, `/admin/billing/invoices/manual`, `/admin/billing/invoices/{invoiceRefId}/issue`, `/admin/billing/bill-now` |
+
+Full schemas: [Billing Manager OpenAPI](/spec/billing-manager/openapi.yaml).
+
+## Related Documentation
+
+- **[Payment Processing](./payment-processing.md)** - Stripe checkout and webhooks
+- **[Billing Administration](./billing-administration.md)** - Manual invoices and KPIs
+- **[Customer Profiles](./customer-profiles.md)** - Required for issuance
+- **[Subscriptions](./subscriptions.md)** - Source of open positions
+- **[Multi-tenancy](./multi-tenancy.md)** - Tenant-scoped invoice data
+
+---
+
+_For invoice payment sequence details, see [Payment Processing](./payment-processing.md)._
diff --git a/docs/decabill/features/multi-tenancy.md b/docs/decabill/features/multi-tenancy.md
new file mode 100644
index 000000000..58c954c3f
--- /dev/null
+++ b/docs/decabill/features/multi-tenancy.md
@@ -0,0 +1,158 @@
+# Multi-tenancy
+
+Tenant-scoped billing data in a shared billing manager deployment. Operators configure allowed tenants; clients select the active tenant per request.
+
+## Overview
+
+Decabill isolates users, subscriptions, invoices, service types, and service plans per tenant. The same email address may exist in different tenants as separate user records.
+
+Multi-tenancy is enforced on:
+
+- HTTP REST API via the `X-Tenant` header
+- Socket.IO dashboard status via handshake `extraHeaders` or `auth.tenantId`
+- Background jobs that iterate all configured tenants
+- Stripe webhook handling that resolves tenant from checkout session metadata
+
+## Tenant Selection
+
+### HTTP Requests
+
+Clients send optional header:
+
+```http
+X-Tenant: one
+```
+
+When omitted, the server uses `default`.
+
+The billing console reads `billing.tenantId` from runtime config and attaches `X-Tenant` on every API call.
+
+### Allowed Tenants
+
+The `TENANTS` environment variable defines the allowlist:
+
+```bash
+TENANTS=default,one,two
+```
+
+Rules:
+
+- `default` is always allowed even when not listed
+- When `TENANTS` is unset or empty, only `default` is allowed
+- Invalid tenant ids return 400 Bad Request
+
+### Per-tenant Frontend URLs
+
+Stripe checkout success and cancel redirects use tenant-specific billing console base URLs:
+
+| Variable | Purpose |
+| ---------------------- | -------------------------------------------- |
+| `BILLING_FRONTEND_URL` | Base URL for the `default` tenant |
+| `TENANT_FRONTEND_URLS` | Comma-separated `tenantId=https://...` pairs |
+
+Example:
+
+```bash
+BILLING_FRONTEND_URL=https://billing.example.com
+TENANT_FRONTEND_URLS=one=https://one.billing.example.com,two=https://two.billing.example.com
+```
+
+## Data Isolation
+
+Each tenant has independent:
+
+- User accounts (same email allowed in multiple tenants)
+- Customer billing profiles
+- Subscriptions and subscription items
+- Invoices and open positions
+- Service types and service plans
+- Backorders
+
+Admin and user routes filter by the request tenant. Interactive auth (Keycloak or users) additionally requires the authenticated user's `tenant_id` to match the request tenant.
+
+## API Key Auth and Tenant Scope
+
+When `AUTHENTICATION_METHOD=api-key` (or api-key is inferred from `STATIC_API_KEY`):
+
+### With `STATIC_API_KEY_TENANT_ID` set
+
+API key requests are accepted only when `X-Tenant` matches the configured tenant id. Mismatch returns 403 Forbidden.
+
+```bash
+STATIC_API_KEY_TENANT_ID=one
+```
+
+### With `STATIC_API_KEY_TENANT_ID` unset
+
+A valid `STATIC_API_KEY` grants admin access to **every** tenant listed in `TENANTS`, selected per request via `X-Tenant`. This is intentional for a single shared automation credential.
+
+**Accepted risk [DR-002](../security/accepted-risks.md#dr-002--billing-multi-tenant-api-key-scope-static_api_key_tenant_id-unset):** Anyone with the deployment API key can read and mutate all configured tenants by changing `X-Tenant`.
+
+**Mitigations:**
+
+- Set `STATIC_API_KEY_TENANT_ID` when automation must target one tenant only
+- Prefer Keycloak or users auth for the billing console in multi-tenant production
+- Rotate and protect `STATIC_API_KEY` as a high-value secret
+- WebSocket dashboard status does not stream to API key clients
+
+Interactive Keycloak and users sessions always enforce the user's `tenant_id` regardless of the above.
+
+## Public Catalog
+
+`GET /public/service-plan-offerings` is unauthenticated. Tenant is selected via `X-Tenant` (defaults to `default`). Restrict exposure with `TENANTS` on public-facing deployments.
+
+## Background Jobs
+
+Schedulers iterate all configured tenants:
+
+- Open position invoice generation on each user's billing day
+- Subscription item update (SSH docker compose pull)
+- Backorder retry processing
+- Other tenant-scoped maintenance tasks
+
+Stripe webhooks resolve tenant from checkout session metadata to apply payment state to the correct tenant's invoice.
+
+## Multi-tenancy Flow
+
+```mermaid
+sequenceDiagram
+ participant Client
+ participant API as Billing Manager
+ participant DB as PostgreSQL
+
+ Client->>API: Request with X-Tenant: one
+ API->>API: Validate tenant in TENANTS allowlist
+ alt Interactive auth (keycloak / users)
+ API->>DB: Load user by id
+ API->>API: user.tenant_id must match X-Tenant
+ else API key auth
+ alt STATIC_API_KEY_TENANT_ID set
+ API->>API: X-Tenant must match STATIC_API_KEY_TENANT_ID
+ else STATIC_API_KEY_TENANT_ID unset
+ API->>API: Admin access to requested tenant (DR-002)
+ end
+ end
+ API->>DB: Query scoped to tenant
+ API-->>Client: Response
+```
+
+## Configuration Summary
+
+| Variable | Required | Description |
+| -------------------------- | -------- | -------------------------------------------------------------- |
+| `TENANTS` | No | Comma-separated allowed tenant ids (always includes `default`) |
+| `STATIC_API_KEY_TENANT_ID` | No | Bind API key auth to one tenant |
+| `BILLING_FRONTEND_URL` | No | Default tenant frontend base URL for Stripe redirects |
+| `TENANT_FRONTEND_URLS` | No | Per-tenant frontend URLs for Stripe redirects |
+
+## Related Documentation
+
+- **[Authentication](./authentication.md)** - Auth methods and API key behavior
+- **[Billing Administration](./billing-administration.md)** - Admin routes and tenant scope
+- **[Security - Accepted risks](../security/accepted-risks.md)** - **DR-002** multi-tenant API key scope
+- **[Environment Configuration](../deployment/environment-configuration.md)** - Full variable reference
+- **[Payment Processing](./payment-processing.md)** - Tenant-aware Stripe redirects
+
+---
+
+_For HTTP header documentation, see [Billing Manager OpenAPI](/spec/billing-manager/openapi.yaml)._
diff --git a/docs/decabill/features/payment-processing.md b/docs/decabill/features/payment-processing.md
new file mode 100644
index 000000000..f938b9b61
--- /dev/null
+++ b/docs/decabill/features/payment-processing.md
@@ -0,0 +1,113 @@
+# Payment Processing
+
+Stripe Checkout integration and webhook-driven payment reconciliation for Decabill invoices.
+
+## Overview
+
+Decabill uses a payment processor abstraction with **Stripe** as the built-in implementation. Customers initiate payment from the billing console; the backend creates a Stripe Checkout Session and records the attempt. Stripe webhooks mark invoices paid idempotently.
+
+Additional processors can be registered via [Dynamic Provider Plugins](./dynamic-provider-plugins.md).
+
+## Default Processor
+
+Set the active processor type:
+
+```bash
+BILLING_DEFAULT_PAYMENT_PROCESSOR=stripe
+```
+
+The `PaymentProcessorFactory` resolves processors by type string at runtime.
+
+## Stripe Configuration
+
+| Variable | Purpose |
+| ----------------------------- | ------------------------------------------------------ |
+| `STRIPE_SECRET_KEY` | Stripe API secret key |
+| `STRIPE_WEBHOOK_SECRET` | Webhook signing secret |
+| `STRIPE_CHECKOUT_SUCCESS_URL` | Legacy full URL or path used with tenant frontend base |
+| `STRIPE_CHECKOUT_CANCEL_URL` | Cancel redirect path |
+
+Per-tenant redirect bases come from [Multi-tenancy](./multi-tenancy.md):
+
+- `BILLING_FRONTEND_URL` for `default`
+- `TENANT_FRONTEND_URLS` for other tenants
+
+Checkout success and cancel URLs are built from the tenant frontend base plus configured path segments.
+
+## Customer Payment Flow
+
+```mermaid
+sequenceDiagram
+ participant User
+ participant API as Billing Manager
+ participant Stripe
+ participant DB as PostgreSQL
+
+ User->>API: POST /invoices/ref/{invoiceRefId}/pay
+ API->>DB: Load invoice + customer profile
+ API->>Stripe: Create Checkout Session
+ Stripe-->>API: checkoutUrl
+ API->>DB: Record payment attempt
+ API-->>User: InitiatePaymentResponse with URL
+
+ User->>Stripe: Complete checkout
+ Stripe->>API: POST /webhooks/payments/stripe
+ API->>DB: Idempotent event + mark paid
+ API-->>Stripe: 200
+```
+
+### Initiate Payment
+
+`POST /invoices/ref/{invoiceRefId}/pay` (and subscription-scoped variant) returns a checkout URL. The user is redirected to Stripe Hosted Checkout.
+
+Session metadata includes tenant id so webhooks apply payment to the correct tenant's invoice.
+
+### Webhook Handling
+
+`POST /webhooks/payments/stripe` is a public endpoint secured by Stripe signature verification.
+
+Behavior:
+
+- Verify webhook signature with `STRIPE_WEBHOOK_SECRET`
+- Process events idempotently (duplicate deliveries safe)
+- Update invoice payment status to `paid` on successful checkout
+- Store Stripe customer id on [Customer Profile](./customer-profiles.md) when created
+
+## Admin Manual Payment Status
+
+Admins can override payment state without Stripe:
+
+- `POST /admin/billing/invoices/{invoiceRefId}/mark-paid`
+- `POST /admin/billing/invoices/{invoiceRefId}/mark-unpaid`
+
+Use for offline payments or reconciliation corrections. Audit logs record admin actions.
+
+## Payment Processor Interface
+
+Built-in and dynamic processors implement:
+
+- Checkout session creation with amount, currency, and metadata
+- Webhook route registration (Stripe uses fixed path)
+- Customer create or update helpers
+
+Stripe implementation: `StripePaymentProcessor` in the billing manager feature module.
+
+## Security
+
+- Never expose `STRIPE_SECRET_KEY` or `STRIPE_WEBHOOK_SECRET` to the frontend
+- Webhook endpoint validates Stripe signatures before state changes
+- Checkout sessions include tenant and invoice references for scoped updates
+- Use HTTPS for production webhook URLs
+
+## Related Documentation
+
+- **[Invoices](./invoices.md)** - Invoice statuses and pay endpoints
+- **[Multi-tenancy](./multi-tenancy.md)** - Tenant-aware redirect URLs
+- **[Dynamic Provider Plugins](./dynamic-provider-plugins.md)** - `DYNAMIC_PAYMENT_PROCESSORS`
+- **[Customer Profiles](./customer-profiles.md)** - Stripe customer id storage
+- **[Stripe Documentation](https://stripe.com/docs)** - External Stripe reference
+- **[Billing Manager OpenAPI](/spec/billing-manager/openapi.yaml)** - Pay and webhook paths
+
+---
+
+_Configure Stripe keys in the billing manager environment before enabling customer checkout._
diff --git a/docs/decabill/features/real-time-status.md b/docs/decabill/features/real-time-status.md
new file mode 100644
index 000000000..d1e29160d
--- /dev/null
+++ b/docs/decabill/features/real-time-status.md
@@ -0,0 +1,123 @@
+# Real-time Status
+
+Socket.IO dashboard status stream for provisioned subscription items in the billing console.
+
+## Overview
+
+The billing manager exposes a dedicated WebSocket gateway (default TCP port **8082**, namespace **`billing`**) separate from the HTTP REST API (default port **3200**). Authenticated users subscribe to periodic server status snapshots for all active subscription items they own.
+
+Specification: [Billing Manager AsyncAPI](/spec/billing-manager/asyncapi.yaml).
+
+## Connection
+
+### URL and Namespace
+
+Configure the billing console runtime config:
+
+```json
+{
+ "billing": {
+ "websocketUrl": "ws://localhost:8082/billing",
+ "tenantId": "default"
+ }
+}
+```
+
+Environment variables on the backend:
+
+| Variable | Default | Purpose |
+| ----------------------- | --------- | ------------------------------- |
+| `WEBSOCKET_PORT` | `8082` | Socket.IO TCP port |
+| `WEBSOCKET_NAMESPACE` | `billing` | Namespace path segment |
+| `WEBSOCKET_CORS_ORIGIN` | `*` | CORS origin for browser clients |
+| `STATUS_POLL_INTERVAL` | `15000` | Default poll interval in ms |
+
+### Authentication
+
+Pass the same credentials as HTTP in the Socket.IO handshake:
+
+- **Keycloak:** `Bearer ` in `auth.Authorization` or handshake headers
+- **Users:** `Bearer ` in `auth.Authorization` or handshake headers
+- **API key:** **Not supported** for dashboard status. `subscribeDashboardStatus` emits `error` with message "User not authenticated". This mirrors REST behavior for API-key-only requests.
+
+### Multi-tenancy
+
+Pass tenant in the handshake:
+
+- **Browser clients:** `auth.tenantId` and `auth.Authorization`
+- **Node clients:** `extraHeaders: { 'X-Tenant': 'default', Authorization: '...' }`
+
+The authenticated user's `tenant_id` must match the socket tenant.
+
+## Events
+
+### Client to Server
+
+| Event | Payload | Purpose |
+| ---------------------------- | ----------------------------- | ------------- |
+| `subscribeDashboardStatus` | `{ pollIntervalMs?: number }` | Start polling |
+| `unsubscribeDashboardStatus` | none | Stop polling |
+
+`pollIntervalMs` is clamped server-side between 10 seconds and 120 seconds. When omitted, the server uses `STATUS_POLL_INTERVAL`.
+
+### Server to Client
+
+| Event | Purpose |
+| ----------------------- | --------------------------------------- |
+| `dashboardStatusUpdate` | Snapshot per poll tick |
+| `error` | Application errors for this socket only |
+
+## Security Model
+
+- The server **never uses Socket.IO rooms** for this feature
+- Subscription ids are chosen **only** from the authenticated user's subscriptions on each poll tick
+- Client-supplied subscription lists are **not** trusted
+- `dashboardStatusUpdate` and `error` are emitted **only** to the subscribing socket
+
+This mirrors REST subscription ownership checks on `GET .../server-info`.
+
+## Payload Shape
+
+Each `dashboardStatusUpdate` contains items matching the REST server-info response for every active provisioned item across the user's subscriptions: subscription id, item id, hostname, FQDN, IPs, provider status, and metadata.
+
+## Connection Flow
+
+```mermaid
+sequenceDiagram
+ participant UI as Billing Console
+ participant GW as Billing Status Gateway
+ participant Svc as Subscription Services
+
+ UI->>GW: Connect (Bearer JWT, tenantId)
+ GW->>GW: Validate auth and tenant
+ UI->>GW: subscribeDashboardStatus
+ GW->>Svc: listSubscriptions(userId)
+ loop Each poll interval
+ GW->>Svc: getServerInfo per active item
+ GW-->>UI: dashboardStatusUpdate
+ end
+ UI->>GW: unsubscribeDashboardStatus
+ GW->>GW: clearPollTimer
+```
+
+## Frontend Integration
+
+NgRx effects in `data-access-billing-console`:
+
+- Connect on overview entry when `websocketUrl` is configured
+- Auto-emit `subscribeDashboardStatus` on `connect`
+- Dispatch `billingDashboardStatusPush` on each update
+- Disconnect on overview destroy or logout
+
+When WebSocket is unavailable, [Dashboard and Server Control](./dashboard-and-server-control.md) falls back to REST.
+
+## Related Documentation
+
+- **[Dashboard and Server Control](./dashboard-and-server-control.md)** - Overview UI and server actions
+- **[Authentication](./authentication.md)** - JWT and Keycloak handshake
+- **[Multi-tenancy](./multi-tenancy.md)** - Tenant in handshake
+- **[Billing Manager AsyncAPI](/spec/billing-manager/asyncapi.yaml)** - Full message schemas
+
+---
+
+_Static API key auth cannot subscribe to dashboard status; use interactive auth for the billing console overview._
diff --git a/docs/decabill/features/server-provisioning.md b/docs/decabill/features/server-provisioning.md
new file mode 100644
index 000000000..42ecb7de7
--- /dev/null
+++ b/docs/decabill/features/server-provisioning.md
@@ -0,0 +1,144 @@
+# Server Provisioning
+
+Automated cloud server provisioning via cloud-init when service plans include infrastructure providers.
+
+## Overview
+
+When a [Subscription](./subscriptions.md) order includes a provisioning-enabled [Service Type](./service-types-and-plans.md), the billing manager:
+
+1. Checks provider availability
+2. Reserves a hostname under `DNS_BASE_DOMAIN`
+3. Creates a cloud server via Hetzner Cloud or DigitalOcean
+4. Runs cloud-init to install Docker and deploy the bundled product stack
+5. Creates a DNS A record (Cloudflare) when configured
+6. Stores the provider server id on the subscription item
+
+## Supported Providers
+
+Built-in providers register at startup when API tokens are configured:
+
+#### Hetzner Cloud
+
+- **Provider id:** `hetzner`
+- **Requires:** `HETZNER_API_TOKEN`
+- **Config keys:** `location` or `region`, `serverType` (required), optional `firewallId`
+- **Server types:** Loaded live from `GET /service-types/providers/hetzner/server-types`
+
+#### DigitalOcean
+
+- **Provider id:** `digital-ocean`
+- **Requires:** `DIGITALOCEAN_API_TOKEN`
+- **Config keys:** `region` (required), `serverType` (required)
+- **Server types:** Loaded live from `GET /service-types/providers/digital-ocean/server-types`
+
+Additional cloud backends can be added via [Dynamic Provider Plugins](./dynamic-provider-plugins.md) (`DYNAMIC_BILLING_PROVIDER_METADATA` and custom provisioning packages where supported).
+
+## Provisioning Process
+
+```mermaid
+sequenceDiagram
+ participant API as Billing Manager
+ participant P as Cloud Provider
+ participant S as Server
+ participant DNS as Cloudflare DNS
+
+ API->>P: Create server (cloud-init user data)
+ P->>S: Provision VM
+ S->>S: cloud-init: Docker + stack
+ P-->>API: serverId + IP
+ API->>DNS: createARecord(hostname, publicIp)
+ API->>API: Update subscription item active
+```
+
+## Bundled Product Stack
+
+Cloud-init installs Docker CE and deploys a docker-compose stack on the instance. The default controller bundle includes:
+
+- **PostgreSQL** - Application database with health checks
+- **Backend API** - NestJS billing or agent controller API container (depending on service plan configuration)
+- **Frontend console** - Angular SSR web application served behind reverse proxy
+- **Nginx** - Terminates HTTP and HTTPS, proxies to backend and frontend containers, serves ACME HTTP-01 challenges at `/.well-known/acme-challenge/`
+
+Containers share a defined application directory on the host (typically under `/opt/`). Environment variables for authentication, database connection, and product-specific settings are interpolated into the generated compose file from the subscription's requested configuration.
+
+Operators choose service kind and image tags through service type and plan configuration; the update scheduler pulls latest tagged images on a schedule.
+
+## TLS and DNS
+
+TLS uses Let's Encrypt via Certbot installed in cloud-init (pip/venv under `/opt/certbot`):
+
+- Initial bootstrap certificate generated with OpenSSL so Nginx can start immediately
+- Production certificates requested with `certbot certonly --webroot` for the instance FQDN (`hostname.DNS_BASE_DOMAIN`)
+- On success, Nginx switches to Let's Encrypt certificate paths
+- Automatic renewal in crontab with deploy hook to reload the Nginx container
+
+Environment:
+
+| Variable | Purpose |
+| ---------------------- | ----------------------------------------------------------------- |
+| `LETS_ENCRYPT_EMAIL` | ACME account email |
+| `DNS_BASE_DOMAIN` | Base domain for hostnames and certificates (default `spirde.com`) |
+| `CLOUDFLARE_API_TOKEN` | DNS API token |
+| `CLOUDFLARE_ZONE_ID` | Zone for A records |
+
+Cloud-init waits for the DNS A record (`proxied: false`) to resolve to the host before requesting the certificate.
+
+## SSH Access
+
+Provisioning templates configure SSH for operational access. Key-based authentication is enabled; password authentication is disabled in generated `sshd` configuration.
+
+**Accepted risk [DR-001](../security/accepted-risks.md#dr-001--provisioning-ssh-cloud-init-templates):** Cloud-init may configure root SSH access and install root `authorized_keys` for first-boot automation. Compensating controls include network restrictions, key rotation, and bastion access. See [Security - Accepted risks](../security/accepted-risks.md).
+
+## Nested Provisioning Tokens
+
+Optional `requestedConfig` keys on subscription order allow the provisioned instance to provision additional servers:
+
+- `hetznerApiToken` - Hetzner API token injected into instance environment
+- `digitaloceanApiToken` - DigitalOcean API token injected into instance environment
+
+Use only when the product stack requires nested cloud automation.
+
+## Server Information and Control
+
+After provisioning:
+
+- `GET /subscriptions/{subscriptionId}/items/{itemId}/server-info` - Live status from provider API
+- Start, stop, restart via action endpoints (see [Dashboard and Server Control](./dashboard-and-server-control.md))
+
+## Subscription Item Update Scheduler
+
+A background scheduler connects to each provisioned host via SSH (key stored on the subscription item) at `SUBSCRIPTION_UPDATE_SCHEDULER_INTERVAL` (default 24 hours) and runs:
+
+```bash
+docker compose up -d --pull=always
+```
+
+in the application directory. This pulls latest container images and recreates services. Failures are logged on the host under `/var/log/agent-controller-update.log` or equivalent per service kind.
+
+## Optional Instance Configuration
+
+Subscription `requestedConfig` can include authentication mode for the provisioned stack (`users`, `api-key`, `keycloak`), SMTP settings, and Git or API credentials where the plan schema allows. Values are passed securely through user-data into the generated compose environment.
+
+## API Endpoints
+
+| Method | Path | Purpose |
+| ------ | ---------------------------------------------------- | -------------------------- |
+| GET | `/service-types/providers` | List providers and schemas |
+| GET | `/service-types/providers/{providerId}/server-types` | Server types and pricing |
+| POST | `/availability/check` | Pre-order capacity check |
+| GET | `/subscriptions/.../server-info` | Live server metadata |
+
+Provisioning itself is triggered internally by subscription and backorder services, not via a standalone public provision endpoint.
+
+## Related Documentation
+
+- **[Subscriptions](./subscriptions.md)** - Order flow
+- **[Backorders](./backorders.md)** - Retry when capacity unavailable
+- **[Service Types and Plans](./service-types-and-plans.md)** - Provider schemas
+- **[Dashboard and Server Control](./dashboard-and-server-control.md)** - Power actions
+- **[Security - Accepted risks](../security/accepted-risks.md)** - **DR-001** provisioning SSH
+- **[Billing Manager OpenAPI](/spec/billing-manager/openapi.yaml)** - Server info schemas
+
+---
+
+_Provisioned instances are owned by the subscribing customer within the current tenant scope._
diff --git a/docs/decabill/features/service-types-and-plans.md b/docs/decabill/features/service-types-and-plans.md
new file mode 100644
index 000000000..f20bcdd6e
--- /dev/null
+++ b/docs/decabill/features/service-types-and-plans.md
@@ -0,0 +1,122 @@
+# Service Types and Plans
+
+Admin-managed catalog of provisioning providers, service types, and priced service plans exposed to customers and public marketing endpoints.
+
+## Overview
+
+Service types define which provisioning provider (if any) backs a product. Service plans attach pricing, billing intervals, margins, and provider default configuration. The billing console admin UI and public catalog consume the same backend registry.
+
+## Service Types
+
+A service type links a product name to a provider id (for example `hetzner`, `digital-ocean`) or no provider for non-infrastructure plans.
+
+### Admin Endpoints
+
+| Method | Path | Purpose |
+| ------ | --------------------- | --------------------------- |
+| GET | `/service-types` | List service types |
+| POST | `/service-types` | Create service type (admin) |
+| GET | `/service-types/{id}` | Get service type |
+| POST | `/service-types/{id}` | Update service type (admin) |
+| DELETE | `/service-types/{id}` | Delete service type (admin) |
+
+### Provider Registry
+
+`GET /service-types/providers` returns registered provisioning providers with:
+
+- Provider id and display name
+- Optional `configSchema` for admin UI and subscription validation
+- Dynamic metadata from `DYNAMIC_BILLING_PROVIDER_METADATA` plugins
+
+Built-in providers include Hetzner Cloud and DigitalOcean when API tokens are configured. Additional providers can be registered via [Dynamic Provider Plugins](./dynamic-provider-plugins.md).
+
+### Config Schema
+
+The optional `configSchema` is a JSON-schema-like object:
+
+- **`properties`** - Field definitions with `type`, `description`, and optional `enum`
+- **`basePriceFromField`** - When set (for example `serverType`), the console loads options from `GET /service-types/providers/{providerId}/server-types` and uses selected `priceMonthly` as plan base price
+
+Enum fields render as select inputs in the billing console.
+
+### Server Types
+
+`GET /service-types/providers/{providerId}/server-types` returns server types with id, name, specs (cores, memory, disk), `priceMonthly`, and `priceHourly`. Requires the provider API token in the billing manager environment.
+
+## Service Plans
+
+Service plans belong to a service type and define customer-facing pricing and billing rules.
+
+### Admin Endpoints
+
+| Method | Path | Purpose |
+| ------ | --------------------- | ------------------- |
+| GET | `/service-plans` | List service plans |
+| POST | `/service-plans` | Create plan (admin) |
+| GET | `/service-plans/{id}` | Get plan |
+| POST | `/service-plans/{id}` | Update plan (admin) |
+| DELETE | `/service-plans/{id}` | Delete plan (admin) |
+
+### Plan Fields (Conceptual)
+
+- Title, description, and active flag
+- Billing interval (monthly, yearly, etc.)
+- Base price, margin, and computed customer total
+- `providerConfigDefaults` merged with customer `requestedConfig` on order
+- `billing_day_of_month` for subscription period alignment
+- `allowCustomerLocationSelection` when geography override is supported
+
+### Customer Geography Selection
+
+When `allowCustomerLocationSelection` is true **and** the merged provider schema defines `region` or `location` as a string with a non-empty enum, customers may pass geography in `POST /subscriptions` `requestedConfig`. Setting the flag without a supported schema returns 400.
+
+For Hetzner and DigitalOcean, `region` and `location` are treated as aliases during merge and provisioning.
+
+## Public Catalog
+
+Unauthenticated endpoints for external pricing pages:
+
+| Method | Path | Purpose |
+| ------ | ----------------------------------------- | ---------------------------------------------- |
+| GET | `/public/service-plan-offerings` | Paginated active plans (marketing fields only) |
+| GET | `/public/service-plan-offerings/cheapest` | Lowest-priced active plan |
+
+Tenant is selected via `X-Tenant` (defaults to `default`). No provider secrets or internal margins are exposed.
+
+## Availability and Pricing
+
+Before order:
+
+- `POST /availability/check` - Validate config against provider capacity
+- `POST /availability/alternatives` - Suggest alternatives when unavailable
+- `POST /pricing/preview` - Estimated customer total for plan and config
+
+## Admin UI
+
+The billing console provides administration routes for service types and service plans. Provider dropdown and dynamic config fields are driven by `GET /service-types/providers` and server type endpoints.
+
+## Architecture
+
+```mermaid
+flowchart LR
+ Admin[Admin Console] --> API[Billing Manager]
+ Public[Public Site] --> API
+ API --> Registry[ProviderRegistryService]
+ Registry --> Hetzner[Hetzner API]
+ Registry --> DO[DigitalOcean API]
+ Registry --> Plugins[Dynamic Metadata Plugins]
+ API --> DB[(PostgreSQL)]
+ DB --> ST[Service Types]
+ DB --> SP[Service Plans]
+```
+
+## Related Documentation
+
+- **[Subscriptions](./subscriptions.md)** - Ordering against plans
+- **[Server Provisioning](./server-provisioning.md)** - Provider provisioning behavior
+- **[Dynamic Provider Plugins](./dynamic-provider-plugins.md)** - Extra providers and UI metadata
+- **[Multi-tenancy](./multi-tenancy.md)** - Tenant-scoped catalog
+
+---
+
+_See [Billing Manager OpenAPI](/spec/billing-manager/openapi.yaml) for DTO schemas._
diff --git a/docs/decabill/features/subscriptions.md b/docs/decabill/features/subscriptions.md
new file mode 100644
index 000000000..3da8c5c86
--- /dev/null
+++ b/docs/decabill/features/subscriptions.md
@@ -0,0 +1,147 @@
+# Subscriptions
+
+Order service plans, manage subscription lifecycle, and provision cloud infrastructure when the plan includes a provisioning provider.
+
+## Overview
+
+Subscriptions link a user to a service plan. Plans reference a service type that may include Hetzner or DigitalOcean provisioning. Each subscription can have one or more subscription items representing provisioned or pending instances.
+
+The order flow requires a complete [Customer Profile](./customer-profiles.md) before `POST /subscriptions` is accepted.
+
+## Subscription Lifecycle
+
+```mermaid
+stateDiagram-v2
+ [*] --> active
+ active --> pending_cancel: cancel request
+ pending_cancel --> active: resume
+ pending_cancel --> canceled: effective_at reached
+ active --> pending_backorder: unavailable
+ pending_backorder --> active: provisioned
+ pending_backorder --> canceled: cancel
+```
+
+### Active
+
+Subscription is in good standing. Recurring charges create open positions according to the plan billing interval.
+
+### Pending Cancel
+
+User requested cancellation. Subscription remains active until `effective_at`. User may resume before that date.
+
+### Canceled
+
+Subscription ended. No further recurring charges. Provisioned items may be decommissioned per operator policy.
+
+### Pending Backorder
+
+Capacity was unavailable at order time or provisioning failed with `autoBackorder`. See [Backorders](./backorders.md).
+
+## Ordering a Subscription
+
+### Prerequisites
+
+1. Authenticated user in the current tenant
+2. Complete customer billing profile (name, email, address, city, country)
+3. Active service plan selected
+
+### Order Flow
+
+```mermaid
+sequenceDiagram
+ participant User
+ participant API as Billing Manager
+ participant Avail as AvailabilityService
+ participant Prov as ProvisioningService
+ participant DB as PostgreSQL
+
+ User->>API: POST /subscriptions (planId, requestedConfig)
+ API->>API: Validate customer profile complete
+ API->>Avail: checkAvailability(provider, config)
+ alt Available and provisioning enabled
+ API->>DB: Create subscription + item
+ API->>Prov: provision server (cloud-init)
+ Prov-->>API: serverId
+ API->>DB: Update item status active
+ else Unavailable with autoBackorder
+ API->>DB: Create backorder
+ end
+ API-->>User: Subscription response
+```
+
+### Request Body
+
+- **`planId`** (required) - UUID of the service plan
+- **`requestedConfig`** (optional) - Provider-specific configuration validated against the service type schema
+- **`autoBackorder`** (optional) - When true, queue a backorder if capacity is unavailable
+
+Provider config keys include `serverType`, `location` or `region`, and optional nested provisioning tokens. See [Service Types and Plans](./service-types-and-plans.md) and [Server Provisioning](./server-provisioning.md).
+
+### Customer Geography
+
+When `allowCustomerLocationSelection` is true on the plan and the provider schema defines `region` or `location` with an enum, customers may override geography in `requestedConfig`. Otherwise geography keys are stripped before merge.
+
+## Cancel and Resume
+
+- `POST /subscriptions/{subscriptionId}/cancel` - Schedule cancellation at period end or immediately per plan rules
+- `POST /subscriptions/{subscriptionId}/resume` - Reverse a pending cancel before `effective_at`
+
+## Subscription Items
+
+Each item tracks:
+
+- Provisioning status (`pending`, `active`, `failed`, etc.)
+- Provider reference (cloud server id)
+- Hostname and FQDN under `DNS_BASE_DOMAIN`
+- Service kind (for example controller stack)
+
+### Server Info
+
+`GET /subscriptions/{subscriptionId}/items/{itemId}/server-info` returns live cloud status: server id, name, public and private IP, hostname, FQDN, and provider metadata.
+
+### Server Control
+
+Start, stop, and restart actions are available for provisioned items. See [Dashboard and Server Control](./dashboard-and-server-control.md).
+
+## Usage Records
+
+Usage-based plans accept metering via `POST /usage/record`. Usage is included in invoice line items when `usagePayload` or `units` and `unitPrice` are present. Summary available at `GET /usage/summary/{subscriptionId}`.
+
+## Pricing Preview
+
+`POST /pricing/preview` returns estimated customer total for a plan and optional config before ordering.
+
+## Availability
+
+- `POST /availability/check` - Check whether requested config is available at the provider
+- `POST /availability/alternatives` - Suggest alternative regions or server types
+
+## API Endpoints
+
+| Method | Path | Purpose |
+| ------ | ---------------------------------------------------------------- | ------------------------- |
+| GET | `/subscriptions` | List user's subscriptions |
+| POST | `/subscriptions` | Create subscription |
+| GET | `/subscriptions/{subscriptionId}` | Get subscription detail |
+| POST | `/subscriptions/{subscriptionId}/cancel` | Cancel subscription |
+| POST | `/subscriptions/{subscriptionId}/resume` | Resume pending cancel |
+| GET | `/subscriptions/{subscriptionId}/items` | List subscription items |
+| GET | `/subscriptions/{subscriptionId}/items/{itemId}/server-info` | Live server info |
+| POST | `/subscriptions/{subscriptionId}/items/{itemId}/actions/start` | Start server |
+| POST | `/subscriptions/{subscriptionId}/items/{itemId}/actions/stop` | Stop server |
+| POST | `/subscriptions/{subscriptionId}/items/{itemId}/actions/restart` | Restart server |
+
+See [Billing Manager OpenAPI](/spec/billing-manager/openapi.yaml) for schemas.
+
+## Related Documentation
+
+- **[Customer Profiles](./customer-profiles.md)** - Required before ordering
+- **[Service Types and Plans](./service-types-and-plans.md)** - Catalog and provider schemas
+- **[Invoices](./invoices.md)** - Open positions and billing-day accumulation
+- **[Backorders](./backorders.md)** - Capacity retry queue
+- **[Server Provisioning](./server-provisioning.md)** - Cloud-init and bundled stacks
+- **[Dashboard and Server Control](./dashboard-and-server-control.md)** - Overview and power actions
+
+---
+
+_For the full subscription order sequence, see the billing manager feature module diagrams._
diff --git a/docs/decabill/getting-started.md b/docs/decabill/getting-started.md
new file mode 100644
index 000000000..87a579e6d
--- /dev/null
+++ b/docs/decabill/getting-started.md
@@ -0,0 +1,232 @@
+# Getting Started with Decabill
+
+This guide helps you run Decabill locally, connect the billing console to the billing manager, and sign in for the first time.
+
+## Prerequisites
+
+Before you begin, ensure you have:
+
+- **Node.js** 24.14.1 or higher
+- **Docker** and **Docker Compose** (recommended for Postgres, Redis, and Mailhog)
+- **Git** (to clone the monorepo)
+- **Keycloak** (optional, for OAuth2/OIDC login in the billing console)
+- **Stripe test keys** (optional, for checkout and payment flows)
+
+## Installation
+
+### Option 1: Docker Compose (Recommended)
+
+The fastest way to run the full backend stack and the SSR billing console is Docker Compose in each application directory.
+
+#### Backend billing manager
+
+```bash
+git clone https://github.com/forepath/one.git
+cd one/apps/decabill/backend-billing-manager
+
+cp .start-containers.env.example .env
+# Edit .env: set STATIC_API_KEY, ENCRYPTION_KEY, issuer fields, and TENANTS as needed
+
+docker compose up -d
+```
+
+This starts:
+
+- PostgreSQL 16
+- Redis 7 (host port **6380** by default)
+- Billing API (`QUEUE_ROLE=api`, HTTP **3200**, WebSocket **8082**)
+- Billing worker (`QUEUE_ROLE=worker`)
+- Billing scheduler (`QUEUE_ROLE=scheduler`)
+- Mailhog for local email capture
+
+Build the API image locally before the first compose run if you changed backend code:
+
+```bash
+cd one
+nx run decabill-backend-billing-manager:api-container-image
+```
+
+#### Frontend billing console
+
+In another terminal:
+
+```bash
+cd one/apps/decabill/frontend-billing-console
+docker compose up -d
+```
+
+The console server listens on **4500** by default. Set `CSP_CONNECT_SRC_EXTRA` in compose if the API is not reachable from the browser at the default billing manager URL.
+
+See **[Docker Deployment](./deployment/docker-deployment.md)** for production-oriented compose notes.
+
+### Option 2: Local Development with Nx
+
+Use Nx when you want hot reload while editing TypeScript or Angular code.
+
+```bash
+git clone https://github.com/forepath/one.git
+cd one
+npm install
+```
+
+Start Postgres and Redis (Docker one-liners or the billing manager compose stack without the API containers). Then run:
+
+```bash
+# Terminal 1: billing manager (QUEUE_ROLE=all runs API, worker, and scheduler in one process)
+cd apps/decabill/backend-billing-manager
+cp .start-containers.env.example .env
+# Set QUEUE_ROLE=all, REDIS_PORT=6380 if Redis is published on the host, and auth variables
+
+nx serve decabill-backend-billing-manager
+
+# Terminal 2: billing console (Angular dev server on port 4500)
+cd apps/decabill/frontend-billing-console
+nx serve decabill-frontend-billing-console
+```
+
+For SSR parity with production, build the console and run the Express server:
+
+```bash
+nx build decabill-frontend-billing-console
+nx run decabill-frontend-billing-console:serve-server
+```
+
+See **[Local Development](./deployment/local-development.md)** for Bull Board, tests, and troubleshooting.
+
+## Configuration
+
+### Backend billing manager
+
+Create or edit `.env` in `apps/decabill/backend-billing-manager`. Minimum local settings:
+
+```bash
+# Database
+DB_HOST=localhost
+DB_PORT=5432
+DB_USERNAME=postgres
+DB_PASSWORD=postgres
+DB_DATABASE=postgres
+
+# Redis (6380 when using compose host mapping)
+REDIS_HOST=localhost
+REDIS_PORT=6380
+REDIS_KEY_PREFIX=decabill-billing
+
+# Process role (local all-in-one)
+QUEUE_ROLE=all
+
+# Authentication (choose one method)
+AUTHENTICATION_METHOD=api-key
+STATIC_API_KEY=dev-api-key-123
+
+# Or Keycloak:
+# AUTHENTICATION_METHOD=keycloak
+# KEYCLOAK_AUTH_SERVER_URL=http://localhost:8380
+# KEYCLOAK_REALM=decabill
+# KEYCLOAK_CLIENT_ID=billing-manager
+# KEYCLOAK_CLIENT_SECRET=your-client-secret
+
+# Or built-in users (default in local Angular environment):
+# AUTHENTICATION_METHOD=users
+# JWT_SECRET=your-jwt-secret
+# DISABLE_SIGNUP=false
+
+# Ports
+PORT=3200
+WEBSOCKET_PORT=8082
+
+# Multi-tenancy and console URL
+TENANTS=decabill
+BILLING_FRONTEND_URL=http://localhost:4500
+CORS_ORIGIN=*
+
+# Encryption (set before storing sensitive provider or SSH fields)
+ENCRYPTION_KEY=
+
+# Stripe (optional)
+STRIPE_SECRET_KEY=
+STRIPE_WEBHOOK_SECRET=
+STRIPE_CHECKOUT_SUCCESS_URL=http://localhost:4500/invoices?payment=success
+STRIPE_CHECKOUT_CANCEL_URL=http://localhost:4500/invoices?payment=cancel
+```
+
+The full variable list is in **[Environment Configuration](./deployment/environment-configuration.md)**.
+
+### Frontend billing console
+
+Local Angular builds use `environment.decabill.ts`, which points the console at:
+
+- REST API: `http://localhost:3200/api`
+- WebSocket: `http://localhost:8082/billing`
+- Frontend URL: `http://localhost:4500`
+- Default tenant id: `decabill`
+
+Match `authentication.type` in the environment file to the backend `AUTHENTICATION_METHOD`. The Docker SSR image exposes runtime config via the Express `/config` endpoint. See **[Frontend Billing Console](./applications/frontend-billing-console.md)**.
+
+## First Login
+
+Choose an authentication method that matches your `.env` and console environment.
+
+### Option A: Static API key (`STATIC_API_KEY`)
+
+Set `AUTHENTICATION_METHOD=api-key` and a non-empty `STATIC_API_KEY`. The billing manager accepts the key on every HTTP request:
+
+```bash
+curl -s -H "Authorization: Bearer dev-api-key-123" \
+ -H "X-Tenant: decabill" \
+ http://localhost:3200/api/config
+```
+
+API key auth grants admin access to billing administration REST routes. It does not provide an end-user identity in the billing console UI or on the dashboard WebSocket stream. Use this path to verify the API, run automation, or integrate scripts before configuring interactive login.
+
+Optional: set `STATIC_API_KEY_TENANT_ID` to bind the key to one tenant. See **[Multi-tenancy](./features/multi-tenancy.md)**.
+
+### Option B: Keycloak
+
+1. Run or connect to a Keycloak realm and client for Decabill.
+2. Set `AUTHENTICATION_METHOD=keycloak` and the Keycloak environment variables on the billing manager.
+3. Configure the billing console for Keycloak (runtime config or build-time environment).
+4. Open `http://localhost:4500/login` and complete the OAuth2/OIDC flow.
+
+The first user synced into a tenant receives the admin role. Subsequent synced users receive the standard user role.
+
+### Option C: Built-in users (common local default)
+
+When `AUTHENTICATION_METHOD=users` and the console `authentication.type` is `users`:
+
+1. Open `http://localhost:4500/register` and create an account (unless `DISABLE_SIGNUP=true`).
+2. Confirm email when prompted (Mailhog UI is on port **8026** when using the billing manager compose stack).
+3. Log in at `http://localhost:4500/login`.
+
+The first registered user in each tenant is auto-confirmed and assigned admin. Later users may need email confirmation.
+
+See **[Authentication](./features/authentication.md)** for flows, roles, and security notes.
+
+## Verify the Stack
+
+After login (users or Keycloak) or API key verification:
+
+1. Open **Overview** at `/dashboard` to see subscriptions and server status.
+2. Open **Plans** at `/subscriptions` to browse service plans (admin users can manage catalog entries under **Administration**).
+3. Open **Invoices** at `/invoices` for billing history and Stripe checkout when configured.
+4. Admins can open **Administration** routes for service types, plans, manual billing, and customer profiles.
+
+Confirm WebSocket dashboard updates on the overview page when logged in as an end user (not when using API key auth alone).
+
+## Next Steps
+
+1. **[System Overview](./architecture/system-overview.md)** for the two-tier architecture
+2. **[Multi-tenancy](./features/multi-tenancy.md)** if you run more than one tenant
+3. **[Subscriptions](./features/subscriptions.md)** to order a service plan
+4. **[Billing Administration](./features/billing-administration.md)** for manual invoices and operator dashboards
+5. **[API Reference](./api-reference/README.md)** for OpenAPI and AsyncAPI specifications
+
+## Troubleshooting
+
+- Database or Redis errors: see **[Local Development](./deployment/local-development.md)**.
+- Port conflicts: billing API **3200**, WebSocket **8082**, console **4500**, Redis host **6380**.
+- Migrations run only when `QUEUE_ROLE` is `api` or `all`. Ensure at least one API role process has started.
+
+---
+
+_For deployment beyond local development, see **[Production Checklist](./deployment/production-checklist.md)**._
diff --git a/docs/decabill/security/README.md b/docs/decabill/security/README.md
new file mode 100644
index 000000000..dae902f64
--- /dev/null
+++ b/docs/decabill/security/README.md
@@ -0,0 +1,49 @@
+# Security documentation
+
+This section collects **security, compliance-oriented transparency, and hardening** information for Decabill: mapping to **EU Cyber Resilience Act (CRA)** and **BSI IT-Grundschutz** documentation themes, a formal **accepted-risk register**, **vulnerability reporting**, **SBOM** artifacts, and pointers to **environment variables** for production.
+
+For disclosure, supported versions, SBOM paths, and response-time commitments, see **[Vulnerability reporting and artifacts](./vulnerability-reporting-and-artifacts.md)**. A concise risk summary table is in **[Accepted risks](./accepted-risks.md)**.
+
+## Overview
+
+Decabill spans browsers, a NestJS billing API, Express SSR frontends (billing console and docs), Redis-backed background jobs, and optional cloud provisioning for bundled product stacks. Security is enforced through authentication modes, tenant guards, sanitized logging, content security policy choices, **hardened container images** (non-root users, no default secrets in images), Stripe webhook verification, and **documented** residual risks where product or deployment constraints apply.
+
+## Documentation structure
+
+### [Compliance and standards](./compliance-and-standards.md)
+
+How public documentation relates to **CRA** (Regulation (EU) 2024/2847) and **BSI IT-Grundschutz** / typical **ISMS** practice: expected artifacts, transparency goals, and a high-level product mapping. **Informative only**; conformity and certification require your own legal and audit advisors.
+
+### [Accepted risks (register)](./accepted-risks.md)
+
+Register **DR-001** through **DR-005**: provisioning SSH posture, billing multi-tenant API key scope, frontend CSP, backend authentication method resolution, and Trivy unfixed-CVE gating. Includes acceptance dates, review cadence, mitigations, and withdrawal paths.
+
+### [Container image security](./container-images.md)
+
+Runtime users (`agenstra` / `node`) for `decabill-billing-api`, `decabill-billing-console-server`, and `decabill-docs-server`.
+
+### [Operational hardening](./operational-hardening.md)
+
+Implemented controls: container image hardening, correlation IDs and access logs, tenant guard, runtime `/config` proxy behavior, CSP and `CSP_ENFORCE`, WebSocket CORS, and authentication resolution behavior.
+
+### [Vulnerability reporting and artifacts](./vulnerability-reporting-and-artifacts.md)
+
+Responsible disclosure, CycloneDX **SBOM** location on Decabill R2 (`decabill-*.cdx.json`), and downloads at **downloads.decabill.com**.
+
+### [CI security scanning (Trivy)](./ci-security-scanning.md)
+
+Automated **Trivy** scans on pull requests; CRITICAL fail gate (fixable issues only; see **[DR-005](./accepted-risks.md#dr-005--ci--local-trivy-unfixed-vulnerabilities-not-gated)**).
+
+## Configuration reference
+
+For variable-by-variable deployment settings, including **`CONFIG_*`**, **`CSP_ENFORCE`**, **`TENANTS`**, **`STATIC_API_KEY_TENANT_ID`**, and Stripe variables, see **[Environment configuration](../deployment/environment-configuration.md)** and **[Production checklist](../deployment/production-checklist.md)**.
+
+## Related documentation
+
+- **[Deployment](../deployment/README.md)** - Docker and production guides
+- **[Architecture](../architecture/README.md)** - Trust boundaries and components
+- **[Features](../features/README.md)** - Product capabilities including multi-tenancy and payments
+
+---
+
+_This folder is maintained for public transparency. Regulatory applicability of the CRA and national schemes depends on how the software is supplied and used; see [Compliance and standards](./compliance-and-standards.md)._
diff --git a/docs/decabill/security/accepted-risks.md b/docs/decabill/security/accepted-risks.md
new file mode 100644
index 000000000..7259f8a39
--- /dev/null
+++ b/docs/decabill/security/accepted-risks.md
@@ -0,0 +1,138 @@
+# Accepted risks (register)
+
+This register records **explicit risk acceptance** for Decabill product and deployment constraints that deviate from stricter security baselines. It supports **BSI / ISMS-style** traceability and **CRA-oriented** technical documentation (risk treatment and transparency). For vulnerability reporting, SBOM paths, and disclosure process, see **[Vulnerability reporting and artifacts](./vulnerability-reporting-and-artifacts.md)**.
+
+**Review cadence:** entries use acceptance **2026-05-06** and next review **2027-05-06** unless a row states otherwise; trigger an early review if cloud-init templates, billing multi-tenancy, CSP integration, authentication resolution, or Trivy gating policy change materially.
+
+---
+
+## DR-001 - Provisioning SSH (cloud-init templates)
+
+| Field | Recorded value |
+| ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **ID** | DR-001 |
+| **Area** | Provisioning SSH in cloud-init templates |
+| **Configuration** | **`PermitRootLogin yes`** and **root** `authorized_keys` installed via provisioning scripts (`libs/domains/decabill/backend/feature-billing-manager/src/lib/utils/cloud-init/agent-controller.utils.ts`, `agent-manager.utils.ts`) |
+| **Residual risk** | Compromise of the provisioning SSH private key or leakage of user-data/metadata can yield **root** on affected instances. |
+| **Mitigations in scope of this repo (templates)** | SSH **key-based** access; **password authentication disabled** in generated `sshd` configuration. |
+| **Compensating controls (deployer / org)** | Restrict network access (security groups, allowlisted IPs, bastion), rotate keys, monitor instances, minimize secrets in user-data. |
+| **Risk owner** | Maintaining party for this repository and product security documentation (Forepath). |
+| **Acceptor** | Repository maintainer (acceptance recorded in project documentation). |
+| **Acceptance date** | **2026-05-06** |
+| **Next review date** | **2027-05-06** |
+| **Rationale (business / technical)** | First-boot automation prioritizes operational simplicity for provisioned hosts bundled with service plans. Non-root SSH and **`PermitRootLogin no`** remain the documented hardening path when constraints allow. |
+
+#### Operator summary (DR-001)
+
+Some Decabill provisioning flows generate cloud-init that configures **SSH for `root`** and installs **`authorized_keys` under `/root/.ssh/`**. This is a **known, documented** property. Mitigations in templates are **key-only** SSH and **disabled password authentication**. Deployers should add network controls, bastions, key rotation, and where possible non-root administration with **`PermitRootLogin no`**.
+
+---
+
+## DR-002 - Billing multi-tenant API key scope (`STATIC_API_KEY_TENANT_ID` unset)
+
+| Field | Recorded value |
+| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **ID** | DR-002 |
+| **Area** | **Billing manager** multi-tenancy with **`AUTHENTICATION_METHOD=api-key`** (or inferred api-key when **`STATIC_API_KEY`** is set). Implementation: `TenantUserGuard` in `libs/domains/decabill/backend/feature-billing-manager/src/lib/guards/tenant-user.guard.ts`; admin bypass via `ensureAdmin()` in `billing-access.utils.ts`. |
+| **Configuration** | A **single** deployment-wide **`STATIC_API_KEY`**. **`STATIC_API_KEY_TENANT_ID`** is **optional**. When unset, a valid API key is accepted for **any** tenant id allowed by **`TENANTS`** / **`X-Tenant`** (each request selects the tenant via header). API key auth is treated as **admin** for billing admin routes. |
+| **Residual risk** | Anyone who possesses **`STATIC_API_KEY`** can read and mutate **all** configured tenants' billing data by changing **`X-Tenant`**, not only one tenant. This is **cross-tenant admin access** from a single shared secret. |
+| **Mitigations in scope of this repo** | **`STATIC_API_KEY_TENANT_ID`** optionally binds API key auth to one tenant (must match **`X-Tenant`**). User/session auth (**keycloak** / **users**) enforces per-user **`tenant_id`**. WebSocket dashboard status does **not** stream data to API key clients. |
+| **Compensating controls (deployer)** | Treat **`STATIC_API_KEY`** as a **high-value** secret (rotation, least exposure, no client-side use). Prefer **keycloak** or **users** for the billing console in multi-tenant production. Set **`STATIC_API_KEY_TENANT_ID`** when automation must use API key against **one** tenant only. Use separate billing deployments or keys per tenant if policy forbids shared cross-tenant automation credentials. |
+| **Risk owner** | Maintaining party for this repository and product security documentation (Forepath). |
+| **Acceptor** | Repository maintainer (acceptance recorded in project documentation). |
+| **Acceptance date** | **2026-06-19** |
+| **Next review date** | **2027-05-06** |
+| **Rationale (business / technical)** | Billing deployments use **one** static API key for automation and operator scripts. Requiring a separate key per tenant would multiply secret management without a current product requirement. **Explicit acceptance:** shared key plus optional tenant header is **intentional**; operators who need single-tenant binding use **`STATIC_API_KEY_TENANT_ID`**. |
+
+#### Operator summary (DR-002)
+
+With **`STATIC_API_KEY`** and **without** **`STATIC_API_KEY_TENANT_ID`**, the key is **not** limited to one tenant: it grants **admin-level access to every tenant** allowed by **`TENANTS`**, selected per request via **`X-Tenant`**. Set **`STATIC_API_KEY_TENANT_ID`** to restrict API key use to one tenant. Interactive users (Keycloak/JWT) remain scoped to their own tenant. See **[Environment configuration multi-tenancy](../deployment/environment-configuration.md#multi-tenancy)**.
+
+---
+
+## DR-003 - Web frontends: CSP `unsafe-inline` / `unsafe-eval` (Monaco)
+
+| Field | Recorded value |
+| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| **ID** | DR-003 |
+| **Area** | **decabill-frontend-billing-console** and **decabill-frontend-docs** Express servers |
+| **Configuration** | **Content Security Policy** allows **`'unsafe-inline'`** and **`'unsafe-eval'`** so **Monaco Editor** and related tooling work; policy is sent as **`Content-Security-Policy-Report-Only`** by default (violations reported, not blocked). Implementation: `libs/domains/shared/frontend/util-express-server/src/lib/security-headers.ts`. |
+| **Residual risk** | XSS impact can be greater than under a strict CSP; **report-only** does not block violations. |
+| **Mitigations in scope of this repo** | Set **`CSP_ENFORCE=true`** only in environments where compatibility is validated; billing console compose defaults to **`CSP_ENFORCE=true`** when operators have verified connectivity to the API. |
+| **Compensating controls (deployer)** | Enforce HTTPS, restrict **CORS** on the billing API, keep dependencies patched, monitor CSP reports if configured. |
+| **Risk owner** | Maintaining party for this repository and product security documentation (Forepath). |
+| **Acceptor** | Repository maintainer (acceptance recorded in project documentation). |
+| **Acceptance date** | **2026-05-06** |
+| **Next review date** | **2027-05-06** |
+| **Rationale (business / technical)** | Monaco and admin tooling are **core** to billing operations; tightening without a validated strategy risks **breaking** the console. Enforcement is **opt-in** or post-verification. |
+
+#### Operator summary (DR-003)
+
+By default, CSP may be **report-only** depending on `CSP_ENFORCE`. Use **`CSP_ENFORCE=true`** only after verification. See **[Operational hardening content security policy](./operational-hardening.md#content-security-policy-frontend-express)** and **[Environment configuration](../deployment/environment-configuration.md)**.
+
+---
+
+## DR-004 - Backend authentication method resolution
+
+| Field | Recorded value |
+| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **ID** | DR-004 |
+| **Area** | **`getAuthenticationMethod`** in `libs/domains/identity/backend/util-auth/src/lib/hybrid-auth.guard.ts` (shared by billing manager) |
+| **Configuration** | **`AUTHENTICATION_METHOD`** is **not** required to be set. When unset: if **`STATIC_API_KEY`** is set → **api-key** mode; otherwise → **keycloak**. **Protected routes are not anonymous**; Keycloak or users-mode guards still enforce authentication per configuration. |
+| **Residual risk** | Deployments may **implicitly** run in **keycloak** mode without a single obvious env flag, which can surprise operators who expect an explicit mode switch. **`STATIC_API_KEY`** remains a **high-value secret** in **api-key** mode. |
+| **Mitigations in scope of this repo** | Documented resolution order; **api-key** mode requires **`STATIC_API_KEY`** and validates the header; **keycloak** / **users** paths delegate to their guards. |
+| **Compensating controls (deployer)** | For **api-key** or **users** deployments, set **`AUTHENTICATION_METHOD`** explicitly; treat **`STATIC_API_KEY`** with rotation and least exposure; prefer **keycloak** with the customer IdP for integrated enterprise setups. |
+| **Risk owner** | Maintaining party for this repository and product security documentation (Forepath). |
+| **Acceptor** | Repository maintainer (acceptance recorded in project documentation). |
+| **Acceptance date** | **2026-05-06** |
+| **Next review date** | **2027-05-06** |
+| **Rationale (business / technical)** | Defaulting to **keycloak** when no API key is configured favors the **enterprise-typical** integrated IdP path while preserving backward compatibility for **`STATIC_API_KEY`**. |
+
+#### Operator summary (DR-004)
+
+Set **`AUTHENTICATION_METHOD`** explicitly if your policy requires **fully explicit** configuration. Never expose **`STATIC_API_KEY`**. See **[Operational hardening authentication mode](./operational-hardening.md#authentication-mode-backends)**.
+
+---
+
+## DR-005 - CI / local Trivy: unfixed vulnerabilities not gated
+
+| Field | Recorded value |
+| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **ID** | DR-005 |
+| **Area** | **Trivy** vulnerability scanning (`trivy.yaml` at repository root, pull-request CI, pre-commit, local image scans) |
+| **Configuration** | **`vulnerability.ignore-unfixed: true`**: findings **without a Fixed Version** are **excluded from the fail gate**. Only **CRITICAL** severities with an available fix fail CI and local hooks (see [CI security scanning](./ci-security-scanning.md)). |
+| **Residual risk** | Known **CRITICAL** issues may remain in dependencies, base images, or OS packages until upstream publishes a fix; they are visible in SARIF/report output but do not block merge. |
+| **Mitigations in scope of this repo** | Trivy still scans for **vuln**, **secret**, and **misconfig**; **HIGH** and below are report-only; per-CVE exceptions use `.trivyignore` with traceability. Decabill SBOMs are published on release (`decabill-*.cdx.json`). |
+| **Compensating controls (deployer)** | Monitor GitHub Security / SARIF artifacts; patch when fixes ship; track accepted CVEs in `.trivyignore` only when a fix exists but cannot be applied yet. |
+| **Risk owner** | Maintaining party for this repository and product security documentation (Forepath). |
+| **Acceptor** | Repository maintainer (acceptance recorded in project documentation). |
+| **Acceptance date** | **2026-05-16** |
+| **Next review date** | **2027-05-06** |
+| **Rationale (business / technical)** | Blocking on unfixed CVEs creates **false failures** with no remediation path and delays delivery without reducing exploitable risk. Gating on **fixable CRITICAL** issues keeps CI actionable while acknowledging vendor lag. |
+
+#### Operator summary (DR-005)
+
+**Unfixed vulnerabilities are acceptable for pipeline gating.** Address **CRITICAL** findings that have a published fix; track anything else via SARIF and release SBOMs. Do not add unfixed CVEs to `.trivyignore` solely to silence the gate. See **[CI security scanning](./ci-security-scanning.md)**.
+
+---
+
+## Hardening paths (if an acceptance is withdrawn)
+
+- **DR-001:** Prefer a non-root admin user, **`PermitRootLogin no`**, least-privilege `sudo`, and cloud-init-native `ssh_authorized_keys` where possible; reduce secrets in user-data.
+- **DR-002:** Require **`STATIC_API_KEY_TENANT_ID`** whenever **`STATIC_API_KEY`** is set and **`TENANTS`** lists more than one id; or reject API key auth when multiple tenants are configured; or issue per-tenant API keys (product change).
+- **DR-003:** Tighten CSP after automated and manual verification so billing console UI (including Monaco) still functions.
+- **DR-004:** Require **`AUTHENTICATION_METHOD`** in all environments if auditors demand explicit configuration.
+- **DR-005:** Set **`vulnerability.ignore-unfixed: false`** if policy requires failing on all CRITICAL findings regardless of fix availability.
+
+---
+
+## Related documentation
+
+- **[Compliance and standards](./compliance-and-standards.md)**
+- **[Operational hardening](./operational-hardening.md)**
+- **[Vulnerability reporting and artifacts](./vulnerability-reporting-and-artifacts.md)**
+- **[CI security scanning (Trivy)](./ci-security-scanning.md)**
+
+---
+
+_Update this register when acceptance is renewed or withdrawn._
diff --git a/docs/decabill/security/ci-security-scanning.md b/docs/decabill/security/ci-security-scanning.md
new file mode 100644
index 000000000..375b33b1b
--- /dev/null
+++ b/docs/decabill/security/ci-security-scanning.md
@@ -0,0 +1,89 @@
+# CI security scanning (Trivy)
+
+The monorepo uses [Trivy](https://trivy.dev/) in GitHub Actions for automated vulnerability, secret, and misconfiguration detection on **all products including Decabill**. Defaults are defined in `trivy.yaml` at the repository root.
+
+## What is scanned
+
+| Scan | When | Scope |
+| -------------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **Filesystem** | Every pull request | Dependencies (lockfiles), secrets, IaC/misconfig in the repo |
+| **Config** | Every pull request | Dockerfiles, Compose, GitHub Actions, and related IaC |
+| **Container images** | Pull request CI | `ghcr.io/forepath/*` images including **decabill-billing-api**, **decabill-billing-console-server**, and **decabill-docs-server** when built on the runner |
+
+Scanners enabled for filesystem scans: **vuln**, **secret**, **misconfig**.
+
+## Workflows
+
+| Workflow | Jobs |
+| ------------------------------------------- | ----------------------------------------------------------------------------------- |
+| `.github/workflows/pull-request-checks.yml` | `trivy-filesystem`, `trivy-config`, plus image scans after each container build job |
+
+The `.github/workflows/release.yml` workflow does **not** run Trivy vulnerability scans; pull-request scans are the CI gate before merge. Releases publish **Nx service SBOMs** and **Trivy CycloneDX container image SBOMs** to Dependency Track and the Decabill R2 bucket.
+
+## Severity policy
+
+| Setting | Value |
+| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **Fail pipeline** | CRITICAL |
+| **Report only** | HIGH, MEDIUM, LOW (visible in SARIF when uploaded) |
+| **Unfixed CVEs** | Ignored (`vulnerability.ignore-unfixed: true`) - findings without a Fixed Version do not fail the gate; see **[DR-005](./accepted-risks.md#dr-005--ci--local-trivy-unfixed-vulnerabilities-not-gated)** |
+
+## Viewing results
+
+1. **GitHub Security → Code scanning alerts** - when code scanning is enabled for the repository.
+2. **Workflow run artifacts** - SARIF files uploaded when code scanning upload is unavailable (`trivy-sarif-*` artifacts).
+
+SARIF categories include `trivy-fs`, `trivy-config`, and `trivy-images-*` on pull requests.
+
+## Triage and exceptions
+
+1. **Prefer fixing** - upgrade dependencies, base images, or configuration.
+2. **Documented ignore** - open a PR that adds the CVE to `.trivyignore`, reference an **[accepted-risk](./accepted-risks.md)** entry (or document a false positive), and note a **review/expiry date** in the PR description.
+3. **Do not** weaken `trivy.yaml` for one-off exceptions.
+
+See **[Accepted risks](./accepted-risks.md)** for deliberate product-level deviations, including **[DR-005](./accepted-risks.md#dr-005--ci--local-trivy-unfixed-vulnerabilities-not-gated)**.
+
+## Local reproduction
+
+**Pre-commit (filesystem + config, same CRITICAL gate as CI):**
+
+```bash
+./tools/ci/trivy-pre-commit.sh
+```
+
+This runs automatically via `.husky/pre-commit` on every commit. Install [Trivy](https://trivy.dev/latest/docs/installation/) before your first commit on a machine.
+
+**Manual full scans:**
+
+```bash
+trivy fs . --config trivy.yaml --quiet
+trivy config . --config trivy.yaml --quiet
+trivy image ghcr.io/forepath/decabill-billing-api:latest --config trivy.yaml --quiet
+```
+
+After building Decabill images locally:
+
+```bash
+./tools/ci/trivy-scan-local-images.sh
+./tools/ci/trivy-generate-image-sboms.sh
+```
+
+| Scan type | Pre-commit | Pull request CI |
+| ----------------------- | ---------- | --------------- |
+| Filesystem (`trivy fs`) | Yes | Yes |
+| Config (`trivy config`) | Yes | Yes |
+| Container images | No | Yes (per build) |
+
+## Relationship to SBOM and Dependency Track
+
+- **Service CycloneDX SBOMs** are generated by Nx (`sbom` target) for Decabill projects (`decabill-*.cdx.json`).
+- **Container image CycloneDX SBOMs** are generated by Trivy (`container-decabill-*.cdx.json`).
+- **Pull requests** upload SBOM files as the `sbom-artifacts` artifact.
+- **Releases** publish to Dependency Track and copy files under `dist/sboms/` to the **Decabill R2 bucket** - see **[Vulnerability reporting and artifacts](./vulnerability-reporting-and-artifacts.md#software-bill-of-materials-sbom)**.
+- **Trivy** vulnerability scans (PR) provide the **CI gate**; SBOM generation uses Trivy CycloneDX output separately from SARIF scans.
+
+## Related documentation
+
+- **[Security overview](./README.md)**
+- **[Vulnerability reporting and artifacts](./vulnerability-reporting-and-artifacts.md)**
+- **[Container image security](./container-images.md)** - Decabill image names and hardening
diff --git a/docs/decabill/security/compliance-and-standards.md b/docs/decabill/security/compliance-and-standards.md
new file mode 100644
index 000000000..128978bab
--- /dev/null
+++ b/docs/decabill/security/compliance-and-standards.md
@@ -0,0 +1,63 @@
+# Compliance and standards (EU CRA and BSI IT-Grundschutz)
+
+This page explains what **EU Cyber Resilience Act (CRA)** and **BSI IT-Grundschutz** frameworks typically expect in terms of **documented** cybersecurity evidence, and how **Decabill** public documentation is intended to support **transparency** and **operator due diligence**. It is **informative**, not legal advice. Conformity, CE marking, organizational certification, and audit scope must be confirmed with qualified advisors for your role (manufacturer, importer, deployer, or integrator) and jurisdiction.
+
+## EU Cyber Resilience Act (CRA)
+
+**Legal act:** [Regulation (EU) 2024/2847](https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX%3A32024R2847) (CRA). Official summary and FAQs: [Cyber Resilience Act](https://digital-strategy.ec.europa.eu/en/policies/cyber-resilience-act) (European Commission).
+
+### Scope and open source (high level)
+
+The CRA applies to **products with digital elements** when they are **made available on the Union market** in the course of a **commercial activity**. Whether Decabill or a particular derivative counts as in scope for you depends on **your** distribution model, not on this documentation alone.
+
+### Documentation and transparency obligations (themes)
+
+| Theme | What the regulation generally expects | Role of Decabill documentation |
+| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **Risk assessment** | Identify and assess cybersecurity risks in light of intended and reasonably foreseeable use. | **[Operational hardening](./operational-hardening.md)** and **[Architecture](../architecture/README.md)** describe trust boundaries and controls. **[Accepted risks](./accepted-risks.md)** records deliberate residual risk and compensating measures. |
+| **Technical documentation** | Document the risk assessment and means chosen to meet **essential cybersecurity requirements** (Annex I). | This security section, deployment and environment docs, and the risk register form the **public** technical narrative. Build pipelines and internal records may hold additional evidence. |
+| **Secure by design and default** | Implement Annex I requirements (hardening, confidentiality and integrity of data, limited attack surface). | **[Operational hardening](./operational-hardening.md)**, **[Container image security](./container-images.md)**, **[Production checklist](../deployment/production-checklist.md)**, and **[Environment configuration](../deployment/environment-configuration.md)** describe production-oriented controls. |
+| **Vulnerability handling** | Establish processes to identify and remediate vulnerabilities **without undue delay**; supply **security updates**. | **[Vulnerability reporting and artifacts](./vulnerability-reporting-and-artifacts.md)** describes coordinated disclosure, supported versions, and response commitments. |
+| **Information for the user (Annex II)** | Provide instructions so users can **install, operate, and maintain** the product securely. | **[Getting Started](../getting-started.md)**, **[Deployment](../deployment/README.md)**, **[Environment configuration](../deployment/environment-configuration.md)**, and **[Vulnerability reporting and artifacts](./vulnerability-reporting-and-artifacts.md)** support operator understanding. |
+| **Conformity assessment** | Complete applicable **conformity assessment** before placing on the market when in scope. | Not asserted here. Deployers integrate Decabill into their own systems; **your** conformity strategy may combine this product with infrastructure and services. |
+
+### Application timeline (CRA)
+
+The CRA **entered into force** on 10 December 2024. **Full application** of many operational provisions is **11 December 2027**. Refer to the Official Journal text and Commission guidance for dates that matter to your role.
+
+## BSI IT-Grundschutz
+
+**Context:** [IT-Grundschutz](https://www.bsi.bund.de/EN/Themen/Unternehmen-und-Organisationen/Standards-und-Zertifizierung/IT-Grundschutz/it-grundschutz_node.html) (German Federal Office for Information Security, BSI) provides a structured method for **information security management** in organizations. **Decabill documentation does not replace** an organizational **security concept** or **IT-Grundschutz audit** for your enterprise.
+
+### Documentation expectations (themes)
+
+| Theme | Typical expectation | How Decabill documentation supports it |
+| --------------------------------------- | --------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **Security concept and scope** | Describe the target of protection, boundaries, and roles. | **[System overview](../architecture/system-overview.md)** and **[Architecture](../architecture/README.md)**. |
+| **Protection needs and risk treatment** | Classify protection needs; treat risks with rationale and owners. | **[Accepted risks](./accepted-risks.md)** gives **explicit acceptance**, **owners**, **dates**, **review cadence**, and **compensating controls**. |
+| **Requirement fulfillment** | Record fulfillment and justify deviations. | Accepted-risk entries document **deviations** with **mitigations** and **review** dates. **[Operational hardening](./operational-hardening.md)** states implemented controls. |
+| **Operational measures** | Logging, configuration management, incident handling, and supplier relationships. | **[Operational hardening](./operational-hardening.md)**, **[Vulnerability reporting and artifacts](./vulnerability-reporting-and-artifacts.md)**, deployment guides. |
+
+Formal IT-Grundschutz certification requires **organizational** processes; this English **open** documentation is aimed at **global** transparency and **supplier** due diligence.
+
+## Trust boundaries (summary)
+
+1. **Browser** to **Express frontend** (billing console or docs) to **billing manager API** (`/api`).
+2. **Browser** to **billing WebSocket** (`/billing` namespace) for dashboard status (interactive auth only; API key clients do not receive dashboard streams).
+3. **Billing manager** to **Stripe** for payments and webhooks.
+4. **Billing manager** to **cloud provider APIs** (Hetzner, DigitalOcean) and **SSH** for provisioning (see **DR-001** in **[Accepted risks](./accepted-risks.md)**).
+5. **Worker processes** to **Redis**, **Postgres**, **SMTP**, and external APIs during background jobs.
+
+Detail: **[Container image security](./container-images.md)**, **[Operational hardening](./operational-hardening.md)**.
+
+## Related documentation
+
+- **[Accepted risks](./accepted-risks.md)**
+- **[Operational hardening](./operational-hardening.md)**
+- **[Container image security](./container-images.md)**
+- **[Vulnerability reporting and artifacts](./vulnerability-reporting-and-artifacts.md)**
+- **[Environment configuration](../deployment/environment-configuration.md)**
+
+---
+
+_For regulatory interpretation and conformity decisions, consult qualified legal and compliance advisors._
diff --git a/docs/decabill/security/container-images.md b/docs/decabill/security/container-images.md
new file mode 100644
index 000000000..2c5598c19
--- /dev/null
+++ b/docs/decabill/security/container-images.md
@@ -0,0 +1,98 @@
+# Container image security
+
+This page documents **first-party Decabill Docker images**: runtime users, secrets handling, and build conventions. It complements **[Operational hardening](./operational-hardening.md)** and **[Docker deployment](../deployment/docker-deployment.md)**.
+
+## Published images
+
+| Image | Application | Registry reference |
+| ----------------------------------- | ---------------------------- | --------------------------------------------------------- |
+| **decabill-billing-api** | Backend billing manager | `ghcr.io/forepath/decabill-billing-api:latest` |
+| **decabill-billing-console-server** | Frontend billing console SSR | `ghcr.io/forepath/decabill-billing-console-server:latest` |
+| **decabill-docs-server** | Frontend docs SSR | `ghcr.io/forepath/decabill-docs-server:latest` |
+
+Build targets:
+
+```bash
+nx docker:api decabill-backend-billing-manager
+nx docker:server decabill-frontend-billing-console
+nx docker:server decabill-frontend-docs
+```
+
+## Runtime users
+
+| Image family | User | Default UID/GID | Notes |
+| -------------------------- | ---------- | --------------- | ---------------------------------- |
+| **decabill-billing-api** | `agenstra` | **10001** | `ARG APP_UID` / `APP_GID` at build |
+| **billing-console-server** | `node` | **1000** | Alpine-based SSR image |
+| **docs-server** | `node` | **1000** | Alpine-based SSR image |
+
+Processes do **not** run as root after container start.
+
+## Billing API image (`decabill-billing-api`)
+
+Source: `apps/decabill/backend-billing-manager/Dockerfile.api`
+
+- Exposes **3200** (HTTP API) and **8082** (WebSocket)
+- Health check: `GET /api/health`
+- **No Docker socket mount** (billing does not orchestrate agent containers on the host)
+- Secrets (database, Stripe, `ENCRYPTION_KEY`, `STATIC_API_KEY`, cloud API tokens) are supplied at **deploy time**, not as default `ENV` in the image
+
+### Volumes (typical compose)
+
+| Mount | Purpose |
+| -------------------- | ----------------------------------------- |
+| `invoice_pdf_data` | Invoice PDFs at `/data/invoices` |
+| `./provider-plugins` | Optional dynamic payment/provider plugins |
+
+### Build arguments
+
+| Argument | Purpose | Default |
+| --------- | --------------------------- | --------- |
+| `APP_UID` | Runtime user `agenstra` UID | **10001** |
+| `APP_GID` | Runtime user `agenstra` GID | **10001** |
+
+Unlike agent orchestration API images, the billing API image does **not** require `DOCKER_GID` because it does not access `/var/run/docker.sock`.
+
+## Billing console server image (`decabill-billing-console-server`)
+
+Source: `apps/decabill/frontend-billing-console/Dockerfile.server`
+
+- Default `PORT=4500`
+- Runs as **`node`** (UID **1000**)
+- Runtime `CONFIG` URL and CSP variables documented in **[Environment configuration](../deployment/environment-configuration.md)**
+- Compose often sets `CSP_CONNECT_SRC_EXTRA` to reach the billing API from the browser
+
+## Docs server image (`decabill-docs-server`)
+
+Source: `apps/shared/frontend-docs/Dockerfile.server` (same pattern as billing console)
+
+- Default `PORT=4200`
+- Runs as **`node`** (UID **1000**)
+- Static documentation content; typically fewer `connect-src` requirements than the billing console
+
+## Secrets and configuration
+
+- Do not rely on image defaults for database passwords, Stripe keys, or API keys
+- Set variables in Compose, Kubernetes secrets, or your orchestrator
+- `ENCRYPTION_KEY` must be provided in production for encrypted subscription item fields
+
+## Image scanning
+
+Container images built in CI are scanned with Trivy on pull requests. Decabill images are included when built in the PR pipeline. See **[CI security scanning](./ci-security-scanning.md)** and **[DR-005](./accepted-risks.md#dr-005--ci--local-trivy-unfixed-vulnerabilities-not-gated)**.
+
+Release publishes CycloneDX SBOMs for Decabill images as `container-decabill-*.cdx.json`. See **[Vulnerability reporting and artifacts](./vulnerability-reporting-and-artifacts.md)**.
+
+## Coordinated upgrades
+
+Deploy **billing API, worker, and scheduler** containers from the **same release tag** when schema migrations or queue job contracts change. Mismatched tags between API and workers can cause job processing errors after deployments.
+
+## Related documentation
+
+- **[Operational hardening](./operational-hardening.md)** - Summary table and cross-links
+- **[Docker deployment](../deployment/docker-deployment.md)** - Compose services
+- **[Production checklist](../deployment/production-checklist.md)** - Pre-flight checks
+- **[Background jobs](../deployment/background-jobs.md)** - Worker and scheduler images use the same billing API image with different `QUEUE_ROLE`
+
+---
+
+_For provisioning SSH accepted risk on provisioned instances, see [DR-001](./accepted-risks.md#dr-001--provisioning-ssh-cloud-init-templates)._
diff --git a/docs/decabill/security/operational-hardening.md b/docs/decabill/security/operational-hardening.md
new file mode 100644
index 000000000..406fffb49
--- /dev/null
+++ b/docs/decabill/security/operational-hardening.md
@@ -0,0 +1,107 @@
+# Operational hardening
+
+This page describes **implemented** security controls for Decabill that operators and security reviewers should know about. For **environment variable names and defaults**, see **[Environment configuration](../deployment/environment-configuration.md)**.
+
+## Container images (Docker)
+
+First-party Decabill images are hardened for production use. Full detail: **[Container image security](./container-images.md)**.
+
+| Practice | Detail |
+| ----------------------- | --------------------------------------------------------------------------------------------------------------------------- |
+| **Non-root runtime** | Billing API runs as **`agenstra`** (UID/GID **10001** by default); frontend servers run as **`node`** (**1000**). |
+| **No baked-in secrets** | Database, Stripe, encryption keys, and API keys are **not** defaulted in image `ENV`; operators supply them at deploy time. |
+| **No Docker socket** | Billing manager does not mount `/var/run/docker.sock`; provisioning uses cloud APIs and SSH from worker processes. |
+| **Image scanning** | Repository `trivy.yaml` configures filesystem/config/image scans; CI fails on fixable CRITICAL findings. |
+
+Deploy **billing API, worker, and scheduler** from the **same release tag** when migrations or job handlers change.
+
+## Authentication mode (backends)
+
+Resolution is implemented in **`getAuthenticationMethod`** (`libs/domains/identity/backend/util-auth/src/lib/hybrid-auth.guard.ts`):
+
+- If **`AUTHENTICATION_METHOD`** is set to **`api-key`**, **`keycloak`**, or **`users`**, that value is used.
+- If **`AUTHENTICATION_METHOD`** is **unset** or invalid:
+ - If **`STATIC_API_KEY`** is set → effective mode **`api-key`**.
+ - Otherwise → effective mode **`keycloak`**.
+
+**`api-key`** without **`STATIC_API_KEY`** fails at runtime. Health endpoints **`/api/health`** and **`/health`** remain unauthenticated by design.
+
+**Operator note:** Set **`AUTHENTICATION_METHOD`** explicitly if your security policy requires unambiguous configuration. See **DR-004** in **[Accepted risks](./accepted-risks.md)**.
+
+## Billing manager multi-tenancy
+
+| Control | Purpose |
+| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------ |
+| **`X-Tenant` header** | Selects tenant context on HTTP and billing WebSocket handshakes. Validated against **`TENANTS`**; unknown ids → **400**. |
+| **`TenantUserGuard`** | Ensures authenticated users' **`tenant_id`** matches the request tenant. |
+| **`STATIC_API_KEY_TENANT_ID`** | Optional bind of API key auth to one tenant. |
+
+**Accepted risk [DR-002](./accepted-risks.md#dr-002--billing-multi-tenant-api-key-scope-static_api_key_tenant_id-unset):** With **`STATIC_API_KEY`** and **without** **`STATIC_API_KEY_TENANT_ID`**, one deployment API key grants **admin access to every tenant** in **`TENANTS`** (tenant chosen per request via **`X-Tenant`**). Interactive **keycloak** / **users** sessions remain limited to the user's tenant.
+
+Code: `libs/domains/decabill/backend/feature-billing-manager/src/lib/guards/tenant-user.guard.ts`, `libs/domains/shared/backend/util-http-context/src/lib/tenant-id.middleware.ts`.
+
+## Stripe webhooks
+
+- Webhook signatures verified with **`STRIPE_WEBHOOK_SECRET`**
+- Invalid signatures are rejected; do not disable verification in production
+- Route webhook URL only to the billing API ingress; restrict by network policy where possible
+
+## Logging and correlation
+
+- **Correlation ID middleware** runs first on the billing API: accepts or generates `X-Correlation-Id` / `X-Request-Id`, binds AsyncLocalStorage, sets the response header, logs one **access line per request** with **path only** (no query string) and **`redactSecretsInString`** (Bearer, Basic, ApiKey-style fragments, email patterns).
+- **`CorrelationAwareConsoleLogger`** adds `[corr=…]` (text) or `correlationId` (JSON) to Nest framework logs inside the request async context.
+
+Code: `libs/domains/shared/backend/util-http-context/`.
+
+Do not log Stripe secrets, full card data, or `STATIC_API_KEY` values.
+
+## Frontend runtime configuration (`GET /config`)
+
+When **`CONFIG`** points to a remote JSON URL, Express servers validate fetches using **`@forepath/shared/frontend/util-runtime-config-server`**:
+
+- Production: **HTTPS** unless **`CONFIG_ALLOW_INSECURE_HTTP=true`**; **`CONFIG_ALLOWED_HOSTS`** required when `CONFIG` is set.
+- Timeout, max bytes, JSON object shape, content-type, redirect blocking, optional key count/depth limits.
+- DNS check against private/loopback resolution (skippable via **`CONFIG_SKIP_DNS_CHECK`** in exceptional cases).
+
+**`CONFIG_ALLOWED_HOSTS`** supports **`*`** to explicitly allow **any host**. Prefer explicit host allowlists in production.
+
+Applies to **decabill-frontend-billing-console** and **decabill-frontend-docs**.
+
+See **[Environment configuration frontend applications](../deployment/environment-configuration.md#frontend-applications-express-ssr)**.
+
+## Content Security Policy (frontend Express)
+
+- CSP includes **`'unsafe-inline'`** and **`'unsafe-eval'`** for Monaco and tooling; default delivery is **`Content-Security-Policy-Report-Only`** unless **`CSP_ENFORCE=true`**.
+- Billing console compose often sets **`CSP_ENFORCE=true`** after operators verify API connectivity via **`CSP_CONNECT_SRC_EXTRA`**.
+
+Accepted risk: **DR-003** in **[Accepted risks](./accepted-risks.md)**.
+
+## WebSocket CORS (billing manager)
+
+- **`WEBSOCKET_CORS_ORIGIN`**: comma-separated allowed origins for the billing Socket.IO server.
+- In **production**, if unset, behavior follows Nest/Socket.IO configuration; set explicitly to your billing console origins.
+
+Dashboard status streaming is available to interactive authenticated users, not to API key clients.
+
+## Origin allowlist (unsafe HTTP methods)
+
+Browser-originated **state-changing** requests can be restricted by origin allowlist middleware on backends (`origin-allowlist.middleware.ts` in identity util-auth). Configure per deployment expectations.
+
+## Bull Board
+
+When enabled, `/admin/queues` uses HTTP Basic auth and bypasses API HybridAuthGuard so operators can manage jobs. Restrict network access to this path in production. See **[Background jobs](../deployment/background-jobs.md)**.
+
+## Provisioning SSH
+
+Cloud-init templates may configure root SSH for first-boot automation. See **DR-001** in **[Accepted risks](./accepted-risks.md)**.
+
+## Related documentation
+
+- **[Accepted risks](./accepted-risks.md)** - DR-001 through DR-005
+- **[Environment configuration](../deployment/environment-configuration.md)**
+- **[Production checklist](../deployment/production-checklist.md)**
+- **[Vulnerability reporting and artifacts](./vulnerability-reporting-and-artifacts.md)** - Disclosure and response commitments
+
+---
+
+_For provisioning SSH, CSP, and API key scope operator summaries, see [Accepted risks](./accepted-risks.md)._
diff --git a/docs/decabill/security/vulnerability-reporting-and-artifacts.md b/docs/decabill/security/vulnerability-reporting-and-artifacts.md
new file mode 100644
index 000000000..b3067c646
--- /dev/null
+++ b/docs/decabill/security/vulnerability-reporting-and-artifacts.md
@@ -0,0 +1,126 @@
+# Vulnerability reporting and artifacts
+
+This page documents **responsible disclosure**, **supported versions**, **SBOM** publication for Decabill, and download locations. It is the copy intended for readers who stay inside the Decabill documentation tree.
+
+## Supported versions and security updates
+
+| Version | Supported |
+| ------------------- | --------- |
+| 2.x.x | Yes |
+| 1.x.x | No |
+| 0.x.x | No |
+| Earlier major lines | No |
+
+Security updates are intended for supported **2.x.x** releases.
+
+**CRA / operator context:** If you **place a product with digital elements on the EU market** and must state a **support period** end date (month and year) for users, derive it from **your** release and maintenance policy. This open-source project publishes **SBOMs** per release and documents supported lines and disclosure here so integrators can align their own statements with upstream practice.
+
+## Our response commitment
+
+- **48-hour acknowledgment** of security reports
+- **Regular updates** on investigation progress
+- **Coordinated disclosure** with security researchers
+- **Timely fixes** for confirmed vulnerabilities
+- **Public acknowledgment** of security researchers
+
+**Target response times** (after acknowledgment): **Critical** - 24 hours; **High** - 48 hours; **Medium** - 1 week; **Low** - 2 weeks. **Emergency contact** for critical security issues is available **24/7** via **soc@forepath.io**. General inquiries: **hi@forepath.io**.
+
+## Reporting a vulnerability
+
+Responsible disclosure is appreciated.
+
+### How to report
+
+**Do not** report security vulnerabilities through **public GitHub issues**.
+
+Report to the security team:
+
+- **Email:** soc@forepath.io
+- **Subject:** `[SECURITY] Decabill Vulnerability Report`
+- **Initial response:** aim to respond within **48 hours**
+
+### What to include
+
+1. **Description** - Clear description of the vulnerability
+2. **Impact** - Potential impact and severity
+3. **Steps to reproduce** - Detailed steps
+4. **Affected versions** - Which releases are affected
+5. **Suggested fix** - If you have ideas
+6. **Contact information** - How to reach you for follow-up
+
+### Process (summary)
+
+1. Acknowledgment (target: within 48 hours)
+2. Assessment and validation
+3. Fix development and testing
+4. Coordinated disclosure
+5. Release and advisory (where applicable)
+
+### If you discover an issue
+
+1. Do **not** create a public issue or post details publicly before coordination.
+2. Email **soc@forepath.io** with as much detail as possible.
+3. Allow time for investigation and fix before public disclosure.
+
+---
+
+## Continuous scanning (CI)
+
+Pull requests run **[Trivy](https://trivy.dev/)** scans (dependencies, secrets, misconfigurations, and container images). CRITICAL findings fail the PR pipeline unless documented in `.trivyignore`. The release workflow does not re-run Trivy vulnerability gating.
+
+See **[CI security scanning (Trivy)](./ci-security-scanning.md)** for workflows, severity policy, SARIF locations, and local reproduction.
+
+---
+
+## Software Bill of Materials (SBOM)
+
+CycloneDX SBOM files are published for each release. **Decabill** publishes to a dedicated object-store bucket; the object key layout matches other Forepath product domains.
+
+| Field | Value |
+| ---------------- | --------------------------- |
+| **Path pattern** | `releases//sboms/` |
+| **Example** | `releases/2.0.0/sboms/` |
+
+**Buckets and credentials (production `production` environment secrets):**
+
+Shared R2 credentials: `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_REGION`
+
+| Domain | Product-specific secrets |
+| -------- | -------------------------------------------------------- |
+| Decabill | `DECABILL_AWS_BUCKET`, `DECABILL_CLOUDFLARE_R2_ENDPOINT` |
+
+**Decabill SBOM files:**
+
+- Service SBOMs: `decabill-*.cdx.json`
+- Container image SBOMs: `container-decabill-*.cdx.json`
+- Legacy container naming: `container-agenstra-billing-*.cdx.json` (older release lines)
+
+**Sources:**
+
+- **Service SBOMs** - Nx `sbom` target (`dist/sboms/.cdx.json`)
+- **Container image SBOMs** - Trivy CycloneDX (`dist/sboms/container-.cdx.json`) for images built in the release pipeline
+
+Pull requests upload service and container SBOM files as the **`sbom-artifacts`** workflow artifact. Releases upload each container image SBOM to Dependency-Track with `forepath/gh-upload-sbom@v2`.
+
+### Downloads
+
+Resolve `` from your deployment or from **[Downloads](https://downloads.decabill.com/)**, then substitute it in the path:
+
+`releases//sboms/decabill-*.cdx.json`
+
+Example public download site: **https://downloads.decabill.com/**
+
+---
+
+## Related documentation
+
+- **[Accepted risks](./accepted-risks.md)** - Documented residual risks
+- **[Operational hardening](./operational-hardening.md)**
+- **[Compliance and standards](./compliance-and-standards.md)**
+- **[Deployment production checklist](../deployment/production-checklist.md)**
+- **[CI security scanning](./ci-security-scanning.md)**
+
+### External references
+
+- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
+- [NIST Cybersecurity Framework](https://www.nist.gov/cyberframework)
diff --git a/docs/decabill/troubleshooting/README.md b/docs/decabill/troubleshooting/README.md
new file mode 100644
index 000000000..1e36e078d
--- /dev/null
+++ b/docs/decabill/troubleshooting/README.md
@@ -0,0 +1,71 @@
+# Troubleshooting Documentation
+
+This section provides guides for troubleshooting common issues and debugging problems in Decabill.
+
+## Overview
+
+Troubleshooting guides help you:
+
+- Identify and resolve common billing and deployment problems
+- Debug API, WebSocket, queue, and payment issues effectively
+- Understand error messages and log patterns
+- Find solutions quickly
+
+## Troubleshooting Guides
+
+### [Common Issues](./common-issues.md)
+
+Common problems and their solutions:
+
+- Connection and CORS issues
+- Authentication and multi-tenancy problems
+- Stripe payment and webhook errors
+- Background job and Redis failures
+- Database and migration issues
+
+### [Debugging Guide](./debugging-guide.md)
+
+Debugging strategies and tools:
+
+- Logging and log analysis
+- Bull Board and queue inspection
+- API and WebSocket testing
+- Performance debugging
+
+## Quick Troubleshooting
+
+### Connection Issues
+
+- Check billing API is running on port **3200**
+- Verify billing console `API_URL` and `CSP_CONNECT_SRC_EXTRA`
+- Confirm `CORS_ORIGIN` includes the console origin
+
+### Authentication Problems
+
+- Verify API key or Keycloak token
+- Check **`X-Tenant`** header matches an allowed tenant
+- Review **`STATIC_API_KEY_TENANT_ID`** if API key auth fails for some tenants
+
+### Background Jobs
+
+- Confirm Redis on port **6380** (compose default) is reachable
+- Ensure API container is healthy before worker starts
+- Open Bull Board at `http://localhost:3200/admin/queues`
+
+## Getting Help
+
+If you encounter issues:
+
+1. Check **[Common Issues](./common-issues.md)**
+2. Review **[Debugging Guide](./debugging-guide.md)**
+3. Check application and worker logs
+4. Consult **[Deployment](../deployment/README.md)** and **[Environment configuration](../deployment/environment-configuration.md)**
+
+## Related Documentation
+
+- **[Security accepted risks](../security/accepted-risks.md)** - Known documented behaviors (DR-001 through DR-005)
+- **[Background jobs](../deployment/background-jobs.md)** - Queue roles and Bull Board
+
+---
+
+_For specific issues, see [Common Issues](./common-issues.md)._
diff --git a/docs/decabill/troubleshooting/common-issues.md b/docs/decabill/troubleshooting/common-issues.md
new file mode 100644
index 000000000..890b84e54
--- /dev/null
+++ b/docs/decabill/troubleshooting/common-issues.md
@@ -0,0 +1,203 @@
+# Common Issues
+
+Common problems and their solutions in Decabill.
+
+## Connection Issues
+
+### Billing Console Cannot Reach API
+
+**Symptoms**: Network errors in browser console; failed `fetch` to billing API
+
+**Solutions**:
+
+- Verify billing manager is running: `nx serve decabill-backend-billing-manager` or `docker compose ps`
+- Check API URL in console configuration: `http://localhost:3200`
+- In Docker, set `CSP_CONNECT_SRC_EXTRA` to the API origin (compose default: `http://host.docker.internal:3200`)
+- Verify `CORS_ORIGIN` on the API includes the console origin (or `*` in development)
+
+### WebSocket Connection Fails
+
+**Symptoms**: Dashboard status does not update; WebSocket errors in browser console
+
+**Solutions**:
+
+- Verify WebSocket URL: `http://localhost:8082` (namespace `billing`)
+- Check `WEBSOCKET_CORS_ORIGIN` includes the console origin
+- Confirm you are using interactive auth (Keycloak/users); API key clients do not receive dashboard streams
+- Ensure **`X-Tenant`** is sent on the handshake when using multi-tenant setups
+
+## Authentication Problems
+
+### "Unauthorized" Errors (401)
+
+**Symptoms**: API requests return 401 Unauthorized
+
+**Solutions**:
+
+- Verify `Authorization` header or Keycloak token is valid
+- Check `STATIC_API_KEY` matches server configuration
+- Review `AUTHENTICATION_METHOD` and implicit resolution (see **[DR-004](../security/accepted-risks.md#dr-004--backend-authentication-method-resolution)**)
+
+### "Forbidden" or Tenant Errors (400/403)
+
+**Symptoms**: Requests fail with tenant-related errors
+
+**Solutions**:
+
+- Send **`X-Tenant`** header matching an id in **`TENANTS`**
+- For user auth, ensure user's `tenant_id` matches **`X-Tenant`**
+- With API key auth, set **`STATIC_API_KEY_TENANT_ID`** if the key is bound to one tenant (see **[DR-002](../security/accepted-risks.md#dr-002--billing-multi-tenant-api-key-scope-static_api_key_tenant_id-unset)**)
+
+### Keycloak Authentication Fails
+
+**Symptoms**: Login redirect loops or token errors
+
+**Solutions**:
+
+- Verify Keycloak server URL, realm, client id, and secret
+- Check Keycloak client redirect URIs include the billing console URL
+- Review Keycloak server logs
+
+## Stripe and Payment Issues
+
+### Checkout Session Fails
+
+**Symptoms**: Stripe checkout does not start or returns errors
+
+**Solutions**:
+
+- Verify `STRIPE_SECRET_KEY` is set and matches Stripe dashboard mode (test vs live)
+- Check `STRIPE_CHECKOUT_SUCCESS_URL` and `STRIPE_CHECKOUT_CANCEL_URL` point to valid console URLs
+- Review billing API logs for Stripe API errors
+
+### Webhook Events Not Processed
+
+**Symptoms**: Payments succeed in Stripe but invoice status does not update
+
+**Solutions**:
+
+- Confirm webhook endpoint URL is reachable from Stripe
+- Verify `STRIPE_WEBHOOK_SECRET` matches the endpoint signing secret in Stripe dashboard
+- Check worker container is running (`QUEUE_ROLE=worker`)
+- Inspect failed jobs in Bull Board at `/admin/queues`
+
+## Background Jobs and Redis
+
+### Jobs Not Running
+
+**Symptoms**: Subscriptions not billed; reminders not sent; overdue invoices not updated
+
+**Solutions**:
+
+- Confirm Redis is running (compose: port **6380** on host)
+- Verify `REDIS_HOST`, `REDIS_PORT`, and `REDIS_KEY_PREFIX=decabill-billing`
+- Ensure scheduler container is running with `QUEUE_ROLE=scheduler`
+- Ensure worker container is running with `QUEUE_ROLE=worker`
+- API must be healthy first (migrations applied)
+
+### Bull Board Not Accessible
+
+**Symptoms**: 401 on `/admin/queues` or connection refused
+
+**Solutions**:
+
+- Use URL `http://localhost:3200/admin/queues` (not `/api/admin/queues`)
+- Set `QUEUE_BULL_BOARD_ENABLED=true` on API container
+- Provide `QUEUE_BULL_BOARD_USERNAME` and `QUEUE_BULL_BOARD_PASSWORD`
+- In production, password is required when board is enabled
+
+### Redis Connection Errors in Worker Logs
+
+**Symptoms**: Worker or scheduler cannot connect to Redis
+
+**Solutions**:
+
+- From host dev: `REDIS_PORT=6380` when using compose Redis mapping
+- Inside compose network: `REDIS_HOST=redis`, `REDIS_PORT=6379`
+- Check Redis health: `docker compose exec redis redis-cli ping`
+
+## Database Issues
+
+### Database Connection Fails
+
+**Symptoms**: API fails to start; migration errors
+
+**Solutions**:
+
+- Verify PostgreSQL is running
+- Check `DB_HOST`, `DB_PORT`, `DB_USERNAME`, `DB_PASSWORD`, `DB_DATABASE`
+- Ensure only API or `all` role runs migrations on startup
+
+### Migration Errors
+
+**Symptoms**: Schema errors after upgrade
+
+**Solutions**:
+
+- Start API container alone and wait for healthy status before workers
+- Review migration logs from `QUEUE_ROLE=api` container
+- Restore from backup if partial migration occurred in production
+
+### Encryption Key Errors
+
+**Symptoms**: Errors reading encrypted subscription item fields
+
+**Solutions**:
+
+- Set `ENCRYPTION_KEY` consistently across API, worker, and scheduler
+- Do not change `ENCRYPTION_KEY` without a data migration plan for encrypted columns
+
+## CORS and CSP Issues
+
+### CORS Errors in Browser
+
+**Symptoms**: CORS errors in browser console on API calls
+
+**Solutions**:
+
+- Set `CORS_ORIGIN` to the billing console origin (comma-separated for multiple)
+- In production, unset `CORS_ORIGIN` disables CORS entirely
+
+### CSP Blocks API Calls
+
+**Symptoms**: CSP violations in console; blocked `connect-src`
+
+**Solutions**:
+
+- Add API origin to `CSP_CONNECT_SRC_EXTRA`
+- In production, HTTPS API URLs must be listed explicitly (scheme keywords alone are insufficient for plain HTTP)
+- Test with `CSP_ENFORCE=false` temporarily to confirm CSP is the cause
+
+## Provisioning Issues
+
+### Server Provisioning Fails
+
+**Symptoms**: Subscription items stuck in provisioning or backorder states
+
+**Solutions**:
+
+- Verify `HETZNER_API_TOKEN` or `DIGITALOCEAN_API_TOKEN` is valid
+- Check worker logs for cloud API errors
+- Inspect `backorder-retry` jobs in Bull Board
+- Review **[DR-001](../security/accepted-risks.md#dr-001--provisioning-ssh-cloud-init-templates)** for SSH posture on new instances
+
+## Rate Limiting Issues
+
+### 429 Too Many Requests
+
+**Symptoms**: API returns 429
+
+**Solutions**:
+
+- Increase `RATE_LIMIT_LIMIT` or `RATE_LIMIT_TTL`
+- Disable in development: `RATE_LIMIT_ENABLED=false`
+
+## Related Documentation
+
+- **[Debugging Guide](./debugging-guide.md)** - Debugging strategies
+- **[Background jobs](../deployment/background-jobs.md)** - Queue architecture
+- **[Environment configuration](../deployment/environment-configuration.md)** - Variable reference
+
+---
+
+_For more detailed debugging, see the [Debugging Guide](./debugging-guide.md)._
diff --git a/docs/decabill/troubleshooting/debugging-guide.md b/docs/decabill/troubleshooting/debugging-guide.md
new file mode 100644
index 000000000..cdac9422d
--- /dev/null
+++ b/docs/decabill/troubleshooting/debugging-guide.md
@@ -0,0 +1,182 @@
+# Debugging Guide
+
+Debugging strategies and tools for troubleshooting Decabill issues.
+
+## Logging
+
+### Application Logs
+
+```bash
+# Billing manager API (local)
+nx serve decabill-backend-billing-manager
+
+# Docker Compose
+cd apps/decabill/backend-billing-manager
+docker compose logs -f backend-billing-manager
+docker compose logs -f backend-billing-manager-worker
+docker compose logs -f backend-billing-manager-scheduler
+```
+
+### Log Levels
+
+Configure log levels via environment variables where supported:
+
+- `LOG_LEVEL=debug` - Detailed debugging information
+- `LOG_LEVEL=info` - General information
+- `LOG_LEVEL=warn` - Warnings
+- `LOG_LEVEL=error` - Errors only
+
+### Correlation IDs
+
+Pass `X-Correlation-Id` or `X-Request-Id` on API requests to trace a single operation across API and worker logs. Access logs include `[corr=…]` in Nest output when inside the request context.
+
+### Log Patterns
+
+Common patterns to search for:
+
+- **Connection errors**: Database, Redis, SMTP, Stripe, cloud APIs
+- **Authentication errors**: Invalid API key, expired JWT, tenant mismatch
+- **Queue errors**: BullMQ connection failures, job processor exceptions
+- **Payment errors**: Stripe signature failures, checkout session errors
+
+Secrets are redacted in access logs; do not rely on logs for full token values.
+
+## Bull Board and Queue Debugging
+
+When `QUEUE_BULL_BOARD_ENABLED=true` on the API:
+
+- URL: `http://localhost:3200/admin/queues`
+- Credentials: `QUEUE_BULL_BOARD_USERNAME` / `QUEUE_BULL_BOARD_PASSWORD`
+
+Use Bull Board to:
+
+- Inspect failed jobs and stack traces
+- Retry stalled subscription billing or provisioning jobs
+- Verify coordinator repeatable jobs are registered
+
+Job names are defined in `apps/decabill/backend-billing-manager/src/queue/job-registry.ts`.
+
+## Debugging Tools
+
+### Browser DevTools
+
+- **Network tab**: Monitor API calls, WebSocket upgrade, Stripe redirects
+- **Console tab**: CSP violations, Angular errors
+- **Application tab**: Storage and cookies for Keycloak sessions
+
+### Docker Debugging
+
+```bash
+docker exec -it billing-manager-api /bin/sh
+docker exec -it billing-manager-worker /bin/sh
+docker stats billing-manager-api
+```
+
+### Redis Debugging
+
+```bash
+# From host (compose default port)
+redis-cli -p 6380 ping
+
+# Inside compose network
+docker compose exec redis redis-cli ping
+docker compose exec redis redis-cli keys 'decabill-billing*'
+```
+
+### Database Debugging
+
+```bash
+psql -h localhost -U postgres -d postgres
+
+# Example tenant-scoped queries
+SELECT id, tenant_id, status FROM subscriptions LIMIT 10;
+SELECT id, tenant_id, status FROM invoices ORDER BY created_at DESC LIMIT 10;
+```
+
+## API Testing
+
+### Health Check
+
+```bash
+curl http://localhost:3200/api/health
+```
+
+### Authenticated Request
+
+```bash
+curl -H "Authorization: ApiKey your-api-key" \
+ -H "X-Tenant: default" \
+ http://localhost:3200/api/billing/subscriptions
+```
+
+Adjust path to match your OpenAPI specification.
+
+### Stripe Webhook (local testing)
+
+Use Stripe CLI to forward webhooks to your local API:
+
+```bash
+stripe listen --forward-to localhost:3200/api/billing/stripe/webhook
+```
+
+Set `STRIPE_WEBHOOK_SECRET` to the signing secret from `stripe listen`.
+
+## WebSocket Testing
+
+Billing WebSocket gateway listens on port **8082** with namespace **`billing`**.
+
+Use browser DevTools Network tab or a Socket.IO client with:
+
+- Correct origin (CORS)
+- Auth token or session as required by your auth mode
+- **`X-Tenant`** header on handshake for multi-tenant setups
+
+## Common Debugging Scenarios
+
+### Subscription Not Billed
+
+1. Check `subscription-billing.coordinator` is repeating in Bull Board
+2. Verify worker is processing `subscription-billing.unit` jobs
+3. Review subscription status and billing dates in database
+4. Check `BILLING_SCHEDULER_INTERVAL` and batch size env vars
+
+### Invoice PDF Missing
+
+1. Verify worker and API share `invoice_pdf_data` volume in compose
+2. Check `BILLING_INVOICE_PDF_STORAGE_PATH` (default `/data/invoices`)
+3. Review worker logs during PDF generation jobs
+
+### Multi-Tenant Data in Wrong Tenant
+
+1. Confirm console sends correct **`X-Tenant`**
+2. For API key auth, review **[DR-002](../security/accepted-risks.md#dr-002--billing-multi-tenant-api-key-scope-static_api_key_tenant_id-unset)**
+3. Verify user's `tenant_id` in database matches expected tenant
+
+## Performance Debugging
+
+### API Performance
+
+- Monitor response times on heavy admin list endpoints
+- Check database query plans for large tenant datasets
+- Review rate limiting metrics if 429 responses appear
+
+### Worker Performance
+
+- Tune `QUEUE_WORKER_CONCURRENCY` for CPU and external API limits
+- Scale horizontally with additional worker containers
+- Watch Redis memory as job history accumulates (jobs are not auto-removed)
+
+### Database Performance
+
+- Index usage on `tenant_id` filtered queries
+- Connection pool sizing under scheduler batch loads
+
+## Related Documentation
+
+- **[Common Issues](./common-issues.md)** - Common problems and solutions
+- **[Background jobs](../deployment/background-jobs.md)** - Queue roles and job registry
+- **[Environment configuration](../deployment/environment-configuration.md)** - Scheduler intervals
+
+---
+
+_For specific issues, see [Common Issues](./common-issues.md)._
diff --git a/libs/domains/agenstra/frontend/feature-docs/README.md b/libs/domains/agenstra/frontend/feature-docs/README.md
deleted file mode 100644
index 341646bce..000000000
--- a/libs/domains/agenstra/frontend/feature-docs/README.md
+++ /dev/null
@@ -1,7 +0,0 @@
-# agenstra-frontend-feature-docs
-
-This library was generated with [Nx](https://nx.dev).
-
-## Running unit tests
-
-Run `nx test agenstra-frontend-feature-docs` to execute the unit tests.
diff --git a/libs/domains/agenstra/frontend/index.ts b/libs/domains/agenstra/frontend/index.ts
index a0cfe74a0..c9a1f03c1 100644
--- a/libs/domains/agenstra/frontend/index.ts
+++ b/libs/domains/agenstra/frontend/index.ts
@@ -2,6 +2,4 @@
export * from './data-access-agent-console/src';
export * from './data-access-portal/src';
export * from './feature-agent-console/src';
-export * from './feature-docs/src';
export * from './feature-landingpage/src';
-export * from './util-docs-parser/src';
diff --git a/libs/domains/agenstra/frontend/util-docs-parser/README.md b/libs/domains/agenstra/frontend/util-docs-parser/README.md
deleted file mode 100644
index 9f98ed074..000000000
--- a/libs/domains/agenstra/frontend/util-docs-parser/README.md
+++ /dev/null
@@ -1,7 +0,0 @@
-# agenstra-frontend-util-docs-parser
-
-This library was generated with [Nx](https://nx.dev).
-
-## Running unit tests
-
-Run `nx test agenstra-frontend-util-docs-parser` to execute the unit tests.
diff --git a/libs/domains/agenstra/frontend/util-docs-parser/project.json b/libs/domains/agenstra/frontend/util-docs-parser/project.json
deleted file mode 100644
index fefa0784c..000000000
--- a/libs/domains/agenstra/frontend/util-docs-parser/project.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "name": "agenstra-frontend-util-docs-parser",
- "$schema": "../../../../../node_modules/nx/schemas/project-schema.json",
- "sourceRoot": "libs/domains/agenstra/frontend/util-docs-parser/src",
- "prefix": "framework",
- "projectType": "library",
- "tags": ["domain:agenstra", "scope:frontend", "type:util"],
- "targets": {
- "test": {
- "executor": "@nx/jest:jest",
- "outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
- "options": {
- "jestConfig": "libs/domains/agenstra/frontend/util-docs-parser/jest.config.ts",
- "tsConfig": "libs/domains/agenstra/frontend/util-docs-parser/tsconfig.spec.json"
- }
- },
- "lint": {
- "executor": "@nx/eslint:lint"
- }
- }
-}
diff --git a/libs/domains/agenstra/frontend/feature-docs/.eslintrc.json b/libs/domains/shared/frontend/feature-docs/.eslintrc.json
similarity index 100%
rename from libs/domains/agenstra/frontend/feature-docs/.eslintrc.json
rename to libs/domains/shared/frontend/feature-docs/.eslintrc.json
diff --git a/libs/domains/shared/frontend/feature-docs/LICENSE b/libs/domains/shared/frontend/feature-docs/LICENSE
new file mode 100644
index 000000000..55cd076da
--- /dev/null
+++ b/libs/domains/shared/frontend/feature-docs/LICENSE
@@ -0,0 +1,33 @@
+Source-Available License - No Permission Granted Except Viewing
+
+Copyright (c) 2025 IPvX UG (haftungsbeschränkt).
+All rights reserved.
+
+This software is provided on a source-available basis only. You may view the
+source code solely to understand how the software works. No other rights are
+granted.
+
+Except for the single permission to view the source code, you may not, in
+whole or in part:
+
+- Copy, reproduce, or duplicate the software in any form.
+- Modify, adapt, translate, or create derivative works of the software.
+- Distribute, publish, make available, or share the software or any portion
+ thereof.
+- Use the software for any purpose, including private, commercial, educational,
+ internal, or research use.
+- Sell, sublicense, rent, lease, or otherwise transfer the software or any
+ rights to it.
+- Host, deploy, or run the software on any system, server, or environment.
+- Reverse-engineer, decompile, disassemble, or otherwise attempt to derive
+ functionality beyond what is visible in the source.
+
+Any attempt to exercise rights not expressly granted above is prohibited and
+constitutes a violation of this license.
+
+The software is provided “as is”, without any warranty of any kind, express or
+implied. In no event shall the copyright holder be liable for any damages
+arising from the possession, viewing, or prohibited use of the software.
+
+If you do not agree to these terms, you are not permitted to access or view the
+source code.
diff --git a/libs/domains/shared/frontend/feature-docs/README.md b/libs/domains/shared/frontend/feature-docs/README.md
new file mode 100644
index 000000000..2ebc2c975
--- /dev/null
+++ b/libs/domains/shared/frontend/feature-docs/README.md
@@ -0,0 +1,7 @@
+# shared-frontend-feature-docs
+
+This library was generated with [Nx](https://nx.dev).
+
+## Running unit tests
+
+Run `nx test shared-frontend-feature-docs` to execute the unit tests.
diff --git a/libs/domains/agenstra/frontend/feature-docs/jest.config.ts b/libs/domains/shared/frontend/feature-docs/jest.config.ts
similarity index 80%
rename from libs/domains/agenstra/frontend/feature-docs/jest.config.ts
rename to libs/domains/shared/frontend/feature-docs/jest.config.ts
index 4f629005a..3073eb9d0 100644
--- a/libs/domains/agenstra/frontend/feature-docs/jest.config.ts
+++ b/libs/domains/shared/frontend/feature-docs/jest.config.ts
@@ -1,8 +1,8 @@
export default {
- displayName: 'agenstra-frontend-feature-docs',
+ displayName: 'shared-frontend-feature-docs',
preset: '../../../../../jest.preset.cjs',
setupFilesAfterEnv: ['/src/test-setup.ts'],
- coverageDirectory: '../../../../../coverage/libs/domains/agenstra/frontend/feature-docs',
+ coverageDirectory: '../../../../../coverage/libs/domains/shared/frontend/feature-docs',
transform: {
'^.+\\.(ts|mjs|js|html)$': [
'jest-preset-angular',
diff --git a/libs/domains/agenstra/frontend/feature-docs/project.json b/libs/domains/shared/frontend/feature-docs/project.json
similarity index 50%
rename from libs/domains/agenstra/frontend/feature-docs/project.json
rename to libs/domains/shared/frontend/feature-docs/project.json
index 19e393a83..09bc35bb2 100644
--- a/libs/domains/agenstra/frontend/feature-docs/project.json
+++ b/libs/domains/shared/frontend/feature-docs/project.json
@@ -1,17 +1,17 @@
{
- "name": "agenstra-frontend-feature-docs",
+ "name": "shared-frontend-feature-docs",
"$schema": "../../../../../node_modules/nx/schemas/project-schema.json",
- "sourceRoot": "libs/domains/agenstra/frontend/feature-docs/src",
+ "sourceRoot": "libs/domains/shared/frontend/feature-docs/src",
"prefix": "framework",
"projectType": "library",
- "tags": ["domain:agenstra", "scope:frontend", "type:feature"],
+ "tags": ["domain:shared", "scope:frontend", "type:feature"],
"targets": {
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
- "jestConfig": "libs/domains/agenstra/frontend/feature-docs/jest.config.ts",
- "tsConfig": "libs/domains/agenstra/frontend/feature-docs/tsconfig.spec.json"
+ "jestConfig": "libs/domains/shared/frontend/feature-docs/jest.config.ts",
+ "tsConfig": "libs/domains/shared/frontend/feature-docs/tsconfig.spec.json"
}
},
"lint": {
diff --git a/libs/domains/agenstra/frontend/feature-docs/src/index.ts b/libs/domains/shared/frontend/feature-docs/src/index.ts
similarity index 100%
rename from libs/domains/agenstra/frontend/feature-docs/src/index.ts
rename to libs/domains/shared/frontend/feature-docs/src/index.ts
diff --git a/libs/domains/agenstra/frontend/feature-docs/src/lib/components/docs-breadcrumbs/docs-breadcrumbs.component.html b/libs/domains/shared/frontend/feature-docs/src/lib/components/docs-breadcrumbs/docs-breadcrumbs.component.html
similarity index 100%
rename from libs/domains/agenstra/frontend/feature-docs/src/lib/components/docs-breadcrumbs/docs-breadcrumbs.component.html
rename to libs/domains/shared/frontend/feature-docs/src/lib/components/docs-breadcrumbs/docs-breadcrumbs.component.html
diff --git a/libs/domains/agenstra/frontend/feature-docs/src/lib/components/docs-breadcrumbs/docs-breadcrumbs.component.scss b/libs/domains/shared/frontend/feature-docs/src/lib/components/docs-breadcrumbs/docs-breadcrumbs.component.scss
similarity index 100%
rename from libs/domains/agenstra/frontend/feature-docs/src/lib/components/docs-breadcrumbs/docs-breadcrumbs.component.scss
rename to libs/domains/shared/frontend/feature-docs/src/lib/components/docs-breadcrumbs/docs-breadcrumbs.component.scss
diff --git a/libs/domains/agenstra/frontend/feature-docs/src/lib/components/docs-breadcrumbs/docs-breadcrumbs.component.ts b/libs/domains/shared/frontend/feature-docs/src/lib/components/docs-breadcrumbs/docs-breadcrumbs.component.ts
similarity index 92%
rename from libs/domains/agenstra/frontend/feature-docs/src/lib/components/docs-breadcrumbs/docs-breadcrumbs.component.ts
rename to libs/domains/shared/frontend/feature-docs/src/lib/components/docs-breadcrumbs/docs-breadcrumbs.component.ts
index 2861eb33c..ec6f5be2c 100644
--- a/libs/domains/agenstra/frontend/feature-docs/src/lib/components/docs-breadcrumbs/docs-breadcrumbs.component.ts
+++ b/libs/domains/shared/frontend/feature-docs/src/lib/components/docs-breadcrumbs/docs-breadcrumbs.component.ts
@@ -1,7 +1,7 @@
import { CommonModule } from '@angular/common';
import { Component, computed, inject, input } from '@angular/core';
import { RouterModule } from '@angular/router';
-import { NavigationNode } from '@forepath/agenstra/frontend/util-docs-parser';
+import { NavigationNode } from '@forepath/shared/frontend/util-docs-parser';
import { DocsNavigationService } from '../../services';
diff --git a/libs/domains/agenstra/frontend/feature-docs/src/lib/components/docs-content/docs-content.component.html b/libs/domains/shared/frontend/feature-docs/src/lib/components/docs-content/docs-content.component.html
similarity index 100%
rename from libs/domains/agenstra/frontend/feature-docs/src/lib/components/docs-content/docs-content.component.html
rename to libs/domains/shared/frontend/feature-docs/src/lib/components/docs-content/docs-content.component.html
diff --git a/libs/domains/agenstra/frontend/feature-docs/src/lib/components/docs-content/docs-content.component.scss b/libs/domains/shared/frontend/feature-docs/src/lib/components/docs-content/docs-content.component.scss
similarity index 100%
rename from libs/domains/agenstra/frontend/feature-docs/src/lib/components/docs-content/docs-content.component.scss
rename to libs/domains/shared/frontend/feature-docs/src/lib/components/docs-content/docs-content.component.scss
diff --git a/libs/domains/agenstra/frontend/feature-docs/src/lib/components/docs-content/docs-content.component.ts b/libs/domains/shared/frontend/feature-docs/src/lib/components/docs-content/docs-content.component.ts
similarity index 95%
rename from libs/domains/agenstra/frontend/feature-docs/src/lib/components/docs-content/docs-content.component.ts
rename to libs/domains/shared/frontend/feature-docs/src/lib/components/docs-content/docs-content.component.ts
index fdde10a95..45a01240f 100644
--- a/libs/domains/agenstra/frontend/feature-docs/src/lib/components/docs-content/docs-content.component.ts
+++ b/libs/domains/shared/frontend/feature-docs/src/lib/components/docs-content/docs-content.component.ts
@@ -14,7 +14,8 @@ import {
ViewContainerRef,
} from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
-import { DocMetadata } from '@forepath/agenstra/frontend/util-docs-parser';
+import { ENVIRONMENT, type Environment } from '@forepath/shared/frontend/util-configuration';
+import { DocMetadata } from '@forepath/shared/frontend/util-docs-parser';
import { catchError, map, of } from 'rxjs';
import { MermaidDiagramComponent } from '../mermaid-diagram/mermaid-diagram.component';
@@ -35,6 +36,11 @@ export class DocsContentComponent implements AfterViewInit {
private readonly platformId = inject(PLATFORM_ID);
private readonly viewContainer = inject(ViewContainerRef);
private readonly http = inject(HttpClient);
+ private readonly environment = inject(ENVIRONMENT);
+
+ private get contentRoot(): string {
+ return this.environment.docs.contentRoot;
+ }
/**
* Documentation metadata
@@ -365,8 +371,8 @@ export class DocsContentComponent implements AfterViewInit {
}
}
- // Remove any "agenstra/" prefix if present (shouldn't happen in markdown, but handle it)
- routePath = routePath.replace(/^agenstra\//, '');
+ // Remove any content-root prefix if present
+ routePath = routePath.replace(new RegExp(`^${this.contentRoot}/`), '');
// If the path ends with /README or README, remove it since path resolution handles README.md automatically
// e.g., "something/README" -> "something", "README" -> ""
@@ -387,8 +393,6 @@ export class DocsContentComponent implements AfterViewInit {
/**
* Remove links to markdown files that don't exist
- * Only files in the "agenstra" folder are considered valid
- * All internal links must resolve to routes starting with /agenstra or /framework
*/
private removeInvalidLinks(): void {
if (!this.contentContainer || !isPlatformBrowser(this.platformId)) {
@@ -420,8 +424,7 @@ export class DocsContentComponent implements AfterViewInit {
// Get the href from the link to check if it starts with /agenstra or /framework
const href = link.getAttribute('href');
- if (!href || (!href.startsWith('/agenstra') && !href.startsWith('/framework'))) {
- // Link doesn't start with /agenstra or /framework, mark as invalid
+ if (!href || !href.startsWith('/docs')) {
const text = link.textContent || link.innerText;
const textNode = this.document.createTextNode(text);
@@ -443,9 +446,7 @@ export class DocsContentComponent implements AfterViewInit {
// Add .md extension back for file checking
const fullFilePath = `${filePath}.md`;
- // Check if file exists by trying to load it with HEAD request
- // Files are served from /agenstra/ route, so we need to prepend "agenstra/"
- const assetPath = `/agenstra/${fullFilePath}`;
+ const assetPath = `/docs/${this.contentRoot}/${fullFilePath}`;
this.http
.head(assetPath, { observe: 'response' })
diff --git a/libs/domains/agenstra/frontend/feature-docs/src/lib/components/docs-layout/docs-layout.component.html b/libs/domains/shared/frontend/feature-docs/src/lib/components/docs-layout/docs-layout.component.html
similarity index 99%
rename from libs/domains/agenstra/frontend/feature-docs/src/lib/components/docs-layout/docs-layout.component.html
rename to libs/domains/shared/frontend/feature-docs/src/lib/components/docs-layout/docs-layout.component.html
index f8e2e7570..b133720ce 100644
--- a/libs/domains/agenstra/frontend/feature-docs/src/lib/components/docs-layout/docs-layout.component.html
+++ b/libs/domains/shared/frontend/feature-docs/src/lib/components/docs-layout/docs-layout.component.html
@@ -6,7 +6,7 @@
diff --git a/libs/domains/agenstra/frontend/feature-docs/src/lib/components/docs-layout/docs-layout.component.scss b/libs/domains/shared/frontend/feature-docs/src/lib/components/docs-layout/docs-layout.component.scss
similarity index 100%
rename from libs/domains/agenstra/frontend/feature-docs/src/lib/components/docs-layout/docs-layout.component.scss
rename to libs/domains/shared/frontend/feature-docs/src/lib/components/docs-layout/docs-layout.component.scss
diff --git a/libs/domains/agenstra/frontend/feature-docs/src/lib/components/docs-layout/docs-layout.component.ts b/libs/domains/shared/frontend/feature-docs/src/lib/components/docs-layout/docs-layout.component.ts
similarity index 90%
rename from libs/domains/agenstra/frontend/feature-docs/src/lib/components/docs-layout/docs-layout.component.ts
rename to libs/domains/shared/frontend/feature-docs/src/lib/components/docs-layout/docs-layout.component.ts
index d72070144..15fafe7bc 100644
--- a/libs/domains/agenstra/frontend/feature-docs/src/lib/components/docs-layout/docs-layout.component.ts
+++ b/libs/domains/shared/frontend/feature-docs/src/lib/components/docs-layout/docs-layout.component.ts
@@ -2,8 +2,8 @@ import { CommonModule } from '@angular/common';
import { AfterViewInit, Component, inject, OnDestroy, signal } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { RouterModule } from '@angular/router';
-import { LocaleService } from '@forepath/shared/frontend/util-configuration';
-import { NavigationNode } from '@forepath/agenstra/frontend/util-docs-parser';
+import { LocaleService, ENVIRONMENT, type Environment } from '@forepath/shared/frontend/util-configuration';
+import { NavigationNode } from '@forepath/shared/frontend/util-docs-parser';
import { DocsNavigationService, ThemeService } from '../../services';
import { DocsNavigationComponent } from '../docs-navigation/docs-navigation.component';
@@ -20,6 +20,7 @@ export class DocsLayoutComponent implements AfterViewInit, OnDestroy {
private readonly navigationService = inject(DocsNavigationService);
protected readonly themeService = inject(ThemeService);
protected readonly localeService = inject(LocaleService);
+ protected readonly productName = inject(ENVIRONMENT).productName;
/**
* Whether we're on a mobile device (width <= 767.98px, Bootstrap md breakpoint)
diff --git a/libs/domains/agenstra/frontend/feature-docs/src/lib/components/docs-navigation/docs-navigation.component.html b/libs/domains/shared/frontend/feature-docs/src/lib/components/docs-navigation/docs-navigation.component.html
similarity index 100%
rename from libs/domains/agenstra/frontend/feature-docs/src/lib/components/docs-navigation/docs-navigation.component.html
rename to libs/domains/shared/frontend/feature-docs/src/lib/components/docs-navigation/docs-navigation.component.html
diff --git a/libs/domains/agenstra/frontend/feature-docs/src/lib/components/docs-navigation/docs-navigation.component.scss b/libs/domains/shared/frontend/feature-docs/src/lib/components/docs-navigation/docs-navigation.component.scss
similarity index 100%
rename from libs/domains/agenstra/frontend/feature-docs/src/lib/components/docs-navigation/docs-navigation.component.scss
rename to libs/domains/shared/frontend/feature-docs/src/lib/components/docs-navigation/docs-navigation.component.scss
diff --git a/libs/domains/agenstra/frontend/feature-docs/src/lib/components/docs-navigation/docs-navigation.component.ts b/libs/domains/shared/frontend/feature-docs/src/lib/components/docs-navigation/docs-navigation.component.ts
similarity index 95%
rename from libs/domains/agenstra/frontend/feature-docs/src/lib/components/docs-navigation/docs-navigation.component.ts
rename to libs/domains/shared/frontend/feature-docs/src/lib/components/docs-navigation/docs-navigation.component.ts
index abd94ab26..5557b8cba 100644
--- a/libs/domains/agenstra/frontend/feature-docs/src/lib/components/docs-navigation/docs-navigation.component.ts
+++ b/libs/domains/shared/frontend/feature-docs/src/lib/components/docs-navigation/docs-navigation.component.ts
@@ -1,7 +1,7 @@
import { CommonModule } from '@angular/common';
import { Component, computed, inject, input, signal } from '@angular/core';
import { Router, RouterModule } from '@angular/router';
-import { NavigationNode } from '@forepath/agenstra/frontend/util-docs-parser';
+import { NavigationNode } from '@forepath/shared/frontend/util-docs-parser';
import { DocsNavigationService } from '../../services';
diff --git a/libs/domains/agenstra/frontend/feature-docs/src/lib/components/docs-search/docs-search.component.html b/libs/domains/shared/frontend/feature-docs/src/lib/components/docs-search/docs-search.component.html
similarity index 100%
rename from libs/domains/agenstra/frontend/feature-docs/src/lib/components/docs-search/docs-search.component.html
rename to libs/domains/shared/frontend/feature-docs/src/lib/components/docs-search/docs-search.component.html
diff --git a/libs/domains/agenstra/frontend/feature-docs/src/lib/components/docs-search/docs-search.component.scss b/libs/domains/shared/frontend/feature-docs/src/lib/components/docs-search/docs-search.component.scss
similarity index 100%
rename from libs/domains/agenstra/frontend/feature-docs/src/lib/components/docs-search/docs-search.component.scss
rename to libs/domains/shared/frontend/feature-docs/src/lib/components/docs-search/docs-search.component.scss
diff --git a/libs/domains/agenstra/frontend/feature-docs/src/lib/components/docs-search/docs-search.component.ts b/libs/domains/shared/frontend/feature-docs/src/lib/components/docs-search/docs-search.component.ts
similarity index 100%
rename from libs/domains/agenstra/frontend/feature-docs/src/lib/components/docs-search/docs-search.component.ts
rename to libs/domains/shared/frontend/feature-docs/src/lib/components/docs-search/docs-search.component.ts
diff --git a/libs/domains/agenstra/frontend/feature-docs/src/lib/components/docs-table-of-contents/docs-table-of-contents.component.html b/libs/domains/shared/frontend/feature-docs/src/lib/components/docs-table-of-contents/docs-table-of-contents.component.html
similarity index 100%
rename from libs/domains/agenstra/frontend/feature-docs/src/lib/components/docs-table-of-contents/docs-table-of-contents.component.html
rename to libs/domains/shared/frontend/feature-docs/src/lib/components/docs-table-of-contents/docs-table-of-contents.component.html
diff --git a/libs/domains/agenstra/frontend/feature-docs/src/lib/components/docs-table-of-contents/docs-table-of-contents.component.scss b/libs/domains/shared/frontend/feature-docs/src/lib/components/docs-table-of-contents/docs-table-of-contents.component.scss
similarity index 100%
rename from libs/domains/agenstra/frontend/feature-docs/src/lib/components/docs-table-of-contents/docs-table-of-contents.component.scss
rename to libs/domains/shared/frontend/feature-docs/src/lib/components/docs-table-of-contents/docs-table-of-contents.component.scss
diff --git a/libs/domains/agenstra/frontend/feature-docs/src/lib/components/docs-table-of-contents/docs-table-of-contents.component.ts b/libs/domains/shared/frontend/feature-docs/src/lib/components/docs-table-of-contents/docs-table-of-contents.component.ts
similarity index 96%
rename from libs/domains/agenstra/frontend/feature-docs/src/lib/components/docs-table-of-contents/docs-table-of-contents.component.ts
rename to libs/domains/shared/frontend/feature-docs/src/lib/components/docs-table-of-contents/docs-table-of-contents.component.ts
index acb489830..37858ace1 100644
--- a/libs/domains/agenstra/frontend/feature-docs/src/lib/components/docs-table-of-contents/docs-table-of-contents.component.ts
+++ b/libs/domains/shared/frontend/feature-docs/src/lib/components/docs-table-of-contents/docs-table-of-contents.component.ts
@@ -1,6 +1,6 @@
import { CommonModule } from '@angular/common';
import { Component, computed, input, signal } from '@angular/core';
-import { DocHeading } from '@forepath/agenstra/frontend/util-docs-parser';
+import { DocHeading } from '@forepath/shared/frontend/util-docs-parser';
@Component({
selector: 'framework-docs-table-of-contents',
diff --git a/libs/domains/agenstra/frontend/feature-docs/src/lib/components/index.ts b/libs/domains/shared/frontend/feature-docs/src/lib/components/index.ts
similarity index 100%
rename from libs/domains/agenstra/frontend/feature-docs/src/lib/components/index.ts
rename to libs/domains/shared/frontend/feature-docs/src/lib/components/index.ts
diff --git a/libs/domains/agenstra/frontend/feature-docs/src/lib/components/mermaid-diagram/mermaid-diagram.component.html b/libs/domains/shared/frontend/feature-docs/src/lib/components/mermaid-diagram/mermaid-diagram.component.html
similarity index 100%
rename from libs/domains/agenstra/frontend/feature-docs/src/lib/components/mermaid-diagram/mermaid-diagram.component.html
rename to libs/domains/shared/frontend/feature-docs/src/lib/components/mermaid-diagram/mermaid-diagram.component.html
diff --git a/libs/domains/agenstra/frontend/feature-docs/src/lib/components/mermaid-diagram/mermaid-diagram.component.scss b/libs/domains/shared/frontend/feature-docs/src/lib/components/mermaid-diagram/mermaid-diagram.component.scss
similarity index 100%
rename from libs/domains/agenstra/frontend/feature-docs/src/lib/components/mermaid-diagram/mermaid-diagram.component.scss
rename to libs/domains/shared/frontend/feature-docs/src/lib/components/mermaid-diagram/mermaid-diagram.component.scss
diff --git a/libs/domains/agenstra/frontend/feature-docs/src/lib/components/mermaid-diagram/mermaid-diagram.component.ts b/libs/domains/shared/frontend/feature-docs/src/lib/components/mermaid-diagram/mermaid-diagram.component.ts
similarity index 100%
rename from libs/domains/agenstra/frontend/feature-docs/src/lib/components/mermaid-diagram/mermaid-diagram.component.ts
rename to libs/domains/shared/frontend/feature-docs/src/lib/components/mermaid-diagram/mermaid-diagram.component.ts
diff --git a/libs/domains/agenstra/frontend/feature-docs/src/lib/docs.routes.ts b/libs/domains/shared/frontend/feature-docs/src/lib/docs.routes.ts
similarity index 100%
rename from libs/domains/agenstra/frontend/feature-docs/src/lib/docs.routes.ts
rename to libs/domains/shared/frontend/feature-docs/src/lib/docs.routes.ts
diff --git a/libs/domains/agenstra/frontend/feature-docs/src/lib/pages/docs-page/docs-page.component.html b/libs/domains/shared/frontend/feature-docs/src/lib/pages/docs-page/docs-page.component.html
similarity index 100%
rename from libs/domains/agenstra/frontend/feature-docs/src/lib/pages/docs-page/docs-page.component.html
rename to libs/domains/shared/frontend/feature-docs/src/lib/pages/docs-page/docs-page.component.html
diff --git a/libs/domains/agenstra/frontend/feature-docs/src/lib/pages/docs-page/docs-page.component.scss b/libs/domains/shared/frontend/feature-docs/src/lib/pages/docs-page/docs-page.component.scss
similarity index 100%
rename from libs/domains/agenstra/frontend/feature-docs/src/lib/pages/docs-page/docs-page.component.scss
rename to libs/domains/shared/frontend/feature-docs/src/lib/pages/docs-page/docs-page.component.scss
diff --git a/libs/domains/agenstra/frontend/feature-docs/src/lib/pages/docs-page/docs-page.component.ts b/libs/domains/shared/frontend/feature-docs/src/lib/pages/docs-page/docs-page.component.ts
similarity index 70%
rename from libs/domains/agenstra/frontend/feature-docs/src/lib/pages/docs-page/docs-page.component.ts
rename to libs/domains/shared/frontend/feature-docs/src/lib/pages/docs-page/docs-page.component.ts
index d8cd2de48..cfb21b8b9 100644
--- a/libs/domains/agenstra/frontend/feature-docs/src/lib/pages/docs-page/docs-page.component.ts
+++ b/libs/domains/shared/frontend/feature-docs/src/lib/pages/docs-page/docs-page.component.ts
@@ -4,13 +4,13 @@ import { toSignal } from '@angular/core/rxjs-interop';
import { Meta, Title } from '@angular/platform-browser';
import { ActivatedRoute, NavigationEnd, Router, RouterModule } from '@angular/router';
import { ENVIRONMENT, type Environment } from '@forepath/shared/frontend/util-configuration';
-import { DocMetadata, NavigationNode } from '@forepath/agenstra/frontend/util-docs-parser';
+import { DocMetadata, NavigationNode } from '@forepath/shared/frontend/util-docs-parser';
import {
addPageMetaTags,
applySocialPreviewMeta,
- DOCS_PAGE_DYNAMIC_META_TAG_STUBS,
+ createDocsPageDynamicMetaTagStubs,
formatAgenstraMetaDescription,
- formatAgenstraMetaTitle,
+ formatProductMetaTitle,
removePageMetaTags,
resolveSocialCanonicalUrl,
} from '@forepath/shared/frontend/util-meta';
@@ -18,6 +18,7 @@ import { catchError, filter, map, Observable, of, startWith, switchMap } from 'r
import { DocsBreadcrumbsComponent, DocsContentComponent, DocsTableOfContentsComponent } from '../../components';
import { DocsContentService, DocsNavigationService } from '../../services';
+import { getDocsMetaDescriptionFallback, getDocsMetaKeywords } from '../../utils/docs-seo-metadata';
@Component({
selector: 'framework-docs-page',
@@ -37,17 +38,34 @@ export class DocsPageComponent implements OnInit {
private readonly environment = inject(ENVIRONMENT);
private readonly locale = inject(LOCALE_ID);
- private readonly metaTitleFallback = $localize`:@@featureDocsPage-metaTitleFallback:Documentation :: Agenstra`;
- private readonly metaDescriptionFallback = $localize`:@@featureDocsPage-metaDescriptionFallback:Official Agenstra documentation: install, deploy, secure, and operate agent hosts, workspaces, tickets, APIs, and integrations for platform teams.`;
+ private get contentRoot(): string {
+ return this.environment.docs.contentRoot;
+ }
+
+ private get docsSiteOrigin(): string {
+ return `https://docs.${this.contentRoot}.com`;
+ }
+
+ private buildPageTitle(pageName: string): string {
+ return formatProductMetaTitle(pageName, this.environment.productName);
+ }
+
+ private get metaDescriptionFallback(): string {
+ return getDocsMetaDescriptionFallback(this.contentRoot);
+ }
+
+ private get docsStaticMetaTags() {
+ return [
+ {
+ name: 'keywords',
+ content: getDocsMetaKeywords(this.contentRoot),
+ },
+ { name: 'author', content: 'IPvX UG (haftungsbeschränkt)' },
+ { name: 'robots', content: 'index, follow' },
+ ];
+ }
+
private readonly destroyRef = inject(DestroyRef);
- private readonly docsStaticMetaTags = [
- {
- name: 'keywords',
- content: $localize`:@@featureDocsPage-metaKeywords:Agenstra, AI agents, agent management, distributed systems, AI agent infrastructure, agent platform, AI agent console, container management, WebSocket agents, Docker agents`,
- },
- { name: 'author', content: 'IPvX UG (haftungsbeschränkt)' },
- { name: 'robots', content: 'index, follow' },
- ];
constructor() {
// Update active path whenever currentPath changes
@@ -99,8 +117,13 @@ export class DocsPageComponent implements OnInit {
// Normalize the path to match navigation.json format
let normalizedPath = path;
- // Convert /agenstra/... to /docs/... to match navigation paths
- if (normalizedPath.startsWith('/agenstra/')) {
+ // Convert legacy /{contentRoot}/... to /docs/... to match navigation paths
+ const contentRootPrefix = `/${this.contentRoot}/`;
+ if (normalizedPath.startsWith(contentRootPrefix)) {
+ normalizedPath = normalizedPath.replace(contentRootPrefix, '/docs/');
+ } else if (normalizedPath === `/${this.contentRoot}`) {
+ normalizedPath = '/docs';
+ } else if (normalizedPath.startsWith('/agenstra/')) {
normalizedPath = normalizedPath.replace('/agenstra/', '/docs/');
} else if (normalizedPath === '/agenstra') {
normalizedPath = '/docs';
@@ -123,7 +146,11 @@ export class DocsPageComponent implements OnInit {
// Initial value based on current router URL
const url = this.router.url.split('?')[0].split('#')[0];
- if (url.startsWith('/agenstra/')) {
+ if (url.startsWith(`/${this.contentRoot}/`)) {
+ return url.replace(`/${this.contentRoot}/`, '/docs/');
+ } else if (url === `/${this.contentRoot}`) {
+ return '/docs';
+ } else if (url.startsWith('/agenstra/')) {
return url.replace('/agenstra/', '/docs/');
} else if (url === '/agenstra') {
return '/docs';
@@ -144,7 +171,16 @@ export class DocsPageComponent implements OnInit {
ngOnInit(): void {
this.destroyRef.onDestroy(addPageMetaTags(this.metaService, this.docsStaticMetaTags));
- this.destroyRef.onDestroy(() => removePageMetaTags(this.metaService, DOCS_PAGE_DYNAMIC_META_TAG_STUBS));
+ this.destroyRef.onDestroy(() =>
+ removePageMetaTags(
+ this.metaService,
+ createDocsPageDynamicMetaTagStubs({
+ docsSiteOrigin: this.docsSiteOrigin,
+ imageUrl: this.environment.socialPreview.imageUrl,
+ siteName: this.environment.productName,
+ }),
+ ),
+ );
// During SSR, skip content loading to avoid loops and timeout issues
// Content will be loaded on the client side
@@ -175,12 +211,14 @@ export class DocsPageComponent implements OnInit {
.replace(/README\.md(\/README\.md)*$/g, '');
path = path.replace(/\/README(\/README)*$/g, '').replace(/README(\/README)*$/g, '');
- // Remove 'agenstra' prefix if present (shouldn't happen, but handle it)
- if (path.startsWith('agenstra/')) {
- path = path.substring('agenstra/'.length);
+ // Remove content root prefix if present
+ if (path.startsWith(`${this.contentRoot}/`)) {
+ path = path.substring(this.contentRoot.length + 1);
}
- path = path.replace(/^agenstra\//, '').replace(/\/agenstra\//g, '/');
+ path = path
+ .replace(new RegExp(`^${this.contentRoot}/`), '')
+ .replace(new RegExp(`/${this.contentRoot}/`, 'g'), '/');
// Remove duplicate slashes
path = path.replace(/\/+/g, '/');
@@ -230,15 +268,15 @@ export class DocsPageComponent implements OnInit {
return;
}
- // Check if URL incorrectly includes "agenstra" in the path
- if (currentUrl.includes('/docs/agenstra/')) {
- // Fix the URL by removing "agenstra" from the path
+ // Check if URL incorrectly includes content root in the path
+ const docsContentRootPath = `/docs/${this.contentRoot}/`;
+ if (currentUrl.includes(docsContentRootPath)) {
const fixedPath = currentUrl
- .replace('/docs/agenstra/', '/docs/')
+ .replace(docsContentRootPath, '/docs/')
.replace(/\/README\.md$/, '')
.replace(/\/README$/, '');
- console.warn('URL incorrectly includes "agenstra", fixing:', currentUrl, '->', fixedPath);
+ console.warn(`URL incorrectly includes "${this.contentRoot}", fixing:`, currentUrl, '->', fixedPath);
this.router.navigate([fixedPath], { replaceUrl: true });
return;
@@ -258,7 +296,9 @@ export class DocsPageComponent implements OnInit {
private applyPageMeta(metadata: DocMetadata | null, path: string): void {
const pageTitle = metadata?.title?.trim();
- const title = pageTitle ? formatAgenstraMetaTitle(pageTitle) : this.metaTitleFallback;
+ const title = pageTitle
+ ? formatProductMetaTitle(pageTitle, this.environment.productName)
+ : this.buildPageTitle($localize`:@@featureDocsPage-metaTitlePrefix:Documentation`);
this.titleService.setTitle(title);
@@ -266,7 +306,7 @@ export class DocsPageComponent implements OnInit {
const descriptionSource = summary || this.metaDescriptionFallback;
const description = formatAgenstraMetaDescription(descriptionSource);
const canonicalUrl = resolveSocialCanonicalUrl(
- `https://docs.agenstra.com${path}`,
+ `${this.docsSiteOrigin}${path}`,
this.locale,
this.environment.production,
);
@@ -276,87 +316,58 @@ export class DocsPageComponent implements OnInit {
applySocialPreviewMeta((tag) => this.metaService.updateTag(tag), {
title,
description: descriptionSource,
- canonicalUrl: `https://docs.agenstra.com${path}`,
+ canonicalUrl: `${this.docsSiteOrigin}${path}`,
imageUrl: this.environment.socialPreview.imageUrl,
localeId: this.locale,
localizeCanonicalUrl: this.environment.production,
type: 'article',
+ siteName: this.environment.productName,
});
}
/**
- * Load content with fallback logic:
- * Always assumes "agenstra" subfolder.
- * For path /docs (empty):
- * 1. Check if there is agenstra/README.md to load
- * 2. Check if there is agenstra/agenstra.md
- * For path /docs/:path:
- * 1. Try agenstra/:path/README.md (directory with README)
- * 2. Try agenstra/:path.md (direct file)
+ * Load content with fallback logic using the configured docs content root.
*/
private loadContentWithFallback(routePath: string): Observable {
- // Normalize the path: remove leading/trailing slashes
+ const contentRoot = this.contentRoot;
let cleanPath = routePath.replace(/^\/+|\/+$/g, '');
- // Remove 'agenstra/' prefix if present (shouldn't happen with correct routes, but handle it)
- if (cleanPath.startsWith('agenstra/')) {
- cleanPath = cleanPath.substring('agenstra/'.length);
+ if (cleanPath.startsWith(`${contentRoot}/`)) {
+ cleanPath = cleanPath.substring(contentRoot.length + 1);
}
- cleanPath = cleanPath.replace(/^agenstra\//, '').replace(/\/agenstra\//g, '/');
-
- // Remove any .md extensions that might have been incorrectly added
+ cleanPath = cleanPath.replace(new RegExp(`^${contentRoot}/`), '').replace(new RegExp(`/${contentRoot}/`, 'g'), '/');
cleanPath = cleanPath.replace(/\.md$/g, '');
-
- // Remove any README.md or README that might have been incorrectly appended (multiple times)
cleanPath = cleanPath.replace(/\/README\.md(\/README\.md)*$/g, '').replace(/README\.md(\/README\.md)*$/g, '');
cleanPath = cleanPath.replace(/\/README(\/README)*$/g, '').replace(/README(\/README)*$/g, '');
-
- // Remove any duplicate slashes
cleanPath = cleanPath.replace(/\/+/g, '/');
- const pathWithoutAgenstra = cleanPath;
-
- // Always use "agenstra" as the base folder
- // If empty path, try README.md first, then agenstra.md
- if (!pathWithoutAgenstra) {
- return this.contentService.loadContent('agenstra/README.md').pipe(
- catchError(() => {
- // Fallback to agenstra.md
- return this.contentService.loadContent('agenstra/agenstra.md');
- }),
- );
+ if (!cleanPath) {
+ return this.contentService
+ .loadContent(`${contentRoot}/README.md`)
+ .pipe(catchError(() => this.contentService.loadContent(`${contentRoot}/${contentRoot}.md`)));
}
- // For paths like /docs/getting-started:
- // 1. Try agenstra/getting-started/README.md (directory with README)
- // 2. Try agenstra/getting-started.md (direct file)
- const readmePath = `agenstra/${pathWithoutAgenstra}/README.md`;
- const directPath = `agenstra/${pathWithoutAgenstra}.md`;
+ const readmePath = `${contentRoot}/${cleanPath}/README.md`;
+ const directPath = `${contentRoot}/${cleanPath}.md`;
- // Try README.md first, then fallback to direct .md file
- // Use switchMap to ensure the first request completes (or fails) before trying the fallback
return this.contentService.loadContent(readmePath).pipe(
switchMap((metadata) => {
- // If README.md was found, return it
if (metadata) {
return of(metadata);
}
- // If README.md returned null (not found), try the direct file
return this.contentService.loadContent(directPath);
}),
- catchError(() => {
- // If README.md request failed (timeout, network error, etc.), try the direct file
- return this.contentService.loadContent(directPath).pipe(
+ catchError(() =>
+ this.contentService.loadContent(directPath).pipe(
catchError(() => {
- // If both fail, return null to prevent infinite loops
console.warn(`Failed to load documentation for path: ${routePath} (tried both README.md and direct file)`);
return of(null);
}),
- );
- }),
+ ),
+ ),
);
}
}
diff --git a/libs/domains/agenstra/frontend/feature-docs/src/lib/pages/docs-search-page/docs-search-page.component.html b/libs/domains/shared/frontend/feature-docs/src/lib/pages/docs-search-page/docs-search-page.component.html
similarity index 100%
rename from libs/domains/agenstra/frontend/feature-docs/src/lib/pages/docs-search-page/docs-search-page.component.html
rename to libs/domains/shared/frontend/feature-docs/src/lib/pages/docs-search-page/docs-search-page.component.html
diff --git a/libs/domains/agenstra/frontend/feature-docs/src/lib/pages/docs-search-page/docs-search-page.component.scss b/libs/domains/shared/frontend/feature-docs/src/lib/pages/docs-search-page/docs-search-page.component.scss
similarity index 100%
rename from libs/domains/agenstra/frontend/feature-docs/src/lib/pages/docs-search-page/docs-search-page.component.scss
rename to libs/domains/shared/frontend/feature-docs/src/lib/pages/docs-search-page/docs-search-page.component.scss
diff --git a/libs/domains/agenstra/frontend/feature-docs/src/lib/pages/docs-search-page/docs-search-page.component.ts b/libs/domains/shared/frontend/feature-docs/src/lib/pages/docs-search-page/docs-search-page.component.ts
similarity index 82%
rename from libs/domains/agenstra/frontend/feature-docs/src/lib/pages/docs-search-page/docs-search-page.component.ts
rename to libs/domains/shared/frontend/feature-docs/src/lib/pages/docs-search-page/docs-search-page.component.ts
index 79b8f5035..66d38ba20 100644
--- a/libs/domains/agenstra/frontend/feature-docs/src/lib/pages/docs-search-page/docs-search-page.component.ts
+++ b/libs/domains/shared/frontend/feature-docs/src/lib/pages/docs-search-page/docs-search-page.component.ts
@@ -4,11 +4,12 @@ import { toSignal } from '@angular/core/rxjs-interop';
import { Meta, Title } from '@angular/platform-browser';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { ENVIRONMENT, type Environment } from '@forepath/shared/frontend/util-configuration';
-import { addPageMetaTags, buildPageMetaTags } from '@forepath/shared/frontend/util-meta';
+import { addPageMetaTags, buildPageMetaTags, formatProductMetaTitle } from '@forepath/shared/frontend/util-meta';
import { filter, map } from 'rxjs';
import { DocsSearchComponent } from '../../components';
import { DocsSearchService, SearchResult } from '../../services';
+import { getDocsSearchMetaDescription } from '../../utils/docs-seo-metadata';
@Component({
selector: 'framework-docs-search-page',
@@ -27,6 +28,10 @@ export class DocsSearchPageComponent implements OnInit {
private readonly locale = inject(LOCALE_ID);
private readonly destroyRef = inject(DestroyRef);
+ private get docsSiteOrigin(): string {
+ return `https://docs.${this.environment.docs.contentRoot}.com`;
+ }
+
/**
* Search query from route
*/
@@ -49,8 +54,11 @@ export class DocsSearchPageComponent implements OnInit {
readonly loading = signal(true);
ngOnInit(): void {
- const metaTitle = $localize`:@@featureDocsSearchPage-metaTitle:Search Documentation :: Agenstra`;
- const metaDescription = $localize`:@@featureDocsSearchPage-metaDescription:Search Agenstra docs for setup guides, API references, security hardening, agent configuration, deployment patterns, and troubleshooting.`;
+ const metaTitle = formatProductMetaTitle(
+ $localize`:@@featureDocsSearchPage-metaTitlePrefix:Search Documentation`,
+ this.environment.productName,
+ );
+ const metaDescription = getDocsSearchMetaDescription(this.environment.docs.contentRoot);
this.titleService.setTitle(metaTitle);
this.destroyRef.onDestroy(
@@ -59,12 +67,13 @@ export class DocsSearchPageComponent implements OnInit {
buildPageMetaTags({
description: metaDescription,
robots: 'noindex, follow',
- canonicalUrl: 'https://docs.agenstra.com/search',
+ canonicalUrl: `${this.docsSiteOrigin}/search`,
socialTitle: metaTitle,
socialDescription: metaDescription,
socialImageUrl: this.environment.socialPreview.imageUrl,
localeId: this.locale,
localizeCanonicalUrl: this.environment.production,
+ siteName: this.environment.productName,
}),
),
);
diff --git a/libs/domains/agenstra/frontend/feature-docs/src/lib/pages/index.ts b/libs/domains/shared/frontend/feature-docs/src/lib/pages/index.ts
similarity index 100%
rename from libs/domains/agenstra/frontend/feature-docs/src/lib/pages/index.ts
rename to libs/domains/shared/frontend/feature-docs/src/lib/pages/index.ts
diff --git a/libs/domains/agenstra/frontend/feature-docs/src/lib/section.scss b/libs/domains/shared/frontend/feature-docs/src/lib/section.scss
similarity index 100%
rename from libs/domains/agenstra/frontend/feature-docs/src/lib/section.scss
rename to libs/domains/shared/frontend/feature-docs/src/lib/section.scss
diff --git a/libs/domains/agenstra/frontend/feature-docs/src/lib/services/docs-content.service.ts b/libs/domains/shared/frontend/feature-docs/src/lib/services/docs-content.service.ts
similarity index 97%
rename from libs/domains/agenstra/frontend/feature-docs/src/lib/services/docs-content.service.ts
rename to libs/domains/shared/frontend/feature-docs/src/lib/services/docs-content.service.ts
index dfa60fe97..8892b9ff9 100644
--- a/libs/domains/agenstra/frontend/feature-docs/src/lib/services/docs-content.service.ts
+++ b/libs/domains/shared/frontend/feature-docs/src/lib/services/docs-content.service.ts
@@ -1,7 +1,7 @@
import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
-import { DocMetadata, MarkdownParserService } from '@forepath/agenstra/frontend/util-docs-parser';
+import { DocMetadata, MarkdownParserService } from '@forepath/shared/frontend/util-docs-parser';
import { catchError, from, Observable, of, shareReplay, switchMap, timeout } from 'rxjs';
/**
diff --git a/libs/domains/agenstra/frontend/feature-docs/src/lib/services/docs-navigation.service.ts b/libs/domains/shared/frontend/feature-docs/src/lib/services/docs-navigation.service.ts
similarity index 96%
rename from libs/domains/agenstra/frontend/feature-docs/src/lib/services/docs-navigation.service.ts
rename to libs/domains/shared/frontend/feature-docs/src/lib/services/docs-navigation.service.ts
index a857c786e..178f74e6a 100644
--- a/libs/domains/agenstra/frontend/feature-docs/src/lib/services/docs-navigation.service.ts
+++ b/libs/domains/shared/frontend/feature-docs/src/lib/services/docs-navigation.service.ts
@@ -1,6 +1,6 @@
import { HttpClient } from '@angular/common/http';
import { inject, Injectable, signal } from '@angular/core';
-import { NavigationNode } from '@forepath/agenstra/frontend/util-docs-parser';
+import { NavigationNode } from '@forepath/shared/frontend/util-docs-parser';
import { catchError, map, Observable, of, shareReplay } from 'rxjs';
/**
diff --git a/libs/domains/agenstra/frontend/feature-docs/src/lib/services/docs-search.service.ts b/libs/domains/shared/frontend/feature-docs/src/lib/services/docs-search.service.ts
similarity index 97%
rename from libs/domains/agenstra/frontend/feature-docs/src/lib/services/docs-search.service.ts
rename to libs/domains/shared/frontend/feature-docs/src/lib/services/docs-search.service.ts
index 7b1193d4b..7ea28ea7e 100644
--- a/libs/domains/agenstra/frontend/feature-docs/src/lib/services/docs-search.service.ts
+++ b/libs/domains/shared/frontend/feature-docs/src/lib/services/docs-search.service.ts
@@ -1,6 +1,6 @@
import { HttpClient } from '@angular/common/http';
import { inject, Injectable, signal } from '@angular/core';
-import { SearchIndex, SearchIndexEntry } from '@forepath/agenstra/frontend/util-docs-parser';
+import { SearchIndex, SearchIndexEntry } from '@forepath/shared/frontend/util-docs-parser';
import { catchError, Observable, of, shareReplay } from 'rxjs';
export interface SearchResult {
diff --git a/libs/domains/agenstra/frontend/feature-docs/src/lib/services/index.ts b/libs/domains/shared/frontend/feature-docs/src/lib/services/index.ts
similarity index 100%
rename from libs/domains/agenstra/frontend/feature-docs/src/lib/services/index.ts
rename to libs/domains/shared/frontend/feature-docs/src/lib/services/index.ts
diff --git a/libs/domains/agenstra/frontend/feature-docs/src/lib/services/theme.service.ts b/libs/domains/shared/frontend/feature-docs/src/lib/services/theme.service.ts
similarity index 100%
rename from libs/domains/agenstra/frontend/feature-docs/src/lib/services/theme.service.ts
rename to libs/domains/shared/frontend/feature-docs/src/lib/services/theme.service.ts
diff --git a/libs/domains/shared/frontend/feature-docs/src/lib/utils/docs-seo-metadata.ts b/libs/domains/shared/frontend/feature-docs/src/lib/utils/docs-seo-metadata.ts
new file mode 100644
index 000000000..61dc3528f
--- /dev/null
+++ b/libs/domains/shared/frontend/feature-docs/src/lib/utils/docs-seo-metadata.ts
@@ -0,0 +1,27 @@
+/**
+ * SEO copy for docs pages. Translation units live in the shared docs app i18n catalogs;
+ * each brand selects units by docs content root while reusing the same id naming scheme.
+ */
+export function getDocsMetaDescriptionFallback(contentRoot: string): string {
+ if (contentRoot === 'decabill') {
+ return $localize`:@@featureDocsPage-metaDescriptionFallbackDecabill:Official Decabill documentation: install, deploy, secure, and operate billing APIs, subscriptions, payment processors, and the billing console for platform teams.`;
+ }
+
+ return $localize`:@@featureDocsPage-metaDescriptionFallback:Official Agenstra documentation: install, deploy, secure, and operate agent hosts, workspaces, tickets, APIs, and integrations for platform teams.`;
+}
+
+export function getDocsMetaKeywords(contentRoot: string): string {
+ if (contentRoot === 'decabill') {
+ return $localize`:@@featureDocsPage-metaKeywordsDecabill:Decabill, billing, subscriptions, payment processing, invoicing, Stripe, billing API, billing console, multi-tenant billing`;
+ }
+
+ return $localize`:@@featureDocsPage-metaKeywords:Agenstra, AI agents, agent management, distributed systems, AI agent infrastructure, agent platform, AI agent console, container management, WebSocket agents, Docker agents`;
+}
+
+export function getDocsSearchMetaDescription(contentRoot: string): string {
+ if (contentRoot === 'decabill') {
+ return $localize`:@@featureDocsSearchPage-metaDescriptionDecabill:Search Decabill docs for setup guides, API references, security hardening, billing configuration, deployment patterns, and troubleshooting.`;
+ }
+
+ return $localize`:@@featureDocsSearchPage-metaDescription:Search Agenstra docs for setup guides, API references, security hardening, agent configuration, deployment patterns, and troubleshooting.`;
+}
diff --git a/libs/domains/agenstra/frontend/feature-docs/src/test-setup.ts b/libs/domains/shared/frontend/feature-docs/src/test-setup.ts
similarity index 100%
rename from libs/domains/agenstra/frontend/feature-docs/src/test-setup.ts
rename to libs/domains/shared/frontend/feature-docs/src/test-setup.ts
diff --git a/libs/domains/agenstra/frontend/feature-docs/tsconfig.json b/libs/domains/shared/frontend/feature-docs/tsconfig.json
similarity index 100%
rename from libs/domains/agenstra/frontend/feature-docs/tsconfig.json
rename to libs/domains/shared/frontend/feature-docs/tsconfig.json
diff --git a/libs/domains/agenstra/frontend/feature-docs/tsconfig.lib.json b/libs/domains/shared/frontend/feature-docs/tsconfig.lib.json
similarity index 100%
rename from libs/domains/agenstra/frontend/feature-docs/tsconfig.lib.json
rename to libs/domains/shared/frontend/feature-docs/tsconfig.lib.json
diff --git a/libs/domains/agenstra/frontend/feature-docs/tsconfig.spec.json b/libs/domains/shared/frontend/feature-docs/tsconfig.spec.json
similarity index 100%
rename from libs/domains/agenstra/frontend/feature-docs/tsconfig.spec.json
rename to libs/domains/shared/frontend/feature-docs/tsconfig.spec.json
diff --git a/libs/domains/shared/frontend/index.ts b/libs/domains/shared/frontend/index.ts
index 511551daa..873e507bc 100644
--- a/libs/domains/shared/frontend/index.ts
+++ b/libs/domains/shared/frontend/index.ts
@@ -1,5 +1,7 @@
// shared domain frontend exports (browser-safe only; import server utilities via @forepath/shared/frontend/util-express-server or util-runtime-config-server)
export * from './util-configuration/src';
export * from './util-cookie-consent/src';
+export * from './feature-docs/src';
+export * from './util-docs-parser/src';
export * from './util-loading-indicator/src';
export * from './util-meta/src';
diff --git a/libs/domains/shared/frontend/util-configuration/src/lib/environment.agenstra.production.ts b/libs/domains/shared/frontend/util-configuration/src/lib/environment.agenstra.production.ts
index 993de543e..9733392a8 100644
--- a/libs/domains/shared/frontend/util-configuration/src/lib/environment.agenstra.production.ts
+++ b/libs/domains/shared/frontend/util-configuration/src/lib/environment.agenstra.production.ts
@@ -46,4 +46,7 @@ export const environment: Environment = {
socialPreview: {
imageUrl: 'https://agenstra.com/assets/images/og-preview.png',
},
+ docs: {
+ contentRoot: 'agenstra',
+ },
};
diff --git a/libs/domains/shared/frontend/util-configuration/src/lib/environment.agenstra.ts b/libs/domains/shared/frontend/util-configuration/src/lib/environment.agenstra.ts
index d735c3d55..94c213773 100644
--- a/libs/domains/shared/frontend/util-configuration/src/lib/environment.agenstra.ts
+++ b/libs/domains/shared/frontend/util-configuration/src/lib/environment.agenstra.ts
@@ -47,4 +47,7 @@ export const environment: Environment = {
socialPreview: {
imageUrl: 'http://localhost:4300/assets/images/og-preview.png',
},
+ docs: {
+ contentRoot: 'agenstra',
+ },
};
diff --git a/libs/domains/shared/frontend/util-configuration/src/lib/environment.decabill.production.ts b/libs/domains/shared/frontend/util-configuration/src/lib/environment.decabill.production.ts
index 4c119d231..e21389b4b 100644
--- a/libs/domains/shared/frontend/util-configuration/src/lib/environment.decabill.production.ts
+++ b/libs/domains/shared/frontend/util-configuration/src/lib/environment.decabill.production.ts
@@ -37,4 +37,7 @@ export const environment: Environment = {
socialPreview: {
imageUrl: 'https://decabill.com/assets/images/og-preview.png',
},
+ docs: {
+ contentRoot: 'decabill',
+ },
};
diff --git a/libs/domains/shared/frontend/util-configuration/src/lib/environment.decabill.ts b/libs/domains/shared/frontend/util-configuration/src/lib/environment.decabill.ts
index ed20c863f..1c2a8241a 100644
--- a/libs/domains/shared/frontend/util-configuration/src/lib/environment.decabill.ts
+++ b/libs/domains/shared/frontend/util-configuration/src/lib/environment.decabill.ts
@@ -38,4 +38,7 @@ export const environment: Environment = {
socialPreview: {
imageUrl: 'http://localhost:4500/assets/images/og-preview.png',
},
+ docs: {
+ contentRoot: 'decabill',
+ },
};
diff --git a/libs/domains/shared/frontend/util-configuration/src/lib/environment.forepath.production.ts b/libs/domains/shared/frontend/util-configuration/src/lib/environment.forepath.production.ts
index 285ba5388..17b89b164 100644
--- a/libs/domains/shared/frontend/util-configuration/src/lib/environment.forepath.production.ts
+++ b/libs/domains/shared/frontend/util-configuration/src/lib/environment.forepath.production.ts
@@ -37,4 +37,7 @@ export const environment: Environment = {
socialPreview: {
imageUrl: 'https://forepath.io/assets/images/og-preview.png',
},
+ docs: {
+ contentRoot: 'forepath',
+ },
};
diff --git a/libs/domains/shared/frontend/util-configuration/src/lib/environment.forepath.ts b/libs/domains/shared/frontend/util-configuration/src/lib/environment.forepath.ts
index 1e2f7e063..8621e6b1e 100644
--- a/libs/domains/shared/frontend/util-configuration/src/lib/environment.forepath.ts
+++ b/libs/domains/shared/frontend/util-configuration/src/lib/environment.forepath.ts
@@ -38,4 +38,7 @@ export const environment: Environment = {
socialPreview: {
imageUrl: 'http://localhost:4400/assets/images/og-preview.png',
},
+ docs: {
+ contentRoot: 'forepath',
+ },
};
diff --git a/libs/domains/shared/frontend/util-configuration/src/lib/environment.interface.spec.ts b/libs/domains/shared/frontend/util-configuration/src/lib/environment.interface.spec.ts
index 35976b0a2..02d5da84a 100644
--- a/libs/domains/shared/frontend/util-configuration/src/lib/environment.interface.spec.ts
+++ b/libs/domains/shared/frontend/util-configuration/src/lib/environment.interface.spec.ts
@@ -44,6 +44,9 @@ describe('Environment interfaces', () => {
privacyPolicyUrl: 'https://example.com/privacy',
termsUrl: 'https://example.com/terms',
},
+ docs: {
+ contentRoot: 'agenstra',
+ },
};
expect(validEnv.production).toBe(true);
@@ -85,6 +88,9 @@ describe('Environment interfaces', () => {
privacyPolicyUrl: 'https://example.com/privacy',
termsUrl: 'https://example.com/terms',
},
+ docs: {
+ contentRoot: 'agenstra',
+ },
};
expect(validEnv.authentication).toBeDefined();
@@ -127,6 +133,9 @@ describe('Environment interfaces', () => {
privacyPolicyUrl: 'https://example.com/privacy',
termsUrl: 'https://example.com/terms',
},
+ docs: {
+ contentRoot: 'agenstra',
+ },
};
expect(envWithController.controller).toBeDefined();
@@ -170,6 +179,9 @@ describe('Environment interfaces', () => {
privacyPolicyUrl: 'https://example.com/privacy',
termsUrl: 'https://example.com/terms',
},
+ docs: {
+ contentRoot: 'agenstra',
+ },
};
expect(env.chatModelOptions).toBeDefined();
diff --git a/libs/domains/shared/frontend/util-configuration/src/lib/environment.interface.ts b/libs/domains/shared/frontend/util-configuration/src/lib/environment.interface.ts
index a49560d29..dc9930580 100644
--- a/libs/domains/shared/frontend/util-configuration/src/lib/environment.interface.ts
+++ b/libs/domains/shared/frontend/util-configuration/src/lib/environment.interface.ts
@@ -54,4 +54,8 @@ export interface Environment {
socialPreview: {
imageUrl: string;
};
+ docs: {
+ /** Folder name under /docs/ and docs/ repo root, e.g. "agenstra" | "decabill" */
+ contentRoot: string;
+ };
}
diff --git a/libs/domains/shared/frontend/util-configuration/src/lib/environment.production.ts b/libs/domains/shared/frontend/util-configuration/src/lib/environment.production.ts
index 993de543e..9733392a8 100644
--- a/libs/domains/shared/frontend/util-configuration/src/lib/environment.production.ts
+++ b/libs/domains/shared/frontend/util-configuration/src/lib/environment.production.ts
@@ -46,4 +46,7 @@ export const environment: Environment = {
socialPreview: {
imageUrl: 'https://agenstra.com/assets/images/og-preview.png',
},
+ docs: {
+ contentRoot: 'agenstra',
+ },
};
diff --git a/libs/domains/shared/frontend/util-configuration/src/lib/environment.token.ts b/libs/domains/shared/frontend/util-configuration/src/lib/environment.token.ts
index c500157ca..354d71ac3 100644
--- a/libs/domains/shared/frontend/util-configuration/src/lib/environment.token.ts
+++ b/libs/domains/shared/frontend/util-configuration/src/lib/environment.token.ts
@@ -26,6 +26,7 @@ function mergeEnvironmentOverrides(base: Environment, overrides: Partial/src/test-setup.ts'],
- coverageDirectory: '../../../../../coverage/libs/domains/agenstra/frontend/util-docs-parser',
+ coverageDirectory: '../../../../../coverage/libs/domains/shared/frontend/util-docs-parser',
transform: {
'^.+\\.(ts|mjs|js|html)$': [
'jest-preset-angular',
diff --git a/libs/domains/shared/frontend/util-docs-parser/project.json b/libs/domains/shared/frontend/util-docs-parser/project.json
new file mode 100644
index 000000000..3dfee2494
--- /dev/null
+++ b/libs/domains/shared/frontend/util-docs-parser/project.json
@@ -0,0 +1,21 @@
+{
+ "name": "shared-frontend-util-docs-parser",
+ "$schema": "../../../../../node_modules/nx/schemas/project-schema.json",
+ "sourceRoot": "libs/domains/shared/frontend/util-docs-parser/src",
+ "prefix": "framework",
+ "projectType": "library",
+ "tags": ["domain:shared", "scope:frontend", "type:util"],
+ "targets": {
+ "test": {
+ "executor": "@nx/jest:jest",
+ "outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
+ "options": {
+ "jestConfig": "libs/domains/shared/frontend/util-docs-parser/jest.config.ts",
+ "tsConfig": "libs/domains/shared/frontend/util-docs-parser/tsconfig.spec.json"
+ }
+ },
+ "lint": {
+ "executor": "@nx/eslint:lint"
+ }
+ }
+}
diff --git a/libs/domains/agenstra/frontend/util-docs-parser/src/index.ts b/libs/domains/shared/frontend/util-docs-parser/src/index.ts
similarity index 100%
rename from libs/domains/agenstra/frontend/util-docs-parser/src/index.ts
rename to libs/domains/shared/frontend/util-docs-parser/src/index.ts
diff --git a/libs/domains/agenstra/frontend/util-docs-parser/src/lib/interfaces/doc-metadata.interface.ts b/libs/domains/shared/frontend/util-docs-parser/src/lib/interfaces/doc-metadata.interface.ts
similarity index 100%
rename from libs/domains/agenstra/frontend/util-docs-parser/src/lib/interfaces/doc-metadata.interface.ts
rename to libs/domains/shared/frontend/util-docs-parser/src/lib/interfaces/doc-metadata.interface.ts
diff --git a/libs/domains/agenstra/frontend/util-docs-parser/src/lib/interfaces/index.ts b/libs/domains/shared/frontend/util-docs-parser/src/lib/interfaces/index.ts
similarity index 100%
rename from libs/domains/agenstra/frontend/util-docs-parser/src/lib/interfaces/index.ts
rename to libs/domains/shared/frontend/util-docs-parser/src/lib/interfaces/index.ts
diff --git a/libs/domains/agenstra/frontend/util-docs-parser/src/lib/interfaces/navigation-node.interface.ts b/libs/domains/shared/frontend/util-docs-parser/src/lib/interfaces/navigation-node.interface.ts
similarity index 100%
rename from libs/domains/agenstra/frontend/util-docs-parser/src/lib/interfaces/navigation-node.interface.ts
rename to libs/domains/shared/frontend/util-docs-parser/src/lib/interfaces/navigation-node.interface.ts
diff --git a/libs/domains/agenstra/frontend/util-docs-parser/src/lib/interfaces/search-index.interface.ts b/libs/domains/shared/frontend/util-docs-parser/src/lib/interfaces/search-index.interface.ts
similarity index 100%
rename from libs/domains/agenstra/frontend/util-docs-parser/src/lib/interfaces/search-index.interface.ts
rename to libs/domains/shared/frontend/util-docs-parser/src/lib/interfaces/search-index.interface.ts
diff --git a/libs/domains/agenstra/frontend/util-docs-parser/src/lib/services/cross-reference-resolver.service.ts b/libs/domains/shared/frontend/util-docs-parser/src/lib/services/cross-reference-resolver.service.ts
similarity index 100%
rename from libs/domains/agenstra/frontend/util-docs-parser/src/lib/services/cross-reference-resolver.service.ts
rename to libs/domains/shared/frontend/util-docs-parser/src/lib/services/cross-reference-resolver.service.ts
diff --git a/libs/domains/agenstra/frontend/util-docs-parser/src/lib/services/index.ts b/libs/domains/shared/frontend/util-docs-parser/src/lib/services/index.ts
similarity index 100%
rename from libs/domains/agenstra/frontend/util-docs-parser/src/lib/services/index.ts
rename to libs/domains/shared/frontend/util-docs-parser/src/lib/services/index.ts
diff --git a/libs/domains/agenstra/frontend/util-docs-parser/src/lib/services/markdown-parser.service.ts b/libs/domains/shared/frontend/util-docs-parser/src/lib/services/markdown-parser.service.ts
similarity index 100%
rename from libs/domains/agenstra/frontend/util-docs-parser/src/lib/services/markdown-parser.service.ts
rename to libs/domains/shared/frontend/util-docs-parser/src/lib/services/markdown-parser.service.ts
diff --git a/libs/domains/agenstra/frontend/util-docs-parser/src/lib/services/navigation-builder.service.ts b/libs/domains/shared/frontend/util-docs-parser/src/lib/services/navigation-builder.service.ts
similarity index 100%
rename from libs/domains/agenstra/frontend/util-docs-parser/src/lib/services/navigation-builder.service.ts
rename to libs/domains/shared/frontend/util-docs-parser/src/lib/services/navigation-builder.service.ts
diff --git a/libs/domains/agenstra/frontend/util-docs-parser/src/lib/services/search-index-builder.service.ts b/libs/domains/shared/frontend/util-docs-parser/src/lib/services/search-index-builder.service.ts
similarity index 100%
rename from libs/domains/agenstra/frontend/util-docs-parser/src/lib/services/search-index-builder.service.ts
rename to libs/domains/shared/frontend/util-docs-parser/src/lib/services/search-index-builder.service.ts
diff --git a/libs/domains/agenstra/frontend/util-docs-parser/src/test-setup.ts b/libs/domains/shared/frontend/util-docs-parser/src/test-setup.ts
similarity index 100%
rename from libs/domains/agenstra/frontend/util-docs-parser/src/test-setup.ts
rename to libs/domains/shared/frontend/util-docs-parser/src/test-setup.ts
diff --git a/libs/domains/agenstra/frontend/util-docs-parser/tsconfig.json b/libs/domains/shared/frontend/util-docs-parser/tsconfig.json
similarity index 100%
rename from libs/domains/agenstra/frontend/util-docs-parser/tsconfig.json
rename to libs/domains/shared/frontend/util-docs-parser/tsconfig.json
diff --git a/libs/domains/agenstra/frontend/util-docs-parser/tsconfig.lib.json b/libs/domains/shared/frontend/util-docs-parser/tsconfig.lib.json
similarity index 100%
rename from libs/domains/agenstra/frontend/util-docs-parser/tsconfig.lib.json
rename to libs/domains/shared/frontend/util-docs-parser/tsconfig.lib.json
diff --git a/libs/domains/agenstra/frontend/util-docs-parser/tsconfig.spec.json b/libs/domains/shared/frontend/util-docs-parser/tsconfig.spec.json
similarity index 100%
rename from libs/domains/agenstra/frontend/util-docs-parser/tsconfig.spec.json
rename to libs/domains/shared/frontend/util-docs-parser/tsconfig.spec.json
diff --git a/libs/domains/shared/frontend/util-express-server/src/index.ts b/libs/domains/shared/frontend/util-express-server/src/index.ts
index af1509f17..a54f8cedb 100644
--- a/libs/domains/shared/frontend/util-express-server/src/index.ts
+++ b/libs/domains/shared/frontend/util-express-server/src/index.ts
@@ -1,3 +1,10 @@
+// Docs SSR factory lives at @forepath/shared/frontend/util-express-server/docs-server
+// so esbuild server bundles (agent/billing console) do not pull in @angular/ssr/node.
+export {
+ registerRuntimeConfigEndpoint,
+ type RuntimeConfigRouteEnv,
+ type RuntimeConfigRouteLogger,
+} from './lib/runtime-config-route';
export {
createSecurityHeadersMiddleware,
parseCspConnectSrcExtra,
@@ -5,9 +12,4 @@ export {
resolveCspFrameAncestorsSources,
type SecurityHeadersEnv,
} from './lib/security-headers';
-export {
- registerRuntimeConfigEndpoint,
- type RuntimeConfigRouteEnv,
- type RuntimeConfigRouteLogger,
-} from './lib/runtime-config-route';
export { buildSsrAllowedHosts } from './lib/ssr-allowed-hosts';
diff --git a/libs/domains/shared/frontend/util-express-server/src/lib/docs-server.ts b/libs/domains/shared/frontend/util-express-server/src/lib/docs-server.ts
new file mode 100644
index 000000000..18fdbd22d
--- /dev/null
+++ b/libs/domains/shared/frontend/util-express-server/src/lib/docs-server.ts
@@ -0,0 +1,50 @@
+import { dirname, join, resolve } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+import { APP_BASE_HREF } from '@angular/common';
+import { CommonEngine } from '@angular/ssr/node';
+import express, { type Express, type NextFunction, type Request, type Response } from 'express';
+
+import { registerRuntimeConfigEndpoint } from './runtime-config-route';
+import { createSecurityHeadersMiddleware } from './security-headers';
+import { buildSsrAllowedHosts } from './ssr-allowed-hosts';
+
+export type DocsServerBootstrap = Parameters[0]['bootstrap'];
+
+export function createDocsServer(apexDomains: readonly string[], bootstrap: DocsServerBootstrap): Express {
+ const serverDistFolder = dirname(fileURLToPath(import.meta.url));
+ const browserDistFolder = resolve(serverDistFolder, '../browser');
+ const indexHtml = join(serverDistFolder, 'index.server.html');
+ const app = express();
+ const commonEngine = new CommonEngine({
+ allowedHosts: buildSsrAllowedHosts(apexDomains),
+ });
+
+ app.use(createSecurityHeadersMiddleware());
+ registerRuntimeConfigEndpoint(app);
+
+ app.get(
+ '**',
+ express.static(browserDistFolder, {
+ maxAge: '1y',
+ index: 'index.html',
+ }),
+ );
+
+ app.get('**', (req: Request, res: Response, next: NextFunction) => {
+ const { protocol, originalUrl, baseUrl, headers } = req;
+
+ commonEngine
+ .render({
+ bootstrap,
+ documentFilePath: indexHtml,
+ url: `${protocol}://${headers.host}${originalUrl}`,
+ publicPath: browserDistFolder,
+ providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }],
+ })
+ .then((html: string) => res.send(html))
+ .catch((err: unknown) => next(err));
+ });
+
+ return app;
+}
diff --git a/libs/domains/shared/frontend/util-express-server/tsconfig.json b/libs/domains/shared/frontend/util-express-server/tsconfig.json
index d382ae1bb..bb2004b1a 100644
--- a/libs/domains/shared/frontend/util-express-server/tsconfig.json
+++ b/libs/domains/shared/frontend/util-express-server/tsconfig.json
@@ -1,7 +1,11 @@
{
"extends": "../../../../../tsconfig.base.json",
"compilerOptions": {
- "module": "commonjs",
+ "target": "es2022",
+ "module": "es2022",
+ "moduleResolution": "bundler",
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
diff --git a/libs/domains/shared/frontend/util-meta/src/lib/utils/page-meta.util.ts b/libs/domains/shared/frontend/util-meta/src/lib/utils/page-meta.util.ts
index 68512d162..5fa7bfc07 100644
--- a/libs/domains/shared/frontend/util-meta/src/lib/utils/page-meta.util.ts
+++ b/libs/domains/shared/frontend/util-meta/src/lib/utils/page-meta.util.ts
@@ -1,18 +1,25 @@
-const META_TITLE_SUFFIX = ' :: Agenstra';
/** Conservative limit for ~580px in Google SERP title rendering. */
const META_TITLE_MAX_LENGTH = 58;
/** Conservative limit for ~920px in Google SERP description rendering. */
const META_DESCRIPTION_MAX_LENGTH = 155;
/**
- * Formats a browser title as " :: Agenstra", truncating when needed.
+ * Formats a browser title as " :: ", truncating when needed.
*/
-export function formatAgenstraMetaTitle(pageTitle: string): string {
+export function formatProductMetaTitle(pageTitle: string, productName: string): string {
+ const suffix = ` :: ${productName}`;
const trimmed = pageTitle.trim();
- const maxPageLength = META_TITLE_MAX_LENGTH - META_TITLE_SUFFIX.length;
+ const maxPageLength = META_TITLE_MAX_LENGTH - suffix.length;
const page = trimmed.length > maxPageLength ? `${trimmed.slice(0, maxPageLength - 1).trimEnd()}…` : trimmed;
- return `${page}${META_TITLE_SUFFIX}`;
+ return `${page}${suffix}`;
+}
+
+/**
+ * Formats a browser title as " :: Agenstra", truncating when needed.
+ */
+export function formatAgenstraMetaTitle(pageTitle: string): string {
+ return formatProductMetaTitle(pageTitle, 'Agenstra');
}
/**
diff --git a/libs/domains/shared/frontend/util-meta/src/lib/utils/social-preview-meta.util.spec.ts b/libs/domains/shared/frontend/util-meta/src/lib/utils/social-preview-meta.util.spec.ts
index f8d2e13cc..1d5b7e877 100644
--- a/libs/domains/shared/frontend/util-meta/src/lib/utils/social-preview-meta.util.spec.ts
+++ b/libs/domains/shared/frontend/util-meta/src/lib/utils/social-preview-meta.util.spec.ts
@@ -3,6 +3,7 @@ import { Meta } from '@angular/platform-browser';
import {
addPageMetaTags,
buildSocialPreviewMetaTags,
+ createDocsPageDynamicMetaTagStubs,
formatSocialPreviewDescription,
formatSocialPreviewTitle,
getMetaTagSelector,
@@ -110,4 +111,36 @@ describe('buildSocialPreviewMetaTags', () => {
]),
);
});
+
+ it('uses the provided site name for og:site_name', () => {
+ const tags = buildSocialPreviewMetaTags({
+ title: 'Documentation :: Decabill',
+ description: 'Short description.',
+ canonicalUrl: 'https://docs.decabill.com/docs',
+ imageUrl: 'https://decabill.com/assets/images/og-preview.png',
+ siteName: 'Decabill',
+ });
+
+ expect(tags).toEqual(expect.arrayContaining([{ property: 'og:site_name', content: 'Decabill' }]));
+ });
+});
+
+describe('createDocsPageDynamicMetaTagStubs', () => {
+ it('builds brand-specific social preview stubs for cleanup', () => {
+ const tags = createDocsPageDynamicMetaTagStubs({
+ docsSiteOrigin: 'https://docs.decabill.com',
+ imageUrl: 'https://decabill.com/assets/images/og-preview.png',
+ siteName: 'Decabill',
+ });
+
+ expect(tags).toEqual(
+ expect.arrayContaining([
+ { name: 'description', content: '' },
+ { name: 'canonical', content: '' },
+ { property: 'og:site_name', content: 'Decabill' },
+ { property: 'og:url', content: 'https://docs.decabill.com' },
+ { property: 'og:image', content: 'https://decabill.com/assets/images/og-preview.png' },
+ ]),
+ );
+ });
});
diff --git a/libs/domains/shared/frontend/util-meta/src/lib/utils/social-preview-meta.util.ts b/libs/domains/shared/frontend/util-meta/src/lib/utils/social-preview-meta.util.ts
index 2bf714b34..fb0c2b0f2 100644
--- a/libs/domains/shared/frontend/util-meta/src/lib/utils/social-preview-meta.util.ts
+++ b/libs/domains/shared/frontend/util-meta/src/lib/utils/social-preview-meta.util.ts
@@ -37,6 +37,13 @@ export interface PageMetaTagsInput {
localeId: string;
localizeCanonicalUrl: boolean;
socialType?: 'website' | 'article';
+ siteName?: string;
+}
+
+export interface DocsPageDynamicMetaTagStubsInput {
+ docsSiteOrigin: string;
+ imageUrl: string;
+ siteName: string;
}
/**
@@ -170,6 +177,7 @@ export function buildPageMetaTags(input: PageMetaTagsInput): MetaDefinition[] {
localeId: input.localeId,
localizeCanonicalUrl: input.localizeCanonicalUrl,
type: input.socialType,
+ siteName: input.siteName,
}),
);
@@ -179,16 +187,19 @@ export function buildPageMetaTags(input: PageMetaTagsInput): MetaDefinition[] {
/**
* Meta tags updated at runtime on documentation pages (description, canonical, social).
*/
-export const DOCS_PAGE_DYNAMIC_META_TAG_STUBS: MetaDefinition[] = [
- { name: 'description', content: '' },
- { name: 'canonical', content: '' },
- ...buildSocialPreviewMetaTags({
- title: '',
- description: '',
- canonicalUrl: 'https://docs.agenstra.com',
- imageUrl: 'https://docs.agenstra.com/assets/images/og-preview.png',
- }),
-];
+export function createDocsPageDynamicMetaTagStubs(input: DocsPageDynamicMetaTagStubsInput): MetaDefinition[] {
+ return [
+ { name: 'description', content: '' },
+ { name: 'canonical', content: '' },
+ ...buildSocialPreviewMetaTags({
+ title: '',
+ description: '',
+ canonicalUrl: input.docsSiteOrigin,
+ imageUrl: input.imageUrl,
+ siteName: input.siteName,
+ }),
+ ];
+}
export function getMetaTagSelector(tag: MetaDefinition): string | null {
if ('name' in tag && tag.name) {
diff --git a/libs/domains/shared/frontend/util-runtime-config-server/README.md b/libs/domains/shared/frontend/util-runtime-config-server/README.md
index 32e1e2d47..4283c2a31 100644
--- a/libs/domains/shared/frontend/util-runtime-config-server/README.md
+++ b/libs/domains/shared/frontend/util-runtime-config-server/README.md
@@ -2,7 +2,7 @@
Shared utilities for Angular SSR **Express** servers that expose `GET /config`: fetch and validate remote JSON from `CONFIG`, plus cache headers for success and error responses.
-Used by `agenstra-frontend-agent-console`, `agenstra-frontend-billing-console`, `agenstra-frontend-landingpage`, and `agenstra-frontend-docs` (`src/server.ts`).
+Used by `agenstra-frontend-agent-console`, `agenstra-frontend-billing-console`, `agenstra-frontend-landingpage`, `agenstra-frontend-docs`, `decabill-frontend-docs`, and the shared docs base (`apps/shared/frontend-docs/src/server.ts`).
## Public API
diff --git a/tools/ci/container-image-sbom-upload-matrix.sh b/tools/ci/container-image-sbom-upload-matrix.sh
index bea48948d..74d73a133 100755
--- a/tools/ci/container-image-sbom-upload-matrix.sh
+++ b/tools/ci/container-image-sbom-upload-matrix.sh
@@ -15,6 +15,7 @@ fi
KNOWN_IMAGE_NAMES=(
decabill-billing-console-server
decabill-billing-api
+ decabill-docs-server
agenstra-billing-console-server
agenstra-console-server
agenstra-controller-api
diff --git a/tools/ci/upload-release-sboms-by-domain.sh b/tools/ci/upload-release-sboms-by-domain.sh
index 406f14358..177f5704b 100755
--- a/tools/ci/upload-release-sboms-by-domain.sh
+++ b/tools/ci/upload-release-sboms-by-domain.sh
@@ -40,6 +40,12 @@ is_decabill_sbom() {
esac
}
+# Docs SBOM routing (no script changes required — covered by patterns above):
+# agenstra-frontend-docs.cdx.json -> agenstra bucket
+# decabill-frontend-docs.cdx.json -> decabill bucket
+# container-agenstra-docs-server.cdx.json -> agenstra bucket
+# container-decabill-docs-server.cdx.json -> decabill bucket
+
staging_dir="$(mktemp -d)"
trap 'rm -rf "$staging_dir"' EXIT
diff --git a/tools/code/src/generators/frontend/files/ssr/delegating-server.ts.template b/tools/code/src/generators/frontend/files/ssr/delegating-server.ts.template
index 0f3302392..3cac67782 100644
--- a/tools/code/src/generators/frontend/files/ssr/delegating-server.ts.template
+++ b/tools/code/src/generators/frontend/files/ssr/delegating-server.ts.template
@@ -30,6 +30,13 @@ async function loadLocaleServers() {
}
function getLocaleFromRequest(req: IncomingMessage): string {
+ const url = new URL(req.url || '', `http://${req.headers.host}`);
+ const pathSegments = url.pathname.split('/').filter(Boolean);
+
+ if (pathSegments.length > 0 && AVAILABLE_LOCALES.includes(pathSegments[0])) {
+ return pathSegments[0];
+ }
+
const acceptLanguage = req.headers['accept-language'];
if (acceptLanguage) {
@@ -40,13 +47,6 @@ function getLocaleFromRequest(req: IncomingMessage): string {
}
}
- const url = new URL(req.url || '', `http://${req.headers.host}`);
- const pathSegments = url.pathname.split('/').filter(Boolean);
-
- if (pathSegments.length > 0 && AVAILABLE_LOCALES.includes(pathSegments[0])) {
- return pathSegments[0];
- }
-
return DEFAULT_LOCALE;
}
@@ -56,26 +56,26 @@ function serveStaticFile(req: IncomingMessage, res: ServerResponse, locale: stri
if (!pathname.startsWith('/api/') && pathname.includes('.')) {
const localeBrowserPath = join(__dirname, 'browser', locale, pathname);
-
+
if (existsSync(localeBrowserPath)) {
const stat = statSync(localeBrowserPath);
if (stat.isFile()) {
const ext = extname(pathname).toLowerCase();
const contentType = getContentType(ext);
-
+
res.writeHead(200, {
'Content-Type': contentType,
'Content-Length': stat.size,
'Cache-Control': 'public, max-age=31536000' // 1 year cache for static assets
});
-
+
const stream = createReadStream(localeBrowserPath);
stream.pipe(res);
return true;
}
}
}
-
+
return false;
}
@@ -96,7 +96,7 @@ function getContentType(ext: string): string {
'.ttf': 'font/ttf',
'.eot': 'application/vnd.ms-fontobject'
};
-
+
return types[ext] || 'application/octet-stream';
}
@@ -104,7 +104,7 @@ const server = createServer(async (req: IncomingMessage, res: ServerResponse) =>
try {
const locale = getLocaleFromRequest(req);
const localeServer = localeServers.get(locale);
-
+
if (!localeServer) {
console.error(`No server found for locale: ${locale}`);
res.writeHead(500, { 'Content-Type': 'text/plain' });
@@ -118,7 +118,7 @@ const server = createServer(async (req: IncomingMessage, res: ServerResponse) =>
const originalCwd = process.cwd();
const localeDir = join(__dirname, locale);
-
+
try {
process.chdir(localeDir);
await localeServer(req, res);
diff --git a/tools/docs/generate-docs.ts b/tools/docs/generate-docs.ts
index 6b07b336e..10c352f1a 100644
--- a/tools/docs/generate-docs.ts
+++ b/tools/docs/generate-docs.ts
@@ -32,21 +32,31 @@ interface SearchIndex {
entries: SearchIndexEntry[];
}
-/**
- * Build-time documentation generator
- * Scans /docs/agenstra and generates navigation.json and index.json
- */
-
-const DOCS_ROOT = path.join(process.cwd(), 'docs', 'agenstra');
-// Generate to a temp location that won't be cleaned by Angular build
-const OUTPUT_DIR = path.join(process.cwd(), 'dist', 'apps', 'frontend-docs-temp', 'assets', 'docs');
-const CONTENT_OUTPUT_DIR = path.join(process.cwd(), 'dist', 'apps', 'frontend-docs-temp', 'public', 'docs');
+interface GenerateDocsOptions {
+ contentRoot: string;
+ outputTemp: string;
+}
interface FileInfo {
path: string;
content: string;
}
+function parseArgs(): GenerateDocsOptions {
+ let contentRoot = 'agenstra';
+ let outputTemp = 'dist/apps/agenstra/frontend-docs-temp';
+
+ for (const arg of process.argv.slice(2)) {
+ if (arg.startsWith('--contentRoot=')) {
+ contentRoot = arg.split('=')[1] ?? contentRoot;
+ } else if (arg.startsWith('--outputTemp=')) {
+ outputTemp = arg.split('=')[1] ?? outputTemp;
+ }
+ }
+
+ return { contentRoot, outputTemp };
+}
+
/**
* Recursively scan directory for markdown files
*/
@@ -72,8 +82,8 @@ async function scanMarkdownFiles(dir: string, basePath: string = ''): Promise {
- const fullPath = path.join(DOCS_ROOT, filePath);
+async function readMarkdownFile(docsRoot: string, filePath: string): Promise {
+ const fullPath = path.join(docsRoot, filePath);
const content = await fs.readFile(fullPath, 'utf-8');
return { path: filePath, content };
}
@@ -155,7 +165,6 @@ function extractHeadings(content: string): Array<{ level: number; text: string;
if (level && text) {
// Strip markdown link syntax from heading text
- // Convert [text](url) to just "text"
text = text.replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1');
const id = text
.toLowerCase()
@@ -170,10 +179,15 @@ function extractHeadings(content: string): Array<{ level: number; text: string;
return headings;
}
+function stripContentRootPrefix(pathWithoutExt: string, contentRoot: string): string {
+ const prefix = `${contentRoot}/`;
+ return pathWithoutExt.startsWith(prefix) ? pathWithoutExt.substring(prefix.length) : pathWithoutExt;
+}
+
/**
* Build navigation tree from file list
*/
-function buildNavigationTree(files: string[], basePath: string = '/docs'): NavigationNode[] {
+function buildNavigationTree(files: string[], contentRoot: string, basePath: string = '/docs'): NavigationNode[] {
const tree: NavigationNode[] = [];
const nodeMap = new Map();
@@ -185,7 +199,7 @@ function buildNavigationTree(files: string[], basePath: string = '/docs'): Navig
const isReadme = fileName.toLowerCase() === 'readme.md';
const pathSegments = parts.slice(0, -1);
- const routePath = buildRoutePath(basePath, file, isReadme);
+ const routePath = buildRoutePath(basePath, file, isReadme, contentRoot);
let parent: NavigationNode[] = tree;
let currentPath = '';
@@ -196,7 +210,7 @@ function buildNavigationTree(files: string[], basePath: string = '/docs'): Navig
let folderNode = nodeMap.get(currentPath);
if (!folderNode) {
- const folderRoutePath = buildRoutePath(basePath, `${currentPath}/README.md`, true);
+ const folderRoutePath = buildRoutePath(basePath, `${currentPath}/README.md`, true, contentRoot);
folderNode = {
title: formatTitle(segment),
path: folderRoutePath,
@@ -245,23 +259,18 @@ function buildNavigationTree(files: string[], basePath: string = '/docs'): Navig
return tree;
}
-function buildRoutePath(basePath: string, file: string, isReadme: boolean): string {
+function buildRoutePath(basePath: string, file: string, isReadme: boolean, contentRoot: string): string {
const pathWithoutExt = file.replace(/\.md$/, '');
-
- // Remove "agenstra/" prefix from file path for route generation
- // Files are in "agenstra/" folder, but routes don't include this prefix
- const pathWithoutAgenstra = pathWithoutExt.startsWith('agenstra/')
- ? pathWithoutExt.substring('agenstra/'.length)
- : pathWithoutExt;
+ const pathWithoutRoot = stripContentRootPrefix(pathWithoutExt, contentRoot);
if (isReadme) {
- const parts = pathWithoutAgenstra.split('/');
- parts.pop(); // Remove 'README'
+ const parts = pathWithoutRoot.split('/');
+ parts.pop();
const dirPath = parts.join('/');
return dirPath ? `${basePath}/${dirPath}` : basePath;
}
- return `${basePath}/${pathWithoutAgenstra}`;
+ return `${basePath}/${pathWithoutRoot}`;
}
function formatTitle(name: string): string {
@@ -272,16 +281,10 @@ function formatTitle(name: string): string {
}
function sortNavigationNodes(nodes: NavigationNode[]): void {
- // Sort nodes: categories (folders with non-empty children) first, then links (files without children property or empty children)
- // Both sorted alphabetically within their group
- // IMPORTANT: Use a stable sort by first separating categories and links, then sorting each group
const categories: NavigationNode[] = [];
const links: NavigationNode[] = [];
- // Separate categories and links
for (const node of nodes) {
- // A node is a category if it has a children property AND has at least one child
- // A node is a link if it doesn't have a children property OR has an empty children array
if (node.children !== undefined && node.children.length > 0) {
categories.push(node);
} else {
@@ -289,15 +292,12 @@ function sortNavigationNodes(nodes: NavigationNode[]): void {
}
}
- // Sort each group alphabetically
categories.sort((a, b) => a.title.localeCompare(b.title));
links.sort((a, b) => a.title.localeCompare(b.title));
- // Replace the original array with sorted categories first, then links
nodes.length = 0;
nodes.push(...categories, ...links);
- // Recursively sort children
for (const node of nodes) {
if (node.children && node.children.length > 0) {
sortNavigationNodes(node.children);
@@ -323,37 +323,35 @@ function extractSearchableContent(content: string): string {
/**
* Main generation function
*/
-async function generateDocs(): Promise {
- console.log('Generating documentation files...');
+async function generateDocs(options: GenerateDocsOptions): Promise {
+ const { contentRoot, outputTemp } = options;
+ const docsRoot = path.join(process.cwd(), 'docs', contentRoot);
+ const outputDir = path.join(process.cwd(), outputTemp, 'assets', 'docs');
+ const contentOutputDir = path.join(process.cwd(), outputTemp, 'public', 'docs');
+
+ console.log(`Generating documentation files for ${contentRoot}...`);
- // Ensure output directory exists
- await fs.mkdir(OUTPUT_DIR, { recursive: true });
+ await fs.mkdir(outputDir, { recursive: true });
- // Check if docs directory exists
try {
- await fs.access(DOCS_ROOT);
+ await fs.access(docsRoot);
} catch {
- console.warn(`Docs directory not found: ${DOCS_ROOT}`);
- // Create empty navigation and index
- await fs.writeFile(path.join(OUTPUT_DIR, 'navigation.json'), JSON.stringify({ sections: [] }, null, 2));
- await fs.writeFile(path.join(OUTPUT_DIR, 'index.json'), JSON.stringify({ entries: [] }, null, 2));
+ console.warn(`Docs directory not found: ${docsRoot}`);
+ await fs.writeFile(path.join(outputDir, 'navigation.json'), JSON.stringify({ sections: [] }, null, 2));
+ await fs.writeFile(path.join(outputDir, 'index.json'), JSON.stringify({ entries: [] }, null, 2));
return;
}
- // Scan for markdown files
- const files = await scanMarkdownFiles(DOCS_ROOT);
+ const files = await scanMarkdownFiles(docsRoot);
console.log(`Found ${files.length} markdown files`);
- // Read all files
- const fileContents = await Promise.all(files.map((f) => readMarkdownFile(f)));
+ const fileContents = await Promise.all(files.map((f) => readMarkdownFile(docsRoot, f)));
- // Configure marked
marked.setOptions({
breaks: true,
gfm: true,
});
- // Parse files and extract metadata
const metadata: DocMetadata[] = await Promise.all(
fileContents.map(async (file) => {
const title = extractTitle(file.content, file.path);
@@ -376,31 +374,23 @@ async function generateDocs(): Promise {
}),
);
- // Build navigation tree
- // Note: Routes don't include "agenstra" prefix, but files are in "agenstra/" folder
- const navigationTree = buildNavigationTree(files, '/docs');
+ const navigationTree = buildNavigationTree(files, contentRoot, '/docs');
- // Build search index
const searchIndex: SearchIndex = {
entries: metadata.map((doc) => {
const pathWithoutExt = doc.path.replace(/\.md$/, '');
const isReadme = doc.path.toLowerCase().endsWith('readme.md');
-
- // Remove "agenstra/" prefix from file path for route generation
- // Files are in "agenstra/" folder, but routes don't include this prefix
- const pathWithoutAgenstra = pathWithoutExt.startsWith('agenstra/')
- ? pathWithoutExt.substring('agenstra/'.length)
- : pathWithoutExt;
+ const pathWithoutRoot = stripContentRootPrefix(pathWithoutExt, contentRoot);
let routePath: string;
if (isReadme) {
- const parts = pathWithoutAgenstra.split('/');
- parts.pop(); // Remove 'README'
+ const parts = pathWithoutRoot.split('/');
+ parts.pop();
const dirPath = parts.join('/');
routePath = dirPath ? `/docs/${dirPath}` : '/docs';
} else {
- routePath = `/docs/${pathWithoutAgenstra}`;
+ routePath = `/docs/${pathWithoutRoot}`;
}
return {
@@ -413,20 +403,13 @@ async function generateDocs(): Promise {
}),
};
- // Write navigation.json
- await fs.writeFile(path.join(OUTPUT_DIR, 'navigation.json'), JSON.stringify({ sections: navigationTree }, null, 2));
-
- // Write index.json
- await fs.writeFile(path.join(OUTPUT_DIR, 'index.json'), JSON.stringify(searchIndex, null, 2));
+ await fs.writeFile(path.join(outputDir, 'navigation.json'), JSON.stringify({ sections: navigationTree }, null, 2));
+ await fs.writeFile(path.join(outputDir, 'index.json'), JSON.stringify(searchIndex, null, 2));
- // Copy markdown files to public directory for serving
- // Preserve the agenstra folder structure: agenstra/file.md -> public/docs/agenstra/file.md
- await fs.mkdir(CONTENT_OUTPUT_DIR, { recursive: true });
+ await fs.mkdir(contentOutputDir, { recursive: true });
for (const file of files) {
- const sourcePath = path.join(DOCS_ROOT, file);
- // Preserve agenstra folder: file is like "getting-started.md" or "features/README.md"
- // We want it at public/docs/agenstra/getting-started.md
- const destPath = path.join(CONTENT_OUTPUT_DIR, 'agenstra', file);
+ const sourcePath = path.join(docsRoot, file);
+ const destPath = path.join(contentOutputDir, contentRoot, file);
const destDir = path.dirname(destPath);
await fs.mkdir(destDir, { recursive: true });
await fs.copyFile(sourcePath, destPath);
@@ -438,12 +421,8 @@ async function generateDocs(): Promise {
console.log(`- Content files: ${files.length} markdown files copied`);
}
-// Run if executed directly
-// For ES modules, we can check if this is the main module by comparing import.meta.url with the script path
-// Since we're compiling with --module esnext, we use ES module syntax
const getCurrentFilePath = () => {
const url = import.meta.url;
- // Remove file:// protocol and handle both absolute and relative paths
if (url.startsWith('file://')) {
return url.replace('file://', '');
}
@@ -452,7 +431,6 @@ const getCurrentFilePath = () => {
const currentFilePath = getCurrentFilePath();
const scriptPath = process.argv[1];
-// Check if the current file matches the script being executed
const isMainModule =
currentFilePath === scriptPath ||
currentFilePath.endsWith(scriptPath) ||
@@ -460,10 +438,10 @@ const isMainModule =
currentFilePath.includes('generate-docs.js');
if (isMainModule) {
- generateDocs().catch((error) => {
+ generateDocs(parseArgs()).catch((error) => {
console.error('Error generating documentation:', error);
process.exit(1);
});
}
-export { generateDocs };
+export { generateDocs, parseArgs };
diff --git a/tsconfig.base.json b/tsconfig.base.json
index eadc7b10b..25d1f9efa 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -15,9 +15,15 @@
"skipDefaultLibCheck": true,
"baseUrl": ".",
"paths": {
+ "@forepath/shared/frontend/util-docs-bootstrap": [
+ "apps/shared/frontend-docs/src/main.server.ts"
+ ],
"@forepath/shared/frontend/util-express-server": [
"libs/domains/shared/frontend/util-express-server/src/index.ts"
],
+ "@forepath/shared/frontend/util-express-server/docs-server": [
+ "libs/domains/shared/frontend/util-express-server/src/lib/docs-server.ts"
+ ],
"@forepath/shared/frontend/util-runtime-config-server": [
"libs/domains/shared/frontend/util-runtime-config-server/src/index.ts"
],
@@ -79,15 +85,9 @@
"@forepath/agenstra/frontend/feature-agent-console": [
"libs/domains/agenstra/frontend/feature-agent-console/src/index.ts"
],
- "@forepath/agenstra/frontend/feature-docs": [
- "libs/domains/agenstra/frontend/feature-docs/src/index.ts"
- ],
"@forepath/agenstra/frontend/feature-landingpage": [
"libs/domains/agenstra/frontend/feature-landingpage/src/index.ts"
],
- "@forepath/agenstra/frontend/util-docs-parser": [
- "libs/domains/agenstra/frontend/util-docs-parser/src/index.ts"
- ],
"@forepath/forepath/frontend/feature-landingpage": [
"libs/domains/forepath/frontend/feature-landingpage/src/index.ts"
],
@@ -110,6 +110,12 @@
],
"@forepath/shared/backend/util-dynamic-provider-registry": [
"libs/domains/shared/backend/util-dynamic-provider-registry/src/index.ts"
+ ],
+ "@forepath/shared/frontend/util-docs-parser": [
+ "libs/domains/shared/frontend/util-docs-parser/src/index.ts"
+ ],
+ "@forepath/shared/frontend/feature-docs": [
+ "libs/domains/shared/frontend/feature-docs/src/index.ts"
]
}
},