# Project export: School of Fish

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: TreeHacks 2026
- Tagline: Stop wasting time finding a study spot. School of Fish monitors room availability in real time and predicts occupancy for any hour of any day using Bluetooth sensors and time-series forecasting.
- Devpost: https://devpost.com/software/school-of-fish
- GitHub: https://github.com/Commandtechno/school-of-fish
- Demo: https://fish.akhilasher.com/
- Video: https://www.youtube.com/embed/G1rw4qj_KAc?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 3 GitHub contributor(s) — Commandtechno (38 commits), Ben Richeson (2 commits), Capy (1 commits)

## Devpost submission (written by the team)

### Inspiration

We’ve all been there: you desperately need to find a quiet place to study, but every room is taken. The Vision Our project, School of Fish, is designed to solve this. It works by placing inexpensive Raspberry Pi-based devices in each classroom and study room. These devices analyze the nearby Bluetooth, or more specifically Bluetooth Low Energy, signals emitted from occupants’ devices (phones, laptops, headphones, smart watches) to estimate the number of occupants in the room. Furthermore, by feeding this data through forecasting algorithms and machine learning models, we can accurately predict when a room is most likely to be vacant days in advance. One benefit of using Bluetooth signals over a computer vision-based solution is that it’s inherently anonymous. The only data we can get out of a Bluetooth signal is a 36 character long randomized identifier, and these are often rotated as frequently as every 15 minutes. Each device communicates with a central ingest API, periodically reporting the number of signals detected in a given room. These signals are stored in a database and plotted on an interactive map. This allows students to find available study spaces all across campus. Because it’s 2026, School of Fish also implements an MCP server. This allows interacting with School of Fish data through your favorite AI agents such as Poke, Claude, and Openclawd. Along with accessing the data, it works really well with calendar integration for scheduling study sessions at optimal times. For campuses, the data our systems provide can help them to determine which buildings on campus they need to expand or bring more attention to. It can also be used to analyze trends in time spent studying and correlations with student mental health during particularly stressful periods.

### How we built it

School of Fish consists of four main components: the site monitoring devices, ingest API, interactive map, and MCP server. The ingest API, written in Go, is responsible for collecting data from each device across a college campus. This data is stored in a PostgreSQL database using the TimescaleDB and PostGIS extensions for aggregating temporal and efficient spatial querying. The interactive map and admin panel are a Next.js app written in TypeScript. The map uses OpenStreetMap imagery and GeoJSON data extracted from Stanford’s campus map to overlay accurate and interactive heatmaps of each building and its tracked study areas. When a study room is selected, a time-series graph is displayed showing historic and forecasted (future) occupancy based on data from days and weeks prior. These predictions are calculated using Holt-Winters seasonal forecasting which works best with our limited dataset. The on-site monitors are completely passive devices with software written in Python that periodically scan for nearby Bluetooth Low Energy Devices with a signal strength above a certain threshold. The data is then sent securely over a Tailscale VPN to the ingest API. The MCP server provides agents with advanced querying capabilities of current and forecasted traffic around campus. We opted to create a general AI integration over a custom one which allows our project to integrate with agents that people already use. This also enables working in tangent with their other integrations such as calendar management.

### Challenges we ran into

The main challenge for this project is that forecasting requires a large amount of historical data. We opted to use weekly seasonality for our forecasting, which means it will recognize weekly trends such as a class that only meets on Monday afternoons, and it won’t influence the forecast of Tuesday afternoons. However, in order to perform forecasts this way, we would need at least two weeks of prior data, which obviously isn’t possible in a 36 hour hackathon. For the sake of demonstration we synthesized historic data based on the 24-ish hours of data collection we were able to perform during the hackathon. Originally, we planned to track the volume of WiFi packets as a means of measuring the number of people in a room, however, we found that this method was very dependent on the environment (e.g. access point setup, firewalling). Bluetooth Low Energy, which is emitted by the vast majority of modern consumer electronics, was a far more accurate way to measure human presence since it can be readily picked up without dependence on network configuration.

### Accomplishments we're proud of

Ben - One of the coolest moments of the hackathon was getting the BLE sensors working. We took many walks with our laptops outside away from the buildings just to watch the number of nearby devices drop to zero, and immediately jump back up when we walked back near the buildings. Akhil - My favorite moment was waking up Saturday morning after my laptop had been collecting real data all night and seeing the gradual drop off at night and massive influx of devices at 9AM.

### What we learned

Bluetooth may not be the best data transfer protocol, but its widespread use makes it a simple and anonymous metric for room occupancy. Accurate data forecasting is incredibly complex and requires a lot of raw data. AI can be, but is not a guaranteed speed boost in writing code. Writing code with AI is a path that has many traps you can fall down. For example, we completely rewrote the UI after AI skipped over major UX red flags.

### What's next

There are so many ways this could be expanded and integrated into students’ lives: Automatically booking a room using the college’s booking system Automatically finding study rooms that fit into the schedules of students working on a group project Determining the average turnaround time for a study room Siri/voice assistant integration, “Hey Siri, find me an open study room” Additionally, it could also be used in settings beyond college campuses: Tracking how busy restaurants are Tracking attendance at event venues

## README (from the GitHub repository)

# School of Fish

Bluetooth crowd density monitoring system.

## Architecture

```
┌──────────────────┐     ┌──────────────────┐     ┌──────────────────┐
│  Bluetooth       │────▶│  API Server (Go) │────▶│  TimescaleDB     │
│  Sensors         │POST │  chi router      │     │  + PostGIS       │
└──────────────────┘     └──────┬───────────┘     └──────────────────┘
                                │
                       ┌────────▼─────────┐
                       │  Frontend         │
                       │  Next.js + MapLibre│
                       └──────────────────┘
```

External Bluetooth sensors detect nearby devices and push detections to the API server via HTTP POST. The server hashes MAC addresses for privacy and bulk-inserts events into TimescaleDB. The frontend visualizes real-time and historical crowd density on a map.

## Tech Stack

| Layer     | Technology                          |
|-----------|-------------------------------------|
| Database  | TimescaleDB (PostgreSQL) + PostGIS  |
| Backend   | Go, chi, pgx                        |
| Frontend  | Next.js 15, React, MapLibre GL, Tailwind CSS |
| Infra     | Docker Compose                      |

## Quick Start

```bash
export INGEST_API_KEY="your-secret-key"
docker compose up
```

This starts:
- **TimescaleDB** on port `5432` (with PostGIS, hypertables, and seed data)
- **Go API server** on port `8080`
- **Next.js frontend** on port `3000`

## Environment Variables

| Variable          | Required | Description                              |
|-------------------|----------|------------------------------------------|
| `DATABASE_URL`    | Yes      | PostgreSQL connection string             |
| `PORT`            | No       | API server port (default: `8080`)        |
| `INGEST_API_KEY`  | Yes      | Shared secret for sensor authentication  |

## Ingest Endpoint

Bluetooth sensors push device detections to the API server.

**`POST /api/ingest`**

### Headers

| Header         | Required | Description                     |
|----------------|----------|---------------------------------|
| `Content-Type` | Yes      | `application/json`              |
| `X-API-Key`    | Yes      | Shared secret matching `INGEST_API_KEY` |

### Request Body

```json
{
  "sensor_id": "550e8400-e29b-41d4-a716-446655440000",
  "devices": [
    {"mac": "AA:BB:CC:DD:EE:FF", "rssi": -65},
    {"mac": "11:22:33:44:55:66", "rssi": -72}
  ]
}
```

- `sensor_id` — UUID of the reporting sensor (must exist in `sensors` table)
- `devices` — array of detected devices (1–1000 entries)
  - `mac` — device MAC address (hashed server-side with SHA-256 before storage)
  - `rssi` — received signal strength indicator

### Responses

**200 OK**
```json
{"ingested": 2}
```

**400 Bad Request**
```json
{"error": "invalid request body"}
{"error": "invalid sensor_id"}
{"error": "devices array is required"}
{"error": "too many devices, max 1000"}
{"error": "each device must have mac and rssi"}
```

**401 Unauthorized**
```json
{"error": "invalid api key"}
```

**500 Internal Server Error**
```json
{"error": "failed to write events"}
```

### Example

```bash
curl -X POST http://localhost:8080/api/ingest \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-secret-key" \
  -d '{
    "sensor_id": "550e8400-e29b-41d4-a716-446655440000",
    "devices": [
      {"mac": "AA:BB:CC:DD:EE:FF", "rssi": -65},
      {"mac": "11:22:33:44:55:66", "rssi": -72}
    ]
  }'
```

### Privacy

MAC addresses are never stored in plaintext. The server computes a SHA-256 hash of each MAC before inserting into the database.

## Development

### Backend

```bash
cd backend
export DATABASE_URL="postgres://fish:fish@localhost:5432/schooloffish?sslmode=disable"
export INGEST_API_KEY="dev-secret"
go run ./cmd/api
```

### Frontend

```bash
cd frontend
npm install
npm run dev
```

### Database

The database is automatically initialized with the schema from `db/init.sql` on first start. To reset:

```bash
docker compose down -v
docker compose up
```

## Project Structure

```
school-of-fish/
├── docker-compose.yml          # TimescaleDB + Go backend + Next.js frontend
├── README.md
├── backend/
│   ├── cmd/
│   │   └── api/main.go         # API server entry point
│   ├── internal/
│   │   ├── db/                  # Database connection + migrations
│   │   ├── models/              # Go structs matching DB tables
│   │   ├── ingest/              # Bluetooth ingest types + batch writer
│   │   ├── api/                 # HTTP router + handlers
│   │   └── forecast/            # Holt-Winters forecasting
│   └── Dockerfile
├── frontend/
│   ├── src/
│   │   ├── app/                 # Next.js App Router pages
│   │   ├── components/          # React components
│   │   └── lib/                 # API client helpers
│   ├── public/
│   │   └── campus.geojson       # Example building polygons
│   └── Dockerfile
└── db/
    └── init.sql                 # Database initialization script
```


## Detected evidence (automated analysis)

Indexed codebase: 39 recognized source files, 134 KB.
- CSS (language) — detected in the code
- Go (language) — detected in the code
- JavaScript (language) — detected in the code
- Next.js (technology) — detected in the code
- PostgreSQL (technology) — 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

## Codebase structure (from repository index)

### Files (60 of 60)

```
.gitignore
AGENT.md
backend/api
backend/cmd/api/main.go
backend/cmd/seed/main.go
backend/cmd/seed2/lib.go
backend/cmd/seed2/main.go
backend/Dockerfile
backend/go.mod
backend/go.sum
backend/internal/api/handlers.go
backend/internal/api/responses.go
backend/internal/api/router.go
backend/internal/db/db.go
backend/internal/db/migrations/001_init.sql
backend/internal/forecast/forecaster.go
backend/internal/forecast/holtwinters_test.go
backend/internal/forecast/holtwinters.go
backend/internal/ingest/writer.go
backend/internal/models/models.go
backend/main
db/init.sql
docker-compose.yml
frontend/Dockerfile
frontend/next-env.d.ts
frontend/next.config.ts
frontend/package.json
frontend/postcss.config.mjs
frontend/public/campus.geojson
frontend/public/index.js
frontend/public/names.json
frontend/public/package.json
frontend/src/app/admin/page.tsx
frontend/src/app/globals.css
frontend/src/app/layout.tsx
frontend/src/app/page.tsx
frontend/src/components/BuildingPopup.tsx
frontend/src/components/CampusMap.tsx
frontend/src/components/DensityChart.tsx
frontend/src/components/Header.tsx
frontend/src/components/Sidebar.tsx
frontend/src/lib/api.ts
frontend/src/lib/density.ts
frontend/tailwind.config.ts
frontend/tsconfig.json
frontend/tsconfig.tsbuildinfo
mcp/Dockerfile
mcp/package.json
mcp/src/db.ts
mcp/src/index.ts
mcp/src/tools.ts
mcp/tsconfig.json
README.md
scanner/.python-version
scanner/main.py
scanner/notes
scanner/pyproject.toml
scanner/README.md
scanner/scan.py
scanner/uv.lock
```

### Dependencies

- backend/go.mod: github.com/go-chi/chi/v5@v5.2.5, github.com/go-chi/cors@v1.2.2, github.com/google/uuid@v1.6.0, github.com/jackc/pgpassfile@v1.0.0, github.com/jackc/pgservicefile@v0.0.0-20240606120523-5a60cdf6a761, github.com/jackc/pgx/v5@v5.8.0, github.com/jackc/puddle/v2@v2.2.2, golang.org/x/sync@v0.17.0, golang.org/x/text@v0.29.0, nhooyr.io/websocket@v1.8.17
- frontend/package.json: @tailwindcss/postcss@^4.1.8, @types/geojson@^7946.0.16, @types/node@^22.15.21, @types/react@^19.1.4, @types/react-dom@^19.1.5, maplibre-gl@^4.7.1, next@^15.3.3, postcss@^8.5.4, react@^19.1.0, react-dom@^19.1.0, react-map-gl@^7.1.9, recharts@^3.7.0, tailwindcss@^4.1.8, typescript@^5.8.3
- frontend/public/package.json: fast-xml-parser@^5.3.6
- mcp/package.json: @hono/mcp@^0.2.3, @hono/node-server@^1.14.1, @modelcontextprotocol/sdk@^1.12.1, @types/node@^22.15.3, @types/pg@^8.11.11, hono@^4.7.11, pg@^8.13.1, tsx@^4.19.4, typescript@^5.8.3, zod@^3.24.4
- scanner/pyproject.toml: bleak@>=2.1.1, python-dotenv@>=1.2.1, requests@>=2.32.5

### Recent commits (newest first)

- wrong url fuck me
- update urls
- huang seed
- Enable date navigation with previous/next day buttons in Sidebar.
- Standardize density color mapping across map and sidebar for consistent visualization.
- Add hidden /admin page with live sensor/zone status overview
- seed2 and logo
- Streamline map interaction by immediately selecting zone on building/sensor click.
- Revert "Immediately select map zone on click for streamlined interaction"
- Immediately select map zone on click for streamlined interaction
- design changes
- Merge pull request #13 from Commandtechno/capy/zone-avg-devices-capacity
- Standardize zone density metrics to average per sensor and use API capacity
- install tzdata
- update seed script and timezone
- Enable forecast date selection and proper scaling in zone sensor expanded view.
- networking?
- AAAAAAAAA
- remove postgres port
- add seed to docker

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

### AGENT.md

```markdown
dont read .geojson files, ignore them completely

```

### docker-compose.yml

```yaml
services:
  db:
    image: timescale/timescaledb-ha:pg17
    container_name: school-of-fish-db-1
    environment:
      TZ: America/Los_Angeles
      POSTGRES_DB: schooloffish
      POSTGRES_USER: fish
      POSTGRES_PASSWORD: fish
    volumes:
      - pgdata:/home/postgres/pgdata/data
      - ./db/init.sql:/docker-entrypoint-initdb.d/001_init.sql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U fish -d schooloffish"]
      interval: 5s
      timeout: 5s
      retries: 5
    networks:
      - fishnet

  backend:
    build: ./backend
    ports:
      - "2026:2026"
    environment:
      TZ: America/Los_Angeles
      DATABASE_URL: postgres://fish:fish@db:5432/schooloffish?sslmode=disable
      PORT: "2026"
      INGEST_API_KEY: "auqGBfFEtNQuhr17Xg80"
    depends_on:
      db:
        condition: service_healthy
    networks:
      - fishnet

  frontend:
    build: ./frontend
    ports:
      - "2028:2028"
    environment:
      TZ: America/Los_Angeles
      PORT: 2028
    networks:
      - fishnet
    # depends_on:
    # - backend

  mcp:
    build: ./mcp
    ports:
      - "2027:2027"
    environment:
      TZ: America/Los_Angeles
      DATABASE_URL: postgres://fish:fish@db:5432/schooloffish?sslmode=disable
      BACKEND_URL: http://backend:2026
      PORT: "2027"
    depends_on:
      db:
        condition: service_healthy
      backend:
        condition: service_started
    networks:
      - fishnet

volumes:
  pgdata:

networks:
  fishnet:
    driver: bridge

```

### mcp/Dockerfile

```
FROM node:22-alpine
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm install
COPY . .
RUN npm run build
CMD ["node", "dist/index.js"]

```

### scanner/pyproject.toml

```
[project]
name = "scanner"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
    "bleak>=2.1.1",
    "python-dotenv>=1.2.1",
    "requests>=2.32.5",
]

```

### frontend/Dockerfile

```
FROM node:22-alpine AS builder

WORKDIR /app

COPY package.json package-lock.json* ./
RUN npm install

COPY . .
RUN npm run build

FROM node:22-alpine AS runner

WORKDIR /app

ENV NODE_ENV=production

COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public

EXPOSE 3000

CMD ["node", "server.js"]

```

### backend/Dockerfile

```
# Build stage
FROM golang:1.24-alpine AS builder

WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN go build -o /bin/api ./cmd/api
RUN go build -o /bin/seed ./cmd/seed
RUN go build -o /bin/seed2 ./cmd/seed2

# Runtime stage
FROM alpine:3.20

RUN apk add --no-cache tzdata ca-certificates

COPY --from=builder /bin/api /bin/api
COPY --from=builder /bin/seed /bin/seed
COPY --from=builder /bin/seed2 /bin/seed2

EXPOSE 8080

CMD ["/bin/api"]

```

### backend/go.mod

```
module github.com/Commandtechno/school-of-fish/backend

go 1.24.0

require (
	github.com/go-chi/chi/v5 v5.2.5
	github.com/go-chi/cors v1.2.2
	github.com/google/uuid v1.6.0
	github.com/jackc/pgx/v5 v5.8.0
	nhooyr.io/websocket v1.8.17
)

require (
	github.com/jackc/pgpassfile v1.0.0 // indirect
	github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
	github.com/jackc/puddle/v2 v2.2.2 // indirect
	golang.org/x/sync v0.17.0 // indirect
	golang.org/x/text v0.29.0 // indirect
)

```

### mcp/package.json

```
{
  "name": "school-of-fish-mcp",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "start": "node dist/index.js",
    "build": "tsc"
  },
  "dependencies": {
    "@hono/mcp": "^0.2.3",
    "@hono/node-server": "^1.14.1",
    "@modelcontextprotocol/sdk": "^1.12.1",
    "hono": "^4.7.11",
    "pg": "^8.13.1",
    "zod": "^3.24.4"
  },
  "devDependencies": {
    "@types/node": "^22.15.3",
    "@types/pg": "^8.11.11",
    "tsx": "^4.19.4",
    "typescript": "^5.8.3"
  }
}

```

### frontend/package.json

```
{
  "name": "school-of-fish-frontend",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "maplibre-gl": "^4.7.1",
    "next": "^15.3.3",
    "react": "^19.1.0",
    "react-dom": "^19.1.0",
    "react-map-gl": "^7.1.9",
    "recharts": "^3.7.0"
  },
  "devDependencies": {
    "@tailwindcss/postcss": "^4.1.8",
    "@types/geojson": "^7946.0.16",
    "@types/node": "^22.15.21",
    "@types/react": "^19.1.4",
    "@types/react-dom": "^19.1.5",
    "postcss": "^8.5.4",
    "tailwindcss": "^4.1.8",
    "typescript": "^5.8.3"
  }
}

```

### frontend/public/package.json

```
{
  "type": "module",
  "dependencies": {
    "fast-xml-parser": "^5.3.6"
  }
}

```

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