Admin Venue Creation - Design โ
Status: Draft (building). Issue: #786. Tier-1 #1 of the alpha-readiness gap map (docs/planning/plans/2026-08-03_alpha-readiness-gap-map.md). Author: autonomous session 2026-08-04.
Problem โ
No admin UI creates or edits a net-new venue. Venues only enter the venues collection through OSM import (web app / CLI), never the admin portal. Without at least one venue in dev, no lantern can be lit and no merchant offer can be associated. This is the top supply-side gate for the December 2026 invite-only soft alpha (1-2 San Diego venues).
Existing admin venue surfaces are read/config only:
apps/admin/src/components/venues/AdminVenuePicker.jsx- pick an existing venue.apps/admin/src/admin/config/ConfigVenue.jsx- edit import config (tiers/thresholds), not venues.
Goals โ
- An admin can create a venue from the portal: name, address, coordinates, category.
- An admin can edit the core fields of an existing venue.
- The create path is safe: server-authoritative, admin-only, keeps the client Firestore lockdown intact.
- Reliable for the 1-2 alpha venues today; no dependency on flaky OSM/Overpass.
Non-goals โ
- In-portal OSM/Overpass bulk import (that is #224, a separate server-side redesign).
- Merchant self-service venue claim/linking (#171).
- Venue deletion from the portal (Firestore rules forbid client delete; the
/venues/admin/cleanup/orphanedjob already handles server-side removal). - A full venue CRUD table with pagination/search at scale. A simple list + form covers 1-2 venues; scale later.
Decision: server-side admin-gated create/edit (recommended), not OSM import, not client rules โ
The clean path extends the venues Cloud Run service's existing admin router. Recommendation first, then the matrix.
| Option | What it does | What it affects | Risks / implications |
|---|---|---|---|
| Server admin endpoint (chosen) | New POST/PUT /venues/admin/venues on venues-api; Admin SDK writes source:'manual' with geohash + nameLower | Adds 2 routes + an admin page; reuses existing admin+AppCheck gating, dedup, geohash | Small new surface. Admin must supply coords (mitigated by reverse-geocode autofill). Correct long-term home. |
| In-portal OSM import trigger | Admin picks an area, portal calls /venues/import/osm | No new write path; reuses import | Overpass timeouts/flakiness; imports a whole area, not one venue; #224 wants this redesigned server-side anyway. Overkill for 1-2 venues. |
| Loosen client Firestore rules | Let an admin browser write a venue doc directly | firestore.rules venue-create | Weakens the #689 manual-only lockdown; trusts the client; regresses a security fix. Rejected. |
Why server-side wins: API-first (AGENTS.md rule 9), reuses requireRole('admin') + App Check already on /venues/admin, reuses importVenuesToFirestore's doc shape (geohash via geofire-common, nameLower dedup key), and keeps every client write path locked. The Admin SDK bypasses firestore.rules, so no rule change is needed and the source:'manual' client branch stays as-is.
Design โ
Server (services/api/venues) โ
New router routes/adminVenues.js (or extend routes/admin.js) mounted under the existing /venues/admin (already verifyAppCheck + verifyFirebaseToken + requireRole('admin')).
POST /venues/admin/venues- create. Zod body:{ name, address?, lat, lng, category, priceLevel? }.- Server computes:
geohash = geohashForLocation([lat,lng]),nameLower = toNameLower(name),source:'manual',activeLanternCount:0,createdAt = serverTimestamp(),addressComponents:null(enrichable later). - If
addressomitted, best-effort reverse-geocode via the in-serviceenrichVenueAddress(lat,lng)to autofill address + components. - Dedup guard: reject (409) if a venue exists with the same
nameLowerwithin ~50m (geohash-neighbor scan), unlessforce:true.
- Server computes:
PUT /venues/admin/venues/:id- edit. Zod body: partial{ name?, address?, lat?, lng?, category?, priceLevel? }. Recompute geohash/nameLower when coords/name change. Never touchesactiveLanternCount,source,osmId, ormerchantId.- Both write via a new
createManualVenue/updateManualVenueinvenue.service.jsto keep route handlers thin and unit-testable. openapi.jsonupdated for both routes (rule 8 / openapi-sync lint; venues-api is inENFORCED_SERVICES).
Admin UI (apps/admin/src/admin) โ
- New nav item Venues under the Platform section (near Merchants), route
/admin/venues. - New
admin/venues/VenuesPage.jsx: a live list of existing venues (FirestoreonSnapshotonvenues, ordered by name, capped) + an Add Venue button opening an inline form/modal, and per-row Edit. - Form (
VenueForm.jsx): name, address (auto-fillable), lat, lng, category (StyledSelect, react-select rule 10), optional price level. Client validation: required name + valid coords; category from a curated normalized list. - New
shared/lib/venuesApi.jsclient module (mirrorsmoderationApi.js:authRequest+parseResponse, baseVITE_VENUE_API_URL). - Category options: curated normalized Lantern categories (
bar,coffee_shop,restaurant,library,gym,bookstore, ... +other) sourced from theCATEGORY_MAPtargets in@lantern/shared/venues.
Tests + verification โ
- Handler-level tests for both endpoints (house pattern, see
roles.route.test.js): create shape, dedup 409, edit recompute, admin-gating already covered by the mount. - Component test for
VenueForm(validation + submit payload) and a smoke test forVenuesPage. - Storybook story for the page/form (story-coverage lint).
- test-plan doc under
docs/engineering/testing/runs/admin-venue-creation/+ emulator exercise of the create/edit endpoints (Admin-SDK-against-emulator orfirebase emulators:exec), per the operator's test-first rule. Live browser pass left for the operator.
Open questions โ
- Curated category list vs. free text: start curated (dropdown) +
other; revisit if alpha venues need a type not in the list. - Price level: OSM venues carry it; keep optional on manual create.
Rollout โ
Single logical commit on the session branch feat/admin-venue-creation โ session draft PR (Closes #786). No flag; additive admin-only surface. Depends on nothing; unblocks lighting + merchant offers on dev once the alpha venues are seeded (gap map Tier-2 #5 pre-warm).