A virtual queue system. People join a queue from wherever they are, see how many people are ahead of them and roughly when their turn will come, and only show up when it is nearly time.
About this version. This is a deliberately basic first version. I built it to learn Django, Django REST Framework and the ideas that come with them (authentication, permissions, database transactions, row locking) by applying them to a real problem. Everything described below works end to end, but a lot is still missing. Email and SMS notifications, richer alerts, payments, live updates without polling and reports for organisers are planned for later versions. The widget is already built so that NextUp can be added to any organisation's official website with one line of code. See Coming next.
- Why I built this
- What it does
- Adding NextUp to a website
- How it works
- Design decisions
- Tech stack
- Project structure
- API
- Running it locally
- Tests
- Limitations
- Coming next
- License
Many places serve people one at a time: campus placement drives, hospital outpatient counters, banks and public service offices. When a lot of people arrive at once, they usually wait together in one place, sometimes for a long time, even though each person only needs a few minutes at the counter. The queue itself is fair and necessary. The same is true online, for one-on-one sessions such as interviews, mentoring calls or doctor consultations, where people often sit in a waiting room or keep refreshing a link without knowing when they will be let in. I wanted to see whether the waiting could happen somewhere more comfortable, and with a clear idea of when it will end.
NextUp keeps the same queue and the same order, but lets people wait wherever they are. You take a token from your phone, watch your position move, and leave for the venue when the app tells you your turn is close. For an online queue, you get the meeting link when it is your turn instead of a venue.
There are four kinds of people in the system.
Visitors join from the organisation's own website through a small widget. No account is needed, only a name and a phone number. They get a token number and a private link to their ticket, which they can open on any device. The ticket shows how many people are ahead, how many minutes that is, and roughly what time their turn will come. As the turn gets close, the ticket changes: first "about 40 minutes left, start heading over", then "almost your turn, please be at the venue", then "it's your turn". If the browser allows notifications, the same messages appear as system notifications. For in-person queues there is an "I'm here" button, so staff know who has actually arrived. For online queues, such as one-on-one interviews or consultations, the ticket shows a "Join the meeting" link at the moment the person is called.
Signed-in users can do the same from NextUp itself, and can also see their past tickets.
Staff work from the staff desk. They see who is being served and who is waiting, call the next person, mark people as done or skipped, and can tap a phone number to call someone who is running late. If someone was skipped and turns up later, staff can bring them back to their original place in the line. A whole queue can be postponed to another day (everyone keeps their position) or cancelled (every reservation is cancelled together).
Organisers create queues and set the rules in the Django admin panel: roughly how many minutes each person takes, the maximum number of people, when the alerts should start, whether the queue is in person or online, and which staff members run it.
From the staff desk, an organisation copies one line and pastes it wherever it wants a "Join the queue" button on its own site:
<script src="https://your-nextup-domain/widget.js" data-queue="1"></script>The script adds a plain button. Clicking it opens the NextUp panel on top of the page. The button has the class nextup-button, so the site can restyle it to match its own design. Adding data-mode="inline" shows the panel directly in the page instead of behind a button.
The panel is an <iframe> served by NextUp. That choice is explained in Design decisions.
The code is split into four layers, and each one has a single job.
| Layer | File | Job |
|---|---|---|
| Models | queues/models.py |
The three tables: Profile (a user's phone number), Queue (one queue and its rules) and Entry (one person's place in one queue). |
| Services | queues/services.py |
Every rule of the queue: joining, calling the next person, finishing, leaving, bringing someone back, postponing and cancelling. |
| Serializers | queues/serializers.py |
Turn database rows into JSON, validate what comes in, and add computed fields such as people ahead, minutes to wait and the alert level. |
| Views | queues/views.py |
Receive a request, check who is asking, call a service, return the result. |
Views never contain queue rules, and pages never talk to the database directly. That separation is what made the rules easy to test on their own.
Here is what happens when a visitor joins:
sequenceDiagram
participant W as Widget (visitor)
participant V as GuestJoinView
participant S as services._join
participant DB as Database
W->>V: POST /api/public/queues/1/join/ (name, phone)
V->>V: Validate input, apply rate limit
V->>S: join_as_guest(queue 1, name, phone)
S->>DB: Lock the queue row (SELECT ... FOR UPDATE)
S->>DB: Check: open? not already in? not full?
S->>DB: Highest token so far, then insert token + 1
DB-->>S: New entry with a random ticket code
S-->>V: Entry
V-->>W: Token, people ahead, wait time, private ticket link
The ticket page then asks for its own status every eight seconds. People ahead are counted as everyone in the same queue who is waiting or being served and has a smaller token. Waiting time is that count multiplied by the organiser's minutes per person.
Two people joining at the same moment never get the same token. Joining runs inside a database transaction that first locks the queue's row with select_for_update(). If two hundred students tap "Join" in the same second, the database makes them take turns for that queue, so each one reads the latest token and adds one. A unique constraint on (queue, token) sits underneath as a second line of defence. I did not use join timestamps to order people, because two requests can share the same timestamp, while a counter cannot tie. One caveat I want to be honest about: row locking behaves this way on PostgreSQL. SQLite, which I use during development, ignores select_for_update but locks the whole database during a write, so it is still safe there, just less concurrent.
A skipped person comes back to their real place, not the end of the line. "Call next" always picks the lowest waiting token. Bringing someone back simply sets their original token to waiting again, so they are called next without any special rule.
Nothing is deleted. An entry moves through statuses (waiting, called, done, skipped, left, cancelled) instead of disappearing. This is what makes the history table and "bring back" possible, and it keeps a record of what actually happened.
Visitors can track a ticket without an account. Every entry gets a ticket code generated with Python's secrets.token_urlsafe. It is long and random enough that it cannot be guessed, so the link itself works like a password for that one ticket. The trade-off is that anyone holding the link can see the ticket, which the page says plainly.
Staff only see their own queues. Every staff endpoint starts from "the queues this user is staff of". A staff member who asks for another organisation's queue gets 404 Not Found rather than 403 Forbidden, so they cannot even confirm the queue exists. This matters because the staff view includes phone numbers.
The widget is an iframe. Rendering the panel inside a frame served by NextUp means the host site's CSS cannot break it and NextUp's CSS cannot leak into the host page. Because the panel calls the API from NextUp's own domain, no cross-origin (CORS) configuration is needed. Django sends X-Frame-Options: DENY by default to protect against clickjacking, so I exempted only the two widget pages and left every other page protected.
Public joining is rate limited. Anyone can reach the join endpoint, so it is limited to ten joins per visitor per hour using DRF's ScopedRateThrottle. The eleventh attempt gets 429 Too Many Requests.
Alerts follow the organiser's rules. Each queue decides how many minutes before, and how many people before, the alerts appear. "Be at the venue" style alerts only show on the queue's own day, so nobody is told to rush to an event that is tomorrow.
Updates use polling, for now. Tickets refresh every few seconds. It is simple and reliable, and it is the first thing I would replace with WebSockets.
| Area | Technology |
|---|---|
| Language | Python |
| Web framework | Django 6.1 |
| API | Django REST Framework |
| Authentication | JSON Web Tokens (djangorestframework-simplejwt), access and refresh tokens |
| Database | SQLite in development; PostgreSQL planned for deployment |
| Admin panel | Django admin, used as the organiser dashboard |
| Frontend | Plain HTML, CSS and JavaScript, no framework |
| Testing | Django's test runner with DRF's APITestCase |
| Configuration | python-dotenv, secrets kept in .env |
NextUp/
├── config/
│ ├── settings.py Settings, read from .env
│ └── urls.py Pages, widget routes and the API prefix
├── queues/
│ ├── models.py Profile, Queue, Entry
│ ├── services.py All queue rules
│ ├── serializers.py JSON in and out, computed fields
│ ├── views.py API endpoints (user, staff, public)
│ ├── urls.py API routes
│ ├── admin.py Organiser panel
│ ├── tests.py 19 tests
│ ├── migrations/
│ └── templates/queues/
│ ├── index.html The NextUp app: queues, tickets, staff desk
│ ├── widget.html The panel a visitor sees, and the private ticket page
│ └── widget.js The one-line embed script
├── docs/ Screenshots for this README
├── manage.py
├── requirements.txt
└── .env.example
All endpoints are under /api/. Signed-in endpoints expect Authorization: Bearer <access token>.
| Method | Endpoint | Who | What it does |
|---|---|---|---|
| POST | /auth/register/ |
Anyone | Create an account (username, email, phone, password) |
| POST | /auth/login/ |
Anyone | Get an access token and a refresh token |
| POST | /auth/refresh/ |
Anyone | Exchange a refresh token for a new access token |
| GET | /queues/ |
Signed in | Open queues |
| POST | /queues/<id>/join/ |
Signed in | Join a queue |
| GET | /my-entries/ |
Signed in | My current tickets |
| GET | /my-history/ |
Signed in | My past tickets |
| POST | /entries/<id>/check-in/ |
Signed in | "I'm here" |
| POST | /entries/<id>/leave/ |
Signed in | Leave a queue |
| GET | /staff/queues/ |
Staff | Queues I manage |
| GET | /staff/queues/<id>/entries/ |
Staff | People waiting, with contact details |
| GET | /staff/queues/<id>/history/ |
Staff | Served, skipped, left and cancelled |
| POST | /staff/queues/<id>/call-next/ |
Staff | Call the next person |
| POST | /staff/entries/<id>/finish/ |
Staff | Mark as done or skipped |
| POST | /staff/entries/<id>/restore/ |
Staff | Bring a skipped person back to their place |
| POST | /staff/queues/<id>/postpone/ |
Staff | Move the queue to a new date |
| POST | /staff/queues/<id>/cancel/ |
Staff | Cancel the queue and every reservation |
| GET | /public/queues/<id>/ |
Anyone | Public queue details for the widget |
| POST | /public/queues/<id>/join/ |
Anyone | Join as a visitor (rate limited) |
| GET | /public/tickets/<code>/ |
Ticket holder | A visitor's ticket |
| POST | /public/tickets/<code>/check-in/ |
Ticket holder | "I'm here" |
| POST | /public/tickets/<code>/leave/ |
Ticket holder | Leave the queue |
You need Python 3.12 or newer.
git clone https://github.com/Srinikith4/NextUp_Virtual_Queue.git
cd NextUp_Virtual_Queue
python -m venv venv
venv\Scripts\activate # Windows
# source venv/bin/activate # macOS / Linux
pip install -r requirements.txtCreate your .env from the example and give it a secret key:
cp .env.example .env
python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"Paste the printed key into DJANGO_SECRET_KEY in .env. Then create the database and an admin account, and start the server:
python manage.py migrate
python manage.py createsuperuser
python manage.py runserver- The app is at
/. Sign in with the account you just created. - The organiser panel is at
/admin/. Create a queue there and add yourself as staff. - The visitor widget for a queue is at
/widget/<queue id>/.
python manage.py test queuesThe 19 tests cover the rules that matter most:
- tokens go up one by one, including a mix of users and visitors
- the same user, or the same visitor phone number, cannot join twice
- full and closed queues refuse new people
- people ahead and waiting time are calculated correctly
- "call next" is first in, first out
- a skipped person is brought back to their original place
- postponing keeps positions, cancelling cancels everyone
- login returns a JWT, and joining without one is refused
- staff cannot see other organisations' queues, and normal users cannot use staff endpoints
- finished people appear in history
- visitors can track a ticket by its code, and a wrong code returns 404
- invalid phone numbers are rejected
- the public join endpoint is rate limited
- No email or SMS yet. Alerts only appear on the ticket page and as browser notifications, so a visitor has to keep the page open or come back to it.
- Updates come from polling every eight seconds rather than being pushed.
- Waiting time uses the organiser's fixed estimate per person, not how fast the queue is actually moving.
- There is no payment or deposit, so people can join and not turn up. Staff handle that with "skip".
- It is not deployed yet, and the development setup uses SQLite.
- Email and SMS alerts sent in the background (Celery with Redis), so visitors do not need to keep the page open
- Live updates over WebSockets instead of polling
- Waiting-time estimates learned from how fast each queue really moves
- Appointment slots alongside walk-in queues
- Payments or deposits for organisations that want them
- Reports for organisers: busiest hours, average wait, no-show rate
- Multiple counters serving one queue at the same time
- Deployment on PostgreSQL, with Docker
Released under the MIT License.


