Third year project · Dublin City University · 2023—2024
Bynle
A ticketing platform for university clubs and societies — events, payments, QR entry and a social layer — built so a committee can sell a ticket directly and a student can safely pass one on to a friend.
- Django
- React
- PostgreSQL
- Stripe Connect
- QR ticketing
- AWS
- Period
- 2023 — 2024
- Context
- Third year project, B.Sc. Computer Science
- Institution
- Dublin City University
- Deployment
- Railway, AWS Amplify and S3
- 134
- Unit tests
- 10
- Domain entities
- 3
- Deployment targets
- 2
- Account types
Covering backend models and views, with no change merged without review.
Users, profiles, clubs, events, tickets, Stripe accounts, follows, friendships and transfer requests.
Django and PostgreSQL on Railway, React on AWS Amplify, uploaded media in an S3 bucket.
Regular users and ticket scanners, separated in the token and in the route tree.
01The idea
From reselling tickets to running the whole event
The project started somewhere else. The original idea was a secure transfer layer sitting on top of existing ticketing platforms: one person initiates a transfer, the other pays inside the app, and the ticket only moves once the payment clears — a fix for the resale scams that follow every sold-out event.
We abandoned it. Working against those platforms’ APIs, we were not satisfied with the security they offered or with the level of verification we could actually perform. If the verification had to be trustworthy, we had to own the ticket.
So we built the ticketing system instead, aimed at the place we knew best: university clubs and societies. A club creates an event, sells tickets directly and scans them at the door. And because we now owned the ticket end to end, the secure transfer that started the whole thing became something we could actually guarantee.
The last shift was social. Once students were discovering events on the platform, it made little sense for discovery to be a list. Following clubs, connecting with friends and seeing what people are going to turned a ticketing tool into something closer to a noticeboard for a campus.
02The product
Three kinds of person, one platform
Bynle has three kinds of person using it, and the interface changes shape for each.
- Students
- Browse events, follow clubs, send and accept friend requests, buy tickets, and transfer a ticket they cannot use to a friend.
- Club admins
- Create and manage events, set pricing, connect a Stripe account, edit the club page, appoint other admins, create scanner accounts and read the club’s statistics.
- Ticket scanners
- A separate account type, created by a club admin, with one job: log in on a phone at the door and scan QR codes.
- Events. Clubs publish events with a date, time, location, capacity, type and cover image, free or paid.
- Tickets. Every ticket carries a unique code and a generated QR code, tied to one event and one owner.
- Transfers. A ticket can be passed to a friend, with payment handled in-app when the ticket was not free.
- Social graph. Friend requests with a pending state, club follows, and the connections two users have in common.
- Club statistics. A club admin can see which courses and year groups follow the club, aggregated in the backend and rendered as charts.
- Media. Club logos, club covers and event posters uploaded by admins and served from cloud storage rather than the application server.


03Architecture
Five layers, three hosts
The system splits into a React client, a Django application layer, a PostgreSQL data layer, an integration layer for payments and media, and a deployment layer that puts each piece where it belongs.
The backend is a REST API. Django views are grouped into handlers by domain — authentication, clubs, statistics, events, friendships, scanners, payments, tickets, transfers and user data — with serializers validating everything on the way in and converting model instances to JSON on the way out.
It runs across three services. The Django application and a PostgreSQL container sit together on Railway, which pulls from a fixed branch on every push and runs a start script that migrates, collects static files and launches Gunicorn. The React frontend is hosted on AWS Amplify. Uploaded media — club logos, covers and event posters — lives in an S3 bucket rather than on the application server.
- Client
- React, with React Router guarding private routes and a separate route tree for scanner accounts.
- Application
- Django REST views grouped into ten domain handlers, with serializers for validation and JSON conversion.
- Data
- PostgreSQL for structured data, running from a Docker image; an S3 bucket for uploaded media.
- Integration
- Stripe for payments and Connect onboarding, with a status check deciding whether a club or user may charge.
- Deployment
- Railway for the backend and database, AWS Amplify for the frontend, Git-based version control for both.


04Tickets
Selling a ticket is easy; moving one safely is not
A ticket is only worth something if exactly one person can use it, exactly once. That constraint shapes the three hardest parts of the system: how money reaches a club, how a ticket changes hands, and what happens at the door.
The transfer is the part we got wrong first. Early on, a ticket in the middle of a transfer was still a valid ticket — so it could be scanned while the transfer was in flight, and someone could walk into an event on a ticket they were busy giving away. The fix was to give tickets a transfer state of their own.
- Stripe Connect. Clubs take payment through their own Stripe Connect account rather than through us. The system checks whether that account is complete and will not let a club charge for an event until it is.
- Transfer state. A ticket being transferred is held in a transfer state. It scans as invalid and belongs to neither party until the recipient accepts or the sender cancels.
- Ownership change. On acceptance the old ticket is deleted and a new one is created for the recipient, so a transferred ticket is a new record rather than an edited one.
- Paid transfers. Where the ticket cost money, the recipient pays in-app and the transfer only finalises once the payment has been processed.
- QR at the door. Each ticket carries a generated QR code pointing at a validation endpoint that only accounts of type ticket scanner are permitted to reach.
- Scanner accounts. Club admins create scanner accounts for an event. They have their own credentials, their own route tree, and can do nothing but scan.


05Quality
What we tested, and what broke
Testing ran on three tracks: code review on every merge request, unit tests for the backend, and user testing sessions with real students.
The backend finished with 134 unit tests across models and views — data integrity and validation rules on one side, request handling and business logic on the other, including permission checks such as confirming that only a scanner account can reach the ticket validation endpoint. No change reached the main branch without review by the other of us.
User testing changed the product. Two requests came back clearly enough that we built them:
- A notification bell. Users wanted friend requests and incoming transfers reachable from anywhere rather than by navigating to the page that held them. A bell went into the navbar, with a count.
- Deleting a sent transfer. Participants wanted to withdraw a transfer sent by mistake, or after a change of plan. Sent transfers became deletable.

What broke
01
Club admins modelled as their own class
Problem
Club administrators started as a dedicated model class. It was the wrong shape: the relationship between users and clubs is many-to-many, and forcing it through a class of its own made administration awkward.
Fix
Refactored to a many-to-many relationship, which let several users administer several clubs without ceremony and simplified the whole administration path.
02
Two people, deleted migrations, one broken database
Problem
Early on we were both working in separate branches and both generating migrations as we changed the models. Neither of us understood Django migrations well enough, and we had both deleted some. Rebasing to merge into main broke the database: it no longer matched the models, and it took a long time to work out why.
Fix
We agreed never to delete a migration again unless the database was backed up and a full wipe was genuinely warranted. A script that repopulated the database from scratch meant we could rebuild a working dataset quickly while we sorted it out.
03
Tickets stayed valid mid-transfer
Problem
While a transfer was in progress the ticket remained active, which risked unauthorised entry on a ticket that was already being given away.
Fix
Tickets hold a transfer state until the recipient accepts or the sender cancels. In that state the ticket cannot be scanned and counts as invalid at the door.
04
Deploying three moving parts
Problem
Getting the Django backend, the React frontend and the database deployed together took days of reading documentation and a long run of failed attempts on the backend server.
Fix
Railway, which integrates with the repository and pulls the latest version of a fixed branch, brought the backend and the database up in one shared instance; the frontend went to AWS Amplify.
Stack
Built with
Frontend
- React
- React Router
- JavaScript
- Axios
Backend
- Django
- Python
- Serializers
- JWT
- Gunicorn
Data
- PostgreSQL
- Docker
- AWS S3
Platform
- Railway
- AWS Amplify
- Stripe Connect
- Pexels API
- GitLab CI
Credits
Team and documentation
Built as a two-person third year project for the B.Sc. in Computer Science at Dublin City University — the same pairing as the 5G SMS Firewall a year later.
- Built with
- Jack Keenan
- Institution
- Dublin City University, 2023—2024
- Payments
- Stripe Connect
- Documentation
- Technical manual and user manual, published with the source.