Skip to content

Backend

The backend is a Python REST API built with FastAPI, following a layered architecture. It handles all business logic, database access, authentication, email notifications, and Telegram bot integration.

Tech Stack

CategoryTechnologyVersion
LanguagePython3.11
FrameworkFastAPI0.110+
ASGI ServerUvicorn0.27+
ORMSQLAlchemy2.0+
DatabasePostgreSQL16
MigrationsAlembic1.13+
ValidationPydantic2.0+
Password HashingPasslib + bcryptbcrypt 4.0+
JWTpython-jose3.3+
Emailfastapi-mail1.5+
HTTP ClientHTTPX0.24+
Metricsprometheus-fastapi-instrumentator6.1+
LintingRufflatest
TestingPytestlatest

Layered Architecture

Project Structure

text
iu-alumni-backend/
├── app/
│   ├── main.py                 # App init, router registration, lifespan
│   ├── api/routes/
│   │   ├── authentication/     # register, login, OTP, password reset
│   │   ├── profile/            # CRUD user profile
│   │   ├── events/             # event CRUD + participation
│   │   ├── admin/              # admin operations
│   │   ├── cities/             # city search
│   │   └── telegram/           # webhook handler
│   ├── core/
│   │   ├── database.py         # SQLAlchemy engine & session
│   │   ├── security.py         # JWT, password hashing, auth dependencies
│   │   └── logging.py          # structured logging
│   ├── models/                 # ORM models (10 tables)
│   ├── schemas/                # Pydantic request/response schemas
│   ├── services/               # business logic & external integrations
│   └── templates/
│       └── email/              # Jinja2 HTML email templates
├── alembic/                    # 15 migration versions
├── scripts/                    # send_event_reminders.py
├── cron/                       # crontab for background jobs
└── tests/

API Endpoints

Authentication (/auth)

MethodPathDescription
POST/registerRegister new alumni
POST/loginLogin with password → JWT
POST/login-otpVerify OTP → JWT
POST/verifyConfirm email verification code
POST/password-reset-requestRequest password reset link
POST/password-reset-confirmSet new password via token

Profile (/profile)

MethodPathDescription
GET/Get own profile
PUT/Update own profile
GET/other/{id}Get another user's public profile
GET/other/{id}/avatarFetch user avatar (base64, cached)
GET/mapAlumni counts grouped by city/country for map pins
GET/usersPaginated alumni list (cursor-based, server-filtered)

Events (/events)

MethodPathDescription
POST/Create event
GET/Paginated list of approved events (cursor-based, server-filtered)
GET/{id}/coverFetch event cover image (base64, cached)
POST/{id}/participantsJoin event

Admin (/admin)

MethodPathDescription
POST/banBan a user
GET/eventsList all events (incl. unapproved)
POST/events/{id}/approveApprove an event

Other

PrefixMethodPathDescription
/citiesGET/searchSearch cities by name
/telegramPOST/webhookTelegram bot webhook

Pagination

All list endpoints (GET /profile/users, GET /events/) use cursor-based pagination to provide stable, efficient paging over large datasets without the offset skew problem.

Response shape

json
{
  "items": [...],
  "next_cursor": "eyJpZCI6ICI0MiJ9",
  "has_more": true
}

Query parameters

ParameterTypeDescription
cursorstring | nullOpaque cursor from previous page's next_cursor
limitintPage size (default 20, max 100)
searchstring | nullServer-side text filter (name / title)

Cursor encoding

The cursor is a base64-encoded JSON object containing the sort key(s) of the last item seen.

text
base64( JSON({ "id": "uuid" }) )               # ID-only cursor
base64( JSON({ "id": "uuid", "dt": "iso8601" }) )  # date+ID cursor (events)

The server decodes the cursor, applies a WHERE (sort_key > cursor_value) clause, and returns the next page. Clients treat the cursor as opaque — never construct or parse it.

Slim response schemas

List endpoints return lightweight schemas to avoid sending large binary fields (avatars, covers) in bulk. Images are fetched separately on demand.

Full schemaSlim list schemaFields omitted
AlumniProfileAlumniListItemavatar (base64)
EventEventListItemcover (base64)

Map Endpoint

GET /profile/map returns alumni counts grouped by city for map-pin display. It is designed to be called once on map load and is read-only.

json
{
  "locations": [
    { "country": "Russia", "city": "Innopolis", "lat": 55.75, "lng": 48.74, "count": 42 }
  ]
}

How it works:

  1. Alumni store their location as a "Country, City" string.
  2. PostgreSQL's split_part() function splits the string into country and city expressions.
  3. These are JOINed against the cities table (indexed on city + country) to look up lat/lng coordinates.
  4. Results are grouped and counted — the mobile client receives a single flat list of pins with no extra round-trips.

Filters applied:

  • show_location = true
  • location IS NOT NULL and matches %, % pattern
  • is_verified = true
  • is_banned = false

Indexes used:

IndexTablePurpose
ix_alumni_show_location_locationalumniLeading filter on show_location + location
idx_city_namecitiesJOIN on city name
idx_countrycitiesJOIN on country

Email Templates

All transactional emails are rendered from Jinja2 HTML templates located in app/templates/email/. The fastapi-mail TEMPLATE_FOLDER config points to this directory; each send function passes a template_body dict and specifies a template_name.

Templates

Template fileTriggered byKey variables
_base.html(base layout, not sent directly)subject, content block
login_code.html2FA login OTPfirst_name, code, expiry_minutes
password_reset.htmlPassword reset requestfirst_name, reset_link, expiry_minutes
verification.htmlRegistration email verificationfirst_name, verification_code
verification_success.htmlAccount approvedfirst_name
manual_verification.htmlAdmin notification for manual reviewuser_name, user_email

Design

  • Table-based layout — required for Outlook and older email clients (no <div> layout).
  • Fully inline CSS — Gmail and many webmail clients strip <style> blocks; all styling is style="" attributes.
  • MSO conditional comments<!--[if mso]> blocks fix Outlook-specific rendering bugs (e.g. button width).
  • Responsive — a single @media (max-width: 620px) block (the one case where <style> is safe) collapses padding and widths on mobile.
  • Brand colour — IU Alumni green #40BA21 used in the header bar and CTA buttons.

SMTP configuration

Environment variableDefaultNotes
MAIL_SERVERsmtp.gmail.comSMTP host
MAIL_PORT587STARTTLS port
MAIL_USERNAMEGmail address or Google Workspace user
MAIL_PASSWORDMust be a Google App Password (16 chars). Regular account passwords are rejected by Google since Sep 2024. Generate at myaccount.google.com → Security → App Passwords.
MAIL_FROMnoreply@innopolis.universityEnvelope From address
MAIL_FROM_NAMEIU Alumni PlatformDisplay name

Authentication Flow

Database Schema (ERD)

Design Patterns

PatternWhere Used
Dependency InjectionDepends(get_db), Depends(get_current_user) in every route
Service LayerEmailService, TelegramBotService, NotificationService encapsulate external I/O
Repository (via ORM)SQLAlchemy session used directly in services/routes for DB access
StrategyDifferent auth flows (password, OTP, reset) behind same /auth prefix
Factoryget_random_token() for password reset & OTP code generation
MiddlewareCORS, Prometheus instrumentation applied globally
LifespanFastAPI lifespan context for startup/shutdown hooks (Telegram polling)

Background Jobs

Events scheduled within ~12 hours are queried and notifications sent via Telegram. The cron job runs either as a Docker container (docker-compose.cron.yml) or a Kubernetes CronJob.

Environment Variables

VariablePurpose
SQLALCHEMY_DATABASE_URLPostgreSQL connection string
SECRET_KEYJWT signing secret
ENVIRONMENTDEV or PROD (controls docs visibility, log level)
MAIL_SERVER / MAIL_USERNAME / MAIL_PASSWORDSMTP email credentials
TELEGRAM_TOKENTelegram Bot API token
ADMIN_CHAT_IDTelegram chat ID for admin notifications
CORS_ORIGINSComma-separated allowed origins
ADMIN_EMAIL / ADMIN_PASSWORDDefault admin account seed

IU Alumni Platform Documentation