Google Maps Integration — EquipQR
Architecture Overview
EquipQR uses Google Maps for three main features: Fleet Map visualization, Places Autocomplete address picking, and Geocoding. The integration spans frontend hooks/components and server-side Supabase Edge Functions.
Browser Supabase Edge Functions
─────── ──────────────────────
useGoogleMapsKey ──→ public-google-maps-key ──→ GOOGLE_MAPS_BROWSER_KEY
useGoogleMapsLoader ──→ Google Maps JS API (loads <script> with browser key)
GooglePlacesAutocomplete ──→ PlaceAutocompleteElement (web component)
└─→ places-autocomplete edge fn (fallback) ──→ GOOGLE_MAPS_SERVER_KEY
MapView (GoogleMap) ──→ @react-google-maps/api
ClickableAddress ──→ maps.google.com deep link
geocode-location edge fn ──→ GOOGLE_MAPS_SERVER_KEY + geocoded_locations cache
API Keys
Two separate keys with different restrictions:
| Key | Env Var | Where Set | Used By |
|---|---|---|---|
| Browser key | GOOGLE_MAPS_BROWSER_KEY | Supabase Edge Function secrets | public-google-maps-key → served to browser |
| Server key | GOOGLE_MAPS_SERVER_KEY | Supabase Edge Function secrets | geocode-location, places-autocomplete |
Local development: Set both in supabase/functions/.env (NOT the root .env).
Production: Set in Supabase Dashboard → Project → Settings → Edge Functions → Secrets.
Important: These are NOT Vercel env vars. Redeploying on Vercel does not affect these secrets.
Legacy key names (still supported with fallback)
| Legacy Name | New Canonical Name |
|---|---|
VITE_GOOGLE_MAPS_BROWSER_KEY | GOOGLE_MAPS_BROWSER_KEY |
GOOGLE_MAPS_API_KEY | GOOGLE_MAPS_SERVER_KEY |
VITE_GOOGLE_MAPS_API_KEY | GOOGLE_MAPS_SERVER_KEY |
All edge functions prefer the new name and fall back to legacy names.
Frontend Components & Hooks
useGoogleMapsKey (src/hooks/useGoogleMapsKey.ts)
Fetches the browser API key from the public-google-maps-key edge function. Returns { googleMapsKey, isLoading, error, retry }.
useGoogleMapsLoader (src/hooks/useGoogleMapsLoader.ts)
Singleton Google Maps JS API loader. Ensures the <script> tag is loaded exactly once across all components. Returns { isLoaded, loadError, googleMapsKey, isKeyLoading, keyError, retry }.
Always use this hook — never load the Maps JS API manually.
GooglePlacesAutocomplete (src/components/ui/GooglePlacesAutocomplete.tsx)
Address picker with three-tier fallback strategy:
- Web component —
PlaceAutocompleteElement(Google's native widget) - Edge function —
places-autocompleteedge function proxy (if web component fails, e.g. Places API New not enabled) - Plain text — simple text input fallback
Props: { value, onPlaceSelect, onClear, placeholder, disabled, className, isLoaded }.
Returns PlaceLocationData: { formatted_address, street, city, state, country, lat, lng }.
MapView (src/features/fleet-map/components/MapView.tsx)
Fleet map component using @react-google-maps/api (GoogleMap, MarkerF, InfoWindowF). Renders colored markers by location source (team, equipment, scan, geocoded) and star markers for team HQ locations.
ClickableAddress (src/components/ui/ClickableAddress.tsx)
Renders an address as a link that opens Google Maps.
Edge Functions
public-google-maps-key
- Purpose: Serve the browser API key to authenticated users
- Auth: JWT required (
verify_jwt = true) - Key:
GOOGLE_MAPS_BROWSER_KEY - Pattern: User-scoped client with
requireUser
geocode-location
- Purpose: Geocode addresses with caching in
geocoded_locationstable - Auth: JWT + org membership verification
- Key:
GOOGLE_MAPS_SERVER_KEY - Rate limit: 30 cache misses per org per minute
- Cache:
geocoded_locationstable (upsert onorganization_id, normalized_text) - Input normalization: lowercase, trim, collapse whitespace
places-autocomplete
- Purpose: Proxy Google Places Autocomplete and Details APIs
- Auth: JWT required
- Key:
GOOGLE_MAPS_SERVER_KEY - Actions:
autocomplete(predictions) anddetails(structured address) - Validation: Zod discriminated union schema
Common Tasks
Adding a new map feature
- Use
useGoogleMapsLoaderto getisLoadedstate - Only render Google Maps components when
isLoaded === true - Handle
loadErrorandkeyErrorgracefully with error boundaries
tsx1import { useGoogleMapsLoader } from '@/hooks/useGoogleMapsLoader'; 2 3function MyMapFeature() { 4 const { isLoaded, loadError, retry } = useGoogleMapsLoader(); 5 6 if (loadError) return <ErrorState onRetry={retry} />; 7 if (!isLoaded) return <Skeleton />; 8 9 return <GoogleMap /* ... */ />; 10}
Using the address picker
tsx1import GooglePlacesAutocomplete, { PlaceLocationData } from '@/components/ui/GooglePlacesAutocomplete'; 2import { useGoogleMapsLoader } from '@/hooks/useGoogleMapsLoader'; 3 4function AddressField() { 5 const { isLoaded } = useGoogleMapsLoader(); 6 7 const handlePlaceSelect = (data: PlaceLocationData) => { 8 // data.formatted_address, data.street, data.city, 9 // data.state, data.country, data.lat, data.lng 10 }; 11 12 return ( 13 <GooglePlacesAutocomplete 14 isLoaded={isLoaded} 15 onPlaceSelect={handlePlaceSelect} 16 placeholder="Enter address..." 17 /> 18 ); 19}
Geocoding an address server-side
Call the geocode-location edge function:
ts1const { data, error } = await supabase.functions.invoke('geocode-location', { 2 body: { organizationId: orgId, input: addressString } 3}); 4// data: { lat, lng, formatted_address } or { lat: null, lng: null }
Adding a new Edge Function that uses Google Maps
- Use the
edge-function-creatorskill to scaffold the function - Read the server key:
Deno.env.get("GOOGLE_MAPS_SERVER_KEY") - Always fall back to legacy names:
|| Deno.env.get("GOOGLE_MAPS_API_KEY") - Never expose the server key in responses — only return processed data
Debugging
"Map Configuration Error" toast
The browser key fetch failed. Check:
GOOGLE_MAPS_BROWSER_KEYis set in Supabase Edge Function secrets- The
public-google-maps-keyfunction is deployed - The user is authenticated (JWT required)
Places autocomplete not showing results
- Check if web component mode failed (console:
AutocompletePlaces blocked) - The component auto-falls-back to edge function mode
- Verify
GOOGLE_MAPS_SERVER_KEYis set in Supabase secrets - Check the
places-autocompleteedge function logs
Geocoding returns { lat: null, lng: null }
- Address may be unresolvable — check raw address string
- Rate limit may be exceeded (30/min/org) — check for 429 responses
- Verify
GOOGLE_MAPS_SERVER_KEYis valid and has Geocoding API enabled
Fleet map markers not appearing
- Confirm
isLoadedis true before rendering<GoogleMap> - Check that equipment has valid
lat/lngcoordinates - Verify the location source in
EquipmentLocation.sourcefield
Google Cloud Console requirements
The project requires these APIs enabled:
| API | Used By |
|---|---|
| Maps JavaScript API | Fleet map, web component autocomplete |
| Places API | Web component autocomplete |
| Places API (Legacy) | places-autocomplete edge function |
| Geocoding API | geocode-location edge function |
Browser key restrictions should allow the app's domain(s). Server key should have no referrer restrictions (IP restrictions recommended).
Key files
| File | Purpose |
|---|---|
src/hooks/useGoogleMapsKey.ts | Fetch browser API key from edge function |
src/hooks/useGoogleMapsLoader.ts | Singleton Maps JS API loader |
src/components/ui/GooglePlacesAutocomplete.tsx | Address picker component |
src/components/ui/ClickableAddress.tsx | Address → Google Maps link |
src/features/fleet-map/components/MapView.tsx | Fleet map with markers |
src/features/fleet-map/pages/FleetMap.tsx | Fleet map page |
src/features/fleet-map/components/FleetMapErrorBoundary.tsx | Map error boundary |
src/utils/effectiveLocation.ts | Location resolution + Maps URL builder |
src/services/placesAutocompleteService.ts | Edge function client for places |
supabase/functions/public-google-maps-key/index.ts | Browser key edge function |
supabase/functions/geocode-location/index.ts | Geocoding edge function |
supabase/functions/places-autocomplete/index.ts | Places proxy edge function |