Eventify
Eventify is a full-stack Laravel platform for creating, promoting, and managing events of any scale — from a small house party to a sold-out concert — with real ticket inventory, geocoded venues, and PDF receipts, so an organizer doesn't need a separate ticketing service bolted onto a listing page.
Architecture and Tech Stack
Core Architecture
- Backend: Laravel 10 (PHP), server-rendered dashboard + a REST API
- Frontend: Blade views + React for interactive pieces, Bootstrap 5 for styling
- Database: MySQL, six related tables (
users,roles,events,tickets,categories,statuses,locations), all soft-deleting viadeleted_at - Geocoding: OpenStreetMap Nominatim — turns an address into
latitude/longitudeon thelocationstable - PDF generation: DomPDF — streams ticket and event reports on demand
- Authorization: a custom role middleware (
role:admin|user), no policy classes or permission bit-masks
Layered Architecture
Two roles share the same dashboard and event/ticket flows; only the admin-only routes (user management, categories, statuses) are gated by the role middleware. The public /explore and /events/{id}/report routes need no authentication at all.
Request Flow
Creating an event does four things in one request: geocode the address, store the image, assign a Disponible/Agotado status from capacity, and persist — all before the organizer sees a response.
Key Features
Features at a Glance
Event Creation with Live Geocoding
Submitting the event form (POST /dashboard/events/store) creates a Location record, then queries Nominatim with the combined address string and stores the returned coordinates if found — the event carries a real, mappable location, not just a text address. Updates re-geocode only when the address or city actually changes, so editing a price doesn't re-hit the geocoding API.
Ticket Purchase with Automatic Capacity Tracking
Buying a ticket (POST /dashboard/tickets/store) is guarded by a single check — reject if capacity < quantity or capacity == 0 — then decrements capacity, increments attendees, and flips the event's status to Agotado (sold out) the moment capacity hits zero, without a separate admin step.
On-Demand PDF Receipts
Both a ticket (GET /dashboard/tickets/{id}/report) and a full event report (GET /events/{id}/report, publicly accessible) render through DomPDF from a Blade template — pulling in the eager-loaded event relationship, formatting dates with Carbon, and computing the total (quantity × price) at render time rather than storing a stale total.
Two-Tier Roles Without a Permission Framework
Access control is a lowercase string comparison in a custom middleware against a route's declared roles (role:admin|user) — no policy classes, no permission bit-masks, no extra package. admin-only sections (user management, categories, statuses) are simply routes the user role never matches.
Technical Highlights
Status transitions are a side effect, not a separate workflow
Disponible/Agotado isn't a field an admin sets manually during normal operation — it's derived automatically from the capacity math on every ticket purchase, and only Finalizado (event concluded) is an explicit admin action. One less state for the UI to manage by hand.
Smart re-geocoding avoids wasted API calls
The update path only re-queries Nominatim when the address or city fields actually changed — every other edit (price, capacity, description) skips the geocoding round-trip entirely.
A known concurrency gap, named rather than hidden
The capacity check (capacity < quantity) has no database-level lock, so a genuine race condition exists under concurrent purchases of the last few tickets — worth calling out explicitly rather than presenting the ticketing flow as bulletproof; a production deployment would wrap it in lockForUpdate().
Project Structure
app/
├── Http/Controllers/
│ ├── EventController.php # Create/update/delete + geocoding + image upload
│ ├── TicketController.php # Purchase flow + capacity/status updates + PDF report
│ ├── CategoryController.php # Admin CRUD
│ └── ...
├── Models/
│ ├── Event.php # belongsTo User/Location/Category/Status, hasMany Ticket
│ ├── Ticket.php # belongsTo User, Event
│ ├── Location.php # address + geocoded lat/lon
│ ├── Category.php / Status.php
│ └── User.php / Role.php
├── Http/Middleware/
│ └── RoleMiddleware.php # role:admin|user route guard
└── Http/Resources/ # API response transformers
database/
├── migrations/ # 6 related tables, all soft-deleting
└── seeders/
└── CategoryTableSeeder.php # Musical, Teatro, Deportivo, Cultural, Familiar, Gastronómico
resources/views/
└── reports/
└── ticket-report.blade.php # DomPDF template for ticket & event reportsImpact and Scalability
- One platform, full lifecycle: listing, geocoded venue, ticket sales, and receipts, without stitching together separate services
- Public API surface: full CRUD for events/categories, read-only for locations/statuses, so the platform can be integrated with beyond its own dashboard
- Simple, auditable authorization: a two-role system that's easy to reason about, at the cost of not supporting finer-grained permissions if the platform grows
- Honest about its limits: the documented concurrency gap in ticket purchasing is a real, named consideration for scaling to high-concurrency sales
Notes
Built with Laravel 10, MySQL, React, and Bootstrap 5. Code is public on GitHub. For a deeper technical deep-dive, see the full documentation wiki.