025 - Matrix Role Ansible Flavor (matrix-docker-ansible-deploy)¶
User Story¶
As a platform administrator of Infinito.Nexus, I want the web-app-matrix role to gain a second deployment flavor that delegates the heavy lifting to the upstream matrix-docker-ansible-deploy (MDAD) playbook, so that the full Matrix ecosystem — Synapse, Element, every supported bridge, Jitsi-backed video conferencing, ma1sd / Dimension / hookshot / etcetera — is available without the role re-implementing each service from scratch. The existing role-internal compose stack stays selectable as the compose flavor for one cycle, marked deprecated.
Background¶
The current roles/web-app-matrix/ ships a self-contained compose stack with Synapse, Element, a handful of bridges and a ChatGPT bot. Adding a new bridge or service today means hand-rolling its compose service, env file, postgres seed, well-known entries and Playwright coverage inside this repo — the marginal cost per bridge is high, and the matrix-docker-ansible-deploy (MDAD) upstream community already maintains all of that under one umbrella role.
Upstream MDAD is a fully-fledged Ansible playbook (setup.yml plus a top-level role matrix-base that pulls in roughly seventy sub-roles). It is NOT a drop-in Ansible role: it expects to be the root playbook with its own inventory hosts (matrix.{{ DOMAIN }} style) and its own host_vars/<host>/vars.yml.
To integrate it into Infinito.Nexus, the consumer-facing role (web-app-matrix) MUST act as an adapter: clone MDAD at a pinned ref into the role artefact directory, generate an MDAD-shaped inventory + vars file from lookup('config', application_id, ...), and invoke MDAD via ansible-playbook as a child process (or via Ansible’s import_playbook). Credentials, domains, Keycloak OIDC client, central postgres / redis endpoints and the existing reverse-proxy front (sys-svc-proxy) all flow from Infinito.Nexus into MDAD via that adapter.
Confirmed Decisions¶
Decisions 2, 4, 7 and 11 were operator-confirmed before implementation started.
# |
Decision |
Rationale |
|---|---|---|
1 |
Add a |
Mirrors the SSO flavor pattern landed in requirement 021. Keeps the existing compose code path intact during one deprecation window. |
2 |
MDAD is pulled in as a git checkout at deploy time, not vendored. Source: |
Keeps the upstream playbook auditable (every consumer sees the exact SHA); avoids carrying the ~10MB upstream tree in this repo. |
3 |
The ansible-flavor tasks render an MDAD inventory under |
One canonical place to map every Infinito.Nexus knob to its MDAD counterpart; keeps MDAD’s own inventory hierarchy intact. |
4 |
The MDAD inventory is templated to enable a conservative-default bridge set plus Jitsi + Element Call. Bridge enablement is gated on |
Operator phrasing: “alle möglichen bridges und services sollen enabled, aktiviert und wenn möglich integriert sein incl. video conference”; conservative default keeps first-deploy clean. Bridges that need an operator-issued token are documented as opt-in in the role README. |
5 |
Keycloak OIDC: MDAD’s |
Matches the convention used by web-app-erpnext / web-app-zammad / web-app-odoo. |
6 |
Central-service reuse: MDAD’s |
Same pattern as the other shared-DB consumers. |
7 |
Reverse proxy: MDAD’s bundled traefik / nginx is disabled (operator-confirmed); the Infinito.Nexus |
Avoids two proxies fighting over port 443; preserves the repo’s “one TLS terminator” invariant. |
8 |
Compose flavor is marked deprecated in |
Operator phrasing: “deprecated flavor soll der alte compose bleiben”. |
9 |
|
Operator phrasing: “die variants sollen die neue integration testen”. |
10 |
Playwright coverage: existing biber / administrator OIDC specs + native admin (break-glass) keep working against the ansible-flavor Element web client (same URL surface). Two new specs are added: |
Matches Decision 19 (playwright meta-services parity) and gives operator-visible signal on the bridge wiring. |
11 |
Lifecycle stage on the new flavor: |
Operator’s signal that the flavor is production-intended from v1; promotion to |
12 |
Out of scope for v1 of the ansible flavor: matrix-appservice-irc bouncer farms, federation tester self-hosting, the federation-discovery delegation chain via |
Bounds scope; bigger surface to ship cleanly first. |
13 |
MDAD’s |
Operator-requested: isolate MDAD’s Ansible runtime from Infinito’s so MDAD can pin its own |
Target Schema¶
Role layout (additions; the existing tasks/, templates/, vars/, meta/ stay)¶
roles/web-app-matrix/
├── tasks/
│ ├── main.yml # dispatches by services.matrix.flavor
│ ├── flavor/compose/ # existing 01_docker.yml, 02_..., 04_webserver.yml moved here
│ │ ├── main.yml
│ │ ├── 01_docker.yml
│ │ ├── 02_create-and-seed-database.yml
│ │ └── 04_webserver.yml
│ └── flavor/ansible/ # NEW
│ ├── main.yml # clone MDAD + render vars + invoke setup.yml
│ ├── 01_clone_upstream.yml
│ ├── 02_render_inventory.yml
│ ├── 03_run_playbook.yml
│ └── 04_proxy_wiring.yml
├── templates/
│ ├── flavor/compose/ # existing compose.yml.j2, synapse.conf.j2, etc moved here
│ └── flavor/ansible/ # NEW
│ ├── mdad-hosts.j2
│ ├── mdad-vars.yml.j2
│ └── mdad-bridges.yml.j2
└── vars/
└── main.yml # existing + new ANSIBLE_FLAVOR_* paths
meta/services.yml delta¶
matrix:
flavor: ansible # NEW: default. operator can pin "compose" per-variant.
upstream:
ref: 9634cc3f7956e312eaf04647d85cbab46173f2e9 # NEW: MDAD git ref pin (master HEAD at PR-cut time; upstream ships untagged from master)
repo: https://github.com/spantaleev/matrix-docker-ansible-deploy
playbook_tags: setup-all,start # existing — passed verbatim to MDAD
server_name: "{{ DOMAIN_PRIMARY }}"
plugins: # existing — bridges. Expanded for v1 ansible flavor:
appservice_irc: true
appservice_kakaotalk: false
discord: true
facebook: false # needs operator-issued FB app token
gitter: true
googlechat: false # needs google workspace API token
heisenbridge: true # IRC bouncer
hookshot: true # GitHub / GitLab / Jira webhooks
instagram: false # needs FB app token
irc: true # appservice-irc
linkedin: false # no upstream bridge
mautrix_signal: true
mautrix_telegram: true
mautrix_twitter: true
mautrix_whatsapp: false # needs operator-issued WA-business token
mautrix_youtube: false
mx_puppet_discord: false # superseded by mautrix-discord
mx_puppet_slack: false # superseded by mautrix-slack
mautrix_slack: true
sms: false # needs operator carrier setup
conferencing:
element_call: true # NEW: Element Call (LiveKit-based)
jitsi: true # NEW: Jitsi widget integration into Element
lifecycle: beta # operator-confirmed; was alpha in the first-pass proposal
meta/variants.yml (matrix dimension × ansible-vs-compose dimension)¶
---
# V1 (ansible): SSO + LDAP + all enabled bridges + Jitsi + Element Call
- services:
matrix:
flavor: ansible
sso: { enabled: true, shared: true }
ldap: { enabled: true, shared: true }
email: { enabled: true, shared: true }
# … other dynamic flags true …
# V2 (ansible): no auth, all bridges off (smoke test of the playbook-only path)
- services:
matrix:
flavor: ansible
plugins: # explicit all-off override
# every plugin: false
sso: { enabled: false, shared: false }
ldap: { enabled: false, shared: false }
email: { enabled: false, shared: false }
# V3 (ansible): LDAP only, no SSO, conference disabled
- services:
matrix:
flavor: ansible
conferencing: { element_call: false, jitsi: false }
sso: { enabled: false, shared: false }
ldap: { enabled: true, shared: true }
email: { enabled: false, shared: false }
# Vcompose-1: the existing compose flavor as regression coverage (existing V1 renamed)
- services:
matrix:
flavor: compose
sso: { enabled: true, shared: true }
ldap: { enabled: true, shared: true }
email: { enabled: true, shared: true }
Acceptance Criteria¶
Flavor dispatch¶
[x]
meta/services.yml::matrix.flavoraccepts the two valuesansible(default) andcompose. Any other value fails role-meta lint.[x]
tasks/main.ymlincludes exactly one oftasks/flavor/ansible/main.ymlortasks/flavor/compose/main.ymlbased onservices.matrix.flavor(no double execution).[x] Existing compose-flavor tasks land under
tasks/flavor/compose/without behavior change; an existing compose-flavor deploy reuses its existing volumes (make compose-deploy mode=update apps=web-app-matrixon the old inventory still works).
MDAD upstream pulled in cleanly¶
[x]
tasks/flavor/ansible/01_clone_upstream.ymlclones the pinnedservices.matrix.upstream.refofservices.matrix.upstream.repointo{{ instance_dir }}mdad/; rerun is idempotent (fetch + checkout, no destructive reset).[x] The clone respects the role-wide CA-trust env so HTTPS clone works behind the local CA terminator.
MDAD inventory rendered¶
[x]
tasks/flavor/ansible/02_render_inventory.ymltemplates{{ instance_dir }}mdad/inventory/hosts(single host:matrix.{{ DOMAIN_PRIMARY }}localhost) and{{ instance_dir }}mdad/inventory/host_vars/matrix.{{ DOMAIN_PRIMARY }}/vars.ymlfromtemplates/flavor/ansible/mdad-vars.yml.j2.[x] Every bridge that is
trueunderservices.matrix.plugins.<bridge>shows up enabled in the renderedvars.ymlwith the matching MDAD enable-flag (matrix_mautrix_telegram_enabled: trueetc.), and every bridge that isfalseshows up disabled. Verified by reading the rendered file.[x]
services.matrix.conferencing.element_call=trueflips the matching MDAD knobs (matrix_element_call_enabledplus the LiveKit/Jitsi backing knobs).
MDAD playbook invocation¶
[ ]
tasks/flavor/ansible/03_run_playbook.ymlinvokesansible-playbook -i inventory/hosts setup.yml --tags={{ services.matrix.playbook_tags }}from{{ instance_dir }}mdad/and exits cleanly on all three ansible-flavor variants (V1 / V2 / V3) in a fresh-host matrix deploy.[x] A long-running tag (e.g.
setup-allplusstart) has a hard timeout consistent with the rest of the role-loop (proposal: 60 minutes) so a stuck upstream task fails the deploy loud instead of hanging.[x] MDAD’s
ansible-playbookis invoked inside a dedicated runner sub-container (image:services.matrix.runner.image, built locally fromfiles/flavor/ansible/runner/Dockerfile) so its Ansible runtime is isolated from Infinito-Nexus’s. The runner mounts the Docker socket, systemd dirs,/matrix,/usr/local/bin, and the MDAD checkout from the deploy container. Amount --make-rshared /runs once in the deploy container before the runner starts because MDAD’s systemd units bind-mount withbind-propagation=slave.
Central-service reuse¶
[x] When
svc-db-postgresis ingroup_names, the rendered MDAD inventory points Synapse + all enabled bridges at the central postgres host alias with the central root credential; no in-stack postgres container is spawned by MDAD.[x] When
svc-db-redisis ingroup_names, the rendered inventory points Synapse + bridges at the central redis alias; no in-stack redis container is spawned by MDAD.[x] When
web-app-mailuis ingroup_names, the rendered inventory configures MDAD’s outbound SMTP against the central Mailu endpoint.
Keycloak OIDC¶
[x] When
web-app-keycloakis ingroup_names, theredirect_urisfilter auto-includeshttps://{{ DOMAIN_PRIMARY }}/_synapse/client/oidc/callbackin the shared Keycloak client (no per-role Keycloak entry needed).[x] MDAD’s
matrix_synapse_oidc_*keys are templated fromOIDC.CLIENT.*so end-to-end OIDC login lands a fresh user in Synapse with email + display name from theid_token.
Reverse proxy¶
[x] MDAD’s bundled traefik is disabled in the rendered inventory.
[x]
sys-svc-proxyproxies the canonical hostname to the MDAD-bound localhost port for Synapse, Element, and the federation listener (port 8448 still public).[x] One TLS certificate covers
{{ DOMAIN_PRIMARY }}plus the well-known aliases MDAD requires.
Variants¶
[x]
meta/variants.ymldefines three variants: V1 ansible all-on, V2 ansible all-off, V3 ansible ldap-only. (The Vcompose-1 regression variant was dropped during implementation: the matrix-deploy lint caps variants at three; compose-flavor regression is exercised via manualmake compose-deploy mode=reinstall apps=web-app-matrix services.matrix.flavor=composeinstead.)[ ] All three ansible variants deploy cleanly on a fresh box (full-matrix gate succeeds end-to-end).
Playwright¶
[ ] Existing
test-login-administrator/test-login-biber/test-login-native-administratorkeep passing against the ansible-flavor Element surface in V1 ansible all-on.[x] New
test-element-call.jsopens a room and triggers the Element Call widget, verifying the LiveKit / Jitsi widget surface renders (gated onservices.matrix.conferencing.element_call).[x] New
test-bridge-roster.jslists enabled bridges via Synapse admin API and asserts that everyservices.matrix.plugins.<bridge>=trueentry has a live appservice record.
Compose-flavor regression¶
[x] An existing operator who pins
flavor: composein their inventory keeps the current compose stack (docker compose up -dagainst the existing compose.yml.j2). No state migration in this deploy direction.[x] The compose flavor’s deprecated status is surfaced in
roles/web-app-matrix/README.md(lifecycle/meta/info.yml::deprecatedis not part of the schema; the deprecation contract lives in the README and thetasks/main.ymldispatch comment).
Documentation¶
[x]
roles/web-app-matrix/README.mddocuments the two flavors, the MDAD ref bump path, the bridge-enablement matrix, the deprecation window for the compose flavor, and the new Playwright specs.[ ] This requirement file is cross-linked from the implementing PR.
Validation Apps¶
The role MUST deploy cleanly under all four variants on a fresh box. V1 (ansible all-on) MUST additionally pass the biber + administrator Playwright personas plus the new test-element-call + test-bridge-roster specs.
INFINITO_APPS="web-app-matrix" \
make deploy-fresh-purged-apps INFINITO_FULL_CYCLE=true
Prerequisites¶
Before starting any implementation work, the agent MUST read AGENTS.md and follow all instructions in it.
Implementation Strategy¶
The agent MUST execute this requirement autonomously once Proposed Decisions are confirmed. Open clarifications only when a decision is genuinely ambiguous and would otherwise block progress; default to the intent already captured in this document and proceed.
Read Compose Loop before starting.
Move the existing compose-flavor tasks + templates into the new
flavor/compose/subdirectories without behavior change; add a flavor dispatch intasks/main.yml.Scaffold
tasks/flavor/ansible/(clone, render inventory, run playbook, wire proxy) plus the matching template tree.Wire central-service reuse + Keycloak OIDC + Mailu SMTP into the rendered MDAD vars.
Add the new variants, run the matrix gate, iterate per the Compose Loop until green.
Add the new Playwright specs.
Mark the compose flavor deprecated in
meta/info.yml; update README.
Commit Policy¶
Single commit (or a tight, related sequence) lands the whole ansible-flavor addition; no half-scaffolded intermediate commits.
When all ACs are checked off,
make testis green, and the four variants deploy cleanly, the agent instructs the operator to rungit-sign-pushoutside the sandbox (per AGENTS.md). The agent MUST NOT push.
Context¶
Upstream playbook: https://github.com/spantaleev/matrix-docker-ansible-deploy
Closest in-repo precedent for adapter-style upstream-playbook invocation: none yet (this requirement establishes it).
Closest in-repo precedent for a
flavor:discriminator: SSO flavor migration (req 021).Closest in-repo precedent for the central-service consumer pattern: web-app-erpnext (req 024) + web-app-zammad (req 022).
Playwright coverage parity contract: req 019.
Role meta layout contract: req 008.