Firebase Google sign-in with SvelteKit
In SvelteKit, initialize Firebase Auth only in the browser (guard with browser and start listeners in onMount), expose the web config as public environment variables, and add localhost and your production domain to Firebase's authorized domains.
To make Google's screen show your own domain, answer /__/auth/* in src/hooks.server.ts by forwarding it to YOUR-PROJECT.firebaseapp.com, then set authDomain to your domain.
Why Google sign-in breaks in SvelteKit
- Server rendering runs your component code first. SvelteKit renders pages on the server and during prerendering. Calling
getAuth()orsignInWithPopup()at module level or in a component's top-level script runs it there too. Svelte'sonMountdoes not run on the server, so it's the right place to subscribe. - SvelteKit 3 changed the env and environment modules. SvelteKit 3 (released October 2026) renamed
$app/environmentto$app/envand deprecated the$env/*modules in favor of variables declared insrc/env.ts. Tutorials written for SvelteKit 1 or 2 import from modules that are now legacy. - Static builds skip your hooks. Requests for static assets, including prerendered pages, aren't handled by SvelteKit, so a proxy in
handlenever runs on anadapter-staticsite. - Unlisted hosts.
vite devserves onlocalhost:5173. Projects created after April 28, 2025 don't getlocalhostas an authorized domain by default. - Redirect sign-in. Firebase says
signInWithRedirect()needs one of its workarounds on Chrome M115+ (from June 24, 2024), Firefox 109+ and Safari 16.1+.
Step-by-step fix
1. Declare public environment variables
SvelteKit 3:
// src/env.ts
import { defineEnvVars } from '@sveltejs/kit/env';
export const variables = defineEnvVars({
FIREBASE_API_KEY: { public: true },
FIREBASE_AUTH_DOMAIN: { public: true },
FIREBASE_PROJECT_ID: { public: true },
FIREBASE_APP_ID: { public: true },
});
# .env.local
FIREBASE_API_KEY=your-web-api-key
FIREBASE_AUTH_DOMAIN=YOUR-PROJECT.firebaseapp.com
FIREBASE_PROJECT_ID=YOUR-PROJECT
FIREBASE_APP_ID=1:000000000000:web:abc123
Variables are private unless marked public: true, and only public ones can be imported into browser code (from $app/env/public). Firebase says API keys restricted to Firebase services don't need to be treated as secrets.
SvelteKit 2: name the variables with the PUBLIC_ prefix (PUBLIC_FIREBASE_API_KEY, and so on) and import them from $env/static/public, which only includes variables that start with PUBLIC_ by default.
2. Create a browser-only Firebase helper
// src/lib/firebase.ts
import { initializeApp, getApps, getApp } from 'firebase/app';
import { getAuth, type Auth } from 'firebase/auth';
import { browser } from '$app/env'; // SvelteKit 2: '$app/environment'
import {
FIREBASE_API_KEY,
FIREBASE_AUTH_DOMAIN,
FIREBASE_PROJECT_ID,
FIREBASE_APP_ID,
} from '$app/env/public'; // SvelteKit 2: PUBLIC_* names from '$env/static/public'
export function firebaseAuth(): Auth {
if (!browser) throw new Error('Firebase Auth runs in the browser only in this app');
const app = getApps().length
? getApp()
: initializeApp({
apiKey: FIREBASE_API_KEY,
authDomain: FIREBASE_AUTH_DOMAIN,
projectId: FIREBASE_PROJECT_ID,
appId: FIREBASE_APP_ID,
});
return getAuth(app);
}
The browser check turns an accidental server-side call into a clear error instead of a confusing SSR failure. getApps() lets the helper reuse the app on later calls.
3. Subscribe in onMount and sign in from a click
<!-- src/lib/GoogleSignIn.svelte -->
<script lang="ts">
import { onMount } from 'svelte';
import {
GoogleAuthProvider,
onAuthStateChanged,
signInWithPopup,
signOut,
type User,
} from 'firebase/auth';
import { firebaseAuth } from './firebase';
let user = $state<User | null | undefined>(undefined); // undefined = still loading
onMount(() => {
// The returned unsubscribe runs when the component is destroyed
return onAuthStateChanged(firebaseAuth(), (u) => (user = u));
});
function signIn() {
signInWithPopup(firebaseAuth(), new GoogleAuthProvider()).catch((e) => console.error(e));
}
</script>
{#if user === undefined}
<span>Loading…</span>
{:else if user}
<button onclick={() => signOut(firebaseAuth())}>Sign out {user.displayName}</button>
{:else}
<button onclick={signIn}>Sign in with Google</button>
{/if}
This component uses Svelte 5 syntax ($state, onclick). Starting from undefined matters because auth.currentUser can be null while Firebase is still restoring the session; the observer reports the real state. Turn Google on in the Firebase console under Security > Authentication > Sign-in method (as of Oct 2026; older guides: Build > Authentication).
4. Authorize your hosts
Open Security > Authentication > Settings > Authorized domains (as of Oct 2026; older guides: Build > Authentication > Settings) and click Add domain for localhost (development only; Firebase recommends removing it from production projects), your production domain such as app.example.com, and your platform's default domain if people sign in there.
5. Show your own domain on Google's sign-in screen
Firebase's docs say users may see Continue to: https://YOUR-PROJECT.firebaseapp.com during Google sign-in, because the SDK opens https://<authDomain>/__/auth/handler. Firebase's redirect best-practices page (option 3) says to forward GET and POST requests for /__/auth/ on your domain to YOUR-PROJECT.firebaseapp.com, transparently (a 302 won't do), then set authDomain to your domain.
SvelteKit's handle hook runs for every request the server receives and can bypass SvelteKit entirely by returning its own Response, so it can do this forwarding on adapter-node, adapter-vercel, adapter-netlify or adapter-cloudflare deployments:
// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit/hooks'; // SvelteKit 2: from '@sveltejs/kit'
const FIREBASE_AUTH_ORIGIN = 'https://YOUR-PROJECT.firebaseapp.com';
export const handle: Handle = async ({ event, resolve }) => {
if (event.url.pathname.startsWith('/__/auth/')) {
const target = new URL(event.url.pathname + event.url.search, FIREBASE_AUTH_ORIGIN);
const headers = new Headers(event.request.headers);
// Drop headers that belong to this hop; fetch sets its own host and connection
for (const name of ['host', 'connection', 'keep-alive', 'transfer-encoding', 'upgrade']) {
headers.delete(name);
}
headers.delete('accept-encoding'); // avoid passing compressed bodies through twice
const method = event.request.method;
const upstream = await fetch(target, {
method,
headers,
body: method === 'GET' || method === 'HEAD' ? undefined : await event.request.arrayBuffer(),
redirect: 'manual', // hand any redirect back to the browser unchanged
});
const responseHeaders = new Headers(upstream.headers);
responseHeaders.delete('content-encoding');
responseHeaders.delete('content-length');
return new Response(upstream.body, { status: upstream.status, headers: responseHeaders });
}
return resolve(event);
};
Test this on a preview deployment before relying on it. This proxy code is a sketch built from the SvelteKit and Firebase docs, not an official sample.
If you already have a handle, combine the two with the sequence helper from @sveltejs/kit/hooks. Put the auth branch first so session checks don't redirect /__/auth/* requests. SvelteKit's CSRF protection also checks the origin of cross-site form POSTs, so a provider that posts a form back to the handler may be rejected unless you allow its origin with csrf.trustedOrigins; Google's popup flow is the case covered here.
Using adapter-static? The hook won't run in production, so set up the rewrite on your host instead. The Vite guide below has Netlify _redirects and vercel.json rules, and the Cloudflare Pages guide covers that host. On those platforms you may also be able to use a host rewrite with a server adapter if you'd rather not run the proxy in your own code.
Then finish on Google's side:
- In the Google Cloud console, open APIs & Services > Credentials, choose the web client under OAuth 2.0 Client IDs (as of Oct 2026 also listed under Google Auth Platform > Clients), and add
https://app.example.com/__/auth/handlerto Authorized redirect URIs. Google says changes can take 5 minutes to a few hours. - Check that
app.example.comis in Firebase's authorized domains. - Set
FIREBASE_AUTH_DOMAIN=app.example.comin your production environment (SvelteKit 2:PUBLIC_FIREBASE_AUTH_DOMAIN, then rebuild, because$env/static/publicvalues are injected at build time). KeepYOUR-PROJECT.firebaseapp.comin.env.localfor development.
How to confirm it worked
- Open
https://app.example.com/__/auth/handler. The response should come from Firebase, not SvelteKit's error page. An error page means the hook isn't deployed, or the request is being served as a static asset. - In DevTools > Network, start sign-in and check that the popup opens
https://app.example.com/__/auth/handler?.... - Google's account chooser should name
app.example.comas the site you're continuing to. - After choosing an account, the component should switch to Sign out with your name, without a page reload.
Related problems
auth/unauthorized-domainonlocalhost:5173: Fix auth/unauthorized-domain on localhost- What
authDomaincontrols: Firebase authDomain explained - Host rewrite rules for static builds: Vite + React Firebase Google sign-in and Cloudflare Pages Firebase Auth
- Redirect sign-in returns no user: signInWithRedirect not working
- Why Google shows the Firebase domain: Why Google says "continue to firebaseapp.com"
- Whether to proxy
/__/firebase/init.jsontoo: Do I need init.json?
FAQ
Why does Firebase Auth crash during SvelteKit server rendering?
Code in a component's top-level script and in shared modules also runs on the server and during prerendering. Start Firebase listeners in onMount, which doesn't run on the server, and guard helpers with browser from $app/env ($app/environment on SvelteKit 2).
Should I use $env/static/public or $app/env/public?
On SvelteKit 3, declare variables in src/env.ts and import them from $app/env/public; the $env/* modules are deprecated and slated for removal in SvelteKit 4. On SvelteKit 2, $env/static/public with PUBLIC_ names is still the standard way.
Do I need a server hook to proxy /__/auth in SvelteKit?
Only if your adapter runs SvelteKit on a server. With adapter-static, or if you prefer platform config, use your host's rewrite rules for /__/auth/* instead.
Can I keep the server session in sync with Firebase sign-in?
Yes, but it's a separate step: after sign-in, send the user's ID token to your server over HTTPS and check it with the Admin SDK's verifyIdToken(). The proxy and client setup on this page don't depend on it.
Sources
- Authenticate Using Google with JavaScript | Firebase Authentication
- Best practices for using signInWithRedirect on browsers that block third-party storage access | Firebase
- Manage Users in Firebase
- Authentication FAQ and troubleshooting | Firebase Authentication
- Learn about and manage API keys for Firebase
- @firebase/app | Firebase JavaScript API reference
- firebase-js-sdk: packages/auth/src/core/util/handler.ts
- Environment variables • SvelteKit Docs
- $app/env • SvelteKit Docs
- Migrating to SvelteKit v3 • SvelteKit Docs
- Hooks • SvelteKit Docs
- Configuration • SvelteKit Docs
- Static site generation • SvelteKit Docs
- $env/static/public (SvelteKit 2 source)
- SvelteKit 3 just works on Netlify | Netlify Changelog
- Lifecycle hooks • Svelte Docs
- $state • Svelte Docs
- Server Options | Vite
- Verify ID Tokens | Firebase Authentication
- Manage OAuth Clients | Google Cloud
How this page was made: drafted with AI from the sources listed, reviewed by Pewterkit. Docs checked Oct 27, 2026. Spotted something out of date? Tell us.