sveltekit-firebase-helpers
v0.0.12
Published
Helpers for using Firebase with SvelteKit
Downloads
88
Readme
sveltekit-firebase-helpers
Helpers for using Firebase with SvelteKit. Because I always end up with the same code on every SvelteKit + Firebase project I have ...
Firebase Auth Gotchas
When using Firebase Auth, the protocol of the auth service needs to match that of your app. What are the implications of this?
Well, if you're using the live Firebase Auth service you need to use https even when running in local dev mode which can be achieved via the vite-plugin-mkcert package. You will also need to make sure that your app follows the Best practices for using signInWithRedirect on browsers that block third-party storage access which, spoiler, you should do via Option 3 of that article (and what this package helps with). You could use signInWithPopup but you really shouldn't as it comes with its own problems.
If you're using the Auth Emulator you have two options. It runs on http only so will work fine if you don't include the vite-plugin-mkcert package in your vite config (but silently fail if you do). If you do need https locally for other reasons you can use Caddy as a reverse proxy to provide the secure access to it. Personally, I prefer to toggle the vite config so I don't have to install and start another service which keeps the project neat and self-contained.
Finally, how do you sync the client-side auth state to your server? If you only use firebase-hosted services you don't need to, but if you have code on your server that needs to know the identity of the user, such as a shopping cart checkout, it will be important. How do you do this?
You could check for the ID Token changing on the client, and send a fetch request to your own server to mirror the state using a cookie, but there are subtle issues with this - the ID Token is automatically refreshed when calling firebase services but the one in the cookie may end up out-of-date if you're calling your own server. You also then have state in two different places, potentially out-of-sync, which is not ideal.
A better option is to implement Session management with service workers where the existing client-side auth state is used to automatically add an Authorization bearer token to each server request - it will even handle an initial page load if you need the auth state for SSR. Adding this auth token is something else this package can help with.
It also handles some edge cases you may otherwise struggle with. Picture this - in response to a form POST action you want to update the custom claims for a user, maybe to indicate which service plan they are now on or that they have some new permission. You might expect to do the form post, have the invalidateAll() method called (which Svelte's use:enhance form action does for you by default) and see your freshly loaded data based on the claim you set during the server action. Except it won't. The Firebase client-side lib syncs the auth state of the main thread with the service-worker but it does it via polling so there can be a half-second or so where it doesn't have the latest auth token, or any auth token at all if you have just loaded a page from a sign-in redirect. Again, this is something this package can help with by providing it's own temporary auth-token-syncing for those situations just to ensure the state seen by the service-worker is completely up-to-date.
Oh, and one other issue - SvelteKit is often very opinionated but sometimes those opinions are a little blinkered. You can't, for instance, use dynamic .env variables in your service-worker code, only static ones which just doesn't cut-it for serious work - you should be able to deploy the same compiled code / docker image to different environments and have it work, having to compile a specific version for each one is tedious and error prone. Anyway, we built something in to handle this as well to save you the work of figuring it out.
Usage
How to use the features of the package. Of course, you need to install it using the package manager of your choice (which should be pnpm):
pnpm i sveltekit-firebase-helpersServer Hooks
The server-side helpers are added as handlers in your app server hooks. You can add them separately (using sequence if you have multiple handlers) or use a single call to add them all. Let's go through each one individually first:
Auth Handle
To decode any http Authorization header added by the service-worker use createAuthHandle passing in the firebase-admin/auth instance it needs to decode the firebase ID token:
import { createAuthHandle } from "sveltekit-firebase-helpers";
import { auth } from './firebase.server'
export const handle = createAuthHandle(auth)Any requests with an Authorication Bearer [token] header will now be decoded and added as a locals.user property.
Options Handle
The service-worker needs access to the firebase config, but SveleKit conspires to make that difficult to import. To get around this we provide our own server-endpoint that the service-worker can request it from.
import { createOptionsHandle } from "sveltekit-firebase-helpers";
import { options } from './firebase'
export const handle = createOptionsHandle(options)Optionally pass an additional number of seconds to the createOptionsHandle call to publicly cache the response (default = 300, set 0 to disable).
Example options.ts:
import { dev } from '$app/environment'
import {
PUBLIC_FIREBASE_API_KEY,
PUBLIC_FIREBASE_AUTH_DOMAIN,
PUBLIC_FIREBASE_DATABASE_URL,
PUBLIC_FIREBASE_PROJECT_ID,
PUBLIC_FIREBASE_STORAGE_BUCKET,
PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
PUBLIC_FIREBASE_APP_ID,
PUBLIC_FIREBASE_MEASUREMENT_ID
} from '$env/static/public'
export const options = {
apiKey: PUBLIC_FIREBASE_API_KEY,
authDomain: dev ? 'localhost:5173' : PUBLIC_FIREBASE_AUTH_DOMAIN,
databaseURL: PUBLIC_FIREBASE_DATABASE_URL,
projectId: PUBLIC_FIREBASE_PROJECT_ID,
storageBucket: PUBLIC_FIREBASE_STORAGE_BUCKET,
messagingSenderId: PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
appId: PUBLIC_FIREBASE_APP_ID,
measurementId: PUBLIC_FIREBASE_MEASUREMENT_ID
}You can test the handle is working correctly by requesting the /__/firebase/init.json endpoint in your app. You should see the client-side Firebase Options config returned (which is perfectly safe and normal to send to the client for the client-side Firebase libraries to work).
Proxy Handle
To proxy auth requests so you can use signInWithRedirect on browsers that block 3rd party cookies (now all of them) use createAuthHandle passing in the application-id.firebaseapp.com domain name from you Firebase app config, e.g. captaincodeman-experiment.firebaseapp.com.
import { createProxyHandle } from "sveltekit-firebase-helpers";
import { env } from '$env/dynamic/public'
const auth_domain = env.PUBLIC_FIREBASE_AUTH_DOMAIN
export const handle = createProxyHandle(auth_domain)Any requests to /__/auth/... will be proxied to the auth_domain configured, effectively making your app serve the firebase auth endpoints itself to get around the 3rd party cookie restrictions.
If you need to set any additional http headers you can pass an optional HeadersInit as a separate object, e.g.:
export const handle = createProxyHandle(
auth_domain,
{ 'Cross-Origin-Embedder-Policy': 'require-corp' }
)Service Worker
To automatically add the Authentication Bearer [idToken] http header to each server request, use the addFirebaseAuth method in your service worker. If running against the Firebase Auth Emulator pass that as a separate auth_emulator parameter. The service-worker code will automatically request and use the /__/firebase/init.json endpoint provided by the Options Handle above so it needs to be added to the Server Hooks.
/// <reference types="@sveltejs/kit" />
/// <reference no-default-lib="true"/>
/// <reference lib="esnext" />
/// <reference lib="webworker" />
import { addFirebaseAuth } from 'sveltekit-firebase-helpers'
addFirebaseAuth({
auth_emulator: 'http://localhost:9099' // if using the Firebase Auth Emulator
})NOTE: when using a service-worker for firebase state, you must implement custom auth initialization specifically using indexedDBLocalPersistence for persistence. This is handled in the addFirebaseAuth implementation but for your own firebase client you can initialize it using:
import {
browserPopupRedirectResolver,
getAuth,
indexedDBLocalPersistence,
initializeAuth,
} from 'firebase/auth'
import { app } from './app'
import { browser } from '$app/environment'
// SSR friendly auth initialization
export const auth = browser
? initializeAuth(app, {
persistence: [indexedDBLocalPersistence],
popupRedirectResolver: browserPopupRedirectResolver,
})
: getAuth(app)The browser check is to avoid an error if using SSR.
One additional advantage of this is that your client-side auth dependencies are reduced so your app should start faster (you can also avoid loading the popupRedirectResolver for initialization and pass it to methods like signInWithRedirect when called for even greater savings).
When using signInWithRedirect your page loads and will automatically handle the redirect token or you may call getRedirectResult to get the result yourself. At this point, your SvelteKit load functions will have already run with an unauthenticated user so to update your app data you may need to call invalidateAll to re-run the load functions. But because of the polling delay, the service-worker may not have the auth token yet so we've provided a syncAuthToken method that will send it to the client - call it before the invalidateAll.
The same situation happens if you update the auth claims on the server inside any endpoint or form action. You can use syncAuthToken to ensure that any invalidateAll call will include the latest token when data is refreshed from the server.
Project configuration
TODO: detail project configutation for local development with and without Firebase Auth Emulator, and what .env settings should be used.
