System Architecture
Technical overview of the Ananas GDS platform — backend, frontend, authentication, and data flow.
Technology Stack
Ananas GDS is a Django-backed REST API served to a Vue 3 single-page application. The platform runs on Mochahost shared hosting with a MariaDB database in production and SQLite for local development.
| Layer | Technology | Notes |
|---|---|---|
| Backend | Django 5.2 + Django REST Framework | Python 3.11, REST API |
| Frontend | Vue 3 + Vuetify 3 + Pinia | Single-page application |
| Database | MariaDB (production) / SQLite (local dev) | utf8mb4 charset required on all tables |
| Async tasks | Celery | CELERY_TASK_ALWAYS_EAGER = True in dev and test environments |
| Auth library | Djoser (partially replaced) | Custom session auth layer built on top |
| Image CDN | ImageKit | Avatar and property logo storage and delivery |
| Payments | Stripe | Subscription billing, webhooks, Stripe.js frontend |
| Hosting | Mochahost (cPanel / LiteSpeed) | Shared hosting, no SSH git access |
Django Apps
The backend is organized into focused Django apps. Each app owns its models, serializers, views, and URL routes.
| App | Purpose |
|---|---|
authapp |
Custom User model, DRF token authentication, session management, sub-user system, permission flags, IP ban logic |
info |
Property management, fact sheets (12+ categories), property photos, surveys, fact publishing and distribution |
stop_sale |
Stop sale calendar events, allotment periods, date-range management per property |
partners |
Partner contract lifecycle, connection requests, contract-level permission scoping between accommodation and TO accounts |
touroperator |
Tour operator dashboard logic, hotel and room mapping configuration, fact override management |
reports |
Activity log, API statistics (APIStats model), pre-aggregated summary model (APIStatsSummary), Celery analytics tasks |
payments |
Stripe subscription management, billing plan enforcement, FeatureAccess gating, Stripe webhook handler |
invoicing |
B2B invoice creation, invoice lifecycle (draft, sent, paid, overdue), PDF generation |
settings |
User and company settings store, get_setting() utility function for consistent settings lookup |
dev |
Public API v1 endpoints (facts, photos, stop-sale), integrations, webhook dispatch, data export |
external |
Reference data endpoints: countries, states, cities, currencies, property widget, application option lists |
notification |
In-app notifications and platform-wide announcements |
mcp_server |
MCP protocol integration for structured tool-based data access |
Facts View Pattern
The info app uses factory functions to avoid repetitive view boilerplate across the 12+ fact sheet categories. Each fact category endpoint is generated by one of two factory functions:
# Single-value facts (one record per property)
_facts_view(field, model, serializer)
# Array facts (multiple records per property, e.g. room types)
_array_facts_view(field, model, serializer)
# Both handle GET + POST on a single endpoint
# GET returns current fact data for the property
# POST creates or updates the fact record
API URL Structure
All API routes are prefixed with /api/. The following table maps URL prefixes to their corresponding Django app.
api/auth/ -> authapp (login, logout, session, sub-users)
api/facts/ -> info (properties, fact sheets, photos)
api/partners/ -> partners (contracts, connection requests)
api/stop-sale/ -> stop_sale (calendar events, allotments)
api/tour-operator/ -> touroperator (mapping, override facts)
api/reports/ -> reports (statistics, activity logs)
api/payments/ -> payments (subscriptions, Stripe)
api/invoicing/ -> invoicing (B2B invoices)
api/settings/ -> settings (user and company settings)
api/v1/ -> dev (public API, integrations, webhooks)
api/external/ -> external (countries, currencies, widget, options)
api/ -> notification (in-app notifications)
mcp/ -> mcp_server (MCP protocol endpoints)
Public API (v1)
The api/v1/ namespace is the externally-facing API consumed by tour operator integration systems. It uses static API tokens passed in the URL path rather than session tokens. There is no authentication header required — the token itself is the credential:
GET /api/v1/facts/{token}/
GET /api/v1/photos/{token}/
GET /api/v1/stop-sale/{token}/
Frontend Architecture
The frontend is a Vue 3 single-page application served separately from the Django backend. Vuetify 3 provides the UI component library; Pinia manages global state.
Routing and Role Guards
Vue Router handles all client-side navigation. Route guards in plugins/1.router/guards.js enforce:
- Authentication gate — Unauthenticated users are redirected to
/login - Role-based access — Accommodation routes are inaccessible to TO accounts and vice versa
- Permission flag checks — Sub-users attempting to access a module they lack permission for are redirected
Pinia Stores
Application state is managed through a set of Pinia stores under @stores/. Key stores include:
| Store | Owns |
|---|---|
auth |
Session token, login/logout actions, session heartbeat timer |
account |
Current user profile, account type, permission flags |
facts |
Fact sheet data per category, dirty state, save status |
stopSale |
Calendar events, allotment records, selected date range |
partners |
Contract list, pending requests, connection status |
notifications |
In-app notification list, unread count |
App Initialization
On every page load, @core/initApp.js runs before the router renders the first view. It:
- Reads the
accessTokencookie — If absent, the user is considered logged out. - Calls
GET /api/auth/session/— Validates the token server-side, returns the current user profile and permission flags. - Populates the auth and account stores — Sets the user object, account type, and permission flags.
- Starts the heartbeat timer — Schedules the session keep-alive ping every 10 minutes.
- Resolves routing — Vue Router proceeds to render the requested route once init completes.
Request Interceptor
All outbound API requests from the frontend are intercepted by jwtInterceptor.js, which attaches the Authorization: Token <key> header automatically. There is no need to manually set authorization headers in any store or component — the interceptor handles it globally.
Authentication Model
Despite the filename jwtInterceptor.js, Ananas GDS does not use JWT tokens. The authentication mechanism is DRF's built-in TokenAuthentication — opaque string tokens stored in the database.
On top of DRF's token auth, a custom SessionTokenAuthentication class adds:
- Inactivity-based session expiry (30 minutes standard, 7 days extended)
- Concurrent session enforcement (max 2 per account)
- Session metadata (IP address, device info, last active timestamp)
For the full authentication reference including login flow, per-request validation, and the IP ban system, see Authentication.
Data Flow
The core value of Ananas GDS is the pipeline from hotel data entry to tour operator API consumption. The following diagram shows how data moves through the system:
Hotel (accommodation user)
|
+-- Creates Property
|
+-- Fills Fact Sheets (12+ categories)
|
+-- Uploads Photos
|
+-- Sets Stop Sales / Allotments
|
+-- Issues API tokens to partners
|
v
Tour Operator (API consumer)
|
+-- GET /api/v1/facts/{token}/
|
+-- GET /api/v1/photos/{token}/
|
+-- GET /api/v1/stop-sale/{token}/
|
+-- Configures hotel/room mappings (internal ID mapping)
|
+-- Applies fact overrides (local edits for own catalogue)
Each API token is scoped to a specific partner contract. A hotel controls exactly which partners receive tokens, and tokens can be revoked at any time from the Dev Tools panel.
Analytics Pipeline
Every public API call is logged asynchronously via Celery tasks to the APIStats model. Stats are keyed on (user, token, endpoint, date) and track:
hits_okandhits_error— separate success and failure countersbytes_transferred— payload size for bandwidth tracking
A nightly Celery rollup task aggregates daily stats into the APIStatsSummary pre-aggregated model, which powers the Dev Tools statistics dashboard without hitting raw log tables on every page load.
Deployment Environment
The platform runs on Mochahost shared hosting. This is a constrained environment with specific limitations that affect development practices.
| Component | Detail |
|---|---|
| Web server | LiteSpeed via cPanel |
| Application server | WSGI (Python 3.11 virtualenv) |
| Database | MariaDB with innodb_strict_mode=0 |
| Cache | File-based (no Redis, no Memcached) |
| Git | Not available on server — deploy by manual file upload |
| SSH git push | Not available — changes go up file-by-file or via zip upload |
| Async workers | Celery tasks run eagerly in dev/test; production uses scheduled tasks |
ASCII-Only Python Files
The cPanel/LiteSpeed WSGI process uses ASCII encoding. Any emoji, special character, or non-ASCII byte in any .py file will silently crash all requests to that file. This applies to comments as well as code. There are no non-ASCII characters anywhere in the Python codebase — keep it that way.
MariaDB Constraints
The production database has several constraints that must be respected during development and migration authoring:
- utf8mb4 charset required on all tables — charset mismatches cause silent migration failures
innodb_strict_mode=0is set as a workaround for Row size too large errors- FK constraint ordering in migrations matters — always verify dependency order before deploying migrations
- Every app with models must have a
migrations/__init__.pyfile or it is silently skipped by the migration runner
Pre-Deployment Checklist
- No non-ASCII characters in any
.pyfile added or modified in this change - Run
makemigrationslocally and verify migration files are clean before upload - Check FK dependency ordering in any new migration files
- New app added? Ensure
migrations/__init__.pyexists - Upload files via cPanel File Manager
- Run migrations on the server via cPanel Python app terminal
- New pip dependency? Set
PKG_CONFIG_PATHbefore install; source~/.bashrcfirst