This is an example/boilerplate/starter of the SvelteKit sample app with Firebase authentication and SSR that has user data.
https://sveltekit-firebase-ssr.vercel.app/
Create a .env
file at the root of the folder with the following entries:
PUBLIC_FIREBASE_CLIENT_CONFIG=Your **client** Firebase config json, stringified
FIREBASE_SERVER_CONFIG=Your **server** Firebase config json, stringified
This value will be sent to the client in the user's session.
The (non-stringified) json has this shape:
{
"apiKey": "",
"authDomain": "",
"databaseURL": "",
"projectId": "",
"storageBucket": "",
"messagingSenderId": "",
"appId": "",
"measurementId": ""
}
To obtain the client config, log in to the Firebase console, click the ⚙️ (settings icon), then select Project Settings
and in the General
tab the config json will be under Your apps
.
This value is only used to retrieve data from Firebase on the server. See src/lib/server/firebase.ts
The (non-stringified) json has this shape:
{
"type": "",
"project_id": "",
"private_key_id": "",
"private_key": "",
"client_email": "",
"client_id": "",
"auth_uri": "",
"token_uri": "",
"auth_provider_x509_cert_url": "",
"client_x509_cert_url": ""
}
To obtain the admin server config, log in to the Firebase console, click the ⚙️ (settings icon), then select Project Settings
and then the Service accounts
tab. In the Firebase Admin SDK
click Generate new private key
.
These credentials contain a private key that should be kept secret (i.e. not shared or committed to Git)
/src/routes/+layout.svelte
uses the Auth
component which shows a button to sign in and out.
The signInWith
and signOut
functions are in /src/lib/utils/firebase.ts
.
The script section of /src/routes/+layout.svelte
invokes listenForAuthChanges()
(/src/lib/utils/firebase.ts
) if the app is running in the browser.
It will update the session data with the logged in user and set a cookie with its token.
The handle
function in /src/hooks.server.ts
reads the cookie and decodes the token to include minimal information about the user in the session object.
Because reading on the server requires firebase-admin
which uses a project's private key, DB operations are separated into the following:
/src/lib/server/firebase.ts
for the server./src/lib/utils/firebase.ts
for the client./src/routes/api/data.ts
to get the components' initial data from both client and server.
At risk of angering the FP gods I decided to go with classes for the document models.
/src/lib/models/Document.ts
is the base class for Firebase documents. _collection
is to be overridden with the path to the collection. _dbFields
holds the fields that will make it to the database (not every field in the model needs to be stored).
/src/lib/models/Counter.ts
holds the definition of the Counter
item. The constructor adds the fields it wants to persist in the DB (in this case it's just count
). It also overrides _collection
with the name of the Firebase collection ('counters'
).
The Counter
component shows how one can subscribe to Firebase changes. It gets the data from the server, creates a Document
, and uses the .getStore()
function which creates a readable store that A) loads the object's data properties on Firebase's onSnapshot
, and B) updates the store via the set
method.
You can open 2 browser windows and see how one changes with the other (as long as they're both logged in with the same user).
The Counter
component doesn't display on the home page if the user isn't logged in.
Counter
uses the helper method /src/lib/utils/firebase.ts -> getDocumentStore
to react to changes in a single document (there's one counters document in Firebase for each user).
There's a bit of a run around when loading and SSR'ing a component which depends on user data. The component should expose a method that returns the data it needs to render correctly. The parent calls this method as part of its load (before rendering) and then feeds the data back to the component as a prop.
For example, Counter.svelte
has the method getUserCountData
which returns the data for the logged in user. +page.svelte
declares export let data: PageData;
which is populated by the return of the load
method in +page.ts
(it calls getUserCountData
). +page.svelte
then checks if data.userCountData
has something and passes the value to the component <Counter userCountData={data.userCountData} />
.