What you are actually seeing
A user clicks "Continue with Facebook" on your Base44 app. The Facebook consent dialog appears, then errors out before they can approve — or they tap Continue and land back on your app with no session, no user object, and a URL that contains an error parameter where the auth code was supposed to be. You can log in fine yourself. Most other users cannot.
Base44 Facebook login fails for four root causes: the Facebook app is still in Development mode so only listed testers can sign in, the redirect URI in the Facebook app settings does not exactly match the callback URL Base44 actually sends, the user denied the email permission and your code crashes on the missing field, or you requested an advanced permission without completing App Review. The fix is to switch the app to Live mode, register every callback URL verbatim, make your code tolerant of missing emails, and submit any advanced scope for review before launch.
Facebook login is the OAuth provider where the most failures come from configuration the developer does not realize they need to set. Google OAuth is permissive by default; Facebook is the opposite — every guardrail is opt-out and the failure modes look identical from the browser even when the underlying cause is completely different. The first job is figuring out which failure mode you are in.
The four root causes — match yours before fixing anything
There are four distinct ways Facebook login breaks in a Base44 app. Misdiagnosing which one you have wastes hours because the fixes do not overlap.
Cause A: The Facebook app is in Development mode. In Dev mode, the only Facebook users who can complete a login are accounts listed as Admins, Developers, or Testers in the app's Roles page. Every other user gets a "this app is not available" or "we are sorry, but you cannot use this app at this time" message inside the consent dialog. Because you created the app, you are an Admin by default — so the flow works perfectly for you and fails universally for everyone else. This is the single most common report we see, and it usually presents as "it works on my machine."
Cause B: The redirect URI does not match exactly. Facebook compares the redirect_uri parameter in your OAuth request against the allowlist under Facebook Login → Settings → Valid OAuth Redirect URIs character-for-character. https://app.base44.app/auth/callback and https://app.base44.app/auth/callback/ are two different URIs as far as Facebook is concerned. So are http:// and https://. So are myapp.base44.app and myapp.base44.dev. The error inside the consent dialog reads "URL Blocked: This redirect failed because the redirect URI is not whitelisted."
Cause C: The user denied the email permission and your code crashes. During the consent dialog, the user can uncheck individual permissions before approving — including email. Many users do this routinely as a privacy habit. Some Facebook accounts are also registered against a phone number and have no email on file. When the OAuth flow completes without an email, your code reads user.email as undefined, throws on a null reference, and the session never establishes.
Cause D: You requested a scope that requires App Review. Facebook divides permissions into two tiers. public_profile and email are available to every Live app with no review. Everything else — user_friends, user_birthday, user_likes, pages_show_list, ads_management, and dozens more — requires you to submit your app for App Review with a screencast demonstrating the exact user flow that needs the permission. Until approved, the permission is granted only to listed testers and silently denied for everyone else. The login completes but the data you wanted is empty.
You may have more than one of these at once. Diagnose the dominant one first by capturing the exact text from the consent dialog and the error parameter in the redirect URL.
How to reproduce and capture the failure
Run this sequence verbatim before changing any settings. Each step rules out a failure mode.
- Open an incognito window in Chrome with DevTools open and "Preserve log" enabled in the Network tab.
- Log into Facebook with a non-tester account — not the account you used to create the Facebook app.
- Navigate to your Base44 app and click "Continue with Facebook."
- Screenshot the full text of the Facebook consent dialog or error page. Do not click past it.
- If the dialog completes and redirects, switch to the Network tab and locate the request to your callback URL. Expand the URL parameters. You are looking for either
code=...(success),error=access_denied(user declined),error=server_error(Facebook-side), or no callback at all (URL Blocked, dialog never fired the redirect). - If you reach your app and the session never establishes, open the Console tab and look for the first thrown exception. The stack trace will name the field your code crashed on.
- Compare the symptom to the four root causes above. Pick the dominant one. Do not start fixing until the match is unambiguous.
The single most useful diagnostic is Graph API Explorer at developers.facebook.com/tools/explorer. Select your app from the dropdown, click "Generate Access Token," grant public_profile and email, and call the /me endpoint. If the call succeeds in Explorer, your Facebook app is configured correctly and the bug is in your Base44 client or server. If it fails in Explorer with the same error you see in the app, the bug is in Facebook settings.
Fix for Cause A: switch the app to Live mode
In the Facebook App Dashboard, the mode toggle sits at the top of every page. Flip it from Development to Live. The toggle will refuse if any prerequisite is missing — most commonly a Privacy Policy URL or an app category. Resolve each prerequisite in the order Facebook surfaces them.
The Privacy Policy URL must point to a publicly reachable page on your own domain. A lorem ipsum page is fine for the immediate Live-mode switch but you must replace it with a real policy before launching to real users. Pretty much every privacy policy generator on the web produces an acceptable starting document.
The app category must be selected from the dropdown under Settings → Basic. Pick the one that most accurately describes your app — Business, Games, Education, and so on. The choice rarely affects review outcomes but Facebook will not allow Live mode without it.
Once Live, every Facebook user can attempt to sign in, subject to the permissions you request and any App Review status.
Fix for Cause B: register every callback URL verbatim
In the Facebook App Dashboard, go to Facebook Login → Settings → Valid OAuth Redirect URIs. List every URL your app actually uses as a callback, one per line:
https://myapp.base44.app/auth/facebook/callback
https://app.myapp.com/auth/facebook/callback
http://localhost:3000/auth/facebook/callback
Capture the exact redirect_uri from a failing OAuth request — read it out of the Network tab in DevTools, not from your code — and confirm it appears in the list character-for-character. Trailing slashes matter. Subdomains matter. Protocol matters.
After saving the change in the Facebook dashboard, wait sixty seconds before retrying. Facebook caches OAuth configuration aggressively and a too-quick retry will still hit the stale allowlist.
If you are using Base44 preview URLs, the preview URL changes per deploy on some configurations. Either register a stable canonical URL and proxy preview deploys through it, or list every preview URL you expect to use. The second option is operationally painful; the first is the right answer for any team shipping more than a deploy a week.
Fix for Cause C: tolerate a missing email field
The Facebook OAuth response is not guaranteed to include an email even when you requested the scope. The user can deny the specific permission, the user may have no email on file, or the user's email may be flagged unverified and Facebook will omit it. Your code must handle all three.
The wrong pattern, which crashes when email is missing:
// BROKEN — assumes email is always present
async function handleFbLogin(token: string) {
const profile = await fetchFbProfile(token);
await base44.users.create({
facebookId: profile.id,
name: profile.name,
email: profile.email.toLowerCase(), // throws if undefined
});
router.push("/dashboard");
}
The correct pattern, which fails gracefully and routes the user to a completion step:
async function handleFbLogin(token: string) {
const profile = await fetchFbProfile(token);
const email = profile.email?.toLowerCase() ?? null;
const user = await base44.users.upsert({
facebookId: profile.id,
name: profile.name,
email, // null is fine; we collect it next if missing
});
if (!email) {
// No email from Facebook. Route to a one-field onboarding step
// where the user types their email manually before we let them
// into the rest of the app.
router.push("/onboarding/email");
return;
}
router.push("/dashboard");
}
On the server side, when you read the profile from the Graph API, request a specific field set so you know which fields are coming back:
async function fetchFbProfile(accessToken: string) {
const fields = "id,name,email,picture";
const res = await fetch(
`https://graph.facebook.com/v18.0/me?fields=${fields}&access_token=${accessToken}`
);
if (!res.ok) {
throw new Error(`Facebook /me failed: ${res.status}`);
}
return res.json() as Promise<{
id: string;
name: string;
email?: string;
picture?: { data: { url: string } };
}>;
}
The optional email in the return type is the contract. The downstream code must respect it.
Fix for Cause D: drop the unreviewed scope or submit for review
If your login button works for testers but throws a permission error for ordinary users, list every scope you currently request and cross-reference against Facebook's permissions reference at developers.facebook.com/docs/permissions/reference. Anything beyond public_profile and email requires App Review.
The fast fix is to drop the advanced scope and stop relying on the data it provides. The right fix, if you genuinely need the permission, is to submit App Review with a screencast showing exactly how your app uses the data. Reviews typically take three to seven business days. Plan for the timeline before launch — discovering you need a review the day you go live is a launch-blocker.
While the review is pending, listed testers continue to receive the permission. You can keep building and validating against tester accounts while waiting on the review.
Server-side token verification — do not skip this
Even when the client-side OAuth flow succeeds, your Base44 backend must verify the token before minting a session. Trusting a client-provided access token without verifying it with Facebook is the most common Facebook auth security bug — an attacker can forge a token claim, send it to your server, and impersonate any user if you skip verification.
// Server-side: verify the Facebook access token before trusting it
async function verifyFbToken(accessToken: string) {
const appId = process.env.FB_APP_ID!;
const appSecret = process.env.FB_APP_SECRET!;
const appToken = `${appId}|${appSecret}`;
const res = await fetch(
`https://graph.facebook.com/debug_token?input_token=${accessToken}&access_token=${appToken}`
);
const json = (await res.json()) as {
data?: {
app_id?: string;
user_id?: string;
is_valid?: boolean;
expires_at?: number;
};
};
const data = json.data;
if (!data?.is_valid) throw new Error("Facebook token is not valid");
if (data.app_id !== appId) throw new Error("Token issued for a different app");
if (!data.user_id) throw new Error("Token has no user_id");
if (data.expires_at && data.expires_at * 1000 < Date.now()) {
throw new Error("Token has expired");
}
return data.user_id;
}
Call this on every login. Only after verifyFbToken returns a confirmed user_id should you mint your own session, set your own cookie, or persist anything to the database. Treat the client-side OAuth flow as a hint about who the user claims to be; treat the server-side verification as the truth.
Correct FB SDK initialization in a Base44 app
The Facebook JavaScript SDK must be initialized exactly once per page load, with the right Graph API version pinned, cookie storage enabled, and the appId loaded from environment configuration rather than hardcoded. The common mistake in Base44 apps is to call FB.init inside a component render, which re-initializes on every re-render and silently breaks the login button.
// app/lib/fb-sdk.ts — initialize exactly once at app boot
declare global {
interface Window {
fbAsyncInit?: () => void;
FB?: { init: (config: Record<string, unknown>) => void; login: Function };
}
}
let initialized = false;
export function ensureFbSdk() {
if (initialized || typeof window === "undefined") return;
initialized = true;
window.fbAsyncInit = () => {
window.FB?.init({
appId: process.env.NEXT_PUBLIC_FB_APP_ID,
cookie: true,
xfbml: false,
version: "v18.0",
});
};
const script = document.createElement("script");
script.src = "https://connect.facebook.net/en_US/sdk.js";
script.async = true;
script.defer = true;
script.crossOrigin = "anonymous";
document.head.appendChild(script);
}
Then in your top-level layout, call ensureFbSdk() once inside an effect — never inside a button's onClick handler, never inside a per-route component. The initialized guard prevents double-init from React strict mode or hot module reloads in development.
When this is not your bug — when it is Facebook's
Facebook does have outages, deprecations, and silent API behavior changes. Three signals that the failure is on Facebook's side rather than yours:
- Graph API Explorer fails for you with the same error. If you cannot complete the OAuth flow in Explorer with your app selected, the failure is in Facebook configuration or in a Facebook outage. Check
developers.facebook.com/statusfor the active incident list. - A field that was returning data yesterday is empty today. Facebook has deprecated public profile fields several times in the last few years with little warning. Check the changelog at
developers.facebook.com/docs/graph-api/changelogand look for a deprecation that matches the field you are missing. - The login worked end-to-end on the same code two weeks ago. Facebook periodically tightens default cookie policies and CORS rules, and a working flow can break with no code changes on your side. Cross-reference the changelog around the date your symptom started.
If you are in one of those buckets, the fix is to adapt to Facebook's new requirement, not to keep changing your code looking for a bug that is not there.
Need this fixed before launch?
Our 48-hour fix-sprint diagnoses the Facebook OAuth break on the first call, switches your app to Live with the right Privacy Policy and category, registers every callback URL, hardens your client and server token handling, and walks you through App Review submission if you need an advanced scope. Fixed price.
Start a fix sprint for the Facebook login break
Related problems
- Base44 Google auth not working — the sibling OAuth break, different provider, similar pattern of misconfiguration plus optimistic client code.
- Base44 third-party OAuth broken — the platform-side routing bug that breaks OAuth providers in general, not just Facebook.
- Base44 white screen after login — the auth-state race that surfaces once the OAuth handshake itself succeeds but the token has not yet persisted to storage.