High-concurrency event ticketing backend and administration platform with Midtrans Core API payment processing, JWT authentication for mobile clients, and asynchronous ticket dispatch.
Run the full stack locally in under 2 minutes using Docker:
# 1. Clone repository & enter directory
git clone https://github.com/pratama1246/ticketly-project.git
cd ticketly-project
# 2. Boot application, web server, and database
docker compose up -d --build
# 3. Access web platform & interactive API documentation
# Web: http://localhost:8080
# API Docs: http://localhost:8080/api/docsOr run via PHP local CLI:
composer install && cp env .env && php spark migrate && php spark serve --port=8080- System Overview
- Practical Use Cases
- Core Engineering Capabilities
- System Architecture
- Directory Structure
- Operational Runbook & Deployment
- Environment Configuration
- REST API Reference & Interactive Docs
- Local Development & Testing
- Default Testing Accounts
- License & Disclaimer
Ticketly Backend is a monolithic yet decoupled ticketing server built with CodeIgniter 4 (PHP 8.2/8.1) and MySQL 8.4. It powers both the server-rendered web administrative dashboard and the public client application, while exposing a stateless, custom JWT-protected RESTful API for the companion mobile application (Ticketly Flutter Client).
The system handles the entire ticketing lifecycle: event cataloging, tiered ticket pricing, dynamic seating allocations, flash-sale inventory reservation, automated payment processing via Midtrans Core API (10 payment rails), webhook status reconciliation, and asynchronous background dispatch of encrypted QR code PDF e-tickets.
All sensitive endpoints are guarded with custom rate limiting, cryptographic Bcrypt OTP storage, token revocation blacklisting, UU PDP personal data masking, and enterprise security headers.
| Scenario | Problem Solved | Realized Benefit |
|---|---|---|
| High-Traffic Ticket Wars | Race conditions during concurrent ticket checkout leading to overselling beyond venue capacity. | Conditional SQL atomic queries (UPDATE ... WHERE quantity_sold + ? <= quantity_total) and row-level pessimistic locks (SELECT ... FOR UPDATE) guarantee zero over-selling. |
| Direct Core API Payments | Clunky redirect popups (Snap) breaking mobile app checkout flows. | Native Midtrans Core API /v2/charge handles direct Virtual Account numbers, QRIS string payloads, and e-wallet deep-links without webview redirection. |
| Gate Check-in Operations | Network latency and fraudulent duplicate barcode scans at physical entrance gates. | Atomic check-in endpoint with state-locked queries returns 200 OK on first validation and 409 Conflict on subsequent scans. |
| Regulatory Compliance (UU PDP) | PII leakage in API transaction responses and administrative logs. | Automatic PII masking filters (mask_email, mask_phone, mask_nik) mask identity data before returning JSON payloads to client devices. |
| Unattended Order Cleanup | Abandoned checkout sessions locking inventory indefinitely. | Background CLI cron command (php spark orders:expire) runs independently of web traffic to release expired quotas back to general stock. |
- Atomic Stock Reservation: Eliminates overselling via conditional SQL statements executing direct decrement checks:
UPDATE ticket_types SET quantity_sold = quantity_sold + ? WHERE (quantity_sold + ?) <= quantity_total. - Pessimistic Seat Allocation: Enforces
SELECT ... FOR UPDATErow-level locks inOrderModel::assignSeats()to prevent race conditions during concurrent seat selections. - Anti-Scalper NIK Quota: Enforces a strict limit of 4 tickets per Indonesian National ID (NIK) across all pending and completed orders per event.
- Idempotency Key Guard: Processes
X-Idempotency-Keyheaders onPOST /api/checkout/startto prevent duplicate billing from network retries or double clicks.
- Direct charge integration across 10 distinct payment rails:
- Virtual Accounts: BCA, BNI, BRI, Permata (AlloBank).
- Bill Payment: Mandiri E-Commerce / Bill Key & Biller Code.
- E-Wallets & Instant Pay: GoPay, ShopeePay, OVO, DANA, QRIS.
- Cardless Credit: Akulaku PayLater.
- Webhook Reconciliation: Secure webhook controller (
POST /api/payment/webhook) verifyingSHA-512payload signatures (order_id + status_code + gross_amount + ServerKey), with automatic inventory rollback on expired/cancelled orders.
- Decoupled Queue Worker: Spark command
php spark queue:workmonitors pending delivery jobs to compile PDF tickets with QR codes via Dompdf and dispatch them through SMTP without blocking the HTTP request thread. - Automated Expiry Scheduler: Standalone CLI runner
php spark orders:expirehandles unblocking held inventory without degrading read latency on public catalog endpoints.
- Dedicated Gate Check-in API (
/api/scanner/validate,/api/scanner/checkin,/api/scanner/stats/:eventId). - Verifies attendee identity against ticket item codes and marks tickets as used in a single atomic database update to prevent double-entry fraud.
- Stateless Custom JWT: Token creation and validation via
firebase/php-jwtwith centralizedJwtFilterinjecting authenticated credentials into$_SERVER['JWT_USER_ID']. - Token Revocation (Blacklist): Instant token invalidation upon
POST /api/auth/logoutvia cache blacklist checks. - Rate Limiting: Sliding-window rate limiter filter (
api_rate_limit:10,60) applied to authentication and checkout endpoints. - Single-Use Hashed OTP: OTP codes stored using Bcrypt (
PASSWORD_BCRYPT) and deleted immediately after password reset. - Enterprise Headers: Global
SecurityHeadersFilterinjectingX-Frame-Options: DENY,X-Content-Type-Options: nosniff,Referrer-Policy: strict-origin-when-cross-origin, andPermissions-Policy.
flowchart TD
subgraph Clients["Client Layer"]
WEB["Desktop / Mobile Browser<br/>(Tailwind CSS + Flowbite)"]
APP["Ticketly Mobile Client<br/>(Flutter / Dart)"]
GATE["Gate Check-in Scanners<br/>(Scanner API)"]
end
subgraph Gateway["Network & Reverse Proxy"]
NGINX["Nginx Web Server<br/>(Port 8080 / Alpine)"]
end
subgraph Backend["Application Layer (CodeIgniter 4)"]
FILTERS["Middleware Filters<br/>(JwtFilter | CorsFilter | ApiRateLimit | SecurityHeaders)"]
subgraph Controllers["Controller Modules"]
API_CTRL["Api/ Controllers<br/>(Auth, Events, Checkout, Orders, Scanner, Vouchers)"]
ADMIN_CTRL["Admin/ Controllers<br/>(Events, Tickets, Orders, Dashboard)"]
USER_CTRL["User/ & Public/ Controllers<br/>(Web Checkout, Catalog, Legal)"]
end
subgraph Services["Services & Helpers"]
MIDTRANS["MidtransService<br/>(Core API /v2/charge)"]
DELIVERY["TicketDeliveryService<br/>(Dompdf + SMTP Mailer)"]
JWT_HELP["jwt_helper.php<br/>(Create, Decode & Blacklist)"]
MASKING["masking_helper.php<br/>(UU PDP PII Masking)"]
end
end
subgraph CLI["Background CLI Daemons"]
WORKER["php spark queue:work<br/>(Async PDF & Email)"]
EXPIRE["php spark orders:expire<br/>(Inventory Auto-release)"]
end
subgraph Storage["Data Persistence"]
DB[("MySQL 8.4 Server<br/>(Port 3307 / utf8mb4)")]
FILES["Local Disk Storage<br/>(writable/uploads & PDFs)"]
end
subgraph External["External Cloud Services"]
MIDTRANS_API["Midtrans Gateway<br/>(Core API & Webhooks)"]
SMTP["SMTP Mail Provider<br/>(Mailtrap / Custom TLS)"]
end
%% Routing Flow
WEB -->|HTTP Requests| NGINX
APP -->|REST API Requests / Bearer JWT| NGINX
GATE -->|Scanner API Requests / Bearer JWT| NGINX
NGINX -->|FastCGI (Port 9000)| FILTERS
FILTERS --> Controllers
API_CTRL --> MIDTRANS
API_CTRL --> JWT_HELP
API_CTRL --> MASKING
MIDTRANS -->|HTTPS /v2/charge| MIDTRANS_API
MIDTRANS_API -->|Webhook POST /api/payment/webhook| NGINX
Controllers -->|Queries & Transactions| DB
WORKER -->|Fetch Pending Delivery| DB
WORKER -->|Generate PDF & Save| FILES
WORKER -->|Send E-Ticket Email| SMTP
EXPIRE -->|Lock & Release Expired Quota| DB
ticketly-project/
├── app/
│ ├── Commands/ # CLI Spark daemons (ExpireOrders, TicketQueueWorker, CleanupImages)
│ ├── Config/ # CI4 configuration (Routes, Filters, Midtrans, Auth, Database)
│ ├── Controllers/
│ │ ├── Admin/ # Admin dashboard web controllers (Event, Ticket, Order)
│ │ ├── Api/ # REST API controllers consumed by Flutter & Gate scanners
│ │ ├── Public/ # Public web catalog controllers (Home, Event, Legal)
│ │ └── User/ # Authenticated web user & web checkout controllers
│ ├── Database/
│ │ ├── Migrations/ # Schema migrations including performance indexes & payment columns
│ │ └── Seeds/ # Database seeders (PaymentMethod, Event, AdminUser, FakeUser)
│ ├── Filters/ # Global and route filters (JwtFilter, ApiRateLimit, SecurityHeaders)
│ ├── Helpers/ # Helpers (jwt_helper.php, masking_helper.php)
│ ├── Libraries/ # Integrations (MidtransService.php, TicketDeliveryService.php)
│ ├── Models/ # Database models (Event, TicketType, Order, OrderItem, Seat, Voucher)
│ └── Views/ # Blade-style CI4 templates (admin, public, user, emails, pdf)
├── docker/ # Docker container configuration
│ ├── nginx/ # Nginx reverse proxy configuration (default.conf)
│ └── php/ # PHP 8.2-FPM Alpine Dockerfile & php.ini / opcache.ini
├── public/ # Public root
│ ├── assets/ # Brand assets, logos, and payment method icons
│ ├── openapi.json # OpenAPI 3.0 specification for Scalar API docs
│ └── uploads/ # Dynamic uploads (posters, seatmaps, user avatars)
├── tests/ # Automated unit and feature test suites
├── writable/ # Cache, logs, temporary PDFs, and session data
├── docker-compose.yml # Container stack definition (app, web, db)
├── DOCKER.md # Containerization operational guide
├── composer.json # Backend dependencies (CI4, Shield, PHP-JWT, Dompdf, Guzzle)
└── spark # CodeIgniter 4 CLI entry point
The application includes a container setup with healthchecks and persistent volumes:
# 1. Build and boot all containers in background
docker compose up -d --build
# 2. Check service status
docker compose ps
# Expected output: ticketly_app (Up), ticketly_web (Up: 8080), ticketly_db (Up: healthy, 3307)
# 3. Initialize or run migrations inside the container
docker compose exec app php spark migrate
# 4. Populate baseline seed data
docker compose exec app php spark db:seed PaymentMethodSeeder
docker compose exec app php spark db:seed EventSeeder
docker compose exec app php spark db:seed AdminUserSeeder
docker compose exec app php spark db:seed FakeUserSeeder
# 5. Tail container logs
docker compose logs -f app# 1. Install production dependencies
composer install --no-dev --optimize-autoloader
# 2. Setup environment configuration
cp env .env
# Edit .env with your production database credentials and keys
# 3. Run database migrations
php spark migrate
# 4. Configure web server (Nginx/Apache) root to the /public directoryConfigure the following systemd services or crontab entries on your production host:
# Order Expiration Cron: Run every minute to release expired reservations
* * * * * cd /path/to/ticketly-project && php spark orders:expire >> /dev/null 2>&1
# Queue Worker: Run continuously via Supervisor or systemd daemon
php spark queue:workConfigure the following keys in your .env file:
| Variable | Default Value | Description |
|---|---|---|
CI_ENVIRONMENT |
development |
Environment mode (development or production). |
app.baseURL |
http://localhost:8080/ |
Canonical base URL of the web server. |
database.default.hostname |
localhost / db |
Database host (localhost for host, db for Docker). |
database.default.database |
ticketly |
MySQL database name. |
database.default.username |
root / ticketly_user |
MySQL database user. |
database.default.password |
"" / ticketly_password |
MySQL database password. |
database.default.port |
3306 / 3307 |
Database port. |
JWT_SECRET_KEY |
(Set secure 64-char key) | Secret key used for signing and verifying JWT tokens. |
MIDTRANS_SERVER_KEY |
SB-Mid-server-... |
Midtrans Server Key for payment charges and signature check. |
MIDTRANS_CLIENT_KEY |
SB-Mid-client-... |
Midtrans Client Key for client-side rendering. |
MIDTRANS_IS_PRODUCTION |
false |
Midtrans environment (true for production, false for sandbox). |
email.SMTPHost |
live.smtp.mailtrap.io |
Outgoing SMTP server address. |
email.SMTPPort |
2525 / 587 |
Outgoing SMTP port. |
email.SMTPUser |
(Your SMTP user) | SMTP authentication username. |
email.SMTPPass |
(Your SMTP pass) | SMTP authentication password. |
All API endpoints are prefixed with /api. Protected routes require the header Authorization: Bearer <token>.
When the application is running, browse schemas, generate client code snippets (Dart, cURL, JavaScript), and test endpoints interactively:
- Interactive Documentation: http://localhost:8080/api/docs
- OpenAPI 3.0 Specification:
public/openapi.json
All API responses strictly adhere to this uniform contract:
{
"status": "success",
"message": "Transaction initiated successfully.",
"data": { }
}Paginated collections include a sidecar meta block:
{
"status": "success",
"message": "Events retrieved.",
"data": [ ],
"meta": {
"total": 42,
"per_page": 10,
"current_page": 1,
"last_page": 5
}
}| Method | Endpoint | Filter | Description |
|---|---|---|---|
GET |
/api/docs |
Public | Interactive Scalar API documentation |
GET |
/api/terms |
Public | Terms and conditions (UU PDP compliance) |
GET |
/api/privacy |
Public | Privacy policy documentation |
POST |
/api/auth/register |
Rate Limit (5/min) | Customer account registration |
POST |
/api/auth/login |
Rate Limit (10/min) | Authentication & JWT token issuance |
POST |
/api/auth/logout |
JWT Filter | Session termination & JWT token revocation |
POST |
/api/auth/forgot-password |
Rate Limit (5/min) | Request Bcrypt hashed OTP code via email |
POST |
/api/auth/verify-code |
Rate Limit (10/min) | Verify 6-digit OTP code |
POST |
/api/auth/reset-password |
Rate Limit (5/min) | Set new password with verified OTP |
GET |
/api/events/featured |
Public | List high-priority headline events |
GET |
/api/events/landing |
Public | Aggregated landing page catalog payload |
GET |
/api/events |
Public | Paginated list of events with category filter |
GET |
/api/events/{slug} |
Public | Event detail by slug |
GET |
/api/events/{id}/tickets |
Public | Available ticket categories & real-time quota |
GET |
/api/vouchers/active |
Public | List valid promo discount vouchers |
POST |
/api/vouchers/apply |
Rate Limit (20/min) | Calculate discount applicability |
GET |
/api/checkout/payment-methods |
Public | List supported Midtrans payment rails |
POST |
/api/checkout/calculate |
Public | Calculate subtotal, fees, vouchers, and total |
POST |
/api/checkout/start |
JWT + Rate Limit | Reserve ticket stock & initiate Midtrans charge |
POST |
/api/checkout/cancel |
JWT Filter | Cancel pending checkout and release held quota |
POST |
/api/payment/webhook |
SHA-512 Check | Midtrans payment notification handler |
GET |
/api/profile |
JWT Filter | Retrieve authenticated user profile |
POST |
/api/profile/update |
JWT Filter | Update profile credentials and avatar |
GET |
/api/orders |
JWT Filter | Paginated list of user order history |
GET |
/api/orders/{id} |
JWT Filter | Order detail with tickets and payment info |
POST |
/api/scanner/validate |
JWT Filter | Gate verification of ticket item code |
POST |
/api/scanner/checkin |
JWT Filter | Atomic gate check-in with anti-double scan |
GET |
/api/scanner/stats/{id} |
JWT Filter | Live gate check-in statistics per event |
To run the local server, compile CSS, and tunnel webhooks concurrently:
# Watch Tailwind styles, start PHP server, and expose port via ngrok:
npm run dev# Execute PHPUnit tests
composer testRun database seeders (php spark db:seed AdminUserSeeder and FakeUserSeeder) to populate these test accounts:
| Role | Username | Password | |
|---|---|---|---|
| Administrator | admin@ticketly.com |
admin |
admin123 |
| Customer 1 | budi@example.com |
budi_santoso |
password123 |
| Customer 2 | ani@example.com |
ani_wijaya |
password123 |
| Customer 3 | dewi@example.com |
dewi_sari |
password123 |
| Customer 4 | rudi@example.com |
rudi_hermawan |
password123 |
- License: Licensed under the MIT License.
- Academic Disclaimer: This platform was developed as an academic project at Politeknik Negeri Cilacap, Informatics Engineering Department. All event posters, trademarks, and artist names used in seed data belong to their respective copyright owners and are utilized strictly for educational demonstration.
Maintained by pratama1246 · Ticketly Backend Platform
