Skip to content

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/orphaned job 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.

The clean path extends the venues Cloud Run service's existing admin router. Recommendation first, then the matrix.

OptionWhat it doesWhat it affectsRisks / implications
Server admin endpoint (chosen)New POST/PUT /venues/admin/venues on venues-api; Admin SDK writes source:'manual' with geohash + nameLowerAdds 2 routes + an admin page; reuses existing admin+AppCheck gating, dedup, geohashSmall new surface. Admin must supply coords (mitigated by reverse-geocode autofill). Correct long-term home.
In-portal OSM import triggerAdmin picks an area, portal calls /venues/import/osmNo new write path; reuses importOverpass timeouts/flakiness; imports a whole area, not one venue; #224 wants this redesigned server-side anyway. Overkill for 1-2 venues.
Loosen client Firestore rulesLet an admin browser write a venue doc directlyfirestore.rules venue-createWeakens 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 address omitted, best-effort reverse-geocode via the in-service enrichVenueAddress(lat,lng) to autofill address + components.
    • Dedup guard: reject (409) if a venue exists with the same nameLower within ~50m (geohash-neighbor scan), unless force:true.
  • PUT /venues/admin/venues/:id - edit. Zod body: partial { name?, address?, lat?, lng?, category?, priceLevel? }. Recompute geohash/nameLower when coords/name change. Never touches activeLanternCount, source, osmId, or merchantId.
  • Both write via a new createManualVenue / updateManualVenue in venue.service.js to keep route handlers thin and unit-testable.
  • openapi.json updated for both routes (rule 8 / openapi-sync lint; venues-api is in ENFORCED_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 (Firestore onSnapshot on venues, 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.js client module (mirrors moderationApi.js: authRequest + parseResponse, base VITE_VENUE_API_URL).
  • Category options: curated normalized Lantern categories (bar, coffee_shop, restaurant, library, gym, bookstore, ... + other) sourced from the CATEGORY_MAP targets 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 for VenuesPage.
  • 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 or firebase 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).

Built with VitePress