WakaStart
Authentication

User context — Cache & Invalidation

When to call /me, React caching strategy, invalidation after token refresh and best practices.

Version v1.04 min read

User context — Cache & Invalidation

This chapter covers the caching strategy for the /me payload and invalidation patterns. Read /me payload structure first.


When to call /me

SituationCall /me?
At login (after enrichment)Yes — store in the context
After a token refreshYes — rights may have changed
On every navigationNo — use the cached data
After a profile change (if an admin modified rights)Yes — invalidate the cache and re-fetch
After a partial logout or organization changeYes

typescript
// lib/auth-context.tsx import { createContext, useContext, useState, useCallback } from "react"; interface MeUser { id: string; wid: string; email: string; firstName: string; lastName: string; adminLevel: string; appRights: string[]; customer: { id: string; wid: string; name: string }; // ... other fields } interface AuthContextValue { user: MeUser | null; loading: boolean; refresh: () => Promise<void>; } const AuthContext = createContext<AuthContextValue>({ user: null, loading: false, refresh: async () => {}, }); export function AuthProvider({ children }: { children: React.ReactNode }) { const [user, setUser] = useState<MeUser | null>(null); const [loading, setLoading] = useState(false); const refresh = useCallback(async () => { setLoading(true); try { const res = await fetch("/api/proxy/me"); // via your server proxy if (res.ok) setUser(await res.json()); } finally { setLoading(false); } }, []); return ( <AuthContext.Provider value={{ user, loading, refresh }}> {children} </AuthContext.Provider> ); } export const useAuth = () => useContext(AuthContext);

/me is called once at login. The response remains stable as long as:

  • The user's rights haven't been modified by an admin.
  • The token hasn't been refreshed.

Recommended strategy:

  1. Store it in a React context (in memory, not in localStorage).
  2. Invalidate the cache after every token refresh (rights may have changed).
  3. Expose a useAuth().refresh() hook to force a reload if the admin signals a change.

Synchronization after token refresh

typescript
// app/api/auth/refresh/route.ts export async function POST(req: NextRequest) { const cookieStore = await cookies(); const refreshToken = cookieStore.get("refresh_token")?.value; if (!refreshToken) { return NextResponse.json({ error: "No refresh token" }, { status: 401 }); } // 1. Refresh the Keycloak tokens const refreshResponse = await fetch(`${BFF_URL}/api/auth/token/refresh`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ refreshToken }), }); if (!refreshResponse.ok) { // Delete cookies and force re-login cookieStore.delete("keycloak_token"); cookieStore.delete("wakastart_token"); cookieStore.delete("refresh_token"); return NextResponse.json({ error: "Refresh failed" }, { status: 401 }); } const { access_token, refresh_token: newRefreshToken } = await refreshResponse.json(); // 2. Re-enrich the new token const enrichResponse = await fetch(`${BFF_URL}/api/auth/enrich`, { method: "POST", headers: { Authorization: `Bearer ${access_token}` }, }); const { token: wakaToken } = await enrichResponse.json(); // 3. Update cookies cookieStore.set("keycloak_token", access_token, { httpOnly: true, sameSite: "lax" }); cookieStore.set("wakastart_token", wakaToken, { httpOnly: true, sameSite: "lax" }); cookieStore.set("refresh_token", newRefreshToken, { httpOnly: true, sameSite: "lax" }); // 4. Signal the client to re-fetch /me return NextResponse.json({ refreshed: true }); }

On the client side, after a successful refresh:

typescript
const { refresh } = useAuth(); // After every successful token refresh await refresh(); // re-fetch /me with the new rights

Best practices

  • Do not store the /me payload in localStorage — it contains sensitive rights.
  • Use appRights only for conditional display on the front end. The backend is the source of authority.
  • If adminLevel === "None" and appRights.length === 0, the user has no rights — show a dedicated page.
  • Check isActive on load: if false, redirect to an "Account disabled" page.
  • Check feature activation via GET /api/config/app-features/check/{customerWid}/{featureCode} to hide entire sections (e.g. if the invitations feature is disabled, hide the whole module) — this is not a precomputed field of /me.

Common pitfalls

  • Stale rights: rights are modified by an admin but the /me cache isn't invalidated. Add a lightweight polling mechanism or a WebSocket notification.
  • Empty appRights but active user: the user is active but has no profile assigned. Contact the Customer's admin.
  • Misinterpreted adminLevel: the level is a string, not an integer — compare by exact value or by inclusion in an ordered array.

Go further