This guide explains how to set up, configure, and manage the Hiking Logbook development database using Firebase Firestore for Sprint 1.
The Hiking Logbook uses Firebase Firestore as its primary database, providing:
Before setting up the development database, ensure you have:
hiking-logbook/
├── users/ # User profiles and authentication data (Sprint 1)
│ └── {userId}/ # Individual user documents
│ ├── uid # Firebase Auth UID (document ID)
│ ├── displayName # User's display name
│ ├── email # User's email address
│ ├── photoURL # Profile picture URL
│ ├── bio # User biography
│ ├── location # User's location (GeoPoint)
│ └── createdAt # Account creation timestamp
Note: This is the Sprint 1 scope. Additional collections (hikes, trails, photos, etc.) will be added in future sprints.
hiking-logbook-dev
# Install globally
npm install -g firebase-tools
# Verify installation
firebase --version
# Login with your Google account
firebase login
# Verify you're logged in
firebase projects:list
# Navigate to project root
cd Hiking-Logbook
# Initialize Firebase
firebase init
# Select options:
# - Firestore: Configure security rules and indexes
# - Hosting: Configure files for Firebase Hosting
# - Use existing project: Select your hiking-logbook-dev project
Create firestore.rules
in the project root:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Users can read/write their own profile (Sprint 1)
match /users/{userId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
}
}
Create firestore.indexes.json
:
{
"indexes": [
{
"collectionGroup": "users",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "email",
"order": "ASCENDING"
}
]
}
],
"fieldOverrides": []
}
serviceAccountKey.json
backend/
directoryCreate .env
in the backend directory:
# Firebase configuration
FIREBASE_PROJECT_ID=hiking-logbook-dev
FIREBASE_PRIVATE_KEY_ID=your_private_key_id
FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"
FIREBASE_CLIENT_EMAIL=firebase-adminsdk-xxxxx@hiking-logbook-dev.iam.gserviceaccount.com
FIREBASE_CLIENT_ID=your_client_id
FIREBASE_AUTH_URI=https://accounts.google.com/o/oauth2/auth
FIREBASE_TOKEN_URI=https://oauth2.googleapis.com/token
FIREBASE_AUTH_PROVIDER_X509_CERT_URL=https://www.googleapis.com/oauth2/v1/certs
FIREBASE_CLIENT_X509_CERT_URL=https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-xxxxx%40hiking-logbook-dev.iam.gserviceaccount.com
// In backend/createUser.js
import admin from "firebase-admin";
import db from "./firebaseAdmin.js";
export async function createUserProfile(uid, userData) {
try {
const userRef = db.collection("users").doc(uid);
await userRef.set({
uid: uid,
displayName: userData.displayName,
email: userData.email,
photoURL: userData.photoURL || "",
bio: userData.bio || "",
location: new admin.firestore.GeoPoint(
userData.latitude || 0,
userData.longitude || 0
),
createdAt: new Date(),
});
return { success: true, userId: uid };
} catch (error) {
throw new Error(`Failed to create user profile: ${error.message}`);
}
}
// In backend/users.js
export async function updateUserProfile(uid, updates) {
try {
const userRef = db.collection("users").doc(uid);
// Only allow updating specific fields
const allowedUpdates = {};
if (updates.displayName) allowedUpdates.displayName = updates.displayName;
if (updates.bio) allowedUpdates.bio = updates.bio;
if (updates.photoURL) allowedUpdates.photoURL = updates.photoURL;
if (updates.location) {
allowedUpdates.location = new admin.firestore.GeoPoint(
updates.location.latitude,
updates.location.longitude
);
}
await userRef.update(allowedUpdates);
return { success: true };
} catch (error) {
throw new Error(`Failed to update user profile: ${error.message}`);
}
}
// Get user profile by UID
export async function getUserProfile(uid) {
try {
const userDoc = await db.collection("users").doc(uid).get();
if (!userDoc.exists) {
throw new Error("User not found");
}
return userDoc.data();
} catch (error) {
throw new Error(`Failed to fetch user profile: ${error.message}`);
}
}
# Deploy security rules
firebase deploy --only firestore:rules
# Deploy indexes
firebase deploy --only firestore:indexes
# Start Firestore emulator
firebase emulators:start --only firestore
# This will start Firestore on localhost:8080
# Update your backend configuration to use the emulator
# Deploy to production
firebase deploy --only firestore
# This will deploy rules and indexes to your production project
// Frontend: Listen for real-time user profile updates
import { db } from "./firebase.js";
const unsubscribe = db
.collection("users")
.doc(currentUser.uid)
.onSnapshot((doc) => {
if (doc.exists) {
const userData = doc.data();
console.log("User profile updated:", userData);
// Update your app state here
}
});
// Enable offline persistence
import { enableIndexedDbPersistence } from "firebase/firestore";
enableIndexedDbPersistence(db).catch((err) => {
if (err.code === "failed-precondition") {
// Multiple tabs open, persistence can only be enabled in one tab at a time
console.log("Persistence failed");
} else if (err.code === "unimplemented") {
// Browser doesn't support persistence
console.log("Persistence not supported");
}
});
# View database rules
firebase firestore:rules:get
# Export data
firebase firestore:export ./backup
# Import data
firebase firestore:import ./backup
# View indexes
firebase firestore:indexes
// Example: Validate user data before saving
function validateUserData(userData) {
const errors = [];
if (!userData.displayName || userData.displayName.trim() === "") {
errors.push("Display name is required");
}
if (!userData.email || !userData.email.includes("@")) {
errors.push("Valid email is required");
}
if (userData.bio && userData.bio.length > 500) {
errors.push("Bio must be 500 characters or less");
}
return errors;
}
// Check if user is authenticated
if (!request.auth) {
return false;
}
// Check if user owns the document
return request.auth.uid == resource.data.uid;
# Create missing indexes
firebase deploy --only firestore:indexes
# Check index status in Firebase Console
# Wait for indexes to build (may take several minutes)
// Ensure consistent data types
const userData = {
uid: String(userData.uid), // Ensure string
displayName: String(userData.displayName), // Ensure string
bio: String(userData.bio || ""), // Ensure string with default
location: new admin.firestore.GeoPoint(
Number(userData.latitude) || 0, // Ensure number
Number(userData.longitude) || 0 // Ensure number
),
};
// In backend/firebaseAdmin.js
import admin from "firebase-admin";
import serviceAccount from "./serviceAccountKey.json" assert { type: "json" };
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
});
export const db = admin.firestore();
export const auth = admin.auth();
# Backend .env file
FIREBASE_PROJECT_ID=hiking-logbook-dev
FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"
FIREBASE_CLIENT_EMAIL=firebase-adminsdk-xxxxx@hiking-logbook-dev.iam.gserviceaccount.com
After setting up the development database:
If you encounter database issues:
This guide covers Sprint 1 database setup (users collection only). For additional features, check the main README.