# Backend — Social Proximity ## Technology Stack | Component | Choice | Reason | |---|---|---| | Framework | FastAPI (Python) | Async, typed, WebSocket support, fast to build | | Session store | Redis | Ephemeral data, TTL-based expiry, low latency | | Database | PostgreSQL | Aggregate stats only — structured, durable | | Real-time | WebSockets (FastAPI native) | Push nudge to app without polling | ## Project Structure ``` backend/ ├── app/ │ ├── main.py ← FastAPI app init, router registration │ ├── api/ │ │ ├── session.py ← POST /session — register BLE token + interests │ │ ├── match.py ← POST /match — attempt proximity match │ │ ├── ws.py ← WebSocket /ws/{token} — real-time nudge channel │ │ ├── stats.py ← GET /stats — anonymised aggregate stats (admin only) │ │ └── health.py ← GET /health │ ├── models/ │ │ ├── session.py ← Pydantic: SessionCreate, SessionResponse │ │ ├── match.py ← Pydantic: MatchRequest, MatchResult │ │ └── stats.py ← Pydantic: StatsResponse │ ├── services/ │ │ ├── session_service.py ← create/get/expire sessions in Redis │ │ ├── match_service.py ← interest overlap logic + nudge dispatch │ │ └── stats_service.py ← aggregate stats queries (PostgreSQL) │ └── core/ │ ├── config.py ← Settings (env vars via pydantic-settings) │ ├── redis.py ← Redis connection pool │ └── database.py ← SQLAlchemy async engine + session ├── tests/ │ ├── test_session.py │ ├── test_match.py │ └── test_stats.py ├── requirements.txt ├── requirements-dev.txt ├── Dockerfile └── .env.example ``` ## API Endpoints ### `POST /session` Register a new ephemeral session when user enables "open to talk". **Request:** ```json { "ble_token": "abc123", "interests": ["devops", "hardstyle", "philosophy"] } ``` **Response:** ```json { "session_id": "uuid", "expires_at": "2024-01-01T12:00:00Z" } ``` - BLE token stored in Redis with 2-hour TTL - Interest list hashed before storage — raw interests never persisted to DB --- ### `POST /match` Called when a user's app detects a nearby BLE token. **Request:** ```json { "own_token": "abc123", "detected_token": "xyz789" } ``` **Response:** ```json { "match": true, "shared_interests": ["devops", "hardstyle"], "nudge_sent": true } ``` - If match found: WebSocket nudge pushed to both parties - Shared interests returned as category labels only — no raw profile data --- ### `WebSocket /ws/{ble_token}` Persistent connection from the app. Used to receive real-time nudges. **Nudge message (server → client):** ```json { "type": "nudge", "shared_interests": ["devops", "hardstyle"], "message": "Someone nearby shares your interest in devops and hardstyle." } ``` --- ### `GET /health` Standard health check. Returns `200 OK` with service status. --- ### `GET /stats` Anonymised aggregate stats for admin dashboard. Internal access only. **Response:** ```json { "total_sessions_today": 142, "total_matches_today": 38, "top_interest_categories": ["tech", "music", "philosophy"], "active_sessions_now": 12 } ``` ## Data Model (Redis) Sessions are stored as Redis hashes: ``` KEY: session:{ble_token} VALUE: { "session_id": "uuid", "interests_hash": "sha256(...)", "interest_categories": ["tech", "music"], "created_at": "iso8601" } TTL: 7200 seconds (2 hours) ``` ## Data Model (PostgreSQL) Only aggregate counters — no user-level rows. ```sql CREATE TABLE daily_stats ( date DATE PRIMARY KEY, total_sessions INTEGER DEFAULT 0, total_matches INTEGER DEFAULT 0 ); CREATE TABLE interest_category_counts ( date DATE, category TEXT, count INTEGER DEFAULT 0, PRIMARY KEY (date, category) ); ``` ## Environment Variables ```env REDIS_URL=redis://localhost:6379 DATABASE_URL=postgresql+asyncpg://user:pass@localhost/sighej SESSION_TTL_SECONDS=7200 ALLOWED_ORIGINS=http://localhost:3000 ``` ## Running Locally ```bash cd backend python -m venv .venv && source .venv/bin/activate pip install -r requirements.txt uvicorn app.main:app --reload --port 8000 ``` Or via Docker Compose from root: ```bash docker-compose up ```