A small React app for learning Firebase. You sign in with your Google account, write journal entries about your cat, and watch them appear in a list that updates in real time.
Everything is built except four pieces. Those four are marked TODO 1 through
TODO 4 in the code, each with a comment explaining exactly what to write and
what it teaches. Fill them in and the app works.
Stack: React + Vite, Firebase (Firestore + Authentication). Nothing else.
- Node.js 18 or newer. Check with
node -v. If you don't have it, get it from nodejs.org. - A Google account, which is all you need to create a free Firebase project.
You should be comfortable with React components, useState, and useEffect.
You don't need to know anything about Firebase yet.
This part happens in your browser, not in the code. It takes about five minutes.
- Go to console.firebase.google.com and sign in with your Google account.
- Click Create a project (or Add project).
- Name it something like
cat-journal. The name doesn't matter. - Google Analytics is optional — turn it off to keep things simple.
- Click Create project and wait for it to finish.
Firestore is the database that will hold the journal entries.
- In the left sidebar, click Database & Storage → Firestore.
- Click Create database.
- Select standard edition. Pick a location near you. You can't change this later, but for a practice project it doesn't matter.
- Choose Start in test mode. This lets anyone read and write your database for the next 30 days, which is fine for learning and not fine for a real app. There's a better rule at the bottom of this README once you're signing in successfully.
- Click Create.
- In the left sidebar, click Security → Authentication.
- Click Get started.
- On the Sign-in method tab, click Google in the list of additional providers.
- Flip the Enable switch, pick a support email from the dropdown, and click Save.
Your app needs to know which Firebase project to talk to.
- Click the gear icon at the top of the sidebar → Settings.
- Scroll to Your apps and click the web icon (
</>). - Give the app a nickname (
cat-journal-webworks). You do not need Firebase Hosting — leave that unchecked. - Click Register app.
- Firebase shows you a
firebaseConfigobject. Keep this tab open — you're about to copy these values into your.envfile.
Are these keys secret? No. The Firebase web API key is meant to be public and ships in every Firebase web app. What actually protects your data is Firestore security rules, not hiding this key. The
.envfile is still gitignored here because it's a good habit for the config files that do hold secrets.
From this project folder:
npm installThen open .env and paste in the values from your firebaseConfig object:
VITE_FIREBASE_API_KEY=AIza...
VITE_FIREBASE_AUTH_DOMAIN=cat-journal-xxxxx.firebaseapp.com
VITE_FIREBASE_PROJECT_ID=cat-journal-xxxxx
VITE_FIREBASE_STORAGE_BUCKET=cat-journal-xxxxx.firebasestorage.app
VITE_FIREBASE_MESSAGING_SENDER_ID=123456789012
VITE_FIREBASE_APP_ID=1:123456789012:web:abc123
No quotes, no spaces around the =. Then start the dev server:
npm run devOpen the URL it prints (usually http://localhost:5173). You'll see the sign-in
screen. The button won't do anything yet — that's TODO 1.
Vite reads
.envonce at startup. If you edit it, stop the server withCtrl+Cand runnpm run devagain.
Do them in order. Each one builds on the last, and each is a few lines of code.
Wire the button to Firebase's Google sign-in popup: create a
GoogleAuthProvider, then call signInWithPopup(auth, provider) when the
button is clicked.
Teaches: Firebase Authentication and OAuth providers. Firebase runs the entire Google login flow for you — you hand it a provider and it handles the popup, the consent screen, and the tokens.
After this: clicking the button opens a Google popup and you can sign in. The screen won't change yet, because nothing is listening. That's next.
Inside a useEffect, call onAuthStateChanged(auth, callback). The callback
gets a user object when someone is signed in and null when they're not — put
that in state. Return the unsubscribe function so React can clean up.
Teaches: auth listeners and useEffect cleanup. This listener fires on
sign-in, on sign-out, and once on page load when Firebase restores your saved
session — which is why refreshing the page keeps you logged in. Returning the
unsubscribe function is how you avoid stacking up listeners every time the
component remounts.
After this: signing in swaps the login screen for the journal, and the sign out button (already written) sends you back.
On submit, call addDoc(collection(db, "entries"), { title, text, userId, createdAt }).
Teaches: writing to Firestore and how documents are shaped. collection(db, "entries")
points at a collection that Firestore creates on your first write. addDoc
generates a random document ID. serverTimestamp() asks Google's server for
the time instead of trusting the user's clock, so entries always sort correctly.
After this: submitting the form clears it, and nothing appears on screen — but go look at Firestore Database in the Firebase console and your entry is there. Reading it back is TODO 4.
Inside a useEffect, build a query on "entries" ordered by createdAt
descending, pass it to onSnapshot, and map snapshot.docs into state.
Return the unsubscribe function.
Teaches: real-time listeners, Firestore queries, and cleanup. onSnapshot
isn't a one-time fetch — it fires immediately with current data and then again
every time the data changes. Note that the document ID lives on doc.id,
separate from the fields in doc.data().
After this: the whole app works. Open it in two browser windows side by side, add an entry in one, and watch it appear in the other without a refresh. That's the payoff.
src/
firebase.js Firebase config and initialization — done for you, exports db and auth
main.jsx React entry point
App.jsx Auth state and layout (TODO 2)
Login.jsx Google sign-in button (TODO 1)
EntryForm.jsx Form to add an entry (TODO 3)
EntryList.jsx Real-time list of entries (TODO 4)
App.css Styling
.env Your Firebase config values — you fill this in
"Missing Firebase config" in the console — .env is empty or the dev
server was started before you filled it in. Fill it in, restart the server.
auth/unauthorized-domain — Firebase console → Authentication → Settings →
Authorized domains. localhost should be there by default; add it if it isn't.
auth/operation-not-allowed — Google sign-in isn't enabled. Go back to
step 1c.
The popup opens and closes instantly — usually a browser popup blocker.
Allow popups for localhost.
"Missing or insufficient permissions" — your Firestore rules are blocking the write. Either your test-mode 30 days expired, or the rules below are in place and you aren't signed in.
Entries save but never show up — TODO 4 isn't done, or the query is
ordering by a field name that doesn't match what TODO 3 writes. Both must say
createdAt.
Test mode expires after 30 days and lets anyone on the internet read and write your database. Once sign-in works, replace your rules with these — Firebase console → Firestore Database → Rules:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /entries/{entryId} {
// Anyone signed in can read entries.
allow read: if request.auth != null;
// You can only create entries stamped with your own user ID.
allow create: if request.auth != null
&& request.resource.data.userId == request.auth.uid;
// Only the author can change or delete an entry.
allow update, delete: if request.auth != null
&& resource.data.userId == request.auth.uid;
}
}
}
This is why TODO 3 stores userId on every entry — it's what the rules check
against.
- Only show the signed-in user's own entries: add
where("userId", "==", user.uid)to the query inEntryList.jsx. Firestore will ask you to build an index — the error in the console includes a link that creates it for you. - Add a delete button using
deleteDoc(doc(db, "entries", entry.id)). - Add a photo of the cat using Firebase Storage.