Cloud Functions
Learn about our Firebase Cloud Functions architecture
Cloud Functions are serverless backend functions that run on Firebase infrastructure. We use them for operations that require Firebase Admin SDK privileges or need to respond to database events.
Located in functions/src/
Function Types
1. HTTP Request Functions
What: HTTP functions that respond to web requests, similar to API routes but running on Firebase.
Location: functions/src/requests.ts
checkAdminClaims Function
Purpose: Verifies if a user has admin or super admin permissions by checking their custom claims.
Why it exists: Vercel Edge Runtime (where our middleware runs) cannot use Firebase Admin SDK. We need a separate Cloud Function to verify admin status.
How it works:
- Receives POST request with user's ID token
- Uses Firebase Admin SDK to verify token
- Extracts custom claims (
admin,superadmin) - Returns admin status to caller
Implementation:
// functions/src/requests.ts
export const checkAdminClaims = onRequest(async (request, response) => {
if (request.method !== "POST") {
response.status(405).json({ error: "Method Not Allowed" });
return;
}
const { token } = request.body;
if (!token) {
response.status(400).json({ error: "Token is required" });
return;
}
const decodedToken = await admin.auth().verifyIdToken(token);
response.json({
isAdmin: decodedToken.admin || false,
isSuperAdmin: decodedToken.superadmin || false,
uid: decodedToken.uid,
});
});Used in Middleware:
Our middleware (src/middleware.ts) calls this function to protect admin routes:
// src/middleware.ts
export async function middleware(request: NextRequest) {
if (!request.nextUrl.pathname.startsWith("/admin")) {
return NextResponse.next();
}
const user = await getAuthenticatedUser();
if (!user) {
return NextResponse.redirect(new URL("/account/login", request.url));
}
const token = await user.getIdToken();
// Call Cloud Function to verify admin status
const response = await fetch(
new URL(process.env.NEXT_PUBLIC_CLOUD_FUNCTIONS_URL! + "/checkAdminClaims"),
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token }),
}
);
const { isAdmin, isSuperAdmin } = await response.json();
if (!isAdmin && !isSuperAdmin) {
return NextResponse.redirect(new URL("/", request.url));
}
return NextResponse.next();
}⚠️ Why Not Use Admin SDK in Middleware?
Vercel Edge Runtime is a lightweight JavaScript environment that doesn't support Node.js features required by Firebase Admin SDK. Cloud Functions run on Google's infrastructure with full Node.js support, making them perfect for admin operations.
2. Firestore Triggers (Observer Pattern)
What: Functions that automatically run when Firestore documents are created, updated, or deleted.
Pattern: Observer Pattern — these functions "observe" database changes and react automatically.
Location: functions/src/triggers.ts
Observer Pattern Explained:
The Observer Pattern defines a one-to-many dependency: when one object (the "subject") changes state, all dependent objects (the "observers") are notified and updated automatically.
In our case:
- Subject: Firestore documents (applications, registrations, collaborations)
- Observers: Cloud Functions that watch for changes
- Notification: Automatic function execution when documents change
- Action: Update related user documents with new associations
Example 1: Application Trigger
When a user applies to a position, automatically add the position ID to their profile:
// functions/src/triggers.ts
export const createApplication = onDocumentWritten(
"positions/{positionId}/applications/{applicationId}",
async (event: any) => {
const positionId = event.params.positionId;
const userId = event.params.applicationId;
await admin
.firestore()
.collection("users")
.doc(userId)
.update({
"associations.applications": FieldValue.arrayUnion(positionId),
});
}
);Observed: positions/{positionId}/applications/{applicationId}
Action: Update user's applications array
Example 2: Registration Trigger
When a user registers for an event, automatically track it:
// functions/src/triggers.ts
export const createRegistration = onDocumentWritten(
"events/{eventId}/registrations/{registrationId}",
async (event: any) => {
const eventId = event.params.eventId;
const userId = event.params.registrationId;
await admin
.firestore()
.collection("users")
.doc(userId)
.update({
"associations.registrations": FieldValue.arrayUnion(eventId),
});
}
);Observed: events/{eventId}/registrations/{registrationId}
Action: Update user's registrations array
Example 3: Collaboration Trigger
When a user joins a project, automatically track it:
// functions/src/triggers.ts
export const createCollaboration = onDocumentWritten(
"projects/{projectId}/collaborations/{collaborationId}",
async (event: any) => {
const projectId = event.params.projectId;
const userId = event.params.collaborationId;
await admin
.firestore()
.collection("users")
.doc(userId)
.update({
"associations.collaborations": FieldValue.arrayUnion(projectId),
});
}
);Observed: projects/{projectId}/collaborations/{collaborationId}
Action: Update user's collaborations array
✅ Benefits of Observer Pattern
- Automatic synchronization: User profiles stay in sync with their activities
- Decoupling: Application logic doesn't need to manually update user profiles
- Reliability: Updates happen even if the client disconnects
- Maintainability: Easy to add new observers without changing existing code
3. Authentication Triggers
What: Functions that run during the user authentication lifecycle.
Why we use it: Automatically create user profiles when someone signs up.
Example: beforeUserCreated
Before a new user is created in Firebase Auth, create their Firestore profile:
// functions/src/triggers.ts
export const beforecreated = beforeUserCreated(async (event) => {
const user = event.data;
const userDocument = {
publicName: user.displayName || "",
updatedAt: Timestamp.now(),
profileImageUrl: "",
bio: "",
linkedin: "",
github: "",
role: "member",
};
await admin
.firestore()
.collection("users")
.doc(user.uid)
.set(userDocument);
logger.info(`Created user document for UID: ${user.uid}`);
});📂 Cloud Functions Structure
functions/
src/
index.ts — Main entry point, exports all functions
requests.ts — HTTP request functions (checkAdminClaims)
triggers.ts — Firestore and Auth triggers
🛠️ Local Development
Cloud Functions can be run locally using Firebase Emulators:
Start local development:
cd functions npm run serve # Starts functions emulator
Deploy to production:
cd functions npm run deploy # Deploys all functions to Firebase
🔐 Environment Variables
Required in .env.local for local development:
# Firebase Admin SDK (for local development) NEXT_PUBLIC_FIREBASE_PROJECT_ID=your-project-id FIREBASE_CLIENT_EMAIL=your-service-account-email FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n" # Cloud Functions URL (for middleware) NEXT_PUBLIC_CLOUD_FUNCTIONS_URL=https://your-region-your-project.cloudfunctions.net
💡 Best Practices
1. Keep Functions Focused
Each function should do one thing well. Don't combine multiple responsibilities.
2. Error Handling
Always wrap operations in try-catch blocks and log errors with logger.error().
3. Idempotency
Triggers may run multiple times. Design functions to be safe when executed repeatedly.
4. Use Type Safety
Define TypeScript interfaces for request/response data to catch errors early.
5. Test Locally First
Always test with Firebase Emulators before deploying to production.