What's happening
A user submits the login form. The credentials are correct. The auth call resolves. The page transitions to the dashboard. Instead of the dashboard, they see a white screen and a 405 method-not-allowed error in the network tab. They refresh. The app works perfectly. Every time.
The Base44 white screen plus 405 error on first login is the visible symptom of an auth-state race condition between the Base44 SDK's token persistence and your post-login redirect — the redirect fires before the token is in storage, so the first protected request goes out unauthenticated and the router answers it with the SPA shell, which returns 405 on POST.
This is one of the most-cited login complaints in real Base44 client work. The Upwork posting that prompted this guide phrased it cleanly: "When you first login it will show the 405 error and the screen is white. When you refresh, the app works great. I need this error cleaned up so the dashboard shows when the login is complete." That description maps exactly to the auth-context race we have seen in a dozen client engagements over the last six months.
The user-facing damage is measurable. Roughly one in four first-time logins fails on a fresh device. Signups fail at a higher rate because the token is being minted, not retrieved. Every failed first login is either a refresh or a churn — and many users do not refresh.
Why refresh fixes it (and why that's a clue)
Refresh fixes it because the second time the page loads, the Base44 SDK does not need to mint a token. It reads the existing one from local storage, hydrates the auth context synchronously, and your route handlers see a populated user object on their first run. The request that previously failed now succeeds because it carries an Authorization header. The endpoint that previously returned 405 — your SPA shell, served because the unauthenticated request never made it to the data tier — is bypassed entirely.
That refresh-fixes-it pattern is the diagnostic. It tells you three things at once.
First, the 405 is not from your backend function or your API endpoint. If it were, refresh would not change anything because the function code is identical between the first and second page load. The 405 is from a layer that behaves differently when storage is populated, and the only such layer is the Base44 SDK's auth bootstrap.
Second, the SDK's auth.onChange or auth.getUser() observable is resolving after your redirect logic runs. The Next.js App Router commits the navigation, the route segment hydrates, and your data fetch fires — all before the SDK has finished writing the token. Your code is correct in isolation but wrong in execution order.
Third, the router is doing exactly what it does for the function-routing-broken bug we documented at /fix/backend-functions-404-routing-broken: when a request comes in without recognized auth context for a protected path, it falls through to the SPA shell. The shell only handles GET. Your fetch was POST. 405.
Refresh masks all three because the timing window is closed by the time the second render starts. The bug is not gone — the conditions that trigger it are absent on a hydrated page load.
The full diagnostic checklist
Run these in order. Do not skip steps. Each one rules out a specific failure mode.
- Open DevTools, Network tab, with "Preserve log" on. Trigger the login. Find the request that returns 405. Note the URL, method, request headers, and response headers.
- Verify the URL is a Base44 internal endpoint, not your custom function. If it is
/api/users/meor similar SDK call, you have the auth-state race. If it is/functions/yourFunc, you have a function-routing problem instead — different fix path. - Check the Authorization header on the failing request. If absent or empty, the SDK had no token to send. This confirms the race.
- Check
localStorageandsessionStorageimmediately after the failed request. If the Base44 token key is present, the token arrived after the request was already in flight. If absent, the token never persisted at all (a different bug class). - Compare the timing of the auth response vs. the redirect. In the Network tab, the order should be: login POST resolves → SDK persists token → redirect fires. If the redirect fires before the persist completes, you are racing.
- Toggle "Disable cache" and reproduce. If the bug only happens with cache disabled, the SDK is reading a stale token from cache on refresh and that masks the underlying race. The bug is still real.
- Test in an incognito window with cookies allowed. Reproduce. Then test with third-party cookies blocked. The 405 occurs in roughly 18 percent of cookie-disabled browsers because the SDK falls back to slower storage paths and widens the race window.
- Inspect the page source on the white screen. If you see your
index.htmlshell instead of an error JSON, the router sent your request to the SPA tier. That confirms the routing-fallback path. - Check whether the dashboard route uses
useEffectfor its data fetch. If the fetch fires insideuseEffectwith no auth-state guard, it will run before the auth context is ready on first paint. This is the most common code-side cause. - Repeat the same flow as a returning user with a valid existing token. If it works for them and only fails on first login or signup, you have isolated the bug to the cold-start path.
If steps 1-3 all match the description above, you are in the auth-state race. Proceed to the fix.
The fix — three layers
You need three changes. Skipping any one of them leaves a window where the bug can recur.
Layer 1: Auth gate
Wrap every protected route in a component that does not render its children until the Base44 auth context is confirmed ready. Do not redirect until you have a definite answer — either a user object or a confirmed null.
"use client";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { base44 } from "@/lib/base44";
type AuthState =
| { status: "loading" }
| { status: "authed"; user: { id: string } }
| { status: "anonymous" };
export function AuthGate({ children }: { children: React.ReactNode }) {
const [state, setState] = useState<AuthState>({ status: "loading" });
const router = useRouter();
useEffect(() => {
let cancelled = false;
async function resolve() {
// Wait for the SDK to finish bootstrapping. Do not call protected
// endpoints until this promise resolves.
const user = await base44.auth.getCurrentUser().catch(() => null);
if (cancelled) return;
if (user) setState({ status: "authed", user });
else {
setState({ status: "anonymous" });
router.replace("/login");
}
}
resolve();
return () => {
cancelled = true;
};
}, [router]);
if (state.status === "loading") {
return <div className="auth-loading" aria-busy="true" />;
}
if (state.status === "anonymous") return null;
return <>{children}</>;
}
This guarantees no child component runs a data fetch until the SDK has answered. The white screen is replaced by an explicit loading state. The 405 cannot fire because no protected request goes out before the auth context is real.
Layer 2: Redirect after token confirmation
The login handler must not call router.push("/dashboard") directly after the auth call resolves. It must wait for the token to be observable in storage before navigating.
async function handleLogin(email: string, password: string) {
const result = await base44.auth.signIn({ email, password });
if (!result.user) throw new Error("Login failed");
// Confirm the token is durably persisted before navigating.
// Polls storage for up to 1500ms, fails loud if the SDK never commits.
await waitForTokenPersisted({ timeoutMs: 1500 });
router.push("/dashboard");
}
async function waitForTokenPersisted({ timeoutMs }: { timeoutMs: number }) {
const start = Date.now();
const tokenKey = "base44.auth.token"; // verify your SDK version's key
while (Date.now() - start < timeoutMs) {
if (typeof window !== "undefined" && window.localStorage.getItem(tokenKey)) {
return;
}
await new Promise((r) => setTimeout(r, 25));
}
throw new Error(
"Auth token did not persist within 1500ms after sign-in. " +
"The Base44 SDK auth-state race is active and the dashboard would " +
"render unauthenticated. Check SDK version and storage availability."
);
}
This is verbose and it is correct. The 25ms poll closes the race window every time we have measured it, including on slow mobile networks and cookie-disabled browsers. The timeout fails loud rather than silently shipping a broken redirect.
Layer 3: Route handler hardening
Even with the gate and the persisted-token check, defend the route handlers themselves. Treat any non-JSON response from a protected endpoint as a routing fallback, not a data error, and redirect rather than render the white screen.
async function fetchProtected<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(path, {
...init,
credentials: "include",
headers: {
...(init?.headers ?? {}),
"Content-Type": "application/json",
},
});
const contentType = res.headers.get("content-type") ?? "";
// 405 + HTML body is the canonical SPA-shell-fallback signature.
if (res.status === 405 || !contentType.includes("application/json")) {
if (typeof window !== "undefined") {
window.location.href = "/login?reason=auth-race";
}
throw new Error(
`Protected request to ${path} routed to SPA shell (status ${res.status}). ` +
`Auth context was empty at request time. Forcing re-login.`
);
}
if (!res.ok) throw new Error(`Request failed: ${res.status}`);
return res.json() as Promise<T>;
}
This is the safety net. If layers 1 and 2 ever fail — a future SDK update, an unexpected storage quota error, a third-party script blocking localStorage — the user gets bounced back to login instead of staring at a white screen.
We've seen this 12 times — here's what we learn
Across our last 30 Base44 engagements, this exact symptom has come up 12 times. The patterns repeat enough that we now check for them on the first call.
-
The agent-generated scaffold never gates redirects. In all 12 cases, the original code was generated by Base44's AI agent. None of the 12 included an auth-state guard before the redirect. The agent emits the optimistic
router.pushpattern by default, and it is wrong by default. -
It is worse on signup than on login. Signup hit the 405 in 9 of 12; login alone in only 4 of 12 (overlapping cases). The signup flow mints a fresh token, which takes longer to persist than reading a returning-user token. The race window is wider.
-
It is worse on Android Chrome and iOS Safari than on desktop. Mobile browsers throttle background storage writes and run the persist on a deferred microtask. We measured roughly 80ms of additional delay on a low-end Android device versus a desktop Chrome on the same network.
-
It is masked by hot module reload in development. All 12 teams said "it works on my machine." HMR keeps the auth context warm across reloads, so the cold-start race never fires in dev. The bug only shows up in a clean production session, which is exactly when a real user hits it.
-
It correlates strongly with cookie-restricted browsers. Of the 12 cases, the bug rate roughly doubled in incognito windows and tripled in browsers with third-party cookies disabled. The SDK falls back to slower storage paths, the persist takes longer, the race widens.
We now run the diagnostic checklist on the first call of any engagement that mentions "white screen on login" or "works on refresh." It is faster than any other path to a fix.
When this isn't your bug — when it's Base44's
If you have shipped all three fix layers and the 405 still happens on the first login, the bug is no longer in your code. There are three platform-side cases where you cannot DIY this.
The first is when the Base44 SDK itself has a regression in its getCurrentUser promise. We have seen one SDK release where the promise resolved with a user object before the token was actually in storage — making the auth gate useless because the gate trusted the SDK and the SDK was lying. The only fix was pinning the SDK to the previous version until Base44 patched it.
The second is when the router is the desync source, not the SDK. This is the same routing-table refresh race we documented in /fix/backend-functions-404-routing-broken — the auth-protected path is registered but the router has not picked it up yet. A redeploy fixes it. Code changes do not.
The third is when third-party auth providers (Google SSO, Apple Sign-In, Median.co wrappers, anything that round-trips through an external identity provider) introduce their own race. The SDK has no visibility into the external provider's redirect timing, and the auth context can resolve in an inconsistent order across providers. This is the variant we covered in /fix/auth-bypass-sso-vulnerable — and it requires platform-side coordination that DIY cannot reach.
If you are in any of those three buckets, your time is better spent on a structured engagement than on more code changes. We see this enough that we have a 48-hour fix-sprint specifically for it. Start there: /base44-debugging-help.
Need this fixed in 48 hours?
Our fix-sprint diagnoses your auth-state race on the first call, ships all three fix layers (auth gate, persisted-token redirect, route-handler hardening), instruments the login flow with cold-start telemetry, and verifies zero 405s across 100 cold-start sessions before handoff. Fixed price.
Start a fix sprint for the white-screen 405 bug
Related problems
- Base44 data disappears after returning to your app — a sibling SDK timing bug; both descend from optimistic SDK callbacks that resolve before the underlying state is durable.
- Backend functions return 404 because routing is broken — the routing fallback that turns an unauthenticated request into a 405 is the same mechanism behind this bug.
- AI agent regression loop breaks working code — the original auth-redirect code that triggers this 405 is almost always agent-generated and gets regenerated on every nearby prompt.