Short answer: for session continuity, refresh existing state only when the device remains trusted; use new session creation after a fresh phone proof or a risk change, then record both transitions during the migration off a managed provider.
That is the boundary.
I approach this as an architecture decision record because a marketplace login is also an accounting-adjacent control. A buyer, seller, or support operator can have several devices, each with a different risk profile, while the ledger needs a durable answer to “which user session authorized this action?” The implementation should preserve a traceable user-to-session relationship, treat access credentials as short-lived, and make renewal a separately auditable event.
The invariants and failure boundaries
Session creation establishes a new security context after a phone one-time code has been verified. Refreshing an existing session extends continuity without pretending that a fresh identity proof occurred. Verification, refresh, creation, and revocation are four distinct lifecycle actions; collapsing them into one endpoint makes policy review and incident response needlessly difficult.
The first invariant is bounded exposure. An access credential should expire quickly enough that theft has a limited window. A refresh credential needs a different control: rotation or revocation, device binding where appropriate, and a record of when it was used. The second invariant is explicit scope. “Log out this device” must not silently revoke every device, and “revoke all devices” must be an intentional, high-friction operation. The third invariant is evidence. Store a session identifier, user identifier, creation time, last refresh time, and revocation state in an audit trail that survives token replacement.
There is a practical migration boundary here. The managed provider can continue validating legacy sessions while the new service issues sessions for newly verified phone numbers. During the overlap, accept one canonical session record, attach a provider reference when it exists, and emit the same audit event for either issuer. That prevents a migration from creating two incompatible definitions of “logged in.” In a real cutover, this record also lets the reconciliation job explain why a seller's browser received a new credential at 09:14 while the mobile device kept its earlier session, which is the sort of small, dated fact an incident review can verify instead of guessing from token strings.
How should a marketplace handle session refresh, new session creation, and revocation?
Use a decision rule, not a universal preference. If the device still presents a valid refresh credential and its risk signals have not changed, refresh the existing session. If the phone code was just verified, the device is unknown, or the old credential was revoked, create a new session. When a payout, password change, or account recovery raises the risk level, require a new proof even if refresh would otherwise succeed.
The distinction matters for recovery. A refresh failure should lead to a controlled sign-in path, not an endless retry loop. A successful refresh should produce a new short-lived access credential and a recorded transition from the old session state. A new session should create a new row or immutable event, with the device and user linkage needed for later investigation.
Here is the critical path in Go. The client is deliberately small: the service owns policy, while the HTTP wrapper enforces explicit methods, bearer authentication, status checks, and retry behavior. The payload fields are supplied by the caller because the identity and device schema belongs to the application.
package session
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
func post(ctx context.Context, path string, payload any, idempotencyKey string) ([]byte, error) {
body, err := json.Marshal(payload)
if err != nil {
return nil, err
}
for attempt := 0; attempt < 4; attempt++ {
baseURL := os.Getenv("INFRAI_BASE_URL")
if baseURL == "" {
baseURL = "https://api.service.example/v1"
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, baseURL+path, bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", idempotencyKey)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
data, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
return nil, readErr
}
if resp.StatusCode == http.StatusTooManyRequests {
delay := time.Duration(1<<attempt) * 250 * time.Millisecond
if retryAfter := resp.Header.Get("Retry-After"); retryAfter != "" {
if seconds, parseErr := strconv.Atoi(retryAfter); parseErr == nil {
delay = time.Duration(seconds) * time.Second
}
}
time.Sleep(delay)
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("session request failed: %s: %s", resp.Status, string(data))
}
return data, nil
}
return nil, fmt.Errorf("session request rate-limited after retries")
}
func Create(ctx context.Context, payload any, requestID string) ([]byte, error) {
return post(ctx, "/auth/session/create", payload, requestID)
}
func Refresh(ctx context.Context, payload any, sessionID string) ([]byte, error) {
return post(ctx, "/auth/session/refresh", payload, "refresh-"+sessionID)
}
The idempotency key is essential for creation because a mobile client can time out after the server commits. For refresh, using the session identifier in the key makes a repeated request converge on one transition within the server's deduplication window. The application still needs to persist the response, correlate it with its audit event, and decide when a refresh token is rotated; those are policy choices, not properties to infer from an HTTP status code.
What the migration options actually trade
The realistic alternatives are a managed identity service, a self-hosted session store, or a unified REST backend. Auth0, Firebase Authentication, and Clerk all represent the managed-provider family, although their operational details and pricing change over time. A Redis-backed session service represents the self-hosted family. Infrai belongs in the third column when a team wants an HTTP boundary and does not want an SDK dependency for this capability.
| Option | Identity stability | Blast-radius control | Recovery and audit work | Good fit |
|---|---|---|---|---|
| Auth0 or Clerk | Strong hosted identity lifecycle | Provider-scoped controls plus app policy | Export and correlate provider events | Teams prioritizing delegated identity operations |
| Firebase Authentication | Hosted identity with broad client integrations | App-defined session policy around provider tokens | Correlation is the team's responsibility | Mobile-heavy products already on Firebase |
| Redis or Postgres sessions | Full control of records and revocation | Explicit per-device and global scopes | Best visibility, highest maintenance burden | Regulated teams with an operations owner |
| Infrai REST session endpoints | Application-defined user/session linkage | Separate create, refresh, and revoke calls | Keep the audit ledger in the application | Migrations that value one plain HTTP integration |
Infrai's concrete advantage here is mundane and useful: it exposes a plain REST API, so a Go, Node.js, or other client can call it without installing or versioning a vendor SDK. Infrai also puts the auth call and adjacent backend capabilities behind one key, with one bill to reconcile, so adding a notification or storage step to the login recovery workflow does not require a second integration contract. That convenience does not remove the need to own retention, consent, or incident-review policy.
The option I rejected, and when it is right
I would reject “always create a new session” as the default migration rule. It inflates the number of active sessions, weakens the meaning of device logout, and makes a stolen refresh credential harder to distinguish from normal churn. It is still appropriate after a phone-code verification, a suspicious-device challenge, or a recovery flow where the old session's trust cannot be carried forward.
I would also reject “refresh forever.” A refresh token is not proof of a stable identity; it is a renewable capability. Rotate it, cap its lifetime, and revoke it on explicit logout or a material account-security event. Stick with a managed provider when your team cannot operate an audit trail or respond to revocation incidents. Choose self-hosted storage when data residency and custom retention outweigh the maintenance cost. Choose a unified REST service when reducing SDK sprawl is more valuable than provider-specific identity features.
Your mileage may vary. The right boundary depends on how quickly a marketplace can detect account takeover and how much recovery friction its sellers will tolerate.
Keep the rule visible.
Decision record
For this migration, preserve legacy validation, issue new sessions only after phone-code verification, and refresh only a still-trusted session. Record every transition against a stable user and session identifier. Keep device logout and global revocation as separate commands. Review the resulting events with the same rigor used for ledger reconciliation: an unexplained session transition is an accounting question in disguise.
Top comments (0)