Firebase Google sign-in with SvelteKit

Updated 26 Oct 2026 · Pewterkit Guides

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() or signInWithPopup() at module level or in a component's top-level script runs it there too. Svelte's onMount does 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/environment to $app/env and deprecated the $env/* modules in favor of variables declared in src/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 handle never runs on an adapter-static site.
  • Unlisted hosts. vite dev serves on localhost:5173. Projects created after April 28, 2025 don't get localhost as 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:

  1. 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/handler to Authorized redirect URIs. Google says changes can take 5 minutes to a few hours.
  2. Check that app.example.com is in Firebase's authorized domains.
  3. Set FIREBASE_AUTH_DOMAIN=app.example.com in your production environment (SvelteKit 2: PUBLIC_FIREBASE_AUTH_DOMAIN, then rebuild, because $env/static/public values are injected at build time). Keep YOUR-PROJECT.firebaseapp.com in .env.local for development.

How to confirm it worked

  1. 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.
  2. In DevTools > Network, start sign-in and check that the popup opens https://app.example.com/__/auth/handler?....
  3. Google's account chooser should name app.example.com as the site you're continuing to.
  4. After choosing an account, the component should switch to Sign out with your name, without a page reload.

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

  1. Authenticate Using Google with JavaScript | Firebase Authentication
  2. Best practices for using signInWithRedirect on browsers that block third-party storage access | Firebase
  3. Manage Users in Firebase
  4. Authentication FAQ and troubleshooting | Firebase Authentication
  5. Learn about and manage API keys for Firebase
  6. @firebase/app | Firebase JavaScript API reference
  7. firebase-js-sdk: packages/auth/src/core/util/handler.ts
  8. Environment variables • SvelteKit Docs
  9. $app/env • SvelteKit Docs
  10. Migrating to SvelteKit v3 • SvelteKit Docs
  11. Hooks • SvelteKit Docs
  12. Configuration • SvelteKit Docs
  13. Static site generation • SvelteKit Docs
  14. $env/static/public (SvelteKit 2 source)
  15. SvelteKit 3 just works on Netlify | Netlify Changelog
  16. Lifecycle hooks • Svelte Docs
  17. $state • Svelte Docs
  18. Server Options | Vite
  19. Verify ID Tokens | Firebase Authentication
  20. Manage OAuth Clients | Google Cloud

How this page was made: drafted with AI from the sources listed, reviewed by Pewterkit. Docs checked 26 Oct 2026. Spotted something out of date? Tell us.

Related guides