How I Built ServiceHub’s Geolocation Backend
When I set out to create ServiceHub I wanted a single source of truth for location data, real‑time matching, and a lightweight admin console. The result is a mix of Firebase Cloud Functions, an Express server hosted on Render.com, and a vanilla‑JS front‑end that talks to both. Below I walk through the key pieces that make it tick.
Project Structure
The repository is split into three logical folders:
functions/– Firebase Functions that expose geolocation utilities, email alerts and Anthropic AI hooks.render-server/– An Express API that handles money, admin actions and rate‑limiting for the public‑facing side.admin‑*.html– Simple HTML shells that load a modular JavaScript app (app.js).
Each part has its own package.json so I can version and deploy them independently.
Firebase Functions – The Core Geolocation Engine
The functions/package.json declares the runtime and all the libraries I need for location math:
{
"name": "servicehub-geo-functions",
"main": "index.js",
"scripts": {
"serve": "firebase emulators:start --only functions",
"deploy": "firebase deploy --only functions"
},
"dependencies": {
"firebase-admin": "^12.6.0",
"firebase-functions": "^5.0.1",
"ngeohash": "^0.6.3",
"geofire-common": "^6.0.0",
"cors": "^2.8.5",
"nodemailer": "^6.9.14",
"@anthropic-ai/sdk": "^0.39.0"
}
}
The most useful libraries are ngeohash for encoding lat/long into geohashes and geofire-common for radius queries. With these I can store a user’s location in Firestore and efficiently query “who’s within 5 km?”.
Lazy‑Loading Optional Modules
In app.js I guard non‑essential code (like the notification bell) behind a tiny helper that swallows import errors. This ensures a broken optional module never brings down the whole app:
const safeImport = async (path) => {
try { return await import(path); }
catch (e) { console.warn("[boot] optional module failed:", path, e); return null; }
};
await safeImport("./js/notifications/notification-bell.js");
The pattern is simple but powerful: the UI stays responsive even if a third‑party script fails to load in a user’s browser.
Express Render Server – Public API & Rate Limiting
The render-server/package.json spins up a tiny Express app:
{
"name": "servicehub-render-server",
"main": "server.js",
"scripts": { "start": "node server.js", "dev": "node --watch server.js" },
"dependencies": {
"express": "^4.19.2",
"cors": "^2.8.5",
"express-rate-limit": "^7.4.0",
"firebase-admin": "^12.6.0",
"geofire-common": "^6.0.0",
"ngeohash": "^0.6.3",
"nodemailer": "^6.9.14"
}
}
Key choices:
- CORS is enabled globally because the admin UI lives on a different sub‑domain.
- express‑rate‑limit protects the public endpoints from abuse (e.g., mass location lookups).
- firebase‑admin gives the server privileged access to Firestore without needing a client SDK.
The server file (server.js) wires these together, exposing routes like /api/nearby that call the same geohash utilities used in the Firebase Functions.
Admin UI – Plain HTML + Modular JS
Instead of a heavy framework I opted for a minimal HTML shell that loads a single ES‑module (app.js). The two admin pages (admin-broadcasts.html and admin-notifications.html) are almost identical; the only difference is the hash they force‑navigate to:
<script>
if (location.hash !== "#admin/broadcasts") location.replace("./#admin/broadcasts");
</script>
The UI pulls in a shared stylesheet, Font Awesome icons, and a small support‑chat widget. All business logic lives in js/ – for example js/router.js creates a client‑side router, and js/utils.js holds helpers like toast and updateWalletHeader.
Development & Deployment Workflow
I keep the three pieces independent but linked by npm scripts:
npm run dev(in the root) starts the local server (serve.cjs) on port 8080.cd functions && npm run servelaunches the Firebase emulator for rapid iteration.cd render-server && npm run devwatchesserver.jsfor changes on Render’s dev environment.
When I’m ready for production I run:
npm run build # (if a build step existed)
cd functions && npm run deploy
cd render-server && npm start # Render.com runs this automatically
The netlify.toml file tells Netlify to publish the static admin UI from the repository root, so the whole stack can be served from a single domain if desired.
Lessons Learned
- Separate concerns: keeping the geolocation logic in Firebase Functions lets me scale it automatically, while the Express server handles rate‑limited public traffic.
- Graceful degradation: the
safeImportpattern prevents optional UI features from breaking the core experience. - Minimal front‑end: a vanilla‑JS app loads faster and is easier to audit than a heavyweight framework, which is important for an admin console that must feel snappy.
- Consistent tooling: using Node 20 across all services avoids version drift and simplifies CI.
ServiceHub now supports real‑time proximity alerts, email broadcasts, and a clean admin experience—all built from a handful of well‑chosen npm packages.