-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture
This page describes the system architecture as of the Final Milestone, including the tech stack, database schema, service layer design, and API structure.
| Layer | Technology |
|---|---|
| Backend | Python 3.11, FastAPI, SQLAlchemy (async), Alembic |
| Database | PostgreSQL 16 |
| Object Storage | MinIO (local dev) / Supabase S3 (production) |
| Frontend | Vanilla HTML/JS served via Nginx |
| Mobile | Capacitor + Android (WebView wrapper) |
| Infrastructure | Docker Compose, GitHub Actions CI/CD, Render (production) |
| Linting | Ruff (backend), ESLint 9 (frontend) |
| Testing | pytest + httpx (backend), Jest + Playwright (frontend), Appium/WebdriverIO (mobile) |
| External APIs | Google OAuth 2.0, OpenAI Whisper API, Google Gemini API |
bounswe2026group2/
├── backend/
│ ├── app/
│ │ ├── main.py # FastAPI app, CORS, health, router wiring
│ │ ├── core/
│ │ │ ├── config.py # pydantic-settings (reads .env)
│ │ │ └── deps.py # get_current_user() JWT dependency
│ │ ├── db/ # SQLAlchemy ORM models + session + enums
│ │ ├── models/ # Pydantic request/response schemas
│ │ ├── routers/ # auth.py, story.py, users.py, admin.py, transcription.py
│ │ └── services/ # auth_service.py, story_service.py, user_service.py,
│ │ # storage.py, transcription_service.py, ai_tagging_system.py
│ ├── alembic/ # Database migration scripts (0001–0018)
│ └── tests/
│ ├── unit/
│ ├── integration/
│ └── api/
├── frontend/
│ ├── *.html # Static pages (login, map, story-create, story-detail, …)
│ ├── js/ # auth.js, likes.js, comments.js, …
│ └── tests/
│ ├── unit/ # Jest unit tests
│ ├── uat/ # Playwright end-to-end acceptance tests
│ └── mobile-e2e/ # Appium/WebdriverIO mobile E2E tests
└── .github/workflows/ # ci.yml, deploy.yml, release.yml
| Column | Type | Notes |
|---|---|---|
id |
UUID PK | |
username |
VARCHAR(50) | UNIQUE |
email |
VARCHAR(255) | UNIQUE |
password_hash |
VARCHAR(255) | NULL for OAuth-only accounts |
google_sub |
VARCHAR(255) | UNIQUE, NULL for password accounts |
display_name |
VARCHAR(100) | nullable |
bio |
TEXT | nullable |
role |
ENUM(user, admin) | default user |
is_active |
BOOLEAN | default true |
created_at / updated_at
|
TIMESTAMP |
| Column | Type | Notes |
|---|---|---|
id |
UUID PK | |
user_id |
UUID FK → users | |
title |
VARCHAR(255) | |
summary |
TEXT | nullable |
content |
TEXT | |
status |
ENUM(draft, published, archived) | |
visibility |
ENUM(private, public, unlisted, anonymous) | |
place_name |
VARCHAR(255) | nullable (legacy single location) |
latitude / longitude
|
FLOAT | nullable (legacy single location) |
date_start / date_end
|
DATE | nullable |
date_precision |
ENUM(year, date) | nullable |
view_count |
INTEGER | default 0, atomically incremented on detail fetch |
created_at / updated_at
|
TIMESTAMP |
| Column | Type | Notes |
|---|---|---|
id |
UUID PK | |
story_id |
UUID FK → stories | CASCADE delete |
bucket_name |
VARCHAR(63) | |
storage_key |
VARCHAR(512) | UNIQUE with bucket |
original_filename |
VARCHAR(255) | |
mime_type |
VARCHAR(255) | |
media_type |
ENUM(image, audio, video, document) | |
file_size_bytes |
BIGINT | |
sort_order |
INT | default 0 |
alt_text / caption
|
TEXT | nullable |
user_id UUID FK → users · story_id UUID FK → stories · composite PK
id UUID PK · user_id FK → users · story_id FK → stories · content TEXT · timestamps
user_id UUID FK → users · story_id UUID FK → stories · composite PK
id UUID PK · recipient_id FK → users · actor_id FK → users · story_id FK → stories · type ENUM(like, comment, bookmark) · is_read BOOLEAN · timestamps
Supports multiple named locations per story (replaces single lat/lng on the stories row for multi-location stories).
| Column | Type | Notes |
|---|---|---|
id |
UUID PK | |
story_id |
UUID FK → stories | CASCADE delete |
latitude |
FLOAT | NOT NULL, −90 to 90 |
longitude |
FLOAT | NOT NULL, −180 to 180 |
label |
VARCHAR(255) | nullable display name for this pin |
sort_order |
INT | default 0 |
created_at / updated_at
|
TIMESTAMP |
Normalised tag dictionary; slugs are lowercase ASCII for search matching.
| Column | Type | Notes |
|---|---|---|
id |
UUID PK | |
name |
VARCHAR(100) | UNIQUE |
slug |
VARCHAR(120) | UNIQUE, lowercased |
created_at / updated_at
|
TIMESTAMP |
story_id UUID FK → stories · tag_id UUID FK → tags · composite PK
Composite index on (tag_id, story_id) for tag-based story lookup.
| Column | Type | Notes |
|---|---|---|
id |
UUID PK | |
name |
VARCHAR(100) | |
description |
TEXT | |
icon_key |
VARCHAR(100) | |
rule_type |
ENUM(BadgeRuleType) | UNIQUE — one badge definition per rule |
user_id UUID FK → users · badge_id UUID FK → badges · composite PK · awarded_at TIMESTAMP
| Service | Responsibility |
|---|---|
auth_service.py |
Register, login, JWT creation/validation, Google OAuth exchange |
story_service.py |
Story CRUD, location upsert, tag upsert, list/search/nearby/timeline queries, like/comment/bookmark/report logic |
user_service.py |
Profile update, avatar upload, password change, dashboard/stats aggregation |
admin_service.py |
Report queue, report status updates, soft-delete stories |
transcription_service.py |
Async audio transcription via OpenAI Whisper API (whisper-1); returns None gracefully when OPENAI_API_KEY is unset |
ai_tagging_system.py |
Background tag generation via Google Gemini; strips markdown fences from response; no-ops when GEMINI_API_KEY is unset |
storage.py |
MinIO / S3-compatible upload, presigned URL generation |
- Routers — HTTP layer only (auth, validation, response shaping). No business logic.
-
Services — All business logic. Called by routers via
await. - Models — Pydantic schemas for request validation and response serialization.
- db/ — SQLAlchemy ORM models, enums, and the async session factory.
- All database access is async:
asyncpgdriver,AsyncSession. - Alembic manages migrations — all new ORM models must be imported in
alembic/env.py.
| Router | Prefix | Tags |
|---|---|---|
| Auth | /auth |
auth |
| Stories | /stories |
stories |
| Users | /users |
users |
| Admin | /admin |
admin |
| Transcription | /transcription |
transcription |
Full interactive API docs are available at /docs (Swagger UI) and /redoc when the backend is running.
| Method | Path | Description |
|---|---|---|
GET |
/auth/google/login |
Initiates Google OAuth flow |
GET |
/auth/google/callback |
Exchanges code for JWT, creates account if needed |
GET |
/stories?tags=tag1&tags=tag2 |
Filter public stories by one or more tags (OR matching) |
GET |
/stories/search?q=query |
Hybrid semantic + fuzzy search across title, content, and tags |
GET |
/stories/timeline |
Chronological timeline, filtered by location or place name |
GET |
/stories/nearby |
Haversine geospatial nearby stories |
GET |
/stories/{id} |
Story detail; increments view count |
GET |
/users/me/stats |
Engagement stats including total views received |
GET |
/users/{id} |
Public profile with earned badges |
POST |
/transcription/preview |
Transcribe audio without persisting |
backend-lint + frontend-lint
↓
health check
↓
unit tests
↓
integration tests
↓
API tests
↓
frontend unit (Jest) UAT (Playwright)
↓ ↓
── test-summary ──
↓
deploy (Render)
The release.yml workflow is manually triggered from main, calls CI + deploy, then publishes a GitHub Release with test results.
Team Members
- Lab 1 Report (12/02/2026)
- Lab 2 Report (19/02/2026)
- Lab 3 Report (26/02/2026)
- Lab 4 Report (05/03/2026)
- Lab 5 Report (12/03/2026)
- Lab 6 Report (26/03/2026)
- Lab 7 Report (02/04/2026)
- Lab 8 Report (16/04/2026)
- Lab 9 Report (30/04/2026)
- Lab 10 Report (07/05/2026)
- Weekly Meeting 1 (14.02.2026)
- Weekly Meeting 2 (21.02.2026)
- Weekly Meeting 3 (28.02.2026)
- Weekly Meeting 4 (07.03.2026)
- Weekly Meeting 5 (14.03.2026)
- Weekly Meeting 6 (19.03.2026)
- Weekly Meeting 7 (28.03.2026)
- Weekly Meeting 8 (05.04.2026)
- Weekly Meeting 9 (11.04.2026)
- Weekly Meeting 10 (18.04.2026)
- Weekly Meeting 11 (02.05.2026)
- Weekly Meeting 12 (09.05.2026)
- Customer Meeting 1 (18.02.2026)
- Stakeholder Meeting 1 (09.03.2026)
- Milestone Review Meeting (05.04.2026)
- MVP Test Meeting (08.04.2026)
- Customer Meeting 2 (30.04.2026)