Signup is the anonymous, public entry through which a company buys a membership and gets its own tenant. It exists only in multi-tenant mode. The routes are in server/routes/signup.py and the flow in server/signup.py; both work on the control plane, never on a tenant database. This page explains validation, the privacy notice, domain verification, payment, provisioning, and the anonymous status poll. The tenant model itself is in multi-tenancy; the web form is described in mail and profile screens.
In short¶
- The routes are mounted only when
multi_tenantis on; otherwiseGET /api/v1/signup/answers{"signup": "closed"}(src/cluster_erp/server/app.py:1440-1457). - The setting
signup_enabledcloses the door on a multi-tenant node: the probe answers closed and a create is refused withSIGNUP_CLOSED(src/cluster_erp/server/routes/signup.py:129-133,src/cluster_erp/server/signup.py:226-234). - A signup needs the current privacy notice version, read from
signup_notice_dirandsignup_notice_version; without a configured notice, signups are refused (src/cluster_erp/server/signup.py:206-253). - The founder's e-mail must be on the claimed mail domain, and with
signup_verify_emailon, provisioning waits until a link mailed to that address is confirmed (src/cluster_erp/server/signup.py:256-280,src/cluster_erp/server/signup.py:336-372). - The anonymous surface accepts only capability tokens in request bodies; the control plane stores their SHA-256 digests (
src/cluster_erp/server/routes/signup.py:8-15,src/cluster_erp/core/tenancy.py:73-84,src/cluster_erp/core/tenancy.py:233-239).
How it works¶
Steps¶
The sequence shows a signup with e-mail verification on.
sequenceDiagram
participant V as visitor browser
participant R as signup routes
participant S as start_signup
participant T as TenantRegistry
participant P as provision
V->>R: GET /api/v1/signup/
R-->>V: open or closed, regions, notice
V->>R: POST /api/v1/signup/
R->>S: SignupRequest
S->>T: domain_owner, create_tenant, Membership
S-->>V: signup_token, state queued
V->>R: POST /api/v1/signup/verify with verify_token
R->>P: _provision_in_background
V->>R: POST /api/v1/signup/status with signup_token
R-->>V: state, tenant_domain when readyIn prose:
GET /api/v1/signup/returns whether the door is open, the regions, the default region, the node country, the country-to-node map, and the notice (src/cluster_erp/server/routes/signup.py:114-143).POST /api/v1/signup/applies a per-client hourly limit ofsignup_rate_limit_per_hour, counted incontrol_rate_limits(src/cluster_erp/server/routes/signup.py:63-85,src/cluster_erp/server/routes/signup.py:146-149).start_signupchecks the door, validates the request, requires the current notice, refuses a domain that any live tenant already owns, and asks the payment port to check out (src/cluster_erp/server/signup.py:291-310).- It derives a tenant key from the domain, checks the region, registers the tenant with status
provisioning, and stores aMembershipwith statusactive, provisioning statequeued, the notice version, and the acceptance time (src/cluster_erp/server/signup.py:312-353). - With verification on, only an Argon2 hash of the founder's password waits on the control plane, and a verification mail goes out (
src/cluster_erp/server/signup.py:344-364). With verification off, provisioning starts at once (src/cluster_erp/server/signup.py:366-372). POST /api/v1/signup/verifystampsverified_atand starts provisioning with the stored identity; an expired link answers HTTP 410 (src/cluster_erp/server/routes/signup.py:171-189,src/cluster_erp/server/signup.py:472-516)._provision_in_backgroundsets the state torunning, thenready, and clears the waiting hash; on failure it setsfailedand logs details only for the operator (src/cluster_erp/server/signup.py:534-568).POST /api/v1/signup/statusreturns the state for a signup token and, only when ready, the sign-in domain (src/cluster_erp/server/routes/signup.py:222-243).
Membership states¶
| Field | Values | Source |
|---|---|---|
Membership.status |
pending, active, suspended, cancelled |
src/cluster_erp/core/tenancy.py:276-279 |
Membership.provision_state |
queued, running, ready, failed |
src/cluster_erp/core/tenancy.py:280-283 |
| State shown by the status route | the provisioning state, or awaiting_verification while a verify token is open |
src/cluster_erp/server/routes/signup.py:229-232 |
Other routes¶
| Route | Effect | Source |
|---|---|---|
POST /api/v1/signup/resend |
Rotates the verify token and mails a fresh link; rate-limited | src/cluster_erp/server/routes/signup.py:192-202, src/cluster_erp/server/signup.py:415-441 |
POST /api/v1/signup/cancel |
Withdraws a signup that is queued or failed; frees the domain |
src/cluster_erp/server/routes/signup.py:205-219, src/cluster_erp/server/signup.py:444-469 |
Ports and settings¶
- Payment:
signup_payment_provider; the only provider in code isoffline, which approves and sets a trial period ofsignup_trial_daysdays (src/cluster_erp/server/signup.py:85-103,src/cluster_erp/server/signup.py:179-188). - Mail:
signup_mail_transportisspool(a JSON file insignup_mail_spool_dir) orsmtpwith thesignup_smtp_*settings (src/cluster_erp/server/signup.py:105-176,src/cluster_erp/config/settings.py:283-289). - Verification:
signup_verify_emailandsignup_verify_ttl_hours(src/cluster_erp/config/settings.py:277-279). The link carries its token in the URL fragment (src/cluster_erp/server/signup.py:384-389). - Notice:
signup_notice_dirholdssignup_notice.<lang>.mdfiles;signup_notice_versionnames the version (src/cluster_erp/server/signup.py:206-223,src/cluster_erp/config/settings.py:299-306).
Types and defaults are in the settings reference.
Why it is built this way¶
- The poll handle is the only key the anonymous surface accepts, so signup state cannot be used to enumerate tenants (
src/cluster_erp/server/signup.py:17-22). - Tokens travel in bodies and fragments so that they reach no access log, browser history, or
Refererheader, and the page confirms with a POST so that a mail scanner's GET changes nothing (src/cluster_erp/server/routes/signup.py:8-15). - Tokens are stored as digests so that a copy of the table cannot be replayed (
src/cluster_erp/core/tenancy.py:233-238). - Verification by a link on the claimed domain stops first-come squatting of another company's domain (
src/cluster_erp/core/tenancy.py:240-243). - The rate limit is counted in the control database so that all workers and nodes count a client together (
src/cluster_erp/server/routes/signup.py:41-44).
Rules and boundaries¶
- The founder becomes the only administrator of the new tenant: provisioning runs with
bootstrap_admin_exclusive, which deactivates any seeded default account first (src/cluster_erp/config/settings.py:253-258,src/cluster_erp/server/app.py:882-890). - A failed provisioning error is never shown to the anonymous poller (
src/cluster_erp/server/signup.py:564-568). - Tests:
tests/test_signup_closed.py(4),tests/test_signup_notice.py(4),tests/test_signup_tokens.py(3),tests/test_signup_validation.py(5), counted withgrep -c "def test_"; e2eweb-client/_e2e_tenant_signup.mjs.
Interaction with other parts¶
- Multi-tenancy: tenants, memberships, and the registry.
- Tenant lifecycle: the
provisioncommand that signup calls. - Operator console: the signup queue view.
Limits and open points¶
- A live payment provider is not in code;
payment_port_forrefuses any provider other thanoffline(src/cluster_erp/server/signup.py:179-188). - Retention of abandoned signups runs in the control-plane retention pass; its rules are in scheduled jobs and retention.
Terms introduced¶
| Term | Code identifier | Definition | Turkish term | Source |
|---|---|---|---|---|
| signup token | signup_token |
The capability handle a signer uses to poll, resend, or cancel. | kayıt belirteci | src/cluster_erp/core/tenancy.py:239 |
| verify token | verify_token |
The capability in the mailed link that proves control of the claimed domain. | doğrulama belirteci | src/cluster_erp/core/tenancy.py:244 |
| provisioning state | provision_state |
How far the creation of a signup's tenant database has progressed. | kurulum durumu | src/cluster_erp/core/tenancy.py:257 |
| payment port | PaymentPort |
The interface behind which a payment provider checks out a signup. | ödeme bağlantı noktası | src/cluster_erp/server/signup.py:85 |
Related pages¶
Verified against commit 92f8051 on 2026-11-13.