186 lines
4.5 KiB
Markdown
186 lines
4.5 KiB
Markdown
|
|
# 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
|
||
|
|
```
|