Architecture Overview
- Frontend (Mobile App): Authenticates via Firebase Auth.
- Security Layer (API Gateway): Validates the Firebase Token and passes the user identity to the backend.
- Compute (Cloud Functions): Private, isolated functions that execute business logic.
- Database (Firestore): NoSQL database storing user and app data.
- Storage (Cloud Storage): Buckets for PDFs, photos, and documents.
Prerequisites
- Google Cloud SDK (
gcloud) installed and authenticated. - Node.js installed.
- Firebase Project created and linked to your GCP project.
- Terminal open at your project root.
Step 1: Environment & Identity Setup
Create specific Service Accounts for API Gateway and Cloud Functions.
# 1. Set Project Variables
export PROJECT_ID="your-project-id"
export REGION="us-central1"
export GATEWAY_SA_NAME="api-gateway-sa"
export FUNC_SA_NAME="backend-function-sa"
# 2. Set the active project
gcloud config set project $PROJECT_ID
# 3. Enable Required Google Cloud APIs
gcloud services enable \
cloudfunctions.googleapis.com \
apigateway.googleapis.com \
servicemanagement.googleapis.com \
servicecontrol.googleapis.com \
firestore.googleapis.com \
storage.googleapis.com \
iam.googleapis.com
# 4. Create Service Account for API Gateway
gcloud iam service-accounts create $GATEWAY_SA_NAME \
--display-name="API Gateway Service Account"
# 5. Create Service Account for Cloud Functions
gcloud iam service-accounts create $FUNC_SA_NAME \
--display-name="Backend Function Service Account"
# 6. Grant Permissions to Function Service Account
# Access to Firestore
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:$FUNC_SA_NAME@$PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/datastore.user"
# Access to Cloud Storage
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:$FUNC_SA_NAME@$PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/storage.admin"
Step 2: Create Storage Buckets
# 1. Define Unique Bucket Names
export SYLLABUS_BUCKET="daycare-management$PROJECT_ID"
# 2. Create Buckets
gcloud storage buckets create gs://$SYLLABUS_BUCKET --location=$REGION
Step 3: Develop the Backend Code
1. Initialize package.json
{
"name": "daycare-backend",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"@google-cloud/functions-framework": "^3.0.0",
"@google-cloud/storage": "^7.0.0",
"firebase-admin": "^11.0.0"
}
}
2. Create index.js (The Logic Core)
const functions = require('@google-cloud/functions-framework');
const admin = require('firebase-admin');
const { Storage } = require('@google-cloud/storage');
if (admin.apps.length === 0) {
admin.initializeApp();
}
const db = admin.firestore();
const storage = new Storage();
// --- HELPER: Security & RBAC ---
async function getAuthenticatedUser(req) {
const userInfoHeader = req.get('X-Apigateway-Api-User-Info');
if (!userInfoHeader) throw new Error('Unauthenticated: Missing Identity Header');
const userPayload = JSON.parse(Buffer.from(userInfoHeader, 'base64').toString());
const uid = userPayload.user_id;
const userDoc = await db.collection('users').doc(uid).get();
if (!userDoc.exists) throw new Error('User profile not found');
return { uid, role: userDoc.data().role };
}
// ==========================================
// 1. STAFF OPERATIONS
// ==========================================
exports.submitDailyLog = async (req, res) => {
try {
const user = await getAuthenticatedUser(req);
if (user.role !== 'staff') return res.status(403).send('Staff only');
const data = req.body;
data.authorStaffId = user.uid;
data.date = admin.firestore.FieldValue.serverTimestamp();
await db.collection('daily_class_logs').add(data);
res.json({ success: true });
} catch (err) { res.status(500).send(err.message); }
};
exports.createSyllabus = async (req, res) => {
try {
const user = await getAuthenticatedUser(req);
if (user.role !== 'staff') return res.status(403).send('Staff only');
const { title, weekNumber, topic, objectives, fileName } = req.body;
const planRef = await db.collection('syllabus_plans').add({
title, weekNumber, topic, objectives,
authorStaffId: user.uid,
status: 'active',
createdAt: admin.firestore.FieldValue.serverTimestamp()
});
const bucket = storage.bucket(process.env.SYLLABUS_BUCKET);
const file = bucket.file(`syllabus/${planRef.id}/${fileName}`);
const [uploadUrl] = await file.getSignedUrl({
action: 'write', expires: Date.now() + 15 * 60 * 1000, contentType: 'application/pdf',
});
await planRef.update({ contentPdfUrl: file.publicUrl() });
res.json({ success: true, planId: planRef.id, uploadUrl });
} catch (err) { res.status(500).send(err.message); }
};
exports.staffLeaveRequest = async (req, res) => {
try {
const user = await getAuthenticatedUser(req);
if (user.role !== 'staff') return res.status(403).send('Staff only');
await db.collection('staff_profiles').doc(user.uid).collection('leave_requests').add({
...req.body, status: 'pending', requestedAt: admin.firestore.FieldValue.serverTimestamp()
});
res.json({ success: true });
} catch (err) { res.status(500).send(err.message); }
};
exports.staffExitRequest = async (req, res) => {
try {
const user = await getAuthenticatedUser(req);
if (user.role !== 'staff') return res.status(403).send('Staff only');
await db.collection('staff_profiles').doc(user.uid).update({
currentStatus: 'exit_requested',
exitRequestDetails: { reason: req.body.reason, requestedAt: admin.firestore.FieldValue.serverTimestamp()
});
res.json({ success: true });
} catch (err) { res.status(500).send(err.message); }
};
exports.createIncidentReport = async (req, res) => {
try {
const user = await getAuthenticatedUser(req);
if (user.role !== 'staff') return res.status(403).send('Staff only');
const { targetType, targetIds, incidentData } = req.body;
let childrenToUpdate = [];
if (targetType === 'single' || targetType === 'list') childrenToUpdate = targetIds;
else if (targetType === 'class') {
const snapshot = await db.collection('children').where('assignedClassroomId', '==', targetIds[0]).get();
childrenToUpdate = snapshot.docs.map(doc => doc.id);
}
const batch = db.batch();
childrenToUpdate.forEach(childId => {
const ref = db.collection('children').doc(childId).collection('incidents').doc();
batch.set(ref, { ...incidentData, authorStaffId: user.uid, timestamp: admin.firestore.FieldValue.serverTimestamp() });
});
await batch.commit();
res.json({ success: true, count: childrenToUpdate.length });
} catch (err) { res.status(500).send(err.message); }
};
exports.updateStudentDevelopment = async (req, res) => {
try {
const user = await getAuthenticatedUser(req);
if (user.role !== 'staff') return res.status(403).send('Staff only');
const { childId, developmentData } = req.body;
await db.collection('children').doc(childId).collection('development_logs').add({
...developmentData, authorStaffId: user.uid, date: admin.firestore.FieldValue.serverTimestamp()
});
res.json({ success: true });
} catch (err) { res.status(500).send(err.message); }
};
exports.approveChildLeave = async (req, res) => {
try {
const user = await getAuthenticatedUser(req);
if (user.role !== 'staff') return res.status(403).send('Staff only');
const { childId, requestId, decision } = req.body;
await db.collection('children').doc(childId).collection('leave_requests').doc(requestId).update({
status: decision, staffActionBy: user.uid, actionDate: admin.firestore.FieldValue.serverTimestamp()
});
res.json({ success: true });
} catch (err) { res.status(500).send(err.message); }
};
// ==========================================
// 2. PARENT OPERATIONS
// ==========================================
exports.submitEnrollment = async (req, res) => {
try {
const user = await getAuthenticatedUser(req);
if (user.role !== 'parent') return res.status(403).send('Parents only');
const data = req.body;
data.submittedByParentUid = user.uid;
data.status = 'pending';
data.submissionDate = admin.firestore.FieldValue.serverTimestamp();
const ref = await db.collection('enrollment_forms').add(data);
res.json({ success: true, formId: ref.id });
} catch (err) { res.status(500).send(err.message); }
};
exports.updateChildDetails = async (req, res) => {
try {
const user = await getAuthenticatedUser(req);
if (user.role !== 'parent') return res.status(403).send('Parents only');
const { childId, extendedDetails } = req.body;
const childRef = db.collection('children').doc(childId);
if (!(await childRef.get()).data().parentUids.includes(user.uid)) return res.status(403).send('Unauthorized');
await childRef.update({ extendedDetails });
res.json({ success: true });
} catch (err) { res.status(500).send(err.message); }
};
exports.childLeaveRequest = async (req, res) => {
try {
const user = await getAuthenticatedUser(req);
if (user.role !== 'parent') return res.status(403).send('Parents only');
const { childId, note, date } = req.body;
const childRef = db.collection('children').doc(childId);
if (!(await childRef.get()).data().parentUids.includes(user.uid)) return res.status(403).send('Unauthorized');
await childRef.collection('leave_requests').add({
note, date, status: 'pending', requestedBy: user.uid, createdAt: admin.firestore.FieldValue.serverTimestamp()
});
res.json({ success: true });
} catch (err) { res.status(500).send(err.message); }
};
exports.childExitRequest = async (req, res) => {
try {
const user = await getAuthenticatedUser(req);
if (user.role !== 'parent') return res.status(403).send('Parents only');
const { childId, reason } = req.body;
const childRef = db.collection('children').doc(childId);
if (!(await childRef.get()).data().parentUids.includes(user.uid)) return res.status(403).send('Unauthorized');
await childRef.update({
status: 'exit_requested', exitDetails: { reason, requestedBy: user.uid, requestedAt: admin.firestore.FieldValue.serverTimestamp() }
});
res.json({ success: true });
} catch (err) { res.status(500).send(err.message); }
};
// ==========================================
// 3. ADMIN OPERATIONS
// ==========================================
exports.approveEnrollment = async (req, res) => {
try {
const user = await getAuthenticatedUser(req);
if (user.role !== 'admin') return res.status(403).send('Admins only');
const { formId } = req.body;
await db.collection('enrollment_forms').doc(formId).update({
status: 'approved', adminActionDate: admin.firestore.FieldValue.serverTimestamp()
});
res.json({ success: true });
} catch (err) { res.status(500).send(err.message); }
};
exports.approveStaffLeave = async (req, res) => {
try {
const user = await getAuthenticatedUser(req);
if (user.role !== 'admin') return res.status(403).send('Admins only');
const { staffUid, requestId, decision } = req.body;
await db.collection('staff_profiles').doc(staffUid).collection('leave_requests').doc(requestId).update({
status: decision, adminDecidedBy: user.uid
});
res.json({ success: true });
} catch (err) { res.status(500).send(err.message); }
};
exports.approveStaffExit = async (req, res) => {
try {
const user = await getAuthenticatedUser(req);
if (user.role !== 'admin') return res.status(403).send('Admins only');
const { staffUid } = req.body;
await db.collection('staff_profiles').doc(staffUid).update({
currentStatus: 'exited', 'exitRequestDetails.approvedBy': user.uid
});
res.json({ success: true });
} catch (err) { res.status(500).send(err.message); }
};
exports.approveChildExit = async (req, res) => {
try {
const user = await getAuthenticatedUser(req);
if (user.role !== 'admin') return res.status(403).send('Admins only');
const { childId } = req.body;
await db.collection('children').doc(childId).update({
status: 'graduated', 'exitDetails.adminFinalizedDate': admin.firestore.FieldValue.serverTimestamp()
});
res.json({ success: true });
} catch (err) { res.status(500).send(err.message); }
};
Step 4: Deploy Cloud Functions
Deploy functions privately using the Function Service Account.
# Common flags for all deployments
FLAGS="--runtime nodejs18 --trigger-http --no-allow-unauthenticated --service-account=$FUNC_SA_NAME@$PROJECT_ID.iam.gserviceaccount.com --region=$REGION"
# 1. Staff Functions
gcloud functions deploy submitDailyLog $FLAGS
gcloud functions deploy createSyllabus $FLAGS --set-env-vars SYLLABUS_BUCKET=$SYLLABUS_BUCKET
gcloud functions deploy staffLeaveRequest $FLAGS
gcloud functions deploy staffExitRequest $FLAGS
gcloud functions deploy createIncidentReport $FLAGS
gcloud functions deploy updateStudentDevelopment $FLAGS
gcloud functions deploy approveChildLeave $FLAGS
# 2. Parent Functions
gcloud functions deploy submitEnrollment $FLAGS
gcloud functions deploy updateChildDetails $FLAGS
gcloud functions deploy childLeaveRequest $FLAGS
gcloud functions deploy childExitRequest $FLAGS
# 3. Admin Functions
gcloud functions deploy approveEnrollment $FLAGS
gcloud functions deploy approveStaffLeave $FLAGS
gcloud functions deploy approveStaffExit $FLAGS
gcloud functions deploy approveChildExit $FLAGS
Step 5: Grant Gateway Permissions
# List of all function names
FUNCTIONS=("submitDailyLog" "createSyllabus" "staffLeaveRequest" "staffExitRequest" "createIncidentReport" "updateStudentDevelopment" "approveChildLeave" "submitEnrollment" "updateChildDetails" "childLeaveRequest" "childExitRequest" "approveEnrollment" "approveStaffLeave" "approveStaffExit" "approveChildExit")
# Loop to grant permissions
for FUNC in "${FUNCTIONS[@]}"
do
gcloud functions add-iam-policy-binding $FUNC \
--region=$REGION \
--member="serviceAccount:$GATEWAY_SA_NAME@$PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/cloudfunctions.invoker"
done
Step 6: Configure & Deploy API Gateway
1. Create api-spec.yaml
swagger: '2.0'
info:
title: DayCare API
description: Secure API for DayCare Mobile App
version: 1.0.0
schemes:
- https
produces:
- application/json
security:
- firebase: []
# Firebase Auth Configuration
securityDefinitions:
firebase:
authorizationUrl: ""
flow: "implicit"
type: "oauth2"
x-google-issuer: "https://securetoken.google.com/[PROJECT_ID]"
x-google-jwks_uri: "https://www.googleapis.com/service_accounts/v1/metadata/x509/securetoken@system.gserviceaccount.com"
x-google-audiences: "[PROJECT_ID]"
paths:
# --- PARENT ENDPOINTS ---
/enrollment/submit:
post:
summary: Parent submits enrollment
operationId: submitEnrollment
x-google-backend:
address: https://[REGION]-[PROJECT_ID].cloudfunctions.net/submitEnrollment
responses: { '200': { description: Success } }
/parent/child-details:
put:
summary: Update child extended details
operationId: updateChildDetails
x-google-backend:
address: https://[REGION]-[PROJECT_ID].cloudfunctions.net/updateChildDetails
responses: { '200': { description: Success } }
/parent/child-leave:
post:
summary: Submit absence/leave note
operationId: childLeaveRequest
x-google-backend:
address: https://[REGION]-[PROJECT_ID].cloudfunctions.net/childLeaveRequest
responses: { '200': { description: Success } }
/parent/child-exit:
post:
summary: Request child exit
operationId: childExitRequest
x-google-backend:
address: https://[REGION]-[PROJECT_ID].cloudfunctions.net/childExitRequest
responses: { '200': { description: Success } }
# --- STAFF ENDPOINTS ---
/staff/daily-log:
post:
summary: Staff submits daily log
operationId: submitDailyLog
x-google-backend:
address: https://[REGION]-[PROJECT_ID].cloudfunctions.net/submitDailyLog
responses: { '200': { description: Success } }
/staff/syllabus:
post:
summary: Create syllabus & get upload URL
operationId: createSyllabus
x-google-backend:
address: https://[REGION]-[PROJECT_ID].cloudfunctions.net/createSyllabus
responses: { '200': { description: Success } }
/staff/leave:
post:
summary: Raise staff leave
operationId: staffLeaveRequest
x-google-backend:
address: https://[REGION]-[PROJECT_ID].cloudfunctions.net/staffLeaveRequest
responses: { '200': { description: Success } }
/staff/exit:
post:
summary: Raise staff exit
operationId: staffExitRequest
x-google-backend:
address: https://[REGION]-[PROJECT_ID].cloudfunctions.net/staffExitRequest
responses: { '200': { description: Success } }
/staff/incident:
post:
summary: Report incident
operationId: createIncidentReport
x-google-backend:
address: https://[REGION]-[PROJECT_ID].cloudfunctions.net/createIncidentReport
responses: { '200': { description: Success } }
/staff/student-dev:
post:
summary: Update student development
operationId: updateStudentDevelopment
x-google-backend:
address: https://[REGION]-[PROJECT_ID].cloudfunctions.net/updateStudentDevelopment
responses: { '200': { description: Success } }
/staff/approve-child-leave:
post:
summary: Approve child leave request
operationId: approveChildLeave
x-google-backend:
address: https://[REGION]-[PROJECT_ID].cloudfunctions.net/approveChildLeave
responses: { '200': { description: Success } }
# --- ADMIN ENDPOINTS ---
/admin/enrollment/approve:
post:
summary: Admin approves enrollment
operationId: approveEnrollment
x-google-backend:
address: https://[REGION]-[PROJECT_ID].cloudfunctions.net/approveEnrollment
responses: { '200': { description: Success } }
/admin/approve-staff-leave:
post:
summary: Approve staff leave
operationId: approveStaffLeave
x-google-backend:
address: https://[REGION]-[PROJECT_ID].cloudfunctions.net/approveStaffLeave
responses: { '200': { description: Success } }
/admin/approve-staff-exit:
post:
summary: Approve staff exit
operationId: approveStaffExit
x-google-backend:
address: https://[REGION]-[PROJECT_ID].cloudfunctions.net/approveStaffExit
responses: { '200': { description: Success } }
/admin/approve-child-exit:
post:
summary: Approve child exit
operationId: approveChildExit
x-google-backend:
address: https://[REGION]-[PROJECT_ID].cloudfunctions.net/approveChildExit
responses: { '200': { description: Success } }
2. Deploy the Gateway
# 1. Create API Config
gcloud api-gateway api-configs create daycare-config-final \
--api=daycare-api \
--openapi-spec=api-spec.yaml \
--project=$PROJECT_ID \
--backend-auth-service-account="$GATEWAY_SA_NAME@$PROJECT_ID.iam.gserviceaccount.com"
# 2. Create/Update Gateway
gcloud api-gateway gateways create daycare-gateway \
--api=daycare-api \
--api-config=daycare-config-final \
--location=$REGION \
--project=$PROJECT_ID
# 3. Get Your API URL
gcloud api-gateway gateways describe daycare-gateway \
--location=$REGION \
--format="get(defaultHostname)"
Final Usage
- Mobile App: Signs user in via Firebase Auth -> Gets ID Token.
- Request: App sends request to
https://[GATEWAY_URL]/staff/syllabuswith headerAuthorization: Bearer [FIREBASE_ID_TOKEN]. - Result: Secure, role-validated execution of your backend logic.