Imported from kwfcfc/idp-register (
AGENTS.md). Install upstream withnpx skills add kwfcfc/idp-register. Copyright stays with the author.
AGENTS.md — Context for AI agents & human developers
Read this first. It captures the why and the hard constraints that are not obvious from the code. Detailed design lives in
docs/ARCHITECTURE.md; the rationale for each major choice lives indocs/DECISIONS.md.
What this is
idp-register is a small, self-hosted user-registration broker that sits in front of
an OIDC identity provider (IdP). It is the OIDC-era equivalent of Synapse's built-in
registration tokens: people apply through a public form, an invite code (limited
by use-count and expiry) can auto-approve them, otherwise an admin reviews the
application. On approval the service provisions the user into the target IdP and lets
the IdP send a single "set your password / activate" email.
Origin: the maintainer runs a Matrix homeserver (Synapse) plus other services (GoToSocial, Forgejo) behind Rauthy as the OIDC IdP, and wants the Synapse registration-token UX, but for Rauthy.
Status (read before assuming)
- An initial AI-generated draft exists as a full-stack SvelteKit (TypeScript) app. It is being re-based to Go backend + decoupled SvelteKit static frontend.
- The draft implements the admin site only; the public application form is not built yet.
- The draft is PostgreSQL-only and stores invite codes as HMAC digests. Both
decisions are superseded (dual-DB; plaintext codes). See
docs/DECISIONS.md.
What survives the re-base (port the design, not the TS): the .svelte UI components &
routes, the SQL schema design (application state machine, audit log, permission
profiles, admin sessions), the generic OIDC login concept, and the Rauthy API client
logic. Everything under src/lib/server/** and *.server.ts gets rewritten in Go.
Hard invariants — do not violate without a new ADR
- Dual-DB portability (SQLite + PostgreSQL). No PG-only column types in shared schema.
Generate UUIDs and timestamps in Go (not DB defaults); store time as epoch
milliseconds (INTEGER); store small lists/objects as JSON in TEXT (handled in Go);
INET→TEXT. Partial indexes are fine (both engines support them). Drivers: pgx (stdlib) for PG, modernc.org/sqlite (pure-Go, no CGO) for SQLite. - Two distinct IdP roles — never conflate them.
- Admin-auth IdP: where admins log in. A plain, standards-only OIDC Relying Party. Never assume Rauthy-specific endpoints here — it may be Authentik, Dex, etc. (No bespoke GitHub OAuth2 adapter; broker GitHub via an OIDC IdP if needed.)
- Provisioning target IdP: where end users get created. Hidden behind the
Provisionerinterface (Rauthy today, Kanidm planned). Rauthy API calls live only inside the Rauthy implementation.
- Invite codes are plaintext "limited-use, time-limited" codes (Synapse-aligned:
uses_allowed/pending/completed/expiry_time). Never log a code. Use-count is reserved withpending++and confirmed withcompleted++(see ARCHITECTURE). - The public form must never choose IdP groups directly. It may only reference
server-side
permission_profiles; group assignment happens server-side on approval. - One user-facing email on the happy path. Defer email verification into the IdP's activation link; don't add a separate "verify your email" round-trip. Keep form responses uniform to avoid account enumeration.
- Single self-contained binary. Frontend is built to static assets and embedded via
go:embed; build withCGO_ENABLED=0. - License: GPLv3-or-later. Every source file starts with
// SPDX-License-Identifier: GPL-3.0-or-later(or the comment form for that language).
Target repository layout
cmd/server/ main.go (wires config, db, provisioner, oidc, http)
internal/
config/ env → typed config
store/ database/sql repos + portable schema (schema_pg.sql, schema_sqlite.sql)
provisioner/ Provisioner interface + rauthy/ (+ kanidm/ later)
oidcauth/ admin OIDC RP (coreos/go-oidc) + opaque sessions
token/ invite-code logic (mint, validate, reserve/complete)
application/ application state machine + review
audit/ audit log
web/ HTTP handlers, embeds frontend build
web/ SvelteKit frontend (adapter-static → build/ embedded by Go)
docs/ ARCHITECTURE.md, DECISIONS.md
migrations/ (later; startup applies schema_*.sql for now)
Build / test / CI (target)
- Build:
CGO_ENABLED=0 go build; multi-stage Dockerfile (Node builds frontend → Go embeds it → distroless/scratch final, ~15 MB). This is the all-in-one packaging; two more (API-only, standalone static frontend) are planned — seedocs/DEPLOYMENT.md(ADR-0011). Those three are the future Crow CI build targets. - Test: run the
storelayer against both SQLite (in-memory) and PostgreSQL (testcontainers) to catch dialect drift — this is the main reason dual-DB needs CI cover. - CI/CD: Crow CI (NOT Forgejo Actions), repo on
forgejo.goba.ip-dynamic.org.- Before touching anything under
.crow/, read.claude/skills/crow-ci/SKILL.mdand the reference files it points to (workflow/jsonnet syntax, plugin ecosystem, CLI). It encodes this repo's conventions and the mandatory local-validation loop:jsonneteval →crow lint --strict .crow/→ optionalcrow exec— never validate by pushing and watching the server. Thecrowbinary is provided by the nix devShell (direnv puts it on PATH; otherwisenix develop -c crow ...). - Pipelines are authored in Jsonnet. Config lives in a
.crow/directory (.jsonnet/.libsonnet/.yaml); alternatively a single.crow.jsonnet. Crow is Woodpecker-derived, so.woodpecker*is a fallback Crow also recognizes. - A Jsonnet file evaluates to JSON/YAML before any other processing, then matrix
expansion →
${CI_*}substitution → lint → compile. Structure: a top-level object withsteps: [{ name, image, commands, when?, depends_on?, services? }]; returning a JSON array yields multiple workflows. CI metadata viastd.extVar("CI_PIPELINE_EVENT")etc. Share helpers via.libsonnetimports. Syntax ref: https://crowci.dev/v5-13/usage/jsonnet/. - Current pipeline (
.crow/):test.jsonnet(frontend check/build + Go suite on both DBs) →image.jsonnet(buildx multi-arch image, main/tag only; tag builds add an SPDX SBOM attestation and a cosign signature — repo secretscosign_private_key/cosign_password, key pair shared withgobro/simple-git-server, public key committed atcosign.pub) +docs.jsonnet(mdBook build onstable/ release-linevX.Ytags, thenappleboy/drone-git-pushto Forgejogh-pages) +e2e.jsonnet(manual smoke). Shared constants inlib.libsonnet.docs.jsonnetbuildsdocs/book/with mdBook, prepares a versioned GitHub Pages tree (/stable/,/vX.Y/, root version index), and pushes Forgejo'sgh-pages; Forgejo's mirror syncsstableandgh-pagesto GitHub. It does not use GitHub Actions or direct GitHub credentials.e2e.jsonnetis Crow-native, not docker-in-docker: a real Rauthy (detached step) + mailcrab (service) + the published app image drive the register flow; adepends_onDAG gates the app's start on Rauthy readiness (its boot OIDC discovery is fatal). The agent runs the docker backend, so adocker:clistep dialing the socket can't feed compose bind-mounts into the daemon — prefer services/detached steps for anything needing a running dependency.
- Before touching anything under
Glossary
- Invite code / token — public, plaintext, limited-use string that auto-approves an application.
- Application — a submitted registration request moving through a review state machine.
- Permission profile — a named server-side bundle of target-IdP groups (e.g.
developer). - Provision — create the user in the target IdP and trigger its activation email.