Access request flow
The access-request flow converts an applicant’s submission on
app.steelmoth.ai/request-invite into a row in
steelmoth.access_requests, lets an admin review it from the dashboard, and
on Approve issues an Authentik invitation bound to the access-enrollment
flow. The applicant uses the invitation URL to enrol into the
steelmoth-clients group.
The flow exists because the public form was inert (PRD 0041): the marketing site sent submissions nowhere prior to this redo. PRD 0041 + RFC 0078 + ADR 0108 (stage-then-promote) + ADR 0109 (secrets via prontera broker) settle the design. This page documents the broker-only redo — a prior attempt (PRD 0040 / RFC 0077) was reverted by PR #189 because it stored four credentials as local files on the steelmoth VPS, violating INV-024 + HARD RULE 10.
End-to-end shape
Section titled “End-to-end shape”- Applicant loads
app.steelmoth.ai/request-invite. The route is public in dashboard middleware and renders a route-scoped copy of the marketing request-invite visual design, not a redirect. - Form POSTs JSON to
/api/v1/public/access-requestthrough the dashboard proxy. The visible app fields arename,email, anduseCase(“What do you want to use Steelmoth for?”). The wire body still includesagentName: nullfor runtime-contract compatibility and includesturnstileToken. - Runtime parses the body into
AccessRequestSubmissionWire. In the current private compose config,STEELMOTH_ACCESS_REQUEST_TURNSTILE_ENABLED=false, so the dashboard form sends a non-empty placeholder token and the runtime uses the noop verifier. The runtime still checks per-IP rate-limit, runs per-email collapse, HMAC-hashes IP/UA (key viasecret_value("ACCESS_REQUEST_IP_HASH_SECRET")), inserts a row withstatus='pending', and returns{ok: true, ref: "ar_<ulid>"}. - Admin opens
app.steelmoth.ai/admin/access-requests(dashboard-adminsgroup gated). Approve →POST /v1/admin/access-requests/{id}/approve→ Authentik invitation API (token viasecret_value("AUTHENTIK_INVITE_TOKEN")→ broker). Row flips tostatus='invited'. - Auto-email (ADR 0119): immediately after the invitation commits, the service best-effort SMTP-sends the URL to the applicant from
Moth <…broker-resolved address…>(sender login viasecret_value("MOTH_OWN_GOOGLE_EMAIL"), App Password viasecret_value("MOTH_OWN_GOOGLE_APP_PASSWORD")→ broker → AVclient-mark; both aliases currently map to the Raven Google account). Send result is stashed inmetadata.invitation_email_sent_at/…_last_error(JSONB) and surfaced in the dashboard as a✓ Emailedor⚠ Email failedpill; the admin page’s default filter includes invited rows and shows a page-level alert whenever any listed invite has a recorded email failure, and the runtime logs the dispatch at WARNING on failure (a failed send must never be silent — 2026-07 incident: a dead app password went unnoticed for days). Send failures do not roll back the Authentik invitation — operator can copy the URL manually or click Re-send email (POST /v1/admin/access-requests/{id}/resend-invitation-email) which re-issues the same URL without minting a new Authentik invitation. - Applicant clicks the URL → Authentik resolves the invitation token →
access-enrollmentflow → password + MFA prompts → user-write stage creates the account insteelmoth-clients→ auto-login.
Secret-resolution path (the redo-defining detail)
Section titled “Secret-resolution path (the redo-defining detail)”All four credentials live in Agent Vault staging on prontera. steelmoth-api resolves them through the existing credentials.py:_BROKER_SECRET_ALIASES extension point:
| steelmoth env-name | broker alias | AV vault | AV key |
|---|---|---|---|
TURNSTILE_SECRET |
turnstile-secret |
staging |
TURNSTILE_SECRET |
TURNSTILE_SITE |
turnstile-site |
staging |
TURNSTILE_SITE |
AUTHENTIK_INVITE_TOKEN |
authentik-invite-token |
staging |
AUTHENTIK_INVITE_TOKEN |
ACCESS_REQUEST_IP_HASH_SECRET |
access-request-ip-hash-secret |
staging |
ACCESS_REQUEST_IP_HASH_SECRET |
MOTH_OWN_GOOGLE_EMAIL |
moth-own-google-email |
client-mark |
MARK_RAVEN_GOOGLE_EMAIL |
MOTH_OWN_GOOGLE_APP_PASSWORD |
moth-own-google-app-password |
client-mark |
MARK_RAVEN_GOOGLE_APP_PASS |
Each runtime call to secret_value("…") returns the broker-resolved value
(cached up to 60 s in _BROKER_SECRET_CACHE). The steelmoth-api session
validates against the client-mark vault; the broker uses its own identity
(mark-steelmoth-secret-broker) to fetch from staging. Zero secret files
on the steelmoth VPS. Zero *_FILE env vars. Zero new compose secrets:
block entries.
When the marketing-site form is deployed, TURNSTILE_SITE is fetched at deploy
time by scripts/deploy-marketing-site.sh (broker GET →
NEXT_PUBLIC_TURNSTILE_SITE_KEY env → build output). The app-hosted
/request-invite page does not currently mount Turnstile because the runtime
verification path is disabled server-side; a real app Turnstile widget would
also need a public dashboard site-key configuration and runtime verification
to be enabled.
Source Map
Section titled “Source Map”| Claim Area | Source |
|---|---|
| Product intent, goals | docs/prds/0041-public-access-request-waitlist.md |
| Technical design | docs/rfcs/0078-access-request-broker-resolved-secrets.md |
| Stage-then-promote decision | docs/adr/0108-access-request-stages-in-steelmoth-promotes-to-authentik-on-approval.md |
| Broker-only secret architecture | docs/adr/0109-access-request-secrets-resolve-via-prontera-broker.md |
| Prontera broker anchor | docs/adr/0105-secret-broker-on-prontera.md |
| Schema | memlink-hybrid/migrations/0051_access_requests.up.sql |
| Domain types | steelmoth-runtime/src/steelmoth_runtime/access_requests/domain.py |
| Service | steelmoth-runtime/src/steelmoth_runtime/access_requests/service.py |
| Outbound adapters | steelmoth-runtime/src/steelmoth_runtime/access_requests/{postgres_repository,authentik_client,turnstile,rate_limit}.py |
| Broker extension | steelmoth-runtime/src/steelmoth_runtime/credentials.py (lines around _BROKER_SECRET_ALIASES) |
| Wire contracts | steelmoth-runtime/src/steelmoth_runtime/web_chat/contracts_access_requests.py |
| FastAPI routes | steelmoth-runtime/src/steelmoth_runtime/web_chat/routes_access_requests.py |
| Dashboard panel | dashboard/app/admin/access-requests/page.tsx |
| Sidebar | dashboard/components/layout/sidebar-nav.ts (Advanced > Operations > Access Requests) |
| Public app form | dashboard/app/request-invite/page.tsx; dashboard/app/request-invite/_components/RequestAccessForm.tsx; dashboard/app/request-invite/RequestInvite.module.css |
| Marketing-site deploy | scripts/deploy-marketing-site.sh |
| Prontera broker | prontera-assets/agent-vault-secret-broker/README.md (reference for alias-JSON format) |
Related Pages
Section titled “Related Pages”web-chat-api.md— overall route inventory; access-request routes mounted fromapp.py:build_app().dashboard.md— admin section pattern + sidebar conventions.security-and-secrets.md— the prontera broker is the canonical secret resolution path; this feature uses it for four credentials.data-and-scope.md—steelmoth.access_requestsfollows operator-global RLS pattern.deployment.md— dashboard viascripts/compose-dashboard-deploy.sh; marketing viascripts/deploy-marketing-site.sh(new with this redo).mail-broker.md—feature_flag.access_request_auto_emailflips delivery to the mail broker; default off.
Maintenance Notes
Section titled “Maintenance Notes”- Update this page when access-request endpoints, schema, Authentik flow slug, dashboard panel shape, or the app-hosted request-invite form changes.
- The four AV
stagingcredentials + matching broker aliases are operator-managed on prontera; changes there are not reflected by code diffs and must be recorded here when they happen. - Adding a new
_BROKER_SECRET_ALIASESentry on steelmoth requires a matching entry in/etc/agent-vault-secret-broker/envon prontera + a broker restart. The PR description must reference both diffs. - Rotating an AV credential propagates to steelmoth-api within the broker’s 60 s cache TTL; restart the broker on prontera to flush immediately.
Known Unknowns
Section titled “Known Unknowns”- Retention policy for
status='rejected'andstatus='expired'rows is not yet defined. - Mail-broker auto-email path is scaffold-only; its end-to-end behavior is unverified until the flag flips.
- Whether the AV credentials API supports scripted
set(vs UI-only proposal) — discovered during Phase 2 of the redo deploy. - The marketing-site source currently lives outside this repo, while this wiki is repo-source grounded. Marketing link changes must be verified against that sibling source or live output when they are made.

