Skip to content

potniq/groundwork

Repository files navigation

Groundwork by Potniq

CircleCI

City transport intelligence for business travelers. You land in an unfamiliar city, Groundwork tells you what transport exists, how to pay, when it runs, and how to get from the airport.

URL: groundwork.potniq.com

Quick Start

# Install uv (one-time)
curl -LsSf https://astral.sh/uv/install.sh | sh
source "$HOME/.local/bin/env"

# Install/pin Python and deps for this repo
uv python install 3.13
uv venv --python 3.13
uv pip install -r requirements.txt

# Run locally (needs Postgres running)
export DATABASE_URL="postgresql://postgres:postgres@localhost:5432/groundwork"
export PERPLEXITY_API_KEY="your-key"
export ADMIN_API_KEY="your-secret-admin-key"
export VERIFY_GENERATED_URLS="true"
export URL_VERIFICATION_TIMEOUT_SECONDS="8"
export POSTHOG_PUBLIC_KEY="phc_your_project_key" # optional
export POSTHOG_HOST="https://us.i.posthog.com"   # optional
export POSTHOG_DEBUG="false"                     # optional
export POSTHOG_CAPTURE_CONSOLE_ERRORS="true"    # optional
export POSTHOG_RECORD_CONSOLE_LOGS="true"       # optional
uv run uvicorn app.main:app --reload --port 8000

# Run tests
uv run pytest tests/ --junitxml=test-results/results.xml

# Run with Docker
docker build -t groundwork .
docker run -p 8000:8000 --env-file .env groundwork

Stack

  • FastAPI + Jinja2 templates (no frontend build step)
  • PostgreSQL (city intel stored in JSONB)
  • Perplexity Sonar Pro for city transport research
  • Docker for runtime/CI
  • Supabase CLI migrations
  • Python 3.13

Data Model

cities

  • slug, city_name, country, country_code
  • optional latitude, longitude, metro_area_name
  • status in generating | ready | failed
  • intel JSONB validated as CityIntel

city_requests

  • raw_input, optional email
  • status in pending | fulfilled | ignored
  • visitor requests are stored for manual admin review

API

  • GET / homepage with search/filter + city request form
  • GET /cities list ready cities
  • GET /cities/{slug} city JSON
  • GET /{slug} city HTML guide
  • POST /cities admin-only generation endpoint (X-API-Key)
  • GET /requests public HTML page listing submitted city requests
  • POST /requests public city request intake
  • GET /health healthcheck

POST /cities payload

{
  "city_name": "Barcelona",
  "country": "Spain",
  "country_code": "ES",
  "latitude": 41.3874,
  "longitude": 2.1686,
  "slug": "barcelona-es"
}
  • If slug is omitted, it auto-generates as slugify("{city_name}-{country_code}").
  • Perplexity is called synchronously; failed generation sets status='failed'.
  • Generated links are validated server-side; if invalid links are found, generation is retried once with corrective feedback.

POST /requests payload

{
  "raw_input": "Joburg",
  "email": "optional@example.com"
}
  • Inserts into city_requests with status='pending'.
  • No automatic city generation is triggered.

Migrations

Create a new migration:

./scripts/new_migration.sh <name>
# or: supabase migration new <name>

Apply migrations to a database URL:

./scripts/run_migrations.sh
# or: supabase db push --db-url "$DATABASE_URL"

Generate A City

Use the helper script to trigger POST /cities against local API by default:

./scripts/research_city.sh \
  --city-name "Barcelona"
  • Defaults to http://127.0.0.1:8000
  • Reads ADMIN_API_KEY from .env automatically
  • Auto-resolves country and country_code from city name if omitted
  • Override API URL/key with --api-url and --api-key

Generate a starter set of 30 cities sequentially:

./scripts/research_cities_batch.sh
  • Uses a predefined list of 30 major cities
  • Calls ./scripts/research_city.sh one-by-one
  • Optionally throttle requests: --delay-seconds 2

Tests

  • Unit tests (no DB):
    • pytest -m unit tests/unit
  • Integration tests (Postgres-backed):
    • pytest -m integration tests/integration
  • Perplexity responses are mocked with pytest-httpx
  • Coverage includes city creation/auth, custom slug, city requests flow, and unit helpers

Analytics

  • PostHog web analytics is optional and anonymous by default.
  • Set POSTHOG_PUBLIC_KEY to enable the browser snippet.
  • POSTHOG_HOST defaults to https://us.i.posthog.com; override it if your PostHog project is hosted elsewhere.
  • POSTHOG_DEBUG enables verbose PostHog SDK logging; it is also enabled automatically on localhost and 127.0.0.1.
  • POSTHOG_CAPTURE_CONSOLE_ERRORS enables PostHog exception capture for browser console errors.
  • POSTHOG_RECORD_CONSOLE_LOGS records console logs alongside session replay when PostHog supports it.
  • Groundwork initializes PostHog with person_profiles="identified_only" so anonymous traffic is captured until a user explicitly shares their email.
  • Groundwork also sends explicit product events for meaningful user actions instead of relying only on generic click autocapture.
  • When a city request is submitted with an email address, Groundwork calls identify(email, { email, latest_requested_city }) and includes requester_email plus requested_city_input on city_request_submitted. This makes PostHog workflows available for thank-you and follow-up emails tied to the submitted request.
  • City pages also register PostHog super properties so all subsequent events on that page can be attributed to the current city guide.
  • Groundwork mirrors custom analytics events to the browser console and sends structured client_log / client_exception data into PostHog for handled client-side issues.
  • Browser-visible 404 and 500 pages are rendered as HTML and tracked with PostHog so broken page hits are measurable.

Event Taxonomy

  • Built-in PostHog events:
    • $pageview
    • $pageleave
  • Custom Groundwork events:
    • city_search_performed: homepage search used, with query length and result count.
    • city_guide_opened: a guide card was opened from the homepage, including city and entrypoint (card_title or card_cta).
    • city_guide_viewed: a city guide page loaded, including city and guide-shape metadata.
    • guide_section_viewed: a guide section entered the viewport for the first time.
    • guide_resource_clicked: a user clicked an outbound authority, app-store, payment, airport, or delay-info link.
    • feedback_survey_requested: the city-guide feedback trigger was clicked. Use this event in PostHog survey display conditions instead of a CSS selector trigger.
    • posthog_loaded: the PostHog SDK loaded with the current debug/error-capture settings.
    • client_log: an application log line was mirrored into PostHog from the browser.
    • client_exception: a handled client-side exception was captured when captureException was not available.
    • page_not_found_viewed: an HTML 404 page was shown to the visitor.
    • application_error_viewed: an HTML 500 page was shown to the visitor.
    • city_request_started: a user engaged with the request-city form.
    • city_request_submitted: a city request was submitted successfully, including the requested city text and requester email when provided.
    • city_request_submission_failed: request submission failed client-side or server-side.
    • navigation_clicked: header navigation back to home.
    • external_link_clicked: footer outbound links to Potniq or GitHub.

Survey Triggering

  • For the city feedback survey, prefer PostHog's "user sends event" display condition with feedback_survey_requested.
  • Do not target .feedback-button as a CSS selector in survey conditions, because selector-based triggers can fire whenever the element is present on the page.

CircleCI Deploy Flow

For a detailed pipeline writeup, see docs/cicd/pipeline.md.

  • Non-main branches: run install-deps -> test-unit + test-integration + scan-python-deps
  • main branch: run install-deps -> test-unit + test-integration + scan-python-deps -> build-docker -> verify-docker + scan-docker-image -> run-production-migrations -> push-docker -> deploy-digitalocean

Required CircleCI Contexts

groundwork_docker context:

  • DOCKERHUB_USERNAME
  • DOCKERHUB_PASSWORD (Docker Hub access token recommended)
  • DOCKERHUB_REPO (example: your-org/groundwork)

groundwork_supabase context:

  • SUPABASE_DB_URL (direct Postgres connection string for production, percent-encoded if needed)

groundwork_digitalocean context:

  • DIGITALOCEAN_ACCESS_TOKEN
  • DIGITALOCEAN_APP_ID

snyk context:

  • SNYK_TOKEN

Deployment Notes

  • CircleCI pushes two tags to Docker Hub on main:
    • ${CIRCLE_SHA1}
    • latest
  • Python dependencies are installed in a dedicated install-deps job, cached, and persisted to workspace as /home/circleci/.local for downstream test/scan jobs.
  • CircleCI runs Snyk dependency scanning against requirements.txt before Docker build.
  • Docker image build and push are split into separate jobs (image is saved/loaded via workspace) to keep deploy sequencing explicit and more reliable.
  • CircleCI verifies the built image by starting Postgres + app containers, polling GET /health, and running API E2E smoke checks (/cities, /cities/{slug}, /requests) with PERPLEXITY_MOCK_RESPONSE_FILE set to a fixture.
  • CircleCI runs Snyk container scanning on the built image (${DOCKERHUB_REPO}:latest) before migrations/push.
  • CircleCI runs production SQL migrations before deploy using:
    • npx supabase@latest db push --db-url "$SUPABASE_DB_URL" --include-all
  • CircleCI then triggers App Platform deploy using:
    • doctl apps create-deployment "$DIGITALOCEAN_APP_ID" --wait
  • Runtime app secrets (DATABASE_URL, PERPLEXITY_API_KEY, ADMIN_API_KEY) should be set in DigitalOcean App Platform settings.

About

Groundwork by Potniq - City Travel Insights

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors