Social LiveDocumentationPLATFORM DOCS

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 phoneNumber or email per 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 if OTP_SECRET is 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.

IntentIdentifier exists?Result
signupNo accountCreates user (+profile +wallet), registers device, returns session
signupAny account holds it409 Conflict -verified: "already exists, log in"; unverified attachment: "already linked to another account"
loginVerified accountSession for that account
loginNo account / unverified email401 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 clears phoneNumber, email, pendingEmail, emailVerifiedAt, firstName, lastName, country, birthdate; usernamedeleted_<id>, displayName → "Deleted account".
  • Deletes all RefreshSession (revokes every session), SocialIdentity, DeviceToken, and Device rows; scrubs the Profile avatar/banner/bio.
  • Keeps Wallet/LedgerTransaction/Withdrawal rows (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.