Authentication & Identity
How people sign in -no passwords, just a one-time code sent to a phone or email.
Last updated Thu Jul 16 2026 00:00:00 GMT+0000 (Coordinated Universal Time)
TL;DR -Social Live has no passwords. You sign in with a one-time code (an OTP) texted or emailed to you. The same flow handles brand-new signups and returning logins, and it's carefully built so it never leaks whether an account exists, enforces an 18+ age gate, and lets people delete their account.
Who this is for -Product, business & executive: read the top through "Intent semantics" to understand the login experience, the age gate, and the Terms/deletion requirements Apple demands. Backend, security & QA: the whole page -hashing, the verify diagram, the intent table, email-change, and the exact account-deletion scrub.
Signing in to Social Live works like most modern apps that skip passwords entirely: you type your phone number or email, we send you a short code, you type it back, and you're in. That short code is called an OTP -a one-time password that's only valid for a few minutes and only once.
There are no passwords to steal, forget, or reuse. The same two-step "request a code, then verify it" flow serves both people creating a brand-new account and people logging back in -the app just tells the server which one you meant.
Social Live is passwordless: every login and signup is a one-time code (OTP) sent to a phone number (SMS via Twilio) or email (via Resend).
In plain terms
A few things worth knowing before the technical detail:
- One code, two purposes. The system distinguishes "I'm new here" (signup) from "I'm coming back" (login). Trying to sign up with an email that already exists gives a polite "you already have an account" message; trying to log in to something that doesn't exist gives the exact same error as a wrong code -on purpose, so no one can fish for which phone numbers or emails have accounts.
- Codes are protected. The code we store is scrambled (hashed), never kept in plain form. It expires in five minutes, and you get five guesses before it's dead.
- You must be 18+. Signup checks a birthdate.
- You agree to the Terms. Because the app hosts user content, signing in requires agreeing to Terms with a zero-tolerance rule for abuse. If the Terms are updated, everyone is asked to re-accept before continuing.
- You can delete your account. A user can permanently delete their own account from the app; their personal information is scrubbed, though anonymized financial records are kept for accounting.
The two-step flow
Here is the full request-a-code-then-verify-it exchange between the app and the server:
Rendering diagram…
Request rules
The rules the server enforces on every code request:
- Exactly one of
phoneNumberoremailper request. - Phone numbers are normalized to
+digits; emails lowercased. - 30-second cooldown per identifier (in-memory) against spam.
- Codes are stored as
sha256(identifier:code:OTP_SECRET)-never plaintext. Production fails closed ifOTP_SECRETis unset.
Intent semantics
This table is the heart of the "one code, two purposes" idea -it spells out exactly what happens for each combination of intent and whether the identifier already belongs to an account.
| Intent | Identifier exists? | Result |
|---|---|---|
signup | No account | Creates user (+profile +wallet), registers device, returns session |
signup | Any account holds it | 409 Conflict -verified: "already exists, log in"; unverified attachment: "already linked to another account" |
login | Verified account | Session for that account |
login | No account / unverified email | 401 Invalid or expired OTP code -deliberately identical to a wrong code, so login never reveals whether an identifier exists |
The unverified-email case matters: a phone-signup account can have typed an
email without ever verifying it. That email is not a login route into
the account, and email-signup against it returns a clean 409 (the email
column is unique).
Signup extras
Two extra things happen only on signup:
- Age gate: signup validates an 18+ birthdate.
- Username auto-generated from the email/phone if not supplied, sanitized
to
[a-z0-9._], 3–28 chars.
Email change (authenticated)
Adding or changing your email after you already have an account is its own small flow, so a typo can't lock you out. Adding/changing an email on an existing account is a separate flow:
requestEmailChange stores the address in pendingEmail and sends a code;
confirmEmailChange verifies it and promotes it to email +
emailVerifiedAt. The account's email only changes after the code is
confirmed.
Terms acceptance (App Store 1.2)
Apple requires apps with user-generated content to make users agree to terms that forbid abuse. Here's how that's wired in. Account creation and sign-in require agreeing to the Terms of Use, which carry
an explicit zero-tolerance clause for objectionable content and abusive users.
The registration screen gates the "Send code" button on a required Terms
checkbox (plus the age confirmation); the login screen shows the agreement with
a link to the full terms before continuing. On any registration path the
backend stamps User.acceptedTermsAt.
Re-acceptance for existing users: the terms have a version date —
TERMS_UPDATED_AT env var, falling back to the constant in
backend/src/common/terms.ts. GET /users/me (self only) returns
termsAcceptanceRequired: true when the user's acceptedTermsAt is null or
predates it; the app then routes to a blocking full-screen accept flow
(/accept-terms) whose only exits are POST /users/me/accept-terms
(re-stamps acceptance) or logging out. To force every user to re-accept
after changing the terms text, bump TERMS_UPDATED_AT (or the code
default) and restart the backend.
Account deletion (App Store 5.1.1v)
Apple also requires that any app allowing account creation let users delete their account from inside the app. This is that feature -and note it's a scrub-and-anonymize, not a wipe, so the books still balance. DELETE /users/me (JWT-guarded) lets a user permanently delete their own
account from Profile → Settings → Delete account. The client gates it
behind two confirmations: the destructive confirm dialog, then a typed
gate -the user must type "delete" (case-insensitive) before the final
button activates. It is a soft-delete + PII scrub, not a hard delete, so anonymized
financial ledger rows survive for accounting integrity (money-code rule):
- Sets
User.deletedAt,isBanned = true, and clearsphoneNumber,email,pendingEmail,emailVerifiedAt,firstName,lastName,country,birthdate;username→deleted_<id>,displayName→ "Deleted account". - Deletes all
RefreshSession(revokes every session),SocialIdentity,DeviceToken, andDevicerows; scrubs theProfileavatar/banner/bio. - Keeps
Wallet/LedgerTransaction/Withdrawalrows (now anonymized).
Freeing the phone/email means the identifiers are reusable and a re-signup creates a fresh account. Existing access tokens are stateless (≤1h TTL) and expire on their own; the client clears its session immediately on deletion.
Anti-abuse properties
The whole design is tuned so an attacker learns nothing and can't brute-force a code:
- Hashed codes, 5-attempt limit per challenge, 5-minute expiry, one-time
consumption (
consumedAt). - Login responses never disclose account existence.
- Challenge rows are created even when delivery is skipped, keeping response shape and timing uniform.
Related pages
- QA Test Allowlists & Dev Overrides -fixed codes for demo/review accounts and their security model.
- Tokens, Sessions & Devices -session lifetime, refresh rotation, and device registration.
- Social Login (Apple & Google) -the other way to obtain a verified identity.