# Project export: Beacon

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: CruzHacks 2026
- Tagline: AI-assisted map of independent, carbon-friendly destinations
- Devpost: https://devpost.com/software/beacon-vz2gr8
- GitHub: https://github.com/Seanathan10/Beacon
- Demo: https://ch2026.vercel.app/
- Video: https://www.youtube.com/embed/hUzXwcyRm8I?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 4 GitHub contributor(s) — Sean Singh (98 commits), Prasiddh Pooskur (97 commits), iconsumeplutonium (78 commits), Jay Tripathi (15 commits)

## Devpost submission (written by the team)

### Inspiration

Over the last decades, corporations have grown to encompass almost all of our lives. We'll get in our carbon dioxide-spewing vehicle, drive to a national gas station megachain to fill our cars full of gas, then drive to a corporate-owned national chain store to buy clothes, followed by a trip to a national megachain of restaurants for food, all while spending hours in traffic. When thinking of places to visit or where to go, these corporate megachains are often the first places we think of planning trips. Yet, looking around, we often see many cool new places popping up around cities every day that get overshadowed by these companies. These include locally sourced farmer's markets, quaint mom-and-pop shops, or trendy cafés touting fancy drinks such as matcha lattes. The simplicity of sharing or finding local attractions would allow users to easily find trendy or local popup spots near where they live, without needing the use of a car.

### What it does

Upon logging in, the user is greeted with a world map that has pins on it. Each pin represents a cool local point of interest, such as a small business or attraction. The user can click on the pin to see a picture of the location, read a description, and read other users' comments. The user can also comment on the pin themselves, leave a like on the pin, or bookmark it to view later. The user can also click on a location on the map, and place a pin in that spot, assigning it a name, uploading an image, and writing a description. This pin is then visible to all other users. On the map, the more likes a pin has, the larger its circle will be. When you zoom out on the map, the user is able to see a heatmap showing where popular hotspots are. In addition, the user is able to plan trips to another city or country. They are able to input two destinations, and an AI assistant will help the user plan their trip by suggesting low-carbon methods of transportation, as well as create an itinerary for each day of their trip. It also provides insights as to how much more green the trip is compared to traditional transportation.

### How we built it

We used a combination of tech that we were familiar with, as well as some new technologies that we challenged ourselves with learning. Our stack consisted of the TypeScript language, using React.js + Vite for the frontend, Express.js and Node for the backend, and Sqlite3 driver for our database. We used the Mapbox API for the map functionality of the app, utilizing its location search APIs for quick address lookups. We used Google Gemini 3 Flash and the Google Maps API for itinerary planning, navigation, and flight suggestions.

### Challenges we ran into

While our group was familiar with most of the technologies we used for our project, we tread new ground by using new technologies we didn't have experience with, such as Mapbox. It took some time for us to get up to speed, especially since we were facing a lot of CORS issues with our backend and APIs early on. Furthermore, due to the fast paced environment of us working on overlapping code, we ran into a lot of merge conflicts, which slowed us down a lot and frustrated us. None of us had prior experience with Amadaeus or Google Maps, so we had a lot of trouble with getting flight tracking, carbon emission counting, and metro pathing to work until we got used to the APIs.

### Accomplishments we're proud of

One thing we were proud of accomplishing was the bells and whistles of our map, along with the interactivity and responsiveness of the navigation. As the user pans around the map, the sidebar refreshes to include the top rated attractions in the current part of the map shown in the viewport. Furthermore, when they zoom out, the user can see a heatmap of which general areas have a lot of attractions, signified through a red overlay on the map which gets darker the more attractions are in that area. We learned how to use the Haversine formula to calculate the distance between two coordinates on a globe, which was really interesting and motivated us to learn more.

### What we learned

We learned how to use Mapbox, server-side Sqlite, and travel planning APIs. Before this hackathon, none of us had prior experience with navigation or flight tracking, and we learned a huge amount along the way. We're all proud of what we built and look forward to expanding on it the future.

### What's next

We plan to further refine the AI travel guide. Users should be able to share their itinerary, invite others to it, and prompt the AI to find new places to visit next.

## README (from the GitHub repository)

# Inspiration
Over the last decades, corporations have grown to encompass almost all of our lives. We'll get in our carbon dioxide-spewing vehicle, drive to a national gas station megachain to fill our cars full of gas, then drive to a corporate-owned national chain store to buy clothes, followed by a trip to a national megachain of restaurants for food, all while spending hours in traffic. When thinking of places to visit or where to go, these corporate megachains are often the first places we think of planning trips. Yet, looking around, we often see many cool new places popping up around cities every day that get overshadowed by these companies. These include locally sourced farmer's markets, quaint mom-and-pop shops, or trendy cafés touting fancy drinks such as matcha lattes. The simplicity of sharing or finding local attractions would allow users to easily find trendy or local popup spots near where they live, without needing the use of a car.

# What it does
Upon logging in, the user is greeted with a world map that has pins on it. Each pin represents a cool local point of interest, such as a small business or attraction. The user can click on the pin to see a picture of the location, read a description, and read other users' comments. The user can also comment on the pin themselves, leave a like on the pin, or bookmark it to view later. The user can also click on a location on the map, and place a pin in that spot, assigning it a name, uploading an image, and writing a description. This pin is then visible to all other users. On the map, the more likes a pin has, the larger its circle will be. When you zoom out on the map, the user is able to see a heatmap showing where popular hotspots are.

In addition, the user is able to plan trips to another city or country. They are able to input two destinations, and an AI assistant will help the user plan their trip by suggesting low-carbon methods of transportation, as well as create an itinerary for each day of their trip. It also provides insights as to how much more green the trip is compared to traditional transportation.

# How we built it
We used a combination of tech that we were familiar with, as well as some new technologies that we challenged ourselves with learning. Our stack consisted of the TypeScript language, using React.js + Vite for the frontend, Express.js and Node for the backend, and Sqlite3 driver for our database. We used the Mapbox API for the map functionality of the app, utilizing its location search APIs for quick address lookups. We used Google Gemini 3 Flash and the Google Maps API for itinerary planning, navigation, and flight suggestions.

# Challenges we ran into
While our group was familiar with most of the technologies we used for our project, we tread new ground by using new technologies we didn't have experience with, such as Mapbox. It took some time for us to get up to speed, especially since we were facing a lot of CORS issues with our backend and APIs early on. Furthermore, due to the fast paced environment of us working on overlapping code, we ran into a lot of merge conflicts, which slowed us down a lot and frustrated us. None of us had prior experience with Amadaeus or Google Maps, so we had a lot of trouble with getting flight tracking, carbon emission counting, and metro pathing to work until we got used to the APIs.

# Accomplishments that we're proud of
One thing we were proud of accomplishing was the bells and whistles of our map, along with the interactivity and responsiveness of the navigation. As the user pans around the map, the sidebar refreshes to include the top rated attractions in the current part of the map shown in the viewport. Furthermore, when they zoom out, the user can see a heatmap of which general areas have a lot of attractions, signified through a red overlay on the map which gets darker the more attractions are in that area. We learned how to use the Haversine formula to calculate the distance between two coordinates on a globe, which was really interesting and motivated us to learn more.

# What we learned
We learned how to use Mapbox, server-side Sqlite, and travel planning APIs. Before this hackathon, none of us had prior experience with navigation or flight tracking, and we learned a huge amount along the way. We're all proud of what we built and look forward to expanding on it the future.

# What's next for Beacon
We plan to further refine the AI travel guide. Users should be able to share their itinerary, invite others to it, and prompt the AI to find new places to visit next.


# Environment Configuration Setup

This document explains the environment-based backend configuration for the Beacon application.

## Overview

The application now automatically uses the correct backend URL based on the environment:

- **Development** (`pnpm dev`): Uses local backend at `http://localhost:3000`
- **Production** (`pnpm build`): Uses hosted backend at `https://api.truthnuke.tech`

## Changes Made

### Frontend Configuration Files

1. **`.env.development`** - Development environment configuration
   - Sets `VITE_API_BASE=` (empty string for relative URLs)
   - Vite dev server proxies `/api` and `/heartbeat` to `localhost:3000`

2. **`.env.production`** - Production environment configuration
   - Sets `VITE_API_BASE=https://api.truthnuke.tech`
   - Used when building for production

3. **`.env`** - Base configuration (fallback)
   - Contains default values and API tokens
   - Overridden by environment-specific files

4. **`.env.example`** - Template for developers
   - Shows required environment variables
   - No sensitive values (safe to commit)

### Code Changes

1. **`Frontend/constants.ts`** - Updated API URL logic
   ```typescript
   // Before: Checked for VITE_API_URL == "local" (unused variable)
   // After: Uses VITE_API_BASE with auto-detection
   export const BASE_API_URL = import.meta.env.VITE_API_BASE !== undefined
       ? import.meta.env.VITE_API_BASE
       : (import.meta.env.MODE === 'development' ? API_URL_DEV : API_URL_PROD);
   ```

2. **`Frontend/vite.config.ts`** - Enhanced proxy configuration
   - Added comments explaining the proxy
   - Added `secure: false` for local development
   - Added `envPrefix: 'VITE_'` for clarity

3. **`Frontend/.gitignore`** - Updated to protect secrets
   - Ignores `.env` and `.env.local` files
   - Keeps versioned environment templates (`.env.development`, `.env.production`)

4. **`Frontend/README.md`** - Comprehensive documentation
   - Explains environment configuration
   - Setup instructions for developers
   - How the system works

## How It Works

### Development Mode

When you run `pnpm dev`:

1. Vite loads `.env.development` (overrides `.env`)
2. `VITE_API_BASE` is set to empty string `""`
3. API calls become relative: `/api/pins`, `/heartbeat`, etc.
4. Vite dev server proxy intercepts these and forwards to `http://localhost:3000`
5. No CORS issues because frontend and backend appear to be on same origin

### Production Mode

When you run `pnpm build`:

1. Vite loads `.env.production` (overrides `.env`)
2. `VITE_API_BASE` is set to `https://api.truthnuke.tech`
3. API calls become absolute: `https://api.truthnuke.tech/api/pins`, etc.
4. Backend CORS allows requests from Vercel production and preview deployments

## Testing

### Test Development Mode

1. Start the backend:
   ```bash
   cd Backend
   pnpm dev
   ```

2. In another terminal, start the frontend:
   ```bash
   cd Frontend
   pnpm dev
   ```

3. Open `http://localhost:5173`

4. Open browser DevTools → Network tab

5. Trigger an API call (e.g., login)

6. Verify the request goes to:
   - Request URL: `http://localhost:5173/api/login`
   - But is proxied to: `http://localhost:3000/api/login`

### Test Production Build

1. Build the frontend:
   ```bash
   cd Frontend
   pnpm build
   ```

2. Preview the build:
   ```bash
   pnpm preview
   ```

3. Open `http://localhost:4173`

4. Open browser DevTools → Network tab

5. Tr

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 202 recognized source files, 2115 KB.
- CSS (language) — detected in the code
- Express (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- SQL (language) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- Node.js (technology) — claimed on Devpost, not found in the code
- Vercel (technology) — claimed on Devpost, not found in the code
- AI coding agent: GitHub Copilot — evidence: config files committed to the repository

## Codebase structure (from repository index)

### Files (120 of 218)

```
.github/copilot-instructions.md
.github/copilot-mcp-config.json
.github/MCP_SETUP.md
.github/workflows/copilot-setup-steps.yml
.gitignore
.mcp.json
Backend/.gitignore
Backend/database/create.sh
Backend/database/create.sql
Backend/database/data.sql
Backend/database/db.ts
Backend/database/landmarks.sql
Backend/database/lc_cleaned.sql
Backend/database/lc.sql
Backend/eslint.config.js
Backend/index.ts
Backend/jest.config.js
Backend/openapi.yml
Backend/package.json
Backend/process_sql.py
Backend/repositories/bookmarkRepo.ts
Backend/repositories/challengeRepo.ts
Backend/repositories/commentRepo.ts
Backend/repositories/followRepo.ts
Backend/repositories/itineraryRepo.ts
Backend/repositories/leaderboardRepo.ts
Backend/repositories/likeRepo.ts
Backend/repositories/notificationRepo.ts
Backend/repositories/pinRepo.ts
Backend/repositories/pinStatusRepo.ts
Backend/repositories/postRepo.ts
Backend/repositories/searchRepo.ts
Backend/repositories/statsRepo.ts
Backend/repositories/userRepo.ts
Backend/routes/auth.ts
Backend/routes/bookmarks.ts
Backend/routes/challenges.ts
Backend/routes/comments.ts
Backend/routes/follows.ts
Backend/routes/leaderboard.ts
Backend/routes/likes.ts
Backend/routes/notifications.ts
Backend/routes/pins.ts
Backend/routes/pinStatus.ts
Backend/routes/plausible.ts
Backend/routes/posts.ts
Backend/routes/search.ts
Backend/routes/share.ts
Backend/routes/stats.ts
Backend/routes/trip.ts
Backend/routes/trips.ts
Backend/routes/users.ts
Backend/scripts/assign_wikidata_creators.py
Backend/scripts/manual_verify_ai.ts
Backend/scripts/MOCK_DATA.csv
Backend/scripts/places.md
Backend/scripts/run-jest.mjs
Backend/scripts/seed_ca_10.py
Backend/scripts/seed_from_places_md.py
Backend/scripts/seed_from_placesmd.py
Backend/scripts/seed_us_15_20.py
Backend/scripts/trim_mock_accounts.py
Backend/scripts/verify_db.ts
Backend/scripts/write_places.py
Backend/services/ai.ts
Backend/services/amadeus.ts
Backend/services/challenges.ts
Backend/services/googleRoutes.ts
Backend/services/hotelService.ts
Backend/services/notifications.ts
Backend/tests/auth.test.ts
Backend/tests/batch2.test.ts
Backend/tests/bookmarks.test.ts
Backend/tests/carbon-stats.test.ts
Backend/tests/carbon.test.ts
Backend/tests/challenges.test.ts
Backend/tests/comments.test.ts
Backend/tests/cors.test.ts
Backend/tests/filters.test.ts
Backend/tests/follows.test.ts
Backend/tests/helpers/testApp.ts
Backend/tests/integration.test.ts
Backend/tests/jest-globals.d.ts
Backend/tests/leaderboard.test.ts
Backend/tests/likes.test.ts
Backend/tests/notifications.test.ts
Backend/tests/pins.test.ts
Backend/tests/posts.test.ts
Backend/tests/privacy.test.ts
Backend/tests/rate-limit.test.ts
Backend/tests/search.test.ts
Backend/tests/setup.ts
Backend/tests/share.test.ts
Backend/tests/trip.test.ts
Backend/tests/trips.test.ts
Backend/tests/users.test.ts
Backend/tests/validation.test.ts
Backend/tsconfig.json
Backend/types/express/index.d.ts
Backend/utils/carbon.ts
Backend/utils/fetchWithTimeout.ts
Backend/utils/geocoding.ts
Backend/utils/logger.ts
Backend/utils/ownership.ts
Backend/utils/pagination.ts
Backend/utils/sanitize.ts
Backend/utils/tripCarbon.ts
Backend/utils/visibility.ts
Frontend/.env.example
Frontend/.gitignore
Frontend/api/upload.ts
Frontend/constants.ts
Frontend/eslint.config.js
Frontend/index.html
Frontend/package.json
Frontend/public/s.js
Frontend/README.md
Frontend/src/App.tsx
Frontend/src/components/AuthModal.tsx
Frontend/src/components/Avatar.tsx
[98 more files omitted for size]
```

### Dependencies

- Backend/package.json: @eslint/js@^9.39.2, @google/genai@^1.37.0, @types/bcrypt@^6.0.0, @types/cookie-parser@^1.4.10, @types/cors@^2.8.19, @types/express@^5.0.6, @types/jest@^29.5.14, @types/jsonwebtoken@^9.0.3, @types/node@^25.0.9, @types/supertest@^6.0.2, @types/uuid@^10.0.0, @typescript-eslint/eslint-plugin@^8.58.2, @typescript-eslint/parser@^8.58.2, bcrypt@^6.0.0, cookie-parser@^1.4.7, cors@^2.8.5, dotenv@^16.3.1, eslint@^10.2.1, express@^5.2.1, express-openapi-validator@^5.6.0, globals@^16.5.0, helmet@^8.1.0, jest@^29.7.0, jsonwebtoken@^9.0.3, supertest@^7.0.0, ts-jest@^29.2.5, tsx@^4.21.0, typescript@^5.9.3, typescript-eslint@^8.58.2, uuid@^13.0.0
- Frontend/package.json: @eslint/js@^9.39.2, @mapbox/mapbox-gl-draw@^1.5.1, @mapbox/polyline@^1.2.1, @radix-ui/react-popover@^1.1.15, @testing-library/jest-dom@^6.9.1, @testing-library/react@^16.3.2, @types/mapbox-gl@^3.4.1, @types/node@^25.0.9, @types/react@^19.2.8, @types/react-dom@^19.2.3, @vercel/blob@^2.0.0, @vercel/node@^5.5.23, @vitejs/plugin-react@^5.1.1, autoprefixer@^10.4.23, clsx@^2.1.1, date-fns@^4.1.0, eslint@^9.39.2, eslint-plugin-react-hooks@^7.0.1, eslint-plugin-react-refresh@^0.4.26, globals@^16.5.0, jsdom@^29.1.1, lucide-react@^0.562.0, mapbox-gl@^3.18.0, playwright@^1.59.1, postcss@^8.5.6, react@^19.2.0, react-day-picker@^9.13.0, react-dom@^19.2.0, react-map-gl@^8.1.0, react-markdown@^10.1.0, react-router@^7.12.0, rehype-sanitize@^6.0.0, remark-gfm@^4.0.1, tailwind-merge@^3.4.0, tailwindcss@^4.1.18, tailwindcss-animate@^1.0.7, typescript@^5.9.3, vite@^7.2.4, vite-plugin-node-polyfills@^0.25.0, vitest@^4.1.9
- package.json: eslint@^9.39.1, playwright@^1.59.1

### Recent commits (newest first)

- waste, fraud, and abuse
- waste fraud and abuse
- begin deslopification process
- begin deslopification process
- refactor(fe): route Home + PostsPage through services, finish auth migration
- refactor(fe): route TripPlanner trip/share calls through tripsApi
- refactor(fe): route SavedPlacesPanel and NewPinModal through services
- refactor(fe): route LocationPin/SearchBar/UserProfile through services
- refactor(fe): route trip/sustainability/followers pages through services
- refactor(fe): route auth/feed/collection/landing calls through services
- refactor(fe): route stats/notifications/posts/profile widgets through services
- feat(fe): add remaining typed API service modules
- refactor(fe): add pins/comments/pinStatus services, migrate DetailedPinModal
- test(frontend): add Vitest + RTL harness with first tests
- build(frontend): add typecheck gate and fix 25 pre-existing type errors
- refactor(trips/share): extract itinerary data access into itineraryRepo
- refactor(gamification): extract challenge/leaderboard/notification SQL into repos
- refactor(auth): move account lookups/creation into userRepo
- refactor(search): extract content search + history into searchRepo
- refactor(stats): extract stats read-model into statsRepo

## Key source files (fetched from GitHub, selected and truncated for size)

### .github/MCP_SETUP.md

```markdown
# Playwright MCP Server Configuration

This project is configured with a **Playwright Model Context Protocol (MCP) server** to extend GitHub Copilot CLI capabilities with browser automation features.

## What This Enables

With Playwright MCP configured, Copilot CLI (me) can now:

- **E2E Testing** — Automatically write and run end-to-end tests for the frontend
- **Visual Testing** — Capture screenshots and validate visual regressions
- **Browser Automation** — Navigate pages, interact with elements, validate UI state
- **Data Scraping** — Extract and validate frontend-rendered data
- **Accessibility Testing** — Validate ARIA attributes, keyboard navigation, screen reader compatibility
- **Performance Testing** — Measure page load times, DOM interaction performance
- **Cross-browser Testing** — Test on Chromium, Firefox, and WebKit

## Configuration Details

**Config File:** `.github/copilot-mcp-config.json`

The configuration specifies:
- **Server:** `@modelcontextprotocol/server-playwright` (auto-installed via npx)
- **Browser:** Chromium (default; can change to `firefox` or `webkit`)
- **Auto-install:** Uses `npx -y` to automatically download and run the latest version

## Usage Examples

Once configured, Copilot CLI can help with tasks like:

```
"Write an E2E test that verifies the map loads and renders pins correctly"
"Take a screenshot of the login page and validate the layout"
"Automate a user journey: login → create a pin → add a comment → verify it appears"
"Check if all buttons are keyboard accessible"
"Measure how long it takes for the heatmap to render with 500 pins"
```

## Activation

The Playwright MCP server activates automatically when you launch Copilot CLI in this repository. No additional setup is required.

If you want to verify it's working, you can run:
```bash
copilot
/mcp
```

This will show you all configured MCP servers, including Playwright.

## Browser Defaults

- **Default:** Chromium (fastest, most compatible)
- **Optional:** Firefox, WebKit for cross-browser testing
- **Headless:** Default is headless mode (no visible browser window)

## Limitations

- Playwright runs in a sandbox on your machine; data stays local
- Browser automation is slower than API testing, so use it for UI-specific validations
- Some enterprise/restricted sites may block automation

## Future Enhancements

This configuration can be extended to:
- Add pytest/jest integration for automated test generation
- Incorporate visual regression testing with baseline snapshots
- Run tests as part of CI/CD workflows
- Validate different screen sizes and devices

```

### migration/REMEDIATION.md

```markdown
# Beacon — Architecture Remediation Plan

> **Companion to** [`ARCHITECTURE.md`](./ARCHITECTURE.md) (current state + SoC assessment).
> **Goal:** describe the recommended industry-standard **end-state**, then a **pragmatic, phased path** to reach it without a rewrite.
>
> **Core problem to solve** (from the SoC assessment): domain logic and data access are fused into controllers on the backend and into components on the frontend. Everything below is in service of pulling those concerns apart along clean seams.

---

## 1. Guiding principles

1. **Single Responsibility per layer** — a controller handles HTTP, a service holds business rules, a repository owns SQL. A React component renders; a hook fetches; a client speaks HTTP.
2. **Dependency direction points inward** — outer layers (HTTP, DB drivers, UI) depend on inner ones (domain logic), never the reverse. Business rules must be testable with no Express and no SQLite.
3. **One source of truth for the contract** — `openapi.yml` already exists; generate types from it for both sides instead of hand-writing DTOs three times.
4. **Strangler-fig, not big-bang** — wrap and migrate one domain at a time; every phase ships independently and keeps the app green.
5. **Behaviour-preserving refactors first** — move code before changing it; leaning on the existing test suite as the safety net.

---

## 2. Target end-state — Backend

A conventional layered architecture. Controllers become thin; a **service layer** holds business rules; a **repository layer** owns all SQL; DTOs sit at the HTTP boundary.

```mermaid
graph TB
    subgraph HTTP["HTTP layer"]
        R["routes/*.ts<br/>(register + middleware only)"]
        CT["controllers/*.ts<br/>(parse req → call service → shape res)"]
    end
    subgraph DOMAIN["Domain layer (no Express, no SQL strings)"]
        SV["services/*.ts<br/>(business rules, ownership, counters,<br/>validation, carbon)"]
    end
    subgraph DATA["Data layer"]
        RP["repositories/*.ts<br/>(all SQL / query builder)"]
        DBH["db.ts (connection only)"]
        MIG["migrations/ (versioned files)"]
    end
    subgraph EXT["Integrations (already isolated ✅)"]
        INT["amadeus, googleRoutes, gemini, hotels"]
    end
    DTO["dto/ — types generated from openapi.yml"]

    R --> CT --> SV
    SV --> RP --> DBH --> SQLITE[("SQLite / Postgres")]
    SV --> INT
    CT -. uses .-> DTO
    SV -. uses .-> DTO
    MIG -. applied by CLI .-> DBH
```

Key moves:
- **Repository layer** — one module per aggregate (`pinRepo`, `postRepo`, `commentRepo`, `userRepo`, …). All `db.query()` calls move here. A shared `findOwned(table, id, userID)` kills the copy-pasted ownership check.
- **Service layer** — business rules extracted from handlers: validation helpers, denormalised-counter maintenance, visibility, trip orchestration. Pure functions where possible → fast unit tests.
- **Thin controllers** — parse/validate input (against generated DTOs), call a service, format the response. No SQL, no
[truncated — 7111 more characters]
```

### package.json

```
{
  "name": "my-app",
  "private": true,
  "packageManager": "pnpm@9.14.3",
  "scripts": {
    "dev": "pnpm -r --parallel dev",
    "build": "pnpm --filter frontend build",
    "start": "pnpm --filter backend start"
  },
  "dependencies": {
    "eslint": "^9.39.1"
  },
  "devDependencies": {
    "playwright": "^1.59.1"
  }
}

```

### Backend/package.json

```
{
  "name": "backend",
  "version": "1.0.0",
  "type": "module",
  "description": "",
  "main": "index.ts",
  "scripts": {
    "test": "node scripts/run-jest.mjs",
    "test:logs": "TEST_LOGS=1 node scripts/run-jest.mjs",
    "test:watch": "node scripts/run-jest.mjs --watch",
    "test:coverage": "node scripts/run-jest.mjs --coverage",
    "test:verbose": "node scripts/run-jest.mjs --verbose",
    "dev": "tsx watch index.ts",
    "start": "tsx index.ts",
    "typecheck": "tsc --noEmit",
    "lint": "eslint .",
    "lint:fix": "eslint . --fix"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@google/genai": "^1.37.0",
    "@types/bcrypt": "^6.0.0",
    "@types/cookie-parser": "^1.4.10",
    "bcrypt": "^6.0.0",
    "cookie-parser": "^1.4.7",
    "cors": "^2.8.5",
    "dotenv": "^16.3.1",
    "express": "^5.2.1",
    "express-openapi-validator": "^5.6.0",
    "helmet": "^8.1.0",
    "jsonwebtoken": "^9.0.3",
    "uuid": "^13.0.0"
  },
  "devDependencies": {
    "@eslint/js": "^9.39.2",
    "@types/cors": "^2.8.19",
    "@types/express": "^5.0.6",
    "@types/jest": "^29.5.14",
    "@types/jsonwebtoken": "^9.0.3",
    "@types/node": "^25.0.9",
    "@types/supertest": "^6.0.2",
    "@types/uuid": "^10.0.0",
    "@typescript-eslint/eslint-plugin": "^8.58.2",
    "@typescript-eslint/parser": "^8.58.2",
    "eslint": "^10.2.1",
    "globals": "^16.5.0",
    "jest": "^29.7.0",
    "supertest": "^7.0.0",
    "ts-jest": "^29.2.5",
    "tsx": "^4.21.0",
    "typescript": "^5.9.3",
    "typescript-eslint": "^8.58.2"
  }
}

```

### Frontend/package.json

```
{
  "name": "frontend",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "packageManager": "pnpm@9.14.3",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "typecheck": "tsc --noEmit",
    "test": "vitest run",
    "test:watch": "vitest",
    "lint": "eslint .",
    "lint:fix": "eslint . --fix",
    "preview": "vite preview"
  },
  "dependencies": {
    "@mapbox/mapbox-gl-draw": "^1.5.1",
    "@mapbox/polyline": "^1.2.1",
    "@radix-ui/react-popover": "^1.1.15",
    "@vercel/blob": "^2.0.0",
    "clsx": "^2.1.1",
    "date-fns": "^4.1.0",
    "mapbox-gl": "^3.18.0",
    "react": "^19.2.0",
    "react-day-picker": "^9.13.0",
    "react-dom": "^19.2.0",
    "react-map-gl": "^8.1.0",
    "react-markdown": "^10.1.0",
    "react-router": "^7.12.0",
    "rehype-sanitize": "^6.0.0",
    "remark-gfm": "^4.0.1",
    "tailwind-merge": "^3.4.0"
  },
  "devDependencies": {
    "@eslint/js": "^9.39.2",
    "@testing-library/jest-dom": "^6.9.1",
    "@testing-library/react": "^16.3.2",
    "@types/mapbox-gl": "^3.4.1",
    "@types/node": "^25.0.9",
    "@types/react": "^19.2.8",
    "@types/react-dom": "^19.2.3",
    "@vercel/node": "^5.5.23",
    "@vitejs/plugin-react": "^5.1.1",
    "autoprefixer": "^10.4.23",
    "eslint": "^9.39.2",
    "eslint-plugin-react-hooks": "^7.0.1",
    "eslint-plugin-react-refresh": "^0.4.26",
    "globals": "^16.5.0",
    "jsdom": "^29.1.1",
    "lucide-react": "^0.562.0",
    "playwright": "^1.59.1",
    "postcss": "^8.5.6",
    "tailwindcss": "^4.1.18",
    "tailwindcss-animate": "^1.0.7",
    "typescript": "^5.9.3",
    "vite": "^7.2.4",
    "vite-plugin-node-polyfills": "^0.25.0",
    "vitest": "^4.1.9"
  }
}

```

### Backend/index.ts

```typescript
import "dotenv/config";

import express, { Request, Response, NextFunction } from "express";
import { fileURLToPath } from "node:url";
import path from "node:path";
import cors from "cors";
import cookieParser from "cookie-parser";
import helmet from "helmet";
import * as OpenApiValidator from "express-openapi-validator";

import * as auth from "./routes/auth.ts";
import * as pins from "./routes/pins.ts";
import * as posts from "./routes/posts.ts";
import * as comments from "./routes/comments.ts";
import * as likes from "./routes/likes.ts";
import * as trip from "./routes/trip.ts";
import * as pinStatus from "./routes/pinStatus.ts";
import * as search from "./routes/search.ts";
import * as bookmarks from "./routes/bookmarks.ts";
import * as stats from "./routes/stats.ts";
import * as users from "./routes/users.ts";
import * as follows from "./routes/follows.ts";
import * as notifications from "./routes/notifications.ts";
import * as trips from "./routes/trips.ts";
import * as challenges from "./routes/challenges.ts";
import * as leaderboard from "./routes/leaderboard.ts";
import { shareRouter } from "./routes/share.ts";
import * as plausible from "./routes/plausible.ts";

const REQUIRED_ENV_VARS = ["SECRET"];
const missing = REQUIRED_ENV_VARS.filter(v => !process.env[v]);
if (missing.length > 0) {
    console.error(`Missing required environment variables: ${missing.join(", ")}`);
    process.exit(1);
}

const app = express();
export { app };
const PORT = parseInt(process.env.PORT || "3000", 10);

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const apiSpec = path.join(__dirname, "./openapi.yml");

app.use(helmet());
app.use(express.json({ limit: '10mb' }));
app.use(cookieParser());

const rateLimitStore = new Map<string, { count: number; resetAt: number }>();

// Hard cap on distinct keys so an attacker rotating IPs cannot grow the map
// without bound between cleanup sweeps and exhaust process memory.
const MAX_RATE_LIMIT_ENTRIES = 50_000;

function rateLimiter(maxRequests: number, windowMs: number, keyFn?: (req: Request) => string) {
    return (req: Request, res: Response, next: NextFunction) => {
        const key = keyFn
            ? keyFn(req)
            : (req.ip || req.socket.remoteAddress || "unknown");
        const now = Date.now();

        const entry = rateLimitStore.get(key);

        if (!entry || now > entry.resetAt) {
            // Bound the store: a new key would be added. If we're at capacity,
            // first evict expired entries; if still full, fail closed.
            if (!entry && rateLimitStore.size >= MAX_RATE_LIMIT_ENTRIES) {
                for (const [k, e] of rateLimitStore.entries()) {
                    if (now > e.resetAt) rateLimitStore.delete(k);
                }
                if (rateLimitStore.size >= MAX_RATE_LIMIT_ENTRIES) {
                    res.setHeader("Retry-After", Math.ceil(windowMs / 1000));
                    return res.status(429).json({ message: "Server busy, please try again later" });
                }
            }
            rateLimitStore.set(key, { count: 1, resetAt: now + windowMs });
            return next();
        }

        if (entry.count >= maxRequests) {
            res.setHeader("Retry-After", Math.ceil((entry.resetAt - now) / 1000));
            return res.status(429).json({ message: "Too many requests, please try again later" });
        }

        entry.count++;
        next();
    };
}

const authRateLimit = rateLimiter(10, 15 * 60 * 1000); // 10 per 15 min per IP

const tripRateLimit = rateLimiter(20, 60 * 1000, (req) => `trip:${req.user?.id ?? req.ip}`);

const shareRateLimit = rateLimiter(100, 60 * 1000, (req) => `share:${req.ip}`); // 100 per minute per IP

// General write limiter for content-mutating endpoints (pins, posts, comments,
// likes, bookmarks, follows, etc.) so an authenticated user cannot flood the DB
// with unlimited writes. Keyed per user (falls back to IP if unauthenticated).
const writeRateLimit = rateLimiter(120, 60 * 1000, (req) => `write:${req.user?.id ?? req.ip}`);

if (process.env.NODE_ENV !== "test") {
    setInterval(() => {
        const now = Date.now();
        for (const [key, entry] of rateLimitStore.entries()) {
            if (now > entry.resetAt) rateLimitStore.delete(key);
        }
    }, 10 * 60 * 1000);
}

export function clearRateLimitStoreForTesting() {
    rateLimitStore.clear();
}

const allowedOrigins = new Set<string>([
    "http://localhost:3000",
    "http://localhost:5173",
    "https://ch2026.vercel.app",
    "https://www.beaconapp.live",
    "https://beaconapp.live",
    "https://api.beaconapp.live"
]);

app.use(
    cors({
        origin: (origin, cb) => {
            if (!origin) return cb(null, true);
            if (allowedOrigins.has(origin)) return cb(null, true);
            return cb(null, false);
        },
        credentials: true,
        methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
        allowedHeaders: ["Content-Type", "Authorization"],
    }),
);

app.use((req, res, next) => {
    if (req.method === "OPTIONS") return res.sendStatus(204);
    next();
});

// CSRF defense-in-depth: for state-changing requests, reject any browser-issued
// cross-origin request. Browsers always send `Origin` on cross-site writes, so a
// forged form/XHR from an attacker's page is blocked server-side even though the
// auth cookie is SameSite=strict. Same-origin and non-browser clients (no Origin
// header, e.g. server-to-server or the test runner) are unaffected.
const MUTATING_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
app.use((req, res, next) => {
    if (MUTATING_METHODS.has(req.method)) {
        const origin = req.headers.origin;
        if (origin && !allowedOrigins.has(origin)) {
            return res.status(403).json({ message: "Cross-origin request blocked" });
        }
    }
    next();
});

// Plausible proxy routes must be registered before OpenAPI validator,
// 
[truncated — 6639 more characters]
```

### Frontend/src/App.tsx

```typescript
export {};

```

### Frontend/src/main.tsx

```typescript
import { lazy } from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import { initializeTheme } from "./utils/theme";

import Landing from "./pages/Landing";
import { RouteBoundary } from "./components/RouteBoundary";
import { AuthProvider } from "./context/AuthContext";

import { createBrowserRouter, RouterProvider } from "react-router";

const Home = lazy(() => import("./pages/Home"));
const PostsPage = lazy(() => import("./pages/PostsPage").then((m) => ({ default: m.PostsPage })));
const SharedItinerary = lazy(() => import("./pages/SharedItinerary"));
const PublicCollection = lazy(() => import("./pages/PublicCollection"));
const UserProfile = lazy(() => import("./pages/UserProfile"));
const FollowersList = lazy(() => import("./pages/FollowersList").then((m) => ({ default: m.FollowersList })));
const FollowingList = lazy(() => import("./pages/FollowersList").then((m) => ({ default: m.FollowingList })));
const ActivityPage = lazy(() => import("./pages/ActivityPage"));
const NotificationsPage = lazy(() => import("./pages/NotificationsPage"));
const MyTrips = lazy(() => import("./pages/MyTrips"));
const MyTripView = lazy(() => import("./pages/MyTripView"));
const SustainabilityPage = lazy(() => import("./pages/SustainabilityPage"));

initializeTheme();

const router = createBrowserRouter([
	{
		path: "/",
		element: <Landing />,
	},
	{
		path: "/home",
		element: (
			<RouteBoundary>
				<Home />
			</RouteBoundary>
		),
	},
	{
		path: "/explore",
		element: (
			<RouteBoundary>
				<PostsPage />
			</RouteBoundary>
		),
	},
	{
		path: "/shared/:id",
		element: (
			<RouteBoundary>
				<SharedItinerary />
			</RouteBoundary>
		),
	},
	{
		path: "/collection/:folderID",
		element: (
			<RouteBoundary>
				<PublicCollection />
			</RouteBoundary>
		),
	},
	{
		path: "/profile/:userID",
		element: (
			<RouteBoundary>
				<UserProfile />
			</RouteBoundary>
		),
	},
	{
		path: "/users/:userID/followers",
		element: (
			<RouteBoundary>
				<FollowersList />
			</RouteBoundary>
		),
	},
	{
		path: "/users/:userID/following",
		element: (
			<RouteBoundary>
				<FollowingList />
			</RouteBoundary>
		),
	},
	{
		path: "/activity",
		element: (
			<RouteBoundary>
				<ActivityPage />
			</RouteBoundary>
		),
	},
	{
		path: "/notifications",
		element: (
			<RouteBoundary>
				<NotificationsPage />
			</RouteBoundary>
		),
	},
	{
		path: "/my-trips",
		element: (
			<RouteBoundary>
				<MyTrips />
			</RouteBoundary>
		),
	},
	{
		path: "/my-trips/:id",
		element: (
			<RouteBoundary>
				<MyTripView />
			</RouteBoundary>
		),
	},
	{
		path: "/sustainability",
		element: (
			<RouteBoundary>
				<SustainabilityPage />
			</RouteBoundary>
		),
	},
]);

const root = document.getElementById("root");

if (!root) {
	throw new Error("Root container missing in index.html");
}

ReactDOM.createRoot(root).render(
	<AuthProvider>
		<RouterProvider router={router} />
	</AuthProvider>,
);

```

### pnpm-workspace.yaml

```yaml
packages:
  - "Frontend"
  - "Backend"


```

### Backend/jest.config.js

```javascript
/** @type {import('jest').Config} */
export default {
  preset: 'ts-jest/presets/default-esm',
  testEnvironment: 'node',
  extensionsToTreatAsEsm: ['.ts'],
  moduleNameMapper: {
    '^(\\.{1,2}/.*)\\.js$': '$1',
  },
  transform: {
    '^.+\\.tsx?$': [
      'ts-jest',
      {
        useESM: true,
        tsconfig: {
          module: 'ESNext',
          moduleResolution: 'bundler',
        },
      },
    ],
  },
  testMatch: ['**/tests/**/*.test.ts'],
  setupFilesAfterEnv: ['./tests/setup.ts'],
  testTimeout: 30000,
  verbose: true,
  forceExit: true,
  detectOpenHandles: true,
};

```

[187 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]