Cover: one account, two sets of sessions

A product manager asks for something that sounds simple: when users bookmark an article in the app, they should see it on the website too.

Many teams first think of sharing the app's token with the browser through a QR code or a redirect. That can appear to work, but it puts the same key into two environments with very different security properties. Using AppKernia's implementation—uni-app x on mobile, a Next.js website and a Go backend—this article explains why user data is shared while session credentials stay separate. The code paths and APIs can be checked in the public repository.

1. The same user does not mean the same session

An account represents a person. A session represents that person's login on a particular device or browser. An account can have several sessions, each with its own identifier and independent revocation.

AppKernia's website uses the app's existing /api/v1/me endpoint rather than creating a separate web user table. Mobile and browser clients therefore see the same profile and bookmarks, but each signs in and holds its own session.

This has three benefits:

  1. Revoking other devices from the phone can revoke the browser session without revoking the phone's own session.
  2. A compromised browser cookie affects that session rather than also exposing the phone's session.
  3. Session identifiers help identify which client produced a request.

Cross-device sign-in, such as scanning a QR code on a phone to sign into a website, needs a separate authorization flow with validated redirects and callbacks and a one-time authorization ticket. Never put the phone's token in a URL. URLs can enter browser history, server access logs and third-party redirect chains.

Administration is another identity boundary. The admin client calls /admin-api/v1; the app and website call /api/v1. Their token audiences are isolated. Signing into the public website does not grant permission to publish through the admin backend.

2. Where credentials belong

Token storage: the browser and app follow separate paths

Diagram of storage and authentication boundaries, not a deployment topology.

Browser scripts never receive the tokens.

The website uses a same-origin backend for frontend (BFF). Pages call the website's /api/session endpoints, and Next.js calls the app API on the server. Access tokens, refresh tokens and device identifiers are stored in HttpOnly cookies with three restrictions:

  • Their path is limited to /api/session.
  • Production cookies use Secure and travel over HTTPS.
  • Tokens are absent from client-facing JSON and localStorage.

Writes also validate Origin and the site source, and business endpoints use an explicit allowlist. HttpOnly prevents scripts from reading cookies; it does not prevent cross-site requests or replace endpoint authorization. Each protection serves a different purpose.

In the app, short-lived credentials stay in memory and long-lived credentials use system secure storage.

An access token disappears when the app process is killed. Refresh tokens use Keychain on iOS, Keystore on Android and Asset Store on HarmonyOS. Non-sensitive language and theme preferences may use ordinary local storage, kept separate from credentials.

One practical constraint is HarmonyOS Asset Store's 900-byte UTF-8 limit per value: an overly long refresh token will not fit. The secure-storage module has no web implementation and does not fall back to ordinary storage. Falling back to plaintext would remove the protection.

3. Recover identity before deciding that a reload logged the user out

Reloading a browser clears page memory while HttpOnly cookies remain. A common mistake is:

// Incorrect: checking only the initial in-memory value
const user = useStore((s) => s.user) // null after a reload
if (!user) redirect('/login')       // mistaken sign-out decision

The user is still signed in, yet the page redirects to login. Instead, the account component enters a loading state, reads the session profile, then chooses between the account page and the sign-in entry.

When an access token is missing or expired, AppKernia's website tries refresh-token rotation on the server and reads /me again. Two failure classes need different handling:

SituationCorrect handling
The session is invalid because its refresh token was revoked or expiredClear cookies and return to the signed-out state
The backend times out, is rate-limited or is temporarily unavailableKeep the session and return a recoverable error

Treating a temporary outage as an invalid session forces users to sign in again after a brief network interruption.

4. Several browser tabs can accidentally reuse a refresh token

Refresh tokens usually rotate: consuming one invalidates it and returns a new token. If an access token expires while three tabs are open, all three may refresh at once.

Race between concurrent refresh requests in several tabs

Diagram of the race and the two coordination layers.

The first request consumes the old token. The next requests present a token that has already been used.

AppKernia stores refresh tokens only as SHA-256 hashes. On detecting reuse, it revokes the entire session family and records a high-severity iam.refresh_token.reuse security event. That is appropriate because reuse can indicate theft. Without client coordination, however, merely opening several tabs can trigger the same response and sign every tab out.

The website coordinates refreshes at two levels:

  1. Browser: on browsers supporting the Web Locks API, a lock serializes session requests across tabs.
  2. Server: concurrent requests in one web process share the result of a single rotation rather than consuming the old token repeatedly.

The deployment limit must be explicit: in-process coordination does not cover multiple web instances. Horizontal scaling requires shared session coordination, for example a Redis-based distributed lock or single-flight mechanism, followed by fresh tests of multiple tabs, cookie updates and concurrent rotation. Those are suggested approaches, not features already implemented in AppKernia. Passing a single-instance test is not distributed-system acceptance.

5. Can a failed write be replayed after refreshing credentials?

Suppose a bookmark request was sent but its response was lost through a gateway timeout or connection failure. The client cannot know whether the database committed the change.

AppKernia's rule is: do not replay writes automatically without an idempotency guarantee. The session proxy completes identity checks, then sends the business write once.

Bookmarking itself is idempotent: a unique constraint on user and article, together with conflict-ignore behavior, keeps a single record. A user can safely click again manually. Payments, orders and messages can duplicate charges or sends if retried without an idempotency key. Retry behavior needs to be designed per endpoint rather than enabled globally in the network layer.

6. A practical acceptance checklist

#ScenarioExpected result
1Sign into the same test account on two clientsThe profile matches; session identifiers differ
2Change the language on one clientRead the saved server preference; report save failures
3Revoke one sessionThat client cannot recover; the other remains valid under the session rules
4Reload with only a refresh token remainingRecover sign-in automatically without redirecting to login
5Reload during a temporary backend outageOffer a retry without clearing the session
6Trigger refresh in three tabs at onceAll stay signed in without a token-reuse error
7Call a protected endpoint after logoutReturn 401
8Forge another app's request contextThe server rejects it
9Bookmark the same article twice as the same userKeep one record

A screenshot proves a page state. Token rotation and authorization require evidence from real requests and server behavior.

Closing thoughts

An account is a business concept; a session is a security concept. Separating them requires a BFF and session management. Combining them can expose its cost during a token leak or simultaneous browser refreshes.

The implementations are in AppKernia's public repository under the MIT license, which permits commercial use:

  • Source: github.com/Payhon/AppKernia
  • Documentation: docs.appkernia.com

For a product with an app, admin backend and website that also needs ready-made commerce, payments, membership points, AI support and a Next.js website, see the Official edition comparison on the AppKernia homepage.