The WayFy backend is a Python Flask application that follows a strict three-layer architecture. Every API feature is split across a routes file (HTTP parsing and response serialisation), a controller file (business logic and database queries), and a model file (SQLAlchemy ORM definition). This separation keeps route handlers thin, testable, and free of SQL — and keeps model files free of HTTP concerns.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/jhonyes04/new-wayfy/llms.txt
Use this file to discover all available pages before exploring further.
Three-Layer Architecture
Routes layer
Parses the incoming HTTP request (query params, JSON body, JWT identity), delegates to the controller, and returns the JSON response. Contains no SQL and no business rules.
Controllers layer
Contains all business logic: validation, DB queries via SQLAlchemy, Cloudinary uploads, Groq API calls. Returns a Flask
jsonify response.Flask Application Setup
app.py initialises every Flask extension and registers the single api Blueprint. The order of initialisation matters — extensions that depend on the app config (JWT, CORS, Limiter, SQLAlchemy) are configured before any Blueprint or command registration.
Production Static Serving
WhenFLASK_DEBUG is not "1", Flask serves the compiled Vite output from ../dist/ (relative to backend/src/). Any path that is not a file on disk returns index.html, enabling React Router’s client-side routing:
Route Blueprints
All blueprints are imported inapi/routes/__init__.py and registered on the parent api Blueprint with their URL prefix. The parent Blueprint is then registered on the app at /api.
Blueprint Registry
| Prefix | Blueprint file | Purpose |
|---|---|---|
/api/users | user_routes.py | Registration, login, forgot-password, profile CRUD, avatar upload |
/api/ai | ai_routes.py | Groq LLM queries for the AI assistant, geocoding, POI geocoding |
/api/accessibility | accessibility_routes.py | Community wheelchair reviews and photo uploads |
/api/trips | trip_routes.py | Trip CRUD, days, places, cover images, forking, public listing |
/api/places | place_routes.py | Community-submitted custom pins (admin-moderated) |
/api/utils | utils_routes.py | OG image scraper, Google Places photo proxy |
Blueprint Details
/api/users — Authentication & Profiles
/api/users — Authentication & Profiles
Handles the full user account lifecycle. Public endpoints (no JWT required):
POST /register, POST /login, POST /forgot-password. All profile and avatar endpoints require @jwt_required().POST /api/users/register— creates a new user with bcrypt-hashed passwordPOST /api/users/login— returns{ token, expiresIn }on successGET /api/users/— lists all users (admin only, checked via JWT claims)GET /api/users/<id>— returns a single user’s profilePUT /api/users/<id>— updates profile fields; users can only update themselvesDELETE /api/users/<id>— soft or hard delete (admin or self)POST /api/users/<id>/avatar— uploads avatar to Cloudinary, stores filenameGET /api/users/avatar/<filename>— serves avatar files from local storage fallback
/api/ai — AI Assistant & Geocoding
/api/ai — AI Assistant & Geocoding
All endpoints are rate-limited via Flask-Limiter. No JWT required — the AI assistant is accessible to anonymous visitors.
POST /api/ai/mapgpt— sends a user prompt to Groq/Llama; rate-limited to 20 per minute. Returns a structured response that the AI assistant module uses to pan the map or display information.GET /api/ai/geocode?q=<query>— resolves a place name to coordinates using Nominatim/Geoapify; rate-limited to 30 per minuteGET /api/ai/geocode-poi?q=<query>— geocodes a POI name with more structured result; rate-limited to 30 per minute
/api/accessibility — Community Reviews & Photos
/api/accessibility — Community Reviews & Photos
Community members submit structured accessibility data that enriches the OSM baseline. One review per OSM place (
osm_id is unique in accessibility_reviews).GET /api/accessibility/<osm_id>— retrieves the community review for a placePOST /api/accessibility/— creates or updates a review (@jwt_required())POST /api/accessibility/<id>/photos— uploads a photo to Cloudinary; requires JWTDELETE /api/accessibility/photos/<id>— deletes a review photo; requires JWTGET /api/accessibility/photos/<filename>— serves locally stored photo files
/api/trips — Trip Planner
/api/trips — Trip Planner
Full CRUD for the trip planner feature. All endpoints require
@jwt_required() except the public listing.GET /api/trips/public— paginated listing of all public trips (no auth required)GET /api/trips/public/<id>— detail view of a single public tripGET /api/trips/— lists the current user’s own tripsPOST /api/trips/— creates a new tripGET /api/trips/<id>— full trip detail including days and placesPUT /api/trips/<id>— updates title, description, visibility, cover imageDELETE /api/trips/<id>— deletes a trip and all its days and places (cascade)POST /api/trips/<id>/fork— duplicates a public trip to the current user’s account, settingoriginal_trip_idPOST /api/trips/<id>/cover— uploads cover image to CloudinaryGET /api/trips/cover/<filename>— serves cover image files- Day and place management sub-routes under
/api/trips/<id>/days/and/api/trips/<id>/days/<day_id>/places/
/api/places — Community Custom Pins
/api/places — Community Custom Pins
Users submit custom location pins that start with
status = 'pending'. Admins review and approve them via the Flask-Admin panel, after which they appear on the map as the community-approved layer. The submitting user always sees their own pending pins via the community-pending layer.GET /api/places/— returns approved places + the current user’s own pending placesPOST /api/places/— submits a new pin (@jwt_required()); status defaults to'pending'PUT /api/places/<id>— updates a pending place (owner or admin only)DELETE /api/places/<id>— deletes a place (owner or admin only)
/api/utils — Utility Proxies
/api/utils — Utility Proxies
Stateless proxy endpoints that keep API keys server-side and work around browser CORS restrictions.
GET /api/utils/og-image?url=<url>— scrapes theog:imageortwitter:imagemeta tag from a given URL; rate-limited to 60 per minuteGET /api/utils/google-photo?name=<name>&lat=<lat>&lon=<lon>— proxies Google Places API v1 to retrieve a place photo URI; requiresGOOGLE_PLACES_KEYenv var; rate-limited to 60 per minute
Error Handling
WayFy uses a single custom exception class,APIException, for all expected API errors. It is defined in api/utils.py and caught globally in app.py.
app.py converts every APIException to a consistent JSON response:
The frontend’s
handleResponse helper reads body.msg from error responses (Flask-JWT-Extended uses msg for its own errors). Controllers should keep the key consistent: use message in APIException payloads and let the frontend handle both shapes.Authentication & Rate Limiting
All protected routes use Flask-JWT-Extended’s@jwt_required() decorator. The current user’s ID is extracted with get_jwt_identity(), which returns the string id embedded at token creation time. Additional claims (such as is_admin) are read from get_jwt():
limiter instance from api/utils.py:
limiter uses get_remote_address as the key function, so limits are enforced per client IP. The limiter is initialised lazily with limiter.init_app(app) in app.py.
Static File Serving
User-uploaded assets are stored in Cloudinary. Only the filename or Cloudinary public ID is persisted in the database. Each route blueprint that owns assets provides a GET endpoint to serve locally cached fallbacks:| Asset type | DB column | Serving endpoint |
|---|---|---|
| User avatars | users.avatar | GET /api/users/avatar/<filename> |
| Accessibility photos | accessibility_photos.filename | GET /api/accessibility/photos/<filename> |
| Trip cover images | trips.cover_image | GET /api/trips/cover/<filename> |
serialize() methods on the relevant models build the full URL path automatically:
Database Models
All models use SQLAlchemy 2.xMapped / mapped_column annotations. osm_id is the shared identifier that links UserFavorite, AccessibilityReview, and TripDayPlace records — always stored as a string.
| Model | Table | Key fields |
|---|---|---|
User | users | email (unique), password (bcrypt hash), selected_mobility (JSON as Text), avatar, is_admin |
UserFavorite | user_favorites | osm_id + user_id unique together; wheelchair, all_tags (JSON as Text) |
AccessibilityReview | accessibility_reviews | osm_id unique — one review per place; wheelchair (‘yes’/‘limited’/‘no’); boolean flags: has_ramp, has_elevator, has_accessible_toilet, has_accessible_parking, automatic_door |
AccessibilityPhoto | accessibility_photos | filename; FK to accessibility_reviews |
Trip | trips | is_public, original_trip_id self-FK (forks), cover_image |
TripDay | trip_days | day_number (ordered); places relationship ordered by order |
TripDayPlace | trip_day_places | favorite_id FK to UserFavorite (optional), osm_id, order, visit_time, visit_time_end |
Place | places | Community pin; status = 'pending' or 'approved'; moderated via Flask-Admin |
Alembic Migrations
Schema changes are managed with Flask-Migrate (Alembic). The migration workflow is:Migrate(app, db, compare_type=True) is initialised in app.py with compare_type=True so Alembic detects column type changes (e.g. String(50) → String(200)) in addition to structural changes.
Flask-Admin
setup_admin(app) in api/admin.py registers Flask-Admin views. The most important view is the Place model admin, which is the primary interface for moderating community-submitted pins. Admins change a pin’s status from 'pending' to 'approved' through the /admin/places panel, which immediately makes the pin visible on the map for all users.
The admin panel is available at /admin/ and is protected by a custom admin check. It is not exposed to regular authenticated users — the AdminRoute guard on the frontend and a corresponding backend check ensure only users with is_admin = True can access it.