Client-side authentication using SvelteKit and Firebase
Content
Introduction
In this article, I’ll demonstrate how to implement client-side authentication in a SvelteKit app using Firebase Authentication. This approach provides a simple and effective method for managing user authentication and navigation within your application.
With the introduction of Svelte runes in Svelte 5 (replacing stores), we’ll utilize runes to manage the authentication state client-side. Runes are powered by JavaScript signals, offering a reactive and straightforward way to handle state across the app. You can learn more about runes here.
Why Client-side Authentication?
- Faster Page Loads. Client-side authentication is particularly beneficial in Single Page Applications (SPAs), where navigation happens within the browser. This results in smoother user experiences with minimal delays between routes.
- Simplicity. While implementing token-based client-side authentication from scratch can be challenging, Firebase Authentication simplifies the process, handling much of the complexity for you.
- Offloading Load from the Server. As your user base grows, handling authentication client-side reduces the server's workload, improving scalability. In simple applications, such as a blog or news site, you might even fully serve your app as a static site with client-side rendering (CSR), leading to reduced hosting and operational costs.
Setup
Project Structure
src/
├─ lib/
│ ├─ components/
│ ├─ state/
│ │ ├─ user.svelte.ts
│ ├─ firebase.ts
│ ├─ utils.ts
├─ routes/
│ ├─ (login)/
│ │ ├─ +page.svelte
│ ├─ protected/
│ │ ├─ +page.svelte
│ ├─ +layout.svelte
│ ├─ +layout.ts
├─ app.html
├─ app.css
- The project structure follows the SvelteKit standard.
- The Firebase configuration and initialization resides in
src/lib/firebase.ts
- Our
userState
rune is defined insrc/lib/state/user.svelte.ts
- The application has only two routes for the sake of this tutorial:
- an uporotected login route (
/
) - a protected route accessible only to authenticated users (
/protected
)
- an uporotected login route (
- UI components are imported from shadcn-svelte.
Firebase Auth
import { initializeApp, getApps } from "firebase/app";
import { getAuth } from "firebase/auth";
// Replace with your project's config
const firebaseConfig = {
apiKey: "",
authDomain: "",
projectId: "",
storageBucket: "",
messagingSenderId: "",
appId: ""
};
const app = getApps()[0] || initializeApp(firebaseConfig);
export const auth = getAuth(app);
- If you haven’t alreay, create your Firebase project in the Firebase console and replace the config in
src/lib/firebase.ts
with your project details. - This code ensures the Firebase app is initialized and exports the
auth
instance.
userState
rune
Creating the function createUserState() {
let user = $state<User | null>(null);
return {
get user() { return user },
set: (newUser: User) => user = newUser,
reset: () => user = null,
}
}
export const userState = createUserState();
This creates a userState rune that manages the authenticated user’s state. It’s mostly based on the official SvelteKit runes announcement. While not critical for authentication itself, this rune helps manage the user’s state across the app.
- The
createUserState
functions by initializes a$state
rune that holds the value of the user (ornull
). - We use the
get
property to retrieve the user state when needed. - The
set
andreset
meothds allow updating and clearing the user state, respectively.
Login and Logout logic
For login:
async function handleLogIn() {
loading = true;
try {
await signInWithEmailAndPassword(auth, email, password);
await goto ('/protected');
} catch {
loading = false;
alert('Invalid credentials');
}
}
For logout:
async function handleLogOut() {
await signOut(auth);
await goto('/');
}
Our login form looks like this:
Managing Authentication and User State
Finally, let’s ensure that authenticated users can access protected routes, and unauthenticated users are redirected appropriately. This logic is placed in the root +layout.ts
file:
import { redirect } from "@sveltejs/kit";
import { userState } from "$lib/state/user.svelte";
import { auth } from "$lib/firebase";
export const ssr = false;
export async function load({ url }) {
await auth.authStateReady();
if (auth.currentUser) {
userState.set({
email: auth.currentUser.email!,
name: auth.currentUser.displayName!,
isVerified: auth.currentUser.emailVerified
});
if (!url.pathname.startsWith('/protected')) {
throw redirect(302, '/protected');
}
} else {
userState.reset();
if (url.pathname.startsWith('/protected')) {
throw redirect(302, '/');
}
}
}
- The exported
const ssr = false
disables server-side rendering for theload
function ensuring it only runs on the client. - The
auth.authStateReady
method waits for Firebase to determine the user’s authentication status. - The
userState
rune is either set or reset depending on the authentication status. - Authenticated users are redirected to the protected page, and unauthenticated users are redirected to the login page.
Conclusion
- Client-side authentication in SvelteKit with Firebase is fast and efficient.
- Protect routes based on the user’s authentication status directly in the client.
- Svelte’s
$state
rune simplifies managing user state across your app. - Firebase Authentication makes handling login/logout and user sessions straightforward.
- While ideal for many apps, sensitive applications may still need server-side authentication for added security.