Sthora
Teams and access

Invitations and signup

The two ways anyone other than the first operator gets an account, and why one of them is off by default.

Invitations

An owner invites someone by email address. The invitation carries a single-use token, expires after AUTH_INVITATION_TTL_HOURS (168 hours — one week), and is listed on the organization's page while pending.

Status
PENDINGIssued, not yet accepted. An owner can cancel it.
ACCEPTEDConsumed.
REJECTEDDeclined.
CANCELLEDWithdrawn by an owner.

Accepting

POST /invitations/accept is public — the one unauthenticated route that creates a user. Accepting collects a name and a password, creates the user and their membership in one transaction, consumes the token, and signs the invitee in.

Acceptance refuses an address that already has an account

It fails with an error saying so, rather than quietly creating a second membership. See one user, one organization.

The token is single-use, enforced by an atomic delete-on-consume, and stored as an HMAC digest rather than as itself.

Signup

POST /signup creates a user and a new organization, with the user as its owner. Unlike first-run setup it never closes.

It is gated on SIGNUP_ENABLED, which defaults to false.

Why it is off by default

The control plane holds the Docker socket, so anyone with admin access to the portal has root on the server. Open registration on a self-hosted box turns "who has root on this machine" into "whoever finds the URL".

Cloud mode — where the control plane is infrastructure somebody else runs — turns it on.

When it is off, the route answers 404 — indistinguishable from a route that does not exist, not a 403 that would advertise the feature to whoever is probing for it.

What signup creates

In one Serializable transaction: a User, an Organization, and a Member row with role OWNER. Serializable because the email and slug uniqueness checks are claims about absent rows, which a weaker isolation level would let two concurrent requests both believe.

It does not create a subscription row. An organization with no subscription is treated as being on the default plan — see plans and limits.

Unlike a first-run account, a signed-up user's emailVerified starts false. The first-run account is verified at creation because the operator made it at the console and there is no one else to verify it against; that justification does not transfer to a web form.

Comparison

/setup/signup/invitations/accept
PublicYesYes, when enabledYes
Creates an organizationYesYesNo — joins an existing one
RoleOWNEROWNERWhatever the invitation says
ClosesPermanently, after one useNeverPer invitation
DefaultOff
emailVerifiedtruefalsetrue

An invitee is verified on acceptance because receiving the token is proof of control over the address. A self-service signup has proved nothing, which is why it starts unverified.

On this page