Architecture Overview¶
System Overview¶
The HNS Ticketing System is a hybrid architecture that extends the existing HNS Drupal infrastructure with a dedicated ticketing backend. The system leverages the existing HNS Drupal 10 webshop for user management and authentication while introducing a new PHP/Symfony-based ticketing system for all ticket sales, quota management, and event operations. This approach maximizes reuse of existing infrastructure, maintains consistency with current user experience, and allows the development team to leverage their PHP expertise.
High-Level Architecture¶
The platform is organized into clear layers. Most user-facing interactions go through the mobile app or web portals and are served by the ticketing backend, while Drupal remains responsible for identity and authentication.
┌───────────────────────────────────────────────────────────────────────────────────────────┐
│ Presentation Layer │
├──────────────────────┬──────────────────────┬─────────────────────────────────────────────┤
│ Quota Web Portal │ Admin Portal │ HNS Mobile App (Customer Interface) │
│ (Delegation/Petrol) │ (HNS Operations) │ (Primary Sales & Ticket Wallet) │
└───────────┬───────────┴───────────┬──────────┴───────────────────────────┬───────────────┘
│ │ │
└───────────────┬───────┴──────────────────────────────┬───────┘
│ │
▼ ▼
┌──────────────────────────────────────────┐ ┌──────────────────────────────────────┐
│ Ticketing Backend │ │ HNS Drupal 10 Webshop (Existing) │
│ (PHP / Symfony) │ │ Identity & Authentication (SSO) │
│ - inventory / quotas / orders / tickets │ │ - user accounts / login / sessions │
│ - seat reservation + snake assignment │ └──────────────────────────────────────┘
│ - transfer/refund + audit trail │
│ - access control exports/imports │
└───────────────┬──────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────────────────────────────────┐
│ Data Layer │
├──────────────────────┬──────────────────────┬──────────────────────┬───────────────────────┤
│ PostgreSQL │ Redis │ Grafana Loki │ Object storage / CDN │
│ (system of record) │ (hot cache + │ (audit logs) │ - images, maps, QR │
│ - tickets, orders, │ reservations, │ - 13 audit domains │ assets │
│ quotas │ TTL-based holds) │ - structured JSON │ │
├──────────────────────┴──────────────────────┴──────────────────────┴───────────────────────┤
│ Grafana (audit dashboards + alerting) │
└───────────────────────────────────────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────────────────────────────────┐
│ External Services / Integrations │
├──────────────────────┬──────────────────────┬──────────────────────┬───────────────────────┤
│ Payment Gateway │ Email + Firebase Push │ Accounting / Fiscaliz. │ │
│ (card + webhooks) │ (queue + delivery) │ e-racuni.com │ │
├──────────────────────┴──────────────────────┴──────────────────────┴───────────────────────┤
└───────────────────────────────────────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────────────────────────────────┐
│ Stadium Access Control (Out of Scope) │
│ Each stadium uses its own access control system. HNS provides Excel exports of valid │
│ barcodes/QR values and (optionally) imports post-match attendance exports (file-based). │
└───────────────────────────────────────────────────────────────────────────────────────────┘
Key interactions:
- The mobile app and web portals call the ticketing backend for all ticketing domain operations (queue, inventory, reservation, checkout, claiming, transfers, refunds, exports).
- The mobile app uses Drupal 10 for identity/authentication; the ticketing backend trusts Drupal identity to link ticketing records to users.
- The ticketing backend integrates with external services for payment, notifications, and invoicing; identity rules (OIB checksum) and blacklist checks are enforced internally based on data managed in the admin portal.
Core Architecture Principles¶
- Reuse existing identity: Drupal remains the system of record for user accounts and authentication.
- Mobile-first: the HNS mobile app is the primary ticket sales and ticket consumption channel.
- Ticketing as a dedicated domain: all match inventory, orders, tickets, quotas, and entry validation are handled by the ticketing backend.
- Explicit integrations: payment, notifications, and invoicing are integrated as external services with clear responsibilities.
- Internal security rules: OIB validation is checksum-based; blacklist is an internally maintained dataset (managed/imported via admin portal).
- Separated audit logging: All audit logs are stored in Grafana Loki, keeping PostgreSQL focused on transactional data. See Audit Logging Infrastructure.
Modular Approach (Recommended)¶
The ticketing backend starts as a modular monolith: a single deployable Symfony application with strict internal module boundaries. This keeps the transactional ticketing domain consistent (inventory ↔ reservation ↔ checkout ↔ ticket issuance) while still allowing future extraction into microservices when there is a clear operational need.
Key points:
- Microservices are an operational choice (independent deploy/scale, failure isolation, separate release cadence). If those benefits are not required yet, a modular monolith is typically faster to build and easier to operate.
- The system is designed so modules can be extracted later without rewriting the domain model.
Internal Modules (Phase 1)¶
Within the ticketing backend, the recommended module boundaries are:
Queue: high-demand waiting room logic, queue tokens, purchase window enforcement (Redis-backed ephemeral state).InventoryReservation: seat availability, TTL holds, release, and allocation logic (including snake assignment).OrdersPayments: cart/order state machine, payment initiation, webhook processing, refunds/voids, idempotency.TicketsWallet: ticket issuance, assignment, QR visibility timing, wallet read patterns.Quotas: quota/sub-quota management, partner workflows (e.g., Petrol), deferred payment flags.ExportsImports: access control exports (Excel) and attendance imports (batch/file processing).Integrations: adapters for email/push and accounting/fiscalization, implemented as clear boundaries.
Where possible, cross-module coordination uses domain events + an outbox pattern so integrations behave “service-like” even when the system is still deployed as one application.
Service Extraction Strategy¶
Based on scale requirements (100k+ concurrent users during high-demand sales), the architecture employs selective service extraction—two components are deployed as separate services while the remaining modules stay in the Symfony monolith.
Extracted Services¶
| Service | Technology | Rationale |
|---|---|---|
| Queue Service | Node.js or Go | Requires 100k+ concurrent WebSocket connections (PHP/Symfony suboptimal for long-lived connections); stateless, Redis-backed; clear API boundary; can be horizontally scaled independently |
| Notification Workers | PHP Workers | High volume during queue scenarios (100k+ push notifications); cross-cutting concern called by all flows; asynchronous by nature; external API rate limits require throttling |
| Stripe Webhook Router | Go | Single entry point for Stripe webhook deliveries; verifies Stripe signatures once at the edge and routes events to internal destinations by metadata.site. See Stripe Webhook Router. |
| Event Bus | NATS JetStream + Go consumer | Persistent, replayable message bus that fans out platform events (Stripe, mail, domain events) to multiple destinations over Bearer-authenticated HTTP; buffers messages across downstream outages. See Event Bus. |
Components Kept in Monolith¶
| Component | Reason |
|---|---|
| E3: Seat Inventory | Central to E4/E7/E12 - extraction creates distributed locking complexity |
| E4: Ticket Purchase | Saga orchestrator - breaking creates distributed transaction issues |
| E7: Quota Management | Uses same seat allocation algorithms as E4 |
| E8: Payment | Transaction boundaries with orders/tickets require DB consistency |
| E6, E10-E14 | Low-scale, tightly coupled to core flows |
For detailed documentation on service boundaries, communication protocols, and deployment considerations, see Microservices Strategy.
Architecture with Extracted Services¶
┌─────────────────────────────────────────────────────────────────────────┐
│ HNS Ticketing Monolith │
│ (PHP/Symfony - PostgreSQL) │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Bounded Contexts (Internal Modules) │ │
│ │ • E1: User/Profile • E7: Quota Management │ │
│ │ • E2: Match/Stadium • E9: Ticket Management │ │
│ │ • E3: Seat Inventory • E10: Customer Support │ │
│ │ • E4: Ticket Purchase • E11: Blacklist/Security │ │
│ │ • E6: Loyalty • E12: Petrol Integration │ │
│ │ • E8: Payment • E13: Access Control │ │
│ │ • E14: Reporting │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────┬───────────────────────────────────────┬──────────┘
│ │
┌──────────▼──────────┐ ┌───────────▼───────────┐
│ Queue Service │ │ Notification Workers │
│ (Node.js or Go) │ │ (PHP Workers) │
│ - Redis Sorted Set │ │ - Email (Mailgun) │
│ - WebSocket/SSE │ │ - Push (Firebase) │
│ - Position Updates │ │ - Redis Job Queue │
└──────────┬──────────┘ └───────────────────────┘
│
▼
┌───────────────┐
│ Redis Cluster │
└───────────────┘
Publishers Subscribers
┌─────────────────────────┐ ┌──────────────────────────────┐
│ Stripe Webhook Router │ │ Drupal webshop │
│ stripe.drupal │─┐ ┌─│ subject: stripe.drupal │
│ stripe.backend │ │ │ └──────────────────────────────┘
└─────────────────────────┘ │ │ ┌──────────────────────────────┐
┌─────────────────────────┐ │ │ │ Mailer microservice │
│ Ticketing Backend │ │ ├─│ subject: mail.> │
│ ticket.> order.> │─┤ │ └──────────────────────────────┘
│ payment.> match.> │ │ │ ┌──────────────────────────────┐
│ queue.> quota.> │ │ │ │ Ticketing Backend (consumer) │
│ blacklist.> loyalty.> │ │ ├─│ subject: > (catch-all) │
│ mail.> │ │ │ │ subject: stripe.backend │
└─────────────────────────┘ │ │ └──────────────────────────────┘
▼ │
┌─────────────────┴─────────────────────────────────┐
│ Event Bus — NATS JetStream + Go consumer │
│ stream: hns_ticketing_events retention: 72h │
│ envelopes payloads and delivers over HTTP with │
│ per-subscription Bearer auth, retries, backoff │
└───────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ Logging Infrastructure │
│ ┌──────────────────────┐ ┌──────────────────────────────────┐ │
│ │ Grafana Loki │ │ Grafana │ │
│ │ (audit log storage) │◄────────│ (dashboards + alerting) │ │
│ │ - 13 audit domains │ │ - embedded in Admin Portal │ │
│ └──────────────────────┘ └──────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
Event Catalog¶
Cross-service communication uses two transports: Redis for queue-service coordination and low-latency job queues, and the Event Bus (NATS JetStream) for platform-wide integration events that need persistence, replay, or fan-out to multiple destinations.
Redis-transport events¶
| Event | Publisher | Consumer |
|---|---|---|
queue.turn_granted |
Queue Service | Monolith (E4) |
queue.expired |
Queue Service | Monolith (E4) |
order.completed |
Monolith (E4) | Notification Workers (push/email job queue) |
ticket.generated |
Monolith (E9) | Notification Workers (push/email job queue) |
Event Bus subjects¶
The stream hns_ticketing_events captures every subject below (72h retention). The subscription column names the NATS durable consumer that forwards messages to its destination over HTTP.
| Subject | Publisher | Subscription | Destination |
|---|---|---|---|
stripe.drupal |
Stripe Webhook Router | stripe-drupal |
Drupal webshop (/stripe-webhook) |
stripe.backend |
Stripe Webhook Router | (bundled in catch-all) | Ticketing backend (/webhooks/stripe) |
mail.* |
Ticketing backend (E15) | mailer-auth-events |
Mailer microservice (/mail-template) |
ticket.*, order.*, payment.*, match.*, queue.*, quota.*, blacklist.*, loyalty.* |
Ticketing backend | backend-all-events (subject >) |
Ticketing backend (/api/v1/webhooks/nats) |
The backend appears on both sides of the bus: as a publisher for domain events (driving notification workflows, partner integrations, audit trails) and as a subscriber via the catch-all > subscription that feeds its internal event-driven workflows plus the dedicated stripe.backend path for incoming Stripe events.
Concurrent Seat Allocation Strategy¶
Seat inventory remains in the monolith using PostgreSQL optimistic locking with FOR UPDATE SKIP LOCKED:
SELECT id, row, seat_number
FROM seats
WHERE sector_id = :sector AND status = 'AVAILABLE'
ORDER BY row_priority, snake_order
LIMIT :quantity
FOR UPDATE SKIP LOCKED;
Key characteristics:
- Retry logic: 3 attempts with exponential backoff on contention
- Cart reservation: Redis TTL (15 minutes) for reserved seats
- Snake algorithm: Runs in PHP service layer for optimal seat grouping
Trade-offs Summary¶
| Decision | Benefit | Trade-off |
|---|---|---|
| Extract Queue Service | WebSocket scaling, technology optimization | Additional service to deploy/monitor |
| Keep Purchase Flow in monolith | Transaction integrity, simpler debugging | Limited independent scaling |
| Notification Workers | Async processing, rate limiting | Workers share monolith codebase |
| PostgreSQL optimistic locking | Simple, battle-tested | Retry overhead under high contention |
Systems and Responsibilities¶
HNS Drupal 10 Webshop (Existing)¶
Primary responsibility: user account management and authentication.
Used in flows:
- App login and session management across all user flows
- User profile base data (e.g., name, date of birth, email), with ticketing-specific profile stored/managed in ticketing context (see Profile Management)
How it interacts:
- The HNS mobile app authenticates users against Drupal (SSO/session/token depending on implementation).
- The ticketing backend trusts Drupal identity (e.g., user id/email) as the authenticated principal and uses it to link ticketing records.
HNS Mobile App (Primary Channel)¶
Primary responsibility: customer-facing UI for discovering matches, entering queue, purchasing tickets, storing and presenting tickets, and receiving notifications.
Used in flows:
- Waiting Queue
- Standard Ticket Purchase
- View My Tickets
- Physical Sales at Petrol Station
- Loyalty Early Access Purchase
How it interacts:
- Calls ticketing APIs for:
- match inventory visibility and seat selection
- seat reservation and checkout
- displaying tickets and QR codes
- quota claiming and delegation dashboards
- pending Petrol PIN purchase requests
- Receives notifications via push (Firebase) and email deep links.
Ticketing Backend (New, PHP/Symfony)¶
Primary responsibility: system of record for ticketing.
Owns:
- matches/events, stadium configuration per match, sectors, seats, seat statuses
- quotas and quota delegation (sub-quotas)
- orders, payments state, invoices references
- tickets (per attendee), ticket holder identities (OIB/passport), delivery assignment (email/app account)
- entry validation state (issued / activated / used), and attendance import
How it interacts:
- Exposes APIs consumed by:
- mobile app (end-user flows)
- admin portal (event operations)
- quota portal (quota holders, Petrol staff)
- Integrates with external services for:
- payment gateway (card payments + webhooks)
- email delivery (order confirmations, ticket delivery, quota invites)
- push notifications (queue, purchase, quota, loyalty)
- invoicing/accounting (
e-racuni.com) for invoices/credit notes and fiscalization
Security validation:
- Enforces OIB format/checksum validation wherever identity is captured.
- Performs blacklist checks against the ticketing backend dataset (maintained/imported via Admin Portal).
Admin Portal (Event Operations)¶
Primary responsibility: HNS staff operations for match lifecycle, inventory, quotas, reporting, and access control exports.
Used in flows:
- Admin Match Management
- Match Stadium Configuration
- Admin Quota Creation
- Financial Reporting
- Deferred Payment Collection
How it interacts:
- Admin authenticates (MFA) and operates via the ticketing backend.
- Publishes match configuration and sales phases.
- Generates exports for access control (valid codes list) and later imports attendance.
- Generates invoices and credit notes through accounting integration.
Quota Web Portal (Quota Holders and Partner Channels)¶
Primary responsibility: web UI for quota claiming and special partner workflows.
Used in flows:
- Quota Holder Web Delegation
- Mobile Quota Claiming
- Petrol physical sales: staff uses a restricted “Petrol role” to process PIN-based sales (see Physical Sales at Petrol Station)
How it interacts:
- Shares the same ticketing backend as the system of record.
- Supports role-based access:
- quota owners (view allocated seats, delegate via sub-quotas)
- partner staff roles (e.g., Petrol) with restricted permissions
Stripe Webhook Router¶
Primary responsibility: single entry point for all Stripe webhook deliveries across the HNS platform.
How it interacts:
- Receives
POST /webhook/stripefrom Stripe, verifies the Stripe signature once against the shared signing secret. - Reads
metadata.sitefrom each event payload and looks it up in a hot-reloaded YAML routing table. - Publishes the raw Stripe payload to the matched NATS subject (
stripe.drupal,stripe.backend, etc.) for the Event Bus to deliver. - Returns
200for unroutable events (missing/unknownmetadata.site) so Stripe does not retry; returns500only on infrastructure errors so Stripe's native retry mechanism applies.
Downstream destinations do not re-verify Stripe signatures — the router is authoritative for that trust boundary. See Stripe Webhook Router for detail.
Event Bus (NATS JetStream + Consumer)¶
Primary responsibility: persistent, replayable cross-system messaging for integration events.
How it interacts:
- Captures every message published to the
hns_ticketing_eventsJetStream stream (subjects:ticket.>,order.>,payment.>,match.>,queue.>,quota.>,blacklist.>,loyalty.>,mail.>,stripe.>). - The consumer subscribes to each configured subject, wraps the payload in a standard envelope (
{subject, data, timestamp, message_id}), and POSTs to the configured destination URL with a Bearer token. - Retries failed deliveries per-subscription policy; keeps undelivered messages up to the stream's 72h retention window.
Current destinations: Drupal webshop (stripe.drupal), mailer microservice (mail.>), ticketing backend (> catch-all and stripe.backend). See Event Bus for detail.
Stadium Access Control (Gate Devices / Turnstiles)¶
Primary responsibility: validate ticket entry at stadium gates.
Scope note: Stadium access control systems are outside the scope of this project. Each stadium uses its own access control solution.
Used in flows:
How it interacts:
- Before the match: HNS exports an Excel file of valid ticket codes (barcodes/QR values) from the admin portal.
- During the match: the stadium access control system performs offline validation and marks codes as used in its own system.
- After the match (optional): the stadium access control operator provides an attendance export (file), which HNS uploads via the admin portal to import attendance into the ticketing backend.
External Integrations¶
Payment Gateway¶
- Card payments are authorized/captured through the payment gateway (Stripe) during standard purchase.
- Refunds are issued through the gateway refund API (and reconciled with accounting).
- Webhook events from Stripe are received by the Stripe Webhook Router, which verifies signatures once and publishes to the Event Bus for delivery to the Drupal webshop and the ticketing backend. Producers set
metadata.siteon every PaymentIntent so the router can determine the destination.
Email Service¶
- Ticket delivery and critical communications are email-backed (order confirmations, quota invitations, ticket reassignment notifications).
- Emails include deep links to the HNS mobile app.
Push Notifications (Firebase)¶
- Used for queue updates, quota availability, purchase confirmations, and loyalty notifications.
Accounting / Fiscalization (e-racuni.com)¶
- Invoices and credit notes are created/fiscalized via
e-racuni.com. - Deferred payment workflows generate payment offers and later pull issued invoices back into the ticketing backend.
How the Systems Work Together (Key Scenarios)¶
1) High-Demand Queue and Purchase Entry¶
Based on Waiting Queue:
- User opens match in the mobile app.
- App calls ticketing backend to determine if queue is required.
- If queue is active, ticketing backend issues a queue token and estimated wait.
- App receives live updates (push/WebSocket/polling) until the user reaches the front.
- Backend grants a time-limited purchase window (20 minutes in the flow).
2) Standard Ticket Purchase and Ticket Delivery¶
Based on Standard Ticket Purchase and View My Tickets:
- User is authenticated via Drupal and selects seats in the app.
- Ticketing backend reserves selected seats (15 minute cart reservation timeout).
- User enters ticket holder identities (OIB/passport) and optional ticket delivery emails.
- Backend validates OIB format and performs blacklist checks for all attendees.
- Payment is processed via payment gateway.
- Tickets are issued in ticketing backend and delivered:
- to the buyer account in-app
- and/or assigned to other emails (delivered when the recipient registers/logs in with matching email)
- QR codes become visible shortly before the match (5 hours in the flow) and are cached for offline entry (24 hours in the flow).
3) Quota Allocation, Claiming, and Delegation¶
Based on Admin Quota Creation, Mobile Quota Claiming, and Quota Holder Web Delegation:
- Admin creates quotas in the admin portal; ticketing backend immediately allocates concrete seats using configured allocation algorithms.
- Backend sends email invitations and push notifications with deep links.
- Quota owner claims tickets in app or quota portal, providing attendee identities.
- Tickets can be paid immediately or marked as deferred payment (partner workflows).
- For quotas with delegation enabled, quota owner creates sub-quotas and assigns tickets to others.
4) Physical Sales at Petrol (PIN + Quota Portal)¶
Based on Physical Sales at Petrol Station:
- Customer enters a hidden Petrol flow via QR/deep link, prepares ticket holder identities in the app, and requests a purchase PIN.
- Ticketing backend validates identities (including blacklist) and creates a pending PIN request.
- Petrol staff logs into quota portal (Petrol role), enters PIN, and the backend reserves seats for a short window to avoid multi-POS conflicts (20 minutes in the flow).
- Payment is taken externally by Petrol; staff confirms sale in the quota portal.
- Backend marks tickets sold and delivers them via push/email to the user.
5) Support-Driven Transfer and Refund¶
Based on Customer Support Ticket Transfer and Customer Support Ticket Refund:
- Support agents operate via admin portal on top of the ticketing backend.
- Transfers update ticket holder identity and delivery assignment, run blacklist checks, and trigger notifications.
- Refunds trigger payment gateway refunds/voids, invalidate tickets immediately, update inventory, and create credit notes via accounting integration.
6) Stadium Entry Validation and Attendance¶
Based on Stadium Entry Validation:
- Admin exports an Excel list of valid ticket codes for the stadium’s access control system.
- At gates, the stadium system validates QR/barcodes offline and marks used locally.
- After the match (optional), the stadium operator provides an attendance export file.
- Admin uploads the attendance export in the Admin Portal; the ticketing backend updates attendance and triggers loyalty point awarding logic.
7) HOME vs AWAY Match Inventory¶
Based on Match Stadium Configuration and Away Match Tickets (Non-Numbered Seats):
- HOME matches use pre-configured Croatian stadium templates; per-match configuration is versioned and can be adjusted during sales with constraints.
- AWAY matches can start as non-numbered capacity allocations and later receive a seat map; the backend assigns seats using snake algorithm and exports attendee lists to the away federation.
Related Documentation¶
- Data Model
- Event Bus
- Stripe Webhook Router
- Audit Logging Infrastructure
- UI/UX flows overview:
docs/ui-ux/flows/
Last Updated: April 2026