Skip to content
Back to projects

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

Covering backend models and views, with no change merged without review.

10
Domain entities

Users, profiles, clubs, events, tickets, Stripe accounts, follows, friendships and transfer requests.

3
Deployment targets

Django and PostgreSQL on Railway, React on AWS Amplify, uploaded media in an S3 bucket.

2
Account types

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.
Browsing events. The sidebar adapts to the account: a club admin also sees the clubs they administer beneath their own navigation.
Browsing events. The sidebar adapts to the account: a club admin also sees the clubs they administer beneath their own navigation.
Transferring a ticket to a friend, from the user manual. A paid ticket cannot be sent until the sender has a Stripe account able to receive the payment.
Transferring a ticket to a friend, from the user manual. A paid ticket cannot be sent until the sender has a Stripe account able to receive the payment.

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.
The operational model from the technical manual: where each component runs, and what talks to what.
The operational model from the technical manual: where each component runs, and what talks to what.
The data model. Ten entities, with the join tables — Follow, Friend and TransferRequest — carrying their own state rather than being plain links.
The data model. Ten entities, with the join tables — Follow, Friend and TransferRequest — carrying their own state rather than being plain links.

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.
Transferring a paid ticket, from the technical manual. The payment branch only runs when the ticket price is above zero, and ownership moves after the payment is confirmed.
Transferring a paid ticket, from the technical manual. The payment branch only runs when the ticket price is above zero, and ownership moves after the payment is confirmed.
A ticket scanned at the door. The endpoint returns the attendee’s name and student number, and records when the ticket was scanned.
A ticket scanned at the door. The endpoint returns the attendee’s name and student number, and records when the ticket was scanned.

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.
The backend suite: 134 tests, all passing.
The backend suite: 134 tests, all passing.

What broke

  1. 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.

  2. 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.

  3. 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.

  4. 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.
Back to projects