newOS for Developers

Skills/Account/perpetual-session

Perpetual Session

Never-expire auth with automatic token refresh

advanced1 endpointauth

When to Use

  • • Keeping users logged in indefinitely
  • • Progressive web apps that need offline-first auth
  • • Background services that make authenticated requests
  • • Avoiding session expiry during long user sessions

Token Lifecycle

7 days
JWT validity period
6 days
Recommended refresh point
1 hour
Check interval

Prerequisites

  • • Existing valid Newgraph JWT (from initial signIn())
  • • Token stored in localStorage under newsafe-auth-token

API Endpoint

EndpointMethodAuthDescription
/auth/generateSessionTokenPOSTExisting JWTGenerate a fresh JWT

Implementation Pattern

Token Refresh Service

// perpetualSession.ts
import { newgraphClient, stage } from "newgraph-signals";
import { ContentType } from "@newstackdev/iosdk-newgraph-client-js";

const JWT_LOCALSTORAGE = "newsafe-auth-token";
const REFRESH_INTERVAL = 60 * 60 * 1000; // 1 hour
const REFRESH_THRESHOLD = 6 * 24 * 60 * 60 * 1000; // 6 days

// Decode JWT to get expiry (without verification)
function getTokenExpiry(jwt: string): number | null {
  try {
    const payload = JSON.parse(atob(jwt.split(".")[1]));
    return payload.exp * 1000; // Convert to ms
  } catch {
    return null;
  }
}

// Check if token needs refresh
function shouldRefresh(jwt: string): boolean {
  const expiry = getTokenExpiry(jwt);
  if (!expiry) return false;
  
  const timeUntilExpiry = expiry - Date.now();
  return timeUntilExpiry < REFRESH_THRESHOLD;
}

// Refresh the token
async function refreshToken(): Promise<string | null> {
  const storedJwt = localStorage.getItem(JWT_LOCALSTORAGE);
  if (!storedJwt) return null;

  try {
    const sessionRes = await newgraphClient.api.request({
      baseUrl: `https://api${stage === "eu-prod" ? "" : `-${stage.split(/-/)[1]}`}.newgra.ph/v1/auth`,
      path: "/generateSessionToken",
      method: "POST",
      body: {
        referer: "newgra.ph",
        appOwner: "newcoinos",
        redirectUrl: window.location.origin + "/start",
        scopes: [],
      },
      secure: true,
      type: ContentType.Json,
      format: "json",
    });

    const newJwt = sessionRes.data.jwt;
    
    // Store and apply new token
    localStorage.setItem(JWT_LOCALSTORAGE, newJwt);
    newgraphClient.updateToken(`newsafe ${newJwt}`);
    
    console.log("[perpetual-session] Token refreshed");
    return newJwt;
  } catch (error) {
    console.error("[perpetual-session] Refresh failed:", error);
    return null;
  }
}

Start the Refresh Loop

// Initialize perpetual session on app load
export function initPerpetualSession() {
  const storedJwt = localStorage.getItem(JWT_LOCALSTORAGE);
  if (!storedJwt) return;

  // Apply stored token
  newgraphClient.updateToken(`newsafe ${storedJwt}`);

  // Check immediately if refresh needed
  if (shouldRefresh(storedJwt)) {
    refreshToken();
  }

  // Set up periodic check
  setInterval(() => {
    const jwt = localStorage.getItem(JWT_LOCALSTORAGE);
    if (jwt && shouldRefresh(jwt)) {
      refreshToken();
    }
  }, REFRESH_INTERVAL);

  console.log("[perpetual-session] Initialized");
}

// Call on app startup
initPerpetualSession();

Best Practices

✓ Do

  • • Refresh before expiry (6-day threshold)
  • • Use background refresh intervals
  • • Handle 401 responses gracefully
  • • Log refresh events for debugging

✗ Don't

  • • Wait until token expires
  • • Refresh on every API call
  • • Store tokens in sessionStorage
  • • Ignore refresh failures

Edge Cases

Token Already Expired

If the token has already expired, /generateSessionToken will return 401. Redirect the user to re-authenticate via phone/OAuth.

Multiple Tabs

Each tab runs its own refresh loop. Use localStorage events to sync tokens across tabs.

Network Offline

If refresh fails due to network, retry on next interval. The 6-day threshold provides buffer for temporary outages.

🧪 Quick Test

Sign in to Test

Use the sidebar to sign in with your phone number