All projects

Soundmind

Unified Login Server

Multiple services share a single account. Parent phones and child devices live under opposite network conditions, so the session policy itself had to be split in two.

Role
Backend · Operations dashboard
Period
2025.07 ~ Present
Stack
Spring BootJava 17MariaDBRedisNext.js
App / clientNativeServerWorkerStorageExternal
Products share one account. Guardians and child devices get different token policies, and auth events retry, then park for manual recovery.Hover a block to highlight its flows. Drag to pan.

Outcome

  • Instant revocation

    All account tokens revoked when token reuse is detected

  • 2 types

    Token policies separated by user type

01The problem

Parents connect frequently from regular smartphones, so the standard approach of continuously refreshing short-lived tokens fits them. Child devices, however, keep their network mostly closed under management policy, so refresh requests themselves fail often. Using the same policy logs child devices out constantly, but handing everyone long-lived tokens amplifies the damage if one is stolen.

02What I decided

I split the policy by user type. Parents use short-lived tokens with rotating refresh tokens; if a refresh token is reused, I treat it as theft, immediately invalidate the account's entire token lineage, and record the event. Child devices keep a long-lived single session with no refresh, with a cap on the number of sessions. Cross-service auth events pass through retries and a holding store so an admin can recover them.

  • Parents: short-lived tokens + rotating refresh, full lineage revocation and audit logging on reuse detection
  • Child devices: long-lived single session, capped concurrent session count
  • Cross-service notifications retry with exponential backoff, then archive failures for manual reprocessing
  • Built scripts that build and deploy only the changed services, plus a monitoring stack

03From one login to an account platform

It began as a login server, but as in-house services joined one by one the requirements grew into an account platform. Patching code for every new service would not hold, so joining, linking, propagation, and cleanup each became structure.

one account, many products

Products join by registration, events fan out as webhooks, and failures are parked, never lost.

  1. Joining is registration

    A new product joins by registration, not by a code change: a client identifier, API key, allowed IPs, and redirect URLs go into the database, and the CORS allowlist is generated from that same registration. Registration is the policy

  2. The family link ledger

    Guardian-child link requests, approvals, and removals live in one ledger inside SSO. A link made in any product is the same relationship everywhere, with caps on both sides

  3. Propagating auth events

    Links and withdrawals fan out to each service as webhooks, carrying idempotency keys, retried three times with exponential backoff, and parked for manual replay if all retries fail. Events never vanish quietly

  4. Account lifecycle

    Dormancy, deletion notices and deletion, and identity-verification retention windows all run on scheduled batches. Personal-data cleanup hangs on a schedule, not on an operator's memory

  5. Operations dashboard

    Member lookup, forced session termination, webhook replay, and audit logs live in a dedicated dashboard. Incident response ends on a screen, not in a database console

04Outcome

Child devices no longer get logged out by network conditions, and token theft scenarios are blocked at the account level the moment reuse is detected. The operations dashboard handles everything from session listing to forced termination.