Context
The v1 product is a public data API, and the auth model was pivoted to self-service with no payments. The model shipped without a record, so open registration and the missing billing coupling could be mistaken for accidents.
Decision
- Anonymous reads. Read routes work without a key under the anonymous rate-limit class, counted per client IP; an authenticated caller is counted in its owner account's bucket (
middleware.RateLimitBySubject). - Open registration.
POST /v1/registermints an account and a first API key with no prior authentication (internal/api/v1/register.go). It is create-only, requiresContent-Type: application/json(internal/api/v1/csrf.go, which forces a CORS preflight on cross-site browser POSTs; the Origin-basedRequireSameSiteWriteis deliberately not mounted because API callers send no Origin), and passes the shared signup IP throttle (internal/auth/signup_ip_throttle.go). - Throttle failure mode. The IP throttle fails open only for the first
DefaultSignupThrottleDwellTime(30 s) of continuous Redis errors; after thatCheckIPreturnsErrThrottleUnavailableand signup returns 503. - Metering is by quota, not charge. Accounts carry a free-tier
RegisterLimitsblock, enforced as API-keyMonthlyQuotaon both the Postgres and Redis validators. - Orphan cleanup. A registration whose validator mirror write failed is suspended with a
signup-race:reason and reclaimed byinternal/signupreaper. Expiredmagic_link_tokensare deleted 48 h past expiry byinternal/magiclinkreaper. - Dashboard credentials are WebAuthn passkeys plus passwordless magic-link login (
internal/api/v1/dashboardauth/,internal/platform/webauthncredential.go). ThePasskeyCeremonyGuardinterface and its in-process guard live indashboardauth/passkey_ceremony_guard.go;internal/auth/passkey_ceremony_guard.gois its Redis adapter. - No payment or billing surface in v1. A paid tier is a new ADR that supersedes this one.
Invariant
POST /v1/registerrejects a non-JSON content type:TestRegister_RequiresContentTypeininternal/api/v1/register_test.go.- Signup fails closed with 503 once the throttle's Redis dwell is exceeded:
TestRegister_ThrottleUnavailableFailsClosedAndCountsininternal/api/v1/register_test.go. - A registration whose mirror write fails leaves an orphan the reaper can reclaim, never a 200:
TestRegister_MirrorFailureSuspendsOrphanForReaperininternal/api/v1/register_test.go. - The signup reaper is bound to the Postgres account store, not the dashboard bundle:
TestRun_SignupReaperBindsToThePostgresAccountStoreNotTheDashboardBundleincmd/stellarindex-api/signup_reaper_wiring_test.go.
Consequences
- Onboarding has no friction, no PCI scope and no password store.
- Open key minting is an abuse surface, bounded by the content-type gate, the IP throttle and free-tier quota; response is rate limits and key revocation, not chargebacks.
- Accounts, keys, sessions and passkeys in Postgres cannot be re-derived from the chain, so their durability rests on the Postgres backup of ADR-0043.
- Invite-gated registration, a dormant payment integration and passwords were rejected for v1; revisit gating if abuse exceeds the rate-limit envelope.
Evidence
internal/api/v1/register.go, internal/api/v1/csrf.go, internal/auth/signup_ip_throttle.go, internal/signupreaper, internal/magiclinkreaper, wiring in cmd/stellarindex-api/main.go.