BASE44DEVS

FIX · AUTH · HIGH

Fix Base44 Infinite Loop on Logout — Stop the Redirect Tug-of-War

The Base44 logout loop is your auth guard and your logout fighting over where the browser goes. base44.auth.logout(redirectUrl) redirects through a server-side logout that clears HTTP-only cookies and the session, then loads your URL. Make it the handler's last line, point it at a route outside the guard, and have the guard stand down while the page leaves.

Last verified
2026-10-17
Category
AUTH
Difficulty
MODERATE
DIY possible
YES

What's happening

A user clicks logout. The dashboard flickers, then the login page, then the dashboard again. The address bar cycles between the same two or three URLs until the tab is closed, and reopening it can show the user still signed in.

The Base44 infinite logout loop is your auth guard and your logout code fighting over where the browser goes. Base44's logout call, base44.auth.logout(redirectUrl), returns nothing and sends the browser through a server-side logout, then to the URL you pass. The loop forms when logout skips that call, lands on a guarded route, or races a guard redirect.

Logout is easy to get wrong because it inverts the login path. It ends a session instead of starting one, lands on a public route instead of a protected one, and asks the guard to stand aside on a page it normally protects. The fix needs no platform support: one logout path, one public landing route, and a guard that knows when to stand down.

What base44.auth.logout() does

Base44 documents base44.auth.logout(redirectUrl?) with a void return and describes it from two sides:

  • The auth module reference: "Removes the authentication token from local storage and Axios headers, then optionally redirects to a URL or reloads the page. Requires a browser environment and can't be used in the backend." Without a URL, it "Reloads the page if not provided."
  • The SDK skill reference: "Logout redirects the user to the server-side logout endpoint (/api/apps/auth/logout) to clear HTTP-only cookies and the session, then redirects to the given URL (or the current page if omitted)."

The SDK's public source shows the order. In packages/sdk/src/modules/auth.ts, read on 17 October 2026, logout() drops the Authorization header, removes the base44_access_token entry from localStorage, sets window.location.href to /api/apps/auth/logout with your URL as from_url, and returns. Three consequences matter for the loop:

  1. Logout is a navigation, not a promise. logout() returns as soon as it has set the new location. Code after it, such as setUser(null) or navigate("/login"), runs while the browser is leaving and starts a competing navigation.
  2. Part of the session is out of your code's reach. MDN's Set-Cookie reference says the HttpOnly attribute "Forbids JavaScript from accessing the cookie, for example, through the Document.cookie property." Only the server-side logout clears those cookies, so no localStorage or document.cookie cleanup can replace it.
  3. No URL means a reload. Called with no argument on a protected page, logout() reloads that page and walks the user straight into your guard.

There is no signOut() in the auth module, in the docs or in the source. A call to base44.auth.signOut() throws a TypeError, and if a try/catch swallows it while the handler goes on to clear storage and navigate, the server-side logout never runs.

Why the loop forms — three root causes

The loop comes from one of three causes, sometimes two at once. The Network log tells them apart (see the next section).

Root cause 1: the logout never reaches Base44's server-side logout

The handler clears localStorage, resets the app's user state and navigates to /login, or it calls a method that does not exist and swallows the error. Base44's HTTP-only cookies and session survive. Your app's state now says signed out while Base44's session says signed in, and any page that checks one and redirects to a page that checks the other bounces the browser between them. The Network log shows no request to /api/apps/auth/logout.

Root cause 2: the logout destination sits behind the auth guard

logout() with no argument reloads the current page, and logout("/login") loads /login. If the landing page sits inside the layout or route group that runs your auth guard, the guard runs on arrival. A guard that sends signed-out users to /login and also wraps /login keeps redirecting to the page it is already on. One that falls back to / sends the user to a protected route, which sends them back to /login.

Root cause 3: the guard redirects while logout is leaving the page

logout() returns as soon as it starts the redirect, and your page keeps running until the browser leaves it. If the handler updates state after the call, or anything else re-renders the guard in that window, the guard finds no token and starts its own redirect to /login. Two navigations now compete for the same tab.

How to reproduce and confirm

Run these in order. Steps 4 and 6 name the cause.

  1. Open the app with DevTools. In the Network tab, turn on Preserve log and Disable cache.
  2. Sign in fully and open a protected page.
  3. Click logout and let the loop run for a few seconds. The log keeps every request.
  4. Filter the log for auth/logout. No request at all means root cause 1. A request followed by alternating URLs means root cause 2 or 3.
  5. Read the URL pattern. The two or three URLs that alternate, for example /dashboard and /login, name the routes in conflict.
  6. Open the logout destination in a private window. Signed out, it should simply load. If it redirects, it sits behind the guard: root cause 2.
  7. Read the logout handler. Anything after base44.auth.logout(...), such as an await chain, a state update or a router call, is root cause 3. Search the codebase for other logout paths too: signOut, localStorage.clear, localStorage.removeItem and navigate("/login").
  8. Read the guard. Note which routes it wraps and what it does when the auth check throws. A guard that treats an error as signed in can keep the loop alive.

Match what you found to a cause and the layer that fixes it:

EvidenceRoot causeFix
No request to /api/apps/auth/logout in the Network log1. The logout never reaches Base44's server-side logoutLayer 1
The logout destination redirects in a private window2. The logout destination sits behind the auth guardLayer 2
Code runs after base44.auth.logout(): an await chain, a state update or a router call3. The guard redirects while logout is leaving the pageLayer 3

The fix — three layers

Each layer closes one root cause. Ship all three: they are small, and the third covers mistakes in the first two.

Layer 1: One logout call, and nothing after it

// src/components/LogoutButton.jsx
import { base44 } from "@/api/base44Client";
import { markLoggingOut } from "@/lib/logoutFlag";

export default function LogoutButton() {
  function handleLogout() {
    markLoggingOut(); // Layer 3: the guard stands down from here on
    // Last line on purpose: logout() navigates the browser through
    // Base44's server-side logout, then to /goodbye.
    base44.auth.logout("/goodbye");
  }

  return (
    <button type="button" onClick={handleLogout}>
      Log out
    </button>
  );
}

Delete from the old handler: any localStorage.removeItem(), localStorage.clear() or document.cookie writes meant to end the session, any signOut() call, any await on logout(), and every state update or router call after it. Per the auth reference, base44.auth.logout() removes the token itself, and its server-side logout clears the cookies your code cannot reach. If you need to record the logout, call your backend function first and logout() last; the secure logout pattern shows that order.

Layer 2: Land on a route outside the guard

// src/App.jsx: public routes first, everything else inside the guard.
import { BrowserRouter, Routes, Route } from "react-router-dom";
import AuthGate from "@/components/AuthGate";
import Goodbye from "@/pages/Goodbye";
import Login from "@/pages/Login";
import Dashboard from "@/pages/Dashboard";

export default function App() {
  return (
    <BrowserRouter>
      <Routes>
        {/* Public: no guard runs here, so logout always has somewhere to land. */}
        <Route path="/goodbye" element={<Goodbye />} />
        <Route path="/login" element={<Login />} />

        {/* Protected: every route inside AuthGate. */}
        <Route element={<AuthGate />}>
          <Route path="/" element={<Dashboard />} />
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

The example uses React Router; keep the same split in whatever router your app uses. The landing page can be as plain as a signed-out message with a sign-in link. Prove it is public: open it in a private window while signed out, and in a new tab while signed in. If either visit redirects, the guard still runs on it, so move the route out of the guarded group and test again.

Layer 3: A guard that stands down during logout

// src/lib/logoutFlag.js
// In memory on purpose: the logout redirect replaces the page and drops it.
let loggingOut = false;

export function markLoggingOut() {
  loggingOut = true;
}

export function isLoggingOut() {
  return loggingOut;
}
// src/components/AuthGate.jsx
import { useEffect, useState } from "react";
import { Navigate, Outlet } from "react-router-dom";
import { base44 } from "@/api/base44Client";
import { isLoggingOut } from "@/lib/logoutFlag";

export default function AuthGate() {
  const [status, setStatus] = useState("checking"); // "checking" | "in" | "out"

  useEffect(() => {
    if (isLoggingOut()) return; // logout() is already taking the browser away
    base44.auth
      .isAuthenticated()
      .then((signedIn) => setStatus(signedIn ? "in" : "out"))
      .catch(() => setStatus("out")); // fail closed: an error is not a session
  }, []);

  // During logout, render nothing that can navigate.
  if (isLoggingOut() || status === "checking") return <div aria-busy="true" />;
  if (status === "out") return <Navigate to="/login" replace />;
  return <Outlet />;
}

The flag lives in memory on purpose: the logout redirect replaces the page, which discards it. A sessionStorage flag would not go away. MDN's sessionStorage reference says a page session "lasts as long as the tab or the browser is open, and survives over page reloads and restores", so the flag would follow the user to the next page and freeze the guard after they sign back in. The guard also fails closed: an error from isAuthenticated() counts as signed out, never as signed in. If your app uses Base44's own login page rather than a custom /login, call base44.auth.redirectToLogin(window.location.href) from the effect instead of rendering Navigate.

Verification — proving the loop is closed

Run this on the published app as well as in preview, with DevTools open, Preserve log on and cache disabled.

  1. Sign in and open a protected page.
  2. In the Application tab, find the auth token entry in Local Storage. The current SDK source names it base44_access_token.
  3. Click logout. The log should show one request to /api/apps/auth/logout, then your landing route. Any repeat is a regression.
  4. Check Local Storage again. The token entry should be gone.
  5. Reload the landing route. It should stay put.
  6. Open a protected URL directly. The guard should send you to /login once and stop.
  7. Press Back from the landing route. The dashboard must not reappear looking signed in.
  8. Sign back in. The guard should let you through; if it does not, something sets the logout flag outside the logout handler.
  9. Repeat the sequence several times, then once in a private window.

If step 7 fails, the browser restored the dashboard from its back/forward cache. web.dev's bfcache guide warns that when "a user signs out of a site on a public computer and the next user clicks the back button", the restored page "could potentially expose private data", and advises to "always update the page after a pageshow event if event.persisted is true". Add this to the protected layout:

// A page restored from the back/forward cache skips your auth check. Reload it.
window.addEventListener("pageshow", (event) => {
  if (event.persisted) window.location.reload();
});

When the loop isn't in your code

If all three layers are in place and the loop persists, check these before rewriting anything else:

  1. A platform incident. Base44's troubleshooting guide says to visit the Base44 status page "before debugging further. If there's a platform-wide incident, your issue may resolve on its own."
  2. A local dev token. If you signed in on the local dev server and then opened the deployed app, Base44's local development guide warns that "Tokens issued by the local dev server are only valid locally. They are signed with a different secret than production, so your deployed app will reject them." Sign out of the local session or delete its tokens from browser storage, then sign in through the deployed app.
  3. A logout the server did not finish. If the log shows /api/apps/auth/logout completing and the next page still reports a signed-in user, the session is not being cleared on Base44's side. Send the captured log to Base44 support; no change to your guard will fix it.

Need the logout loop fixed?

Our fix sprint covers one high-impact issue: root cause, fix, regression test plan and written summary. For the logout loop that means tracing every logout path, moving the landing route outside the guard, hardening the guard and checking the result on your published app. It starts within 48 hours of payment and access, at a fixed price.

Start a fix sprint for the logout loop

QUERIES

Frequently asked questions

Q.01Why does my Base44 app loop forever when I click logout?
A.01

Because your logout code and your auth guard disagree about where the browser should go. Three causes produce it: a logout that never calls base44.auth.logout(), so Base44's session survives; a logout destination that sits behind the guard; and code that redirects while logout() is still leaving the page. The DevTools Network log tells them apart: check whether a request to /api/apps/auth/logout appears, then read which URLs alternate.

Q.02Is there a base44.auth.signOut() method?
A.02

No. Base44's auth module documents logout(redirectUrl?), which returns nothing and navigates the browser, and the SDK's source has no signOut() either. A call to signOut() throws a TypeError. If a try/catch swallows that error and the handler then clears storage and navigates, Base44's server-side logout never runs, which is the first root cause of the loop.

Q.03Why doesn't clearing localStorage fix the logout loop?
A.03

Because part of the session is out of its reach. Base44's SDK skill reference says logout() goes through /api/apps/auth/logout 'to clear HTTP-only cookies and the session', and MDN says the HttpOnly attribute 'Forbids JavaScript from accessing the cookie'. localStorage.clear() also wipes every other key your app stores. Call base44.auth.logout(redirectUrl) instead; per the auth reference, it removes the token from local storage itself.

Q.04Will redirecting to /login fix the loop?
A.04

Only if /login sits outside your auth guard. Base44's reference shows base44.auth.logout('/login') as a valid call, but if the guard also wraps /login, signed-out users get redirected away from the page they are on. Test it: open the destination in a private window while signed out. If it redirects, move the route out of the guarded group or log out to a dedicated public page such as /goodbye.

Q.05Why do sign-in and logout behave differently on the local dev server?
A.05

Base44's local development guide says 'Tokens issued by the local dev server are only valid locally' and that 'your deployed app will reject them.' If you switch from the local dev server to the deployed app with a local token still stored, auth errors follow. Sign out of the local session or delete its tokens from browser storage, then sign in through the deployed app.

Q.06Can I call base44.auth.logout() from a backend function?
A.06

No. The auth reference says it 'Requires a browser environment and can't be used in the backend.' To record a logout on the server, call a backend function with base44.functions.invoke() first, then call base44.auth.logout() in the browser. Our secure logout pattern shows that order.

Q.07How do I stop the logout loop coming back?
A.07

Keep one logout path: a single handler that calls base44.auth.logout(redirectUrl) as its last line, pointed at a public route. Search the codebase for other logout code (signOut, localStorage.clear, a navigate('/login') after logout) whenever the AI agent edits auth. Keep the guard's logout flag in memory, and re-run the Network-log check after every auth change.

NEXT STEP

Need this fix shipped this week?

Book a free 15-minute call or order a $497 audit. We will respond within one business day.