Setup Steps
Create GCP Project
- Sign in to Google Cloud: Go to the Google Cloud Console and sign in with your Google account.
- Open the Project Selector: At the top of the screen, click the project dropdown menu (next to "Google Cloud").
- Create a New Project: In the project selector window, click the NEW PROJECT button in the top right.
- Enter Project Details:
- Project Name: Enter a descriptive name for your project.
- Project ID: (Optional) You can click Edit to change the default project ID. Note: You cannot change the project ID after the project is created, and it must be globally unique.
- Location: If you are part of an organization, select the organization/folder to place the project under. Otherwise, select "No organization".
- Create Project: Click Create.
- Enable Billing (Required for most services):
- Go to the Billing page.
- Select the project you just created.
- If you don't have a billing account, you will be prompted to create one to enable services.
Create Firebase project
- Sign in to Firebase: Open the Firebase console and sign in with your Google account.
- Add Project: Click "Create a project" (or "+ Add project" if you already have others).
- Project Name: Enter a name for your project.
- Note: Firebase will automatically generate a unique Project ID based on this name. You can click the edit icon to change the ID, but it cannot be changed after creation.
- Accept Terms: Review and accept the Firebase terms.
- Configure Google Analytics (Optional): You will be asked if you want to enable Google Analytics. This is highly recommended for using products like Crashlytics, Remote Config, or A/B Testing. You can select an existing Google Analytics account or create a new one.
- Create Project: Click "Create project" (or "Continue" if you added analytics).
- Finalize: Wait for Firebase to provision resources. Once it is ready, click "Continue" to enter the Project Overview page.
Create database
- In the left menu, click Build and select Firestore Database or Realtime Database.
- Click Create database.
- Select a location and choose Test Mode (for initial development) or Locked Mode (for production).
Create Application
Once the project is created, you must register your application to connect it to Firebase:
- Select Platform: In the center of the project overview page, click the icon for your platform:
- iOS+ (for Apple platforms)
- Android (for Android apps)
- Web (</>) (for JavaScript/Web apps)
- Register App: Enter your app's bundle ID (iOS) or package name (Android). This must match the ID in your Xcode project or app/build.gradle file.
- Download Config File: Download the configuration file (google-services.json for Android or GoogleService-Info.plist for iOS).
- Add to Codebase: Move this file into your app's root directory.
- Add SDKs: Follow the on-screen instructions to add the Firebase SDKs to your project.
Install Tools
- Firebase CLI: If you haven't already, install the official Firebase CLI. Most developers use Node.js to install it globally:
npm install -g firebase-tools - Log In: Sign into your Google account through the CLI:
firebase login - FlutterFire CLI: Activate the FlutterFire CLI globally to access Flutter-specific commands:
dart pub global activate flutterfire_cli
Generate Firebase Options
- Select Project: Prompt you to choose an existing Firebase project from your account or create a new one.
- Select Platforms: Ask which platforms your app supports (e.g., Android, iOS, Web). Select them using the spacebar.
- Auto-Register: Automatically create and register Firebase apps (Android, iOS, etc.) within your Firebase project.
- Generate File: Create a
lib/firebase_options.dartfile containing all necessary configuration keys for your selected platforms.
Steps to Generate a Private Key (Service Account JSON key)
- Open the Firebase Console: Sign in to the Firebase Console and select your project.
- Access Project Settings: Click the gear icon (⚙️) in the top-left sidebar and select "Project settings".
- Navigate to Service Accounts: Click on the "Service accounts" tab at the top of the page.
- Generate the Key:
- Ensure "Firebase Admin SDK" is selected (usually the default).
- Scroll to the bottom and click the "Generate new private key" button.
- A confirmation dialog will appear; click "Generate key".
- Download and Secure: A JSON file containing your new private key will automatically download to your computer.
Create Authentication - email/password signing
- Open your project in the Firebase Console.
- In the left sidebar, navigate to Build > Authentication.
- Click on the Sign-in method tab.
- If this is your first time, click Get started.
- Under the Native providers list, select Email/Password.
- Toggle the Enable switch to On.
- (Optional) Toggle Email link (passwordless sign-in) if you want users to sign in by clicking a link in their email instead of typing a password.
Create Function and API
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const { getFirestore } = require('firebase-admin/firestore');
admin.initializeApp();
const db = getFirestore(admin.app(), 'myconcierge');
// 1. GUEST LOGIC: Create Service Request
exports.createServiceRequest = functions.https.onRequest(async (req, res) => {
// Basic Auth Check (In production, validate JWT from API Gateway)
const { guest_id, service_type, details } = req.body;
// Map service types to their specific collection names
const serviceCollections = {
'HOUSEKEEPING': 'housekeeping_details',
'DINING': 'dining_orders',
'TRANSPORT': 'transport_bookings',
'FRONT_DESK': 'front_desk_requests',
'MAINTENANCE': 'maintenance_tickets'
};
const collectionName = serviceCollections[service_type];
if (!collectionName) {
res.status(400).send(`Invalid service_type: ${service_type}`);
return;
}
try {
// Create Master Request
const requestRef = await db.collection('master_requests').add({
guest_id: guest_id,
service_type: service_type,
status: 'PENDING',
created_at: admin.firestore.FieldValue.serverTimestamp(),
assigned_staff_id: null
});
// Create Service Specific Detail
await db.collection(collectionName).add({
request_id: requestRef.id,
...details
});
res.status(200).json({ result: 'success', request_id: requestRef.id });
} catch (error) {
res.status(500).send(error.message);
}
});
// 5. GUEST LOGIC: Retrieve Menu Items
exports.getMenuItems = functions.https.onRequest(async (req, res) => {
try {
const snapshot = await db.collection('menu_items')
.where('available', '==', true)
.get();
const menu = [];
snapshot.forEach(doc => {
menu.push({ id: doc.id, ...doc.data() });
});
res.status(200).json(menu);
} catch (error) {
res.status(500).send(error.message);
}
});
// 3. GUEST LOGIC: Retrieve Requests for a specific User
exports.getGuestRequests = functions.https.onRequest(async (req, res) => {
const guest_id = req.query.guest_id || req.body.guest_id;
if (!guest_id) {
res.status(400).send('Missing guest_id');
return;
}
try {
const snapshot = await db.collection('master_requests')
.where('guest_id', '==', guest_id)
.orderBy('created_at', 'desc')
.get();
const serviceCollections = {
'HOUSEKEEPING': 'housekeeping_details',
'DINING': 'dining_orders',
'TRANSPORT': 'transport_bookings',
'FRONT_DESK': 'front_desk_requests',
'MAINTENANCE': 'maintenance_tickets'
};
const promises = snapshot.docs.map(async (doc) => {
const reqData = { id: doc.id, ...doc.data() };
const collectionName = serviceCollections[reqData.service_type];
if (collectionName) {
const detailSnapshot = await db.collection(collectionName).where('request_id', '==', doc.id).limit(1).get();
reqData.details = detailSnapshot.empty ? {} : detailSnapshot.docs[0].data();
} else {
reqData.details = {};
}
return reqData;
});
const requests = await Promise.all(promises);
res.status(200).json(requests);
} catch (error) {
res.status(500).send(error.message);
}
});
// 4. STAFF LOGIC: Retrieve Requests for Service Provider
exports.getServiceRequests = functions.https.onRequest(async (req, res) => {
const service_type = req.query.service_type || req.body.service_type;
if (!service_type) {
res.status(400).send('Missing service_type');
return;
}
try {
const snapshot = await db.collection('master_requests')
.where('service_type', '==', service_type)
.orderBy('created_at', 'desc')
.get();
const serviceCollections = {
'HOUSEKEEPING': 'housekeeping_details',
'DINING': 'dining_orders',
'TRANSPORT': 'transport_bookings',
'FRONT_DESK': 'front_desk_requests',
'MAINTENANCE': 'maintenance_tickets'
};
const promises = snapshot.docs.map(async (doc) => {
const reqData = { id: doc.id, ...doc.data() };
const collectionName = serviceCollections[reqData.service_type];
if (collectionName) {
console.log(`[DEBUG] Fetching details for Req ID: ${doc.id} Type: ${reqData.service_type} Collection: ${collectionName}`);
const detailSnapshot = await db.collection(collectionName).where('request_id', '==', doc.id).limit(1).get();
console.log(`[DEBUG] Found ${detailSnapshot.size} detail docs for ${doc.id}`);
reqData.details = detailSnapshot.empty ? { _debug_message: "No details found in DB" } : detailSnapshot.docs[0].data();
} else {
console.log(`[DEBUG] Unknown service type: ${reqData.service_type} for Req ID: ${doc.id}`);
reqData.details = {};
}
return reqData;
});
const requests = await Promise.all(promises);
res.status(200).json(requests);
} catch (error) {
res.status(500).send(error.message);
}
});
// 2. STAFF LOGIC: Update Request Status
exports.updateRequestStatus = functions.https.onRequest(async (req, res) => {
const { request_id, staff_id, new_status } = req.body;
try {
const requestRef = db.collection('master_requests').doc(request_id);
const doc = await requestRef.get();
if (!doc.exists) {
res.status(404).send('Request not found');
return;
}
const updateData = { status: new_status }; // 'ASSIGNED' or 'COMPLETED'
// If accepting task, lock it to staff member
if (new_status === 'ASSIGNED') {
updateData.assigned_staff_id = staff_id;
}
await requestRef.update(updateData);
// Trigger Notification Logic
if (new_status === 'COMPLETED') {
console.log(`Notify Guest: Request ${request_id} (${doc.data().service_type}) is complete.`);
}
res.status(200).json({ result: 'updated' });
} catch (error) {
res.status(500).send(error.message);
}
});
// 6. STAFF LOGIC: Create Guest User
// Staff creates the user manually since booking integration is pending
exports.createGuestUser = functions.https.onRequest(async (req, res) => {
const { email, password, first_name, last_name, room_number, valid_from, valid_to } = req.body;
if (!email || !password || !first_name || !last_name) {
res.status(400).send('Missing required fields: email, password, first_name, last_name');
return;
}
try {
// 1. Create User in Firebase Auth
const userRecord = await admin.auth().createUser({
email: email,
password: password,
displayName: `${first_name} ${last_name}`
});
// 2. Create User Profile in Firestore
await db.collection('users').doc(userRecord.uid).set({
email: email,
role: 'GUEST',
first_name: first_name,
last_name: last_name,
room_number: room_number || null,
valid_from: valid_from || null,
valid_to: valid_to || null,
created_at: admin.firestore.FieldValue.serverTimestamp(),
});
res.status(200).json({ result: 'success', uid: userRecord.uid, message: 'Guest user created' });
} catch (error) {
res.status(500).send(error.message);
}
});
// 7. STAFF LOGIC: Update User Details
exports.updateUserDetails = functions.https.onRequest(async (req, res) => {
// In production: Verify req.user.role === 'STAFF'
const { target_user_id, first_name, last_name, room_number, valid_from, valid_to } = req.body;
if (!target_user_id) {
res.status(400).send('Missing target_user_id');
return;
}
try {
await db.collection('users').doc(target_user_id).update({
first_name: first_name,
last_name: last_name,
room_number: room_number,
valid_from: valid_from, // ISO Date String expected
valid_to: valid_to, // ISO Date String expected
updated_at: admin.firestore.FieldValue.serverTimestamp()
});
res.status(200).json({ result: 'success', message: 'User details updated' });
} catch (error) {
res.status(500).send(error.message);
}
});
// 8. STAFF LOGIC: Get a single user's profile
exports.getUserProfile = functions.https.onRequest(async (req, res) => {
const { uid } = req.query;
if (!uid) {
return res.status(400).send('Missing uid query parameter.');
}
try {
const userDoc = await db.collection('users').doc(uid).get();
if (!userDoc.exists) {
return res.status(404).send('User not found.');
}
res.status(200).json({ id: userDoc.id, ...userDoc.data() });
} catch (error) {
res.status(500).send(error.message);
}
});
// 9. STAFF LOGIC: Get overview counts for the staff dashboard
exports.getStaffOverview = functions.https.onRequest(async (req, res) => {
try {
const overview = {
'HOUSEKEEPING': { pending: 0, inProgress: 0 },
'DINING': { pending: 0, inProgress: 0 },
'TRANSPORT': { pending: 0, inProgress: 0 },
'FRONT_DESK': { pending: 0, inProgress: 0 },
'MAINTENANCE': { pending: 0, inProgress: 0 },
};
const snapshot = await db.collection('master_requests')
.where('status', 'in', ['PENDING', 'ASSIGNED']).get();
snapshot.forEach(doc => {
const request = doc.data();
if (overview[request.service_type]) {
if (request.status === 'PENDING') {
overview[request.service_type].pending++;
} else if (request.status === 'ASSIGNED') {
overview[request.service_type].inProgress++;
}
}
});
res.status(200).json(overview);
} catch (error) {
res.status(500).send(error.message);
}
});
// 10. STAFF LOGIC: Get all active guests
exports.getActiveGuests = functions.https.onRequest(async (req, res) => {
try {
const snapshot = await db.collection('users').where('role', '==', 'GUEST').get();
const guests = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
// In a production app with many guests, this should be optimized,
// for example by storing a request count on the user document itself.
res.status(200).json(guests);
} catch (error) {
res.status(500).send(error.message);
}
});
Deployment Command
Use the below command for each of the function to create a function in GCP.
gcloud functions deploy <FunctionName> \
--gen2 \
--runtime=nodejs20 \
--region=us-central1 \
--trigger-http \
--allow-unauthenticated \
--entry-point=<FunctionNameInFile> \
--source .
Create API
Step 1: Build the API Specification
Create a file named openapi-spec.yaml with the following content. Replace <GCPPROJECTID> and YOURPROJECT with your actual Project ID.
swagger: '2.0'
info:
title: Hotel Concierge API
description: API for Guest Requests and Staff Management
version: 1.0.0
host: "hotel-api.endpoints.<GCPPROJECTID>.cloud.goog"
schemes:
- https
produces:
- application/json
securityDefinitions:
# Secure via API Key as requested
api_key:
type: "apiKey"
name: "key"
in: "query"
# Maximum Security: Google ID Token (Firebase Auth)
firebase_auth:
authorizationUrl: ""
flow: "implicit"
type: "oauth2"
x-google-issuer: "https://securetoken.google.com/<GCPPROJECTID>"
x-google-jwks_uri: "https://www.googleapis.com/service_accounts/v1/metadata/x509/securetoken@system.gserviceaccount.com"
x-google-audiences: "<GCPPROJECTID>"
paths:
/request/create:
post:
summary: Create a new guest request
operationId: createServiceRequest
x-google-backend:
address: https://us-central1-<GCPPROJECTID>.cloudfunctions.net/createServiceRequest
security:
- firebase_auth: []
responses:
'200':
description: A successful response
/request/update:
put:
summary: Update request status (Staff)
operationId: updateRequestStatus
x-google-backend:
address: https://us-central1-<GCPPROJECTID>.cloudfunctions.net/updateRequestStatus
security:
- firebase_auth: []
responses:
'200':
description: A successful response
/menu/items:
get:
summary: Retrieve available menu items
operationId: getMenuItems
x-google-backend:
address: https://us-central1-<GCPPROJECTID>.cloudfunctions.net/getMenuItems
security:
- firebase_auth: []
responses:
'200':
description: List of available menu items
/request/guest:
get:
summary: Retrieve requests for a specific guest
operationId: getGuestRequests
x-google-backend:
address: https://us-central1-<GCPPROJECTID>.cloudfunctions.net/getGuestRequests
parameters:
- in: query
name: guest_id
type: string
required: true
security:
- firebase_auth: []
responses:
'200':
description: List of guest requests
/request/service:
get:
summary: Retrieve requests for a specific service type
operationId: getServiceRequests
x-google-backend:
address: https://us-central1-<GCPPROJECTID>.cloudfunctions.net/getServiceRequests
parameters:
- in: query
name: service_type
type: string
required: true
security:
- firebase_auth: []
responses:
'200':
description: List of service requests
/user/update:
put:
summary: Update user details (Staff)
operationId: updateUserDetails
x-google-backend:
address: https://us-central1-<GCPPROJECTID>.cloudfunctions.net/updateUserDetails
security:
- firebase_auth: []
responses:
'200':
description: User details updated successfully
/user/create:
post:
summary: Create a new guest user (Staff)
operationId: createGuestUser
x-google-backend:
address: https://us-central1-<GCPPROJECTID>.cloudfunctions.net/createGuestUser
security:
- firebase_auth: []
responses:
'200':
description: Guest user created successfully
/user/profile:
get:
summary: Get a user's profile by UID
operationId: getUserProfile
x-google-backend:
address: https://us-central1-<GCPPROJECTID>.cloudfunctions.net/getUserProfile
parameters:
- in: query
name: uid
type: string
required: true
security:
- firebase_auth: []
responses:
'200':
description: User profile data
/staff/overview:
get:
summary: Get task counts for the staff dashboard
operationId: getStaffOverview
x-google-backend:
address: https://us-central1-<GCPPROJECTID>.cloudfunctions.net/getStaffOverview
security:
- firebase_auth: []
responses:
'200':
description: An object with service types and their task counts.
/users/guests:
get:
summary: Get a list of all active guests
operationId: getActiveGuests
x-google-backend:
address: https://us-central1-<GCPPROJECTID>.cloudfunctions.net/getActiveGuests
security:
- firebase_auth: []
responses:
'200':
description: A list of guest user objects.
Step 2: Enable Required Services
Make sure you have the necessary GCP services enabled.
gcloud services enable apigateway.googleapis.com
gcloud services enable servicemanagement.googleapis.com
gcloud services enable servicecontrol.googleapis.com
Step 3: Create the API and API Config
Use the gcloud CLI to create an API resource and then an API configuration from the YAML file.
1. Create the API resource:
gcloud api-gateway apis create myconcierge-api --project=<GCPPROJECTID>
2. Create the API Config:
gcloud api-gateway api-configs create myconcierge-api-config \
--api=myconcierge-api --openapi-spec=openapi-spec.yaml \
--project=<GCPPROJECTID> --backend-auth-service-account=0
(Note: The service account flag will be configured in a later step for security)
Step 4: Create the Gateway
Create the gateway and attach the API configuration to it. Replace us-central1 with your region if different.
gcloud api-gateway gateways create myconcierge-gateway \
--api=myconcierge-api --api-config=myconcierge-api-config \
--location=us-central1 --project=<GCPPROJECTID>
After running this, you'll get a default hostname for your gateway (e.g., myconcierge-gateway-a1b2c3d4.uc.gateway.dev). This is your new single endpoint for the functions defined in the OpenAPI spec.
Step 5: Grant Permissions (Crucial!)
Your new gateway needs permission to invoke your Cloud Functions.
- Get the Gateway's Service Account: Find the service account that was automatically created for your gateway. You can find this in the Google Cloud Console under "API Gateway" by selecting your gateway and looking at its details.
- Grant Invoker Role: For each function listed in your openapi-spec.yaml, you must grant the "Cloud Functions Invoker" role to the gateway's service account.
gcloud functions add-iam-policy-binding $YOURFUNCTIONNAME \
--region=$REGION --member="serviceAccount:<GCPPROJECTID>@appspot.gserviceaccount.com" --role="roles/cloudfunctions.invoker"
Build Code
Helper Functions
Below are the helper functions, they are available in gcp_deployment folder of the repo link.
- Creation of sample records
- Creation of menu items
- Convert icon from svg to png
- Hotel Staff record creation