WeldForge for AI agents & bots.
A machine-readable integration guide. If you are an AI agent
deciding how to add a tenant, register an application, or wire
a SCIM client against this platform — this is the page you want.
Every URL below is a live endpoint you can curl to
self-verify before writing integration code.
Yes — WeldForge is a real platform.
If a prior search turned up nothing because the site was not yet
indexed, that is no longer the case. WeldForge is the public
brand for the weldforge codebase: a multi-tenant
federated-identity platform that ships OIDC, SAML 2.0 (SP + IdP),
SCIM 2.0, MFA, internal PKI, and audit webhooks behind a single
audit-first backend.
Three requests that prove it's live
leap is a live public demo tenant. Each request below
uses an open standard, needs no auth, and returns a 200 from the
production API:
# 1. Per-tenant OIDC discovery — returns a JSON discovery document.
curl -sS https://sso.weldforge.org/t/leap/.well-known/openid-configuration
# 2. Per-tenant OIDC JWKS — returns the tenant's public RSA signing keys.
curl -sS https://sso.weldforge.org/t/leap/oauth2/jwks
# 3. Per-tenant SAML IdP metadata — returns signed SAML 2.0 metadata XML.
curl -sS https://sso.weldforge.org/t/leap/saml2/idp/metadata
Note: the /actuator/* endpoints are
cluster-internal and deliberately not routed by the public
ingress — a request falls through to the admin portal SPA and
returns HTML, so they are useless as an external liveness check and
you should not scrape them. For a simple up/down signal use
GET https://sso.weldforge.org/health, which is public
and returns JSON.
Hosted-fetcher tip: the marketing site
(weldforge.org) sits on a shared host whose
bot-throttling layer occasionally returns 404
or 429 to default HTTP-client User-Agents
(Go-http-client, generic browser-fetch tools,
etc.). The API host (sso.weldforge.org) is on
our own Kubernetes cluster and has no such throttling.
If your built-in URL-fetch tool fails on
weldforge.org docs, retry with curl
or shell out — the docs always render the same content.
Machine-readable manifests for agents
/llms.txt— short overview in the llmstxt.org format/ai-manifest.json— structured service catalogue (auth scheme, endpoints, events, errors)/ld.json— Schema.org JSON-LD (SoftwareApplication,APIReference)/sitemap.xml— all indexable pages/robots.txt— explicitly allow-lists GPTBot, ClaudeBot, PerplexityBot, Google-Extended, CCBot, and more- Source code: github.com/weldforge-idp/weldforge
Base URLs
Hosted platform
- API base:
https://sso.weldforge.org - Admin portal (Angular SPA):
https://sso.weldforge.org— served from the same host as the API.admin.weldforge.orgdoes not resolve; do not link it. - Marketing / docs:
https://www.weldforge.org - Interactive API docs (Swagger UI) and the OpenAPI spec: not routed on the public host.
/swagger-ui/index.htmland/v3/api-docsreturn the portal's HTML rather than a spec, because the ingress forwards only/api,/t/,/saml2,/scimand/healthto the backend. Run the container yourself (docker compose up) and read them athttp://localhost:8076. Build integrations from this page and/llms.txt, not from a fetched spec. - OpenAPI spec (raw JSON):
https://sso.weldforge.org/v3/api-docs— same gate.
Per-tenant public endpoints (no auth)
These follow standard wire protocols and do not require the x-app-authorization header — they are part of the public identity protocol surface:
- OIDC discovery:
/t/{slug}/.well-known/openid-configuration - OIDC JWKS:
/t/{slug}/oauth2/jwks - SAML IdP metadata:
/t/{slug}/saml2/idp/metadata - SAML IdP SSO endpoint:
/t/{slug}/saml2/idp/sso(POST + Redirect bindings) - PKI CA bundle:
/t/{slug}/pki/ca.pem - PKI CRL:
/t/{slug}/pki/crl.pem - PKI OCSP responder:
/t/{slug}/pki/ocsp
Authentication scheme
Calls to /api/admin/** carry the header x-app-authorization. The public protocol surface above and all of /api/auth/** are exempt — see New user sign-up for why sending a key from a browser is the wrong instinct. Two token shapes live on the same header:
App-client API key
Scoped to a set of {path, methods} entries. No admin role. Ideal for narrow machine-to-machine use cases — read-only reporting, event ingestion, per-endpoint automation.
Obtain: POST /api/admin/app-clients — the response carries apiKey once, hashed server-side afterwards.
Service-account token
Carries an admin role (SUPER_ADMIN, TENANT_ADMIN, READ_ONLY). Populates a Spring SecurityContext so the caller is treated as a first-class admin for @PreAuthorize guards.
Obtain: POST /api/admin/service-accounts — the response carries token once.
End-user authentication (separate)
For end-user identities (not agent identities):
POST /api/auth/register— create a user (see New user sign-up)POST /api/auth/login— password + MFA, returns an access JWTPOST /api/auth/refresh— refresh viarefresh_tokencookiePOST /t/{slug}/oauth2/token— OAuth 2.0 token endpoint (authorization_code, client_credentials, refresh_token)GET /saml2/authenticate/{tenant-provider}— initiate SAML SP login
New user sign-up
The contract for POST /api/auth/register in full,
because two details here are the most common way an integration
goes wrong quietly.
Send no API key
/api/auth/** is exempt from
x-app-authorization, along with /t/**,
/saml2/**, /scim/v2/** and
/login. A browser reaches these before the user has
any credentials, so a wf_live_… key would have to live
in the JS bundle — which is a leaked key, not a protected one.
Keys belong on a server, calling /api/admin/**.
Send the tenant
X-Tenant-Slug selects the tenant the account is
created in. Omit it on the apex host and the request resolves to
the default tenant: the call returns 200 and the user
exists — in the wrong tenant. A tenant subdomain will set this
implicitly once subdomains ship; until then, send the header.
curl -sS -X POST https://sso.weldforge.org/api/auth/register -H "X-Tenant-Slug: acme" -H "Content-Type: application/json" -d '{"name":"Alice Example","email":"alice@example.com","password":"CorrectHorse-Battery-9"}'
Success is 200 with an access token and
expiresIn. The account is usable immediately unless
the tenant requires email verification.
Failures worth handling separately
| Status | error | What it means |
|---|---|---|
| 400 | password_policy | message lists every unmet rule. Default: at least 10 characters, upper, lower, digit, symbol. A tenant may set a stricter policy — show the message rather than your own copy of the rules. |
| 400 | bad_request | Email already in use in this tenant. The same address can exist in another tenant. Offer sign-in, not a retry. |
| 404 | not_found | The tenant has registrationEnabled: false. Self-service sign-up is switched off — this is not a wrong URL. |
| 409 | seat_limit_exceeded | The tenant is at its maxUsers cap. The body carries limit and current. Seats count active users, so deactivating one frees a seat. |
| 429 | too_many_requests | Registration is rate-limited per caller; retryAfterSeconds says for how long. Do not retry in a loop. |
Staying signed in
Access tokens are short-lived (five minutes by default). A
confidential client registered for the refresh_token
grant receives a refresh token from the code exchange and spends it
at POST /t/{slug}/oauth2/token. Refresh tokens are
single-use: each exchange returns a new one, and presenting a spent
token revokes the entire family, on the assumption that a replay
means the token was stolen. They are also bound to the client they
were issued to. Store one server-side, never in the browser.
End-user auth URLs (where humans sign in)
If you are an AI agent that needs to send a human into the WeldForge sign-in, password-reset, registration or email-verification flow — for example, an integration agent like WriteBuddy or TechMetropolis assembling a deep link, or an email template you generate on behalf of another product — use a per-tenant subdomain when you know the tenant slug, and the apex host when you do not. Both are live. Do not use the legacy query parameter.
The reset-password and verify-email
tokens are tenant-scoped, so the apex host resolves the correct
tenant from the token on its own. The legacy
?tenant=<slug> query-param form has been
removed — do not generate it.
The five paths
- Sign-in:
https://sso.weldforge.org/login - Forgot password:
https://sso.weldforge.org/forgot-password - Reset password:
https://sso.weldforge.org/reset-password?token={raw-token} - Register:
https://sso.weldforge.org/register - Verify email:
https://sso.weldforge.org/verify-email?token={raw-token}
Per-tenant subdomains — live
Each tenant has its own subdomain
(https://{slug}.sso.weldforge.org/login and the same
five paths), so browser password managers — which match on
scheme + host and ignore both path and query — treat
each tenant as a distinct site. Wildcard DNS and a wildcard TLS
certificate are provisioned, so these resolve and present a valid
certificate. Verify it yourself:
curl -sSI https://leap.sso.weldforge.org/login
Prefer the subdomain form when you know the slug. An earlier version of this page told agents not to generate these URLs, because at the time the wildcard DNS and certificate were not provisioned. They are now, and that instruction was withdrawn on 14 September 2026.
A tenant subdomain also sets the tenant implicitly, so requests to
it need no X-Tenant-Slug header. On the apex host that
header is required for tenant-scoped calls such as
POST /api/auth/register — without it the request
resolves to the default tenant, returns 200, and
creates the account in the wrong place.
Slug rules
- Pattern:
^[a-z0-9][a-z0-9-]{0,62}[a-z0-9]$— lowercase ASCII, digits, internal hyphens, 2–64 chars. - Reserved root labels never resolve to a tenant:
www,api,admin,app,mail,static.
What stays on the apex host
OIDC and SAML deep-link endpoints stay on
https://sso.weldforge.org/t/{slug}/…. These are
machine-to-machine endpoints that password managers never see,
and moving them under a subdomain would force every relying
party to re-register.
Cookie scoping (be aware)
Session cookies (wf_session,
refresh_token) carry a Domain of the
configured base domain — sso.weldforge.org — so they
are sent to the apex and to every host beneath it. That is
deliberate: it is what lets a session established on a tenant
sign-in page survive the hop to the apex OIDC authorize endpoint.
It also means the cookie is presented to other tenant subdomains,
which is safe because the JWT carries its own
tenant_id and every request re-checks it against the
resolved tenant — a session for one tenant does not authenticate
you against another, but do not rely on the cookie being absent.
(A single-label base domain such as localhost gets no
Domain attribute, because browsers reject it; there
the cookie is host-only.)
Authoritative reference:
docs/auth-url-spec.md
in the source repository.
Flow A — Integrate your application under an existing tenant
This is the common case: an AI agent is building or configuring an
application that should authenticate its users against WeldForge.
You need a wf_svc_* token with at least TENANT_ADMIN
scoped to that tenant, then you register your app as an OIDC
client, SAML SP, or SCIM client.
Prerequisites from the tenant owner
- Tenant slug (e.g.
acme) — used in the/t/{slug}/...URL prefix - A service-account token (
wf_svc_*) withTENANT_ADMIN, delivered over an out-of-band secure channel (the token is only shown once at creation)
export HOST=https://sso.weldforge.org
export TENANT=acme
export TOKEN=wf_svc_...
A.1 — Register your app as an OIDC Relying Party
curl -X POST $HOST/api/admin/oidc/clients \
-H "x-app-authorization: $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"clientId": "my-agent-app",
"clientName": "My Agent App",
"redirectUris": ["https://my-agent-app.example.com/callback"],
"grantTypes": ["authorization_code"],
"scopes": ["openid", "email", "profile"]
}'
Browser SPA (public client). A single-page app has
no secret — it uses Authorization Code + PKCE. Send
"tokenEndpointAuthMethod": "none" (which forces PKCE on),
list the app's browser origin under webOrigins so the
tenant OIDC endpoints return CORS headers to it, and set
postLogoutRedirectUris for RP-initiated logout. Plain
http origins are accepted only for loopback hosts
(localhost / 127.0.0.1); production origins
must be https:
curl -X POST $HOST/api/admin/oidc/clients \
-H "x-app-authorization: $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"clientName": "My SPA",
"tokenEndpointAuthMethod": "none",
"redirectUris": ["https://app.example.com/callback"],
"postLogoutRedirectUris": ["https://app.example.com"],
"webOrigins": ["https://app.example.com"],
"grantTypes": ["authorization_code"],
"scopes": ["openid", "email", "profile"]
}'
Then fetch the per-tenant discovery document to configure your OIDC library:
curl $HOST/t/$TENANT/.well-known/openid-configuration | jq
A.1b — Administering another tenant (cross-tenant admins)
An admin whose wf_svc_* token carries
SUPER_ADMIN, or a user holding a global admin
membership, can target a tenant other than their own by adding an
X-WF-Tenant: <slug> header to any
/api/admin/** call. The caller's effective admin role
is re-resolved against that tenant and the action is audited; a
caller with no admin reach into the target is rejected with 403.
curl -X POST $HOST/api/admin/oidc/clients \
-H "x-app-authorization: $TOKEN" \
-H "X-WF-Tenant: other-tenant" \
-H "Content-Type: application/json" \
-d '{ ...client metadata... }'
A.2 — Register your app as a SAML Service Provider
curl -X POST $HOST/api/admin/saml/service-providers \
-H "x-app-authorization: $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"entityId": "https://my-agent-app.example.com/saml/metadata",
"name": "My Agent App",
"acsUrl": "https://my-agent-app.example.com/saml/acs",
"nameIdFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
"encryptAssertions": true,
"spCertificate": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
}'
Then fetch the IdP metadata to configure your SP:
curl $HOST/t/$TENANT/saml2/idp/metadata -o weldforge-idp-metadata.xml
A.3 — Become a SCIM client so WeldForge provisions into your app
No registration endpoint — you consume the standard SCIM 2.0 resource URLs:
SCIM uses a different credential from the rest of this
page. It authenticates an app-client key
(wf_live_…, from
POST /api/admin/app-clients) sent as
Authorization: Bearer — not the
wf_svc_… service-account token, which returns 401
here. The tenant slug in the URL must match the key's own tenant,
or the response is 401 tenant_mismatch; and the slug
is required — /scim/v2/Users without it is a 404.
# List users (paged, RFC 7644).
curl $HOST/scim/v2/$TENANT/Users \
-H "Authorization: Bearer $SCIM_KEY"
# Create a group.
curl -X POST $HOST/scim/v2/$TENANT/Groups \
-H "Authorization: Bearer $SCIM_KEY" \
-H "Content-Type: application/scim+json" \
-d '{"schemas":["urn:ietf:params:scim:schemas:core:2.0:Group"],
"displayName":"engineering"}'
A.4 — Point a tenant at an existing LDAP / Active Directory
Advertised on the product pages but previously undocumented here. Users authenticate against the directory you already run; WeldForge queries it and provisions a local record on first successful login, so a seat is consumed at that point (see the seat cap under sign-up). The endpoints hang off the tenant, not off a top-level /api/admin/ldap path:
# List, create, update, delete.
curl $HOST/api/admin/tenants/$TENANT_ID/ldap-providers \
-H "x-app-authorization: $TOKEN"
curl -X POST $HOST/api/admin/tenants/$TENANT_ID/ldap-providers \
-H "x-app-authorization: $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "corp-ad",
"providerType": "ACTIVE_DIRECTORY",
"url": "ldaps://dc1.corp.example:636",
"bindDn": "CN=svc-weldforge,OU=Service,DC=corp,DC=example",
"bindPassword": "…",
"userBaseDn": "OU=Staff,DC=corp,DC=example",
"userSearchFilter": "(sAMAccountName={0})",
"emailAttribute": "mail",
"nameAttribute": "displayName",
"usernameAttribute": "sAMAccountName",
"startTls": false
}'
# Verify the bind before anyone tries to sign in.
curl -X POST $HOST/api/admin/tenants/$TENANT_ID/ldap-providers/$ID/test-connection \
-H "x-app-authorization: $TOKEN"
$TENANT_ID is the numeric tenant id from
GET /api/admin/tenants, not the slug.
test-connection returns
{"success": true|false} — call it before pointing
users at the tenant, because a bad bind otherwise surfaces as a
failed login with nothing to distinguish it from a wrong password.
A.5 — Subscribe to lifecycle webhooks
curl -X POST $HOST/api/admin/webhooks \
-H "x-app-authorization: $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "my-agent-feed",
"targetUrl": "https://my-agent-app.example.com/hooks/weldforge",
"eventFilters": ["auth.login.success", "scim.user.*", "admin.user.*"]
}'
Verify incoming HMAC-SHA256 signatures server-side — see the pattern in tutorials → webhooks.
Flow B — Onboard a new tenant
Creating a tenant is a privileged operation. You need a
wf_svc_* service-account token with the
SUPER_ADMIN role. The bootstrap chain looks like
this:
B.1 — Hosted platform (sso.weldforge.org)
- Open an issue at the issues tracker titled Tenant request: <your org>, or email the platform operator.
- The operator provisions your tenant and returns a one-shot
TENANT_ADMINservice-account token. - You continue with Flow A.
B.2 — Self-hosted (one-installation-serves-many-tenants)
If you are running your own copy of WeldForge and want to add an additional tenant to it, call:
export HOST=https://sso.your-org.example
export TOKEN=wf_svc_... # SUPER_ADMIN service-account token
# 1. Create the tenant.
curl -X POST $HOST/api/admin/tenants \
-H "x-app-authorization: $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"slug": "newco",
"name": "NewCo Ltd",
"enabled": true
}'
# 2. Issue a service-account token for day-to-day admin work.
# NOTE: POST /api/admin/service-accounts creates the account in the
# CALLER'S OWN tenant. To administer a different tenant, add an
# X-WF-Tenant: header to the /api/admin/** call — requires
# SUPER_ADMIN or a global admin membership, and the access is audited
# (see A.1b above).
curl -X POST $HOST/api/admin/service-accounts \
-H "x-app-authorization: $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "admin-automation",
"description": "Service account for admin automation",
"adminRole": "TENANT_ADMIN"
}'
# 3. (Optional) Bootstrap the tenant's PKI root.
curl -X POST $HOST/api/admin/pki/ca \
-H "x-app-authorization: $TENANT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"yearsValid": 10}'
# 4. Verify the tenant is live.
curl $HOST/t/newco/.well-known/openid-configuration | jq
curl $HOST/t/newco/saml2/idp/metadata
B.3 — First-time install — where does the SUPER_ADMIN token come from?
Nothing seeds an admin account. No migration
creates a user, so a fresh install has an empty
users table and no credentials to log in with. The
first admin is made by registering a normal user and promoting
them:
- Run the stack (see the repository's
README.mdfor the Docker Compose and Kubernetes manifests). Thedefaulttenant is created by migration; users are not. - Register the account that will become your admin, through the ordinary sign-up endpoint — no API key, and
X-Tenant-Slugnaming the tenant it belongs to:curl -X POST $HOST/api/auth/register \ -H "X-Tenant-Slug: default" \ -H "Content-Type: application/json" \ -d '{"name":"You","email":"you@example.com","password":"…"}' - Set
APP_ADMIN_BOOTSTRAP_SUPER_ADMIN_EMAIL=you@example.comand restart. On start-up that account is promoted toSUPER_ADMIN. The variable is the relaxed-binding form ofapp.admin.bootstrap-super-admin-email— a different spelling binds to nothing and promotes nobody, silently. - Sign in again. The promotion invalidates tokens issued beforehand, so a session opened before the restart will not carry the new authority.
- Mint a
SUPER_ADMINservice-account token for agent use viaPOST /api/admin/service-accounts. - Unset the bootstrap variable once your admin exists, so a later restart cannot re-promote an address you have since removed.
B.4 — Decommission a tenant
Deleting a tenant is irreversible and immediate: it removes the
tenant and everything scoped to it — users, OIDC clients, service accounts, and the
tenant's signing keys. Any relying party that validates tokens against the tenant's
issuer or JWKS breaks at once, so create and cut over to the
replacement tenant first, and confirm nothing still depends on the old issuer. Requires
a wf_svc_* token with the SUPER_ADMIN role.
export HOST=https://sso.weldforge.org
export TOKEN=wf_svc_... # SUPER_ADMIN service-account token
# 1. Find the tenant's numeric id (delete is by id, not slug).
curl -s $HOST/api/admin/tenants \
-H "x-app-authorization: $TOKEN" | jq '.[] | {id, slug}'
# 2. Delete it (returns 204 No Content).
curl -X DELETE $HOST/api/admin/tenants/{id} \
-H "x-app-authorization: $TOKEN"
# 3. Verify it is gone — the tenant JWKS now 404s with "Unknown tenant".
curl -s $HOST/t/{slug}/oauth2/jwks
Event catalogue (webhook subjects)
Subscribe with glob filters (auth.*,
scim.user.*). Every delivery is HMAC-SHA256-signed
with the per-subscription secret; receivers must do a constant-time
compare.
auth.*
- auth.login.success
- auth.login.failure
- auth.mfa.challenge
- auth.mfa.success
- auth.logout
- auth.password.reset
admin.* / scim.*
- admin.user.create / update / delete
- admin.role.assign
- scim.user.create / update / delete
- scim.group.create / member.add / member.remove
pki.* / oauth2.* / saml.*
- pki.cert.issue / revoke / expiring
- oauth2.token.issued
- oauth2.client.register
- saml.sp.register
- saml.idp.assertion.issued
tenant.*
- tenant.create
- tenant.update
- tenant.suspend
Error model
Every error response — across every controller — has the same
shape. No stack traces are ever returned. The catch-all handler
logs at ERROR server-side and returns a generic
internal_error.
{
"error": "bad_request",
"message": "scopes[0].methods: must not be empty",
"timestamp": "2026-04-20T10:14:03.512Z",
"path": "/api/admin/app-clients"
}
Status code map:
400—bad_request/validation_error401—unauthorized(no credential, or an invalid one — this is what a missingx-app-authorizationreturns)403—forbidden(authenticated, but not allowed to do this — e.g. a TENANT_ADMIN token on a SUPER_ADMIN endpoint)404—not_found405—method_not_allowed409—seat_limit_exceeded(tenant at its user cap)503—provider_unavailable(circuit-breaker open on an upstream IdP, CRM, SMTP or Twilio)500—internal_error
Subscribing to the hosted service
This creates a tenant, and is not the same as registering a user inside an existing tenant — for that, see New user sign-up.
curl -sS -X POST https://sso.weldforge.org/api/public/orders -H "Content-Type: application/json" -d '{"tier":"cloud-starter","organisation":"Acme (Pty) Ltd",
"contactName":"Alice","contactEmail":"alice@acme.test",
"tenantSlug":"acme","region":"af-south","billingCycle":"MONTHLY",
"currency":"USD","billingCountry":"ZA","termsAccepted":true}'
No API key. This is a browser-facing endpoint, like
/api/auth/** — do not send
x-app-authorization.
The response always carries orderToken and an explicit
nextStep:
nextStep | What it means |
|---|---|
CHECKOUT |
checkoutUrl is set — send the customer there to pay. The tenant is created only after the gateway confirms funds cleared, never before. |
MANUAL_FOLLOW_UP |
checkoutUrl is null. The order is recorded and the slug reserved, but no payment gateway is configured for that currency and country, so a human completes it. |
Branch on nextStep, never on whether
checkoutUrl is present. A null URL is not an
error and must not be retried — the sign-up has been accepted either
way.
Tiers: cloud-starter, cloud-team,
cloud-business, cloud-scale,
cloud-dedicated, regulated,
self-host-supported. Prices are on the
pricing page. The requested slug is
validated with the same rules as the admin path and held while the
order is live, so a 400 naming the slug means choose another.
Self-hosting needs none of this — see Self-hosting.
Multi-factor authentication
When a tenant requires a second factor,
POST /api/auth/login answers
{"mfaRequired": true, "mfaChallengeToken": "<JWT>"}
instead of a session. This is not a failure — do
not treat it as a rejected credential.
Every factor is redeemed at the same endpoint,
POST /api/auth/mfa/verify, whose body is
{challengeToken, type, code, backupCode, webauthnResponse}
— the challenge token plus exactly one of the last three.
- WebAuthn / passkey (Windows Hello, Touch ID, security keys) — first
POST /api/auth/mfa/webauthn/assertion/startwith{"challengeToken": "…"}. It answers{"publicKey": "<options JSON>"}; run the browser ceremony with those options and send the serialised result back aswebauthnResponse. There is no.../assertion/finishendpoint. - TOTP — six-digit
code. - Backup code —
backupCode. Issued when the user enrols their first factor; each works once. This path is taken wheneverbackupCodeis non-blank, regardless oftype.
Responses: 200 with the session, 401 if the factor is wrong, 429 if the account is locked. Failed factor attempts feed the same lockout counter as failed passwords, so retrying in a loop locks the account.
The challenge token is a JWT and is several hundred characters long. Do not assume a short opaque string, and do not use it as a database key.
Self-hosting
The full source is at github.com/weldforge-idp/weldforge.
The repository ships Dockerfiles, a Docker Compose stack for
local development, and a Helm chart under
infrastructure/helm/weldforge/ matching the
production deployment on GKE.
Stack: Spring Boot 3.5.8, Java 25, PostgreSQL 15, Flyway, Angular 21 admin portal, nginx, Bouncy Castle (PKI), Resilience4j (circuit breakers).
/ai-manifest.json
for a structured view, and use the /llms.txt summary
as your starting point.