Skip to content

Debugging "I can't log in"

The iOS app now shows why sign-in failed (see the session gate's error screen and, in DEBUG builds, a monospaced detail line with the status/URLError code and the API base URL). Use that message first. It usually points straight at one of the items below.

Checklist

  1. Is the backend actually running, on 127.0.0.1:8080, and restarted since you last pulled?
  2. just run backend (or just run for backend + mobile together).
  3. A stale backend process from before a pull won't have the new code. Restart it after every pull that touches apps/backend.
  4. Sanity check: curl -s http://127.0.0.1:8080/health should return 200.

  5. Does .env.keys decrypt .env.local?

  6. .env.local is committed encrypted (dotenvx); .env.keys is the gitignored private key file that decrypts it. Without it, Privy credentials and DATABASE_URL silently resolve to nothing.
  7. Quick check: dotenvx get PRIVY_APP_ID -f .env.local should print a value, not an error.

  8. Did scripts/ensure-ios-privy-config.sh regenerate the iOS Privy config after you pulled?

  9. It writes apps/mobile/Config/Privy.local.xcconfig, Privy.local.Info.plist and Privy.local.Debug.Info.plist (the one Debug builds use) from .env.local. All are gitignored, so a fresh pull (or a change to PRIVY_APP_ID/PRIVY_APP_CLIENT_ID in .env.local) leaves them stale until you re-run it:
    dotenvx run -q -f .env.local -- bash scripts/ensure-ios-privy-config.sh
    
  10. just run mobile runs this for you before launching the simulator. If you build straight from Xcode instead, run it manually first.

  11. Do the app's Privy credentials match the backend's?

  12. PRIVY_APP_ID / PRIVY_APP_CLIENT_ID in .env.local (what the app builds with) must point at the same Privy app as the backend's PRIVY_APP_ID / PRIVY_APP_SECRET / verification key.
  13. A mismatch here is the classic "can't log in" cause: the app authenticates against Privy fine, but POST /v1/auth/session then 401s because the backend can't verify a token minted for a different Privy app. This now surfaces on the login screen as "We couldn't verify your sign-in. Try again." instead of silently bouncing you back. If you see that message, start here.

  14. Where the logs are.

  15. Backend: the api log, .logs/<timestamp>/backend.log today (one directory per just run / just run backend invocation; use the most recent).
  16. Mobile: .logs/<timestamp>/mobile.log, plus the app's own os.Logger output (category session) in Console.app or log stream --predicate 'subsystem == "<bundle id>"'. See Read iOS app logs.
  17. Useful greps against the api log:

    • grep 'POST /v1/auth/session' confirms the app is even reaching the backend, and shows the response status.
    • grep 'invalid_token' shows the backend rejected the access token (expired, wrong Privy app, or clock skew).
  18. Xcode confused after a launch-screen or storyboard change?

  19. just reset mobile does an xcodebuild clean targeting the resolved simulator. Reach for it if the build looks stale or a UI change (e.g. the launch screen) doesn't take effect.

Reading the on-screen error

The session gate maps failures onto three user-facing messages (see SessionErrorMapping in apps/mobile/Monaco/Features/Shell/SessionErrorMapping.swift):

Symptom Message shown Likely cause
Can't connect at all (URLError) "Can't reach Monaco. Is the server running?" Backend not running / wrong host / wrong port. See checklist item 1.
Backend returned 5xx "Monaco's server hit a problem. Try again in a moment." Backend crash, unhandled panic, or a downstream dependency (DB, RPC) is down. Check the api log.
Backend returned 401 opening the session (shown on the login screen, after signing out) "We couldn't verify your sign-in. Try again." Privy app-id / verification-key mismatch between the app build and the backend env. See checklist items 3 and 4.

In DEBUG builds only, a small monospaced line under the message on the session gate adds the exact status code / URLError code and the API base URL the app is hitting, so you don't have to guess.

Staying signed in

Privy access tokens last about an hour. The app never signs a user out just because one expired:

  • At launch a returning user sees a splash (sessionRestoringView) while Privy restores the saved session, not the login form. If Privy can't be reached, they get "Can't sign you in yet" with Try again (it also retries when the app comes back to the foreground). They stay signed in.
  • While the app is open every request goes through MonacoHTTPTransport (packages/mobile-core/Sources/MonacoCore/Networking). On a 401 it asks PrivyAuthService.refreshedAccessToken(replacing:) for a fresh token and retries the request once. Concurrent 401s share one refresh.
  • Only a real rejection signs out: Privy reports no session, or the backend still answers 401 with a freshly minted token. The login screen then says "Your session expired. Sign in again." (or the verification message above for POST /v1/auth/session). A refresh that fails because the device is offline surfaces as a connection error instead.

If a user reports being bounced to login, check the session log category for Session restore: / getAccessToken failed lines, and the api log for invalid_token right before it.

One-time code errors

What happened What the user sees Code field
Wrong or expired code "That code didn't work. Check it, or send a new one." stays, retry or Send a new code
Offline "No connection. Check your internet and try again." stays
Privy rate limit (429) "Too many attempts. Wait a minute, then try again." back to Send code
Anything else "Couldn't send the code…" / "Couldn't sign you in…" plus Privy's own detail back to Send code

The mapping lives in PrivyAuthService.loginFailure(from:step:) and LoginFailureCopy (MonacoCore). The raw Privy error is logged under the session category.