Skip to content
arunsabu21Public

About

Bus Booking REST API built with Django REST Framework

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Halt API

A scalable Pan-India Bus Booking REST API

Built with Django, Django REST Framework, PostgreSQL, Redis, Celery, Stripe, Twilio SendGrid, JWT Authentication, Docker, GitHub Actions, and Render.

Python Django DRF PostgreSQL Redis Celery Stripe Twilio SendGrid JWT Docker GitHub Actions Render Vercel Sentry


Table of Contents


Overview

Halt is a scalable Pan-India Bus Booking REST API built with Django and Django REST Framework.

The project follows a service-oriented architecture that separates business logic into services, serializers, and views for better maintainability, scalability, and testability.

Halt covers the full booking lifecycle end to end: authentication, trip search, deck-aware seat selection, boarding/drop point selection, per-passenger booking, Stripe-powered payments with Redis-backed seat holds to prevent overbooking, PDF ticket generation, transactional email, per-passenger cancellation with partial refunds, background job processing, and automated production monitoring — all containerized and deployed through a CI/CD pipeline.


Live Deployment

Frontend

https://halt-zeta.vercel.app

Backend API

https://halt-api.onrender.com

API Documentation

https://halt-api.onrender.com/api/docs/

Note: The backend runs on Render's free tier, which spins down after periods of inactivity. The first request after idle time may take 30–60 seconds to respond while the service cold-starts. See Known Limitations for details.


Features

Authentication

  • User registration
  • Email OTP verification
  • JWT authentication (access + refresh tokens)
  • Login and logout
  • JWT token blacklisting on logout
  • Forgot password / reset password flow
  • Login attempt rate limiting
  • Custom user model

Bus & Trip Management

  • Cities, operators, routes, and buses
  • Route stops with ordered boarding/drop points
  • Trip search by source, destination, and travel date
  • Deck-aware seat maps (single-deck seater and double-deck sleeper layouts)
  • Real-time seat availability

Booking System

  • Multi-step booking flow: seat selection → boarding/drop point → passenger details → payment
  • Per-passenger booking (name, age, gender, seat assignment)
  • Seat validation against real bus layout (seater/sleeper aware)
  • Redis-backed seat holds to prevent two users booking the same seat simultaneously
  • Booking reference generation
  • Per-passenger cancellation with partial Stripe refunds
  • Automatic booking status transition once all passengers on a booking are cancelled
  • Booking history with payment summary (total paid, refunded, net paid)

Payments

  • Stripe Checkout Sessions for payment collection
  • Stripe webhook-driven booking confirmation (checkout.session.completed, checkout.session.expired)
  • Idempotent webhook handling, safe against duplicate Stripe event delivery
  • Automatic refund if a seat is lost to a race condition after payment succeeds
  • Partial refunds on per-passenger cancellation

Tickets & Email

  • Individual PDF ticket generation per passenger (WeasyPrint)
  • Twilio SendGrid email integration for OTP, booking confirmation (with ticket attachments), and cancellation emails
  • HTML email templates

Background Processing

  • Celery for asynchronous email delivery and PDF generation
  • Celery Beat scheduled job that automatically seeds upcoming trips daily, so trip inventory never runs dry without manual intervention

Performance

  • Redis-based caching for trip search, trip details, and route stop lookups
  • select_related() / prefetch_related() for optimized database queries
  • Atomic, concurrency-safe seat booking using select_for_update()

Security

  • Custom user model with hashed passwords
  • Django password validators
  • JWT authentication with token blacklisting
  • Environment-based configuration, split by environment (development / production)
  • Login attempt rate limiting
  • Secrets managed entirely through environment variables, never committed

Production

  • Gunicorn production application server
  • Supervisord process management (gunicorn + Celery worker + Celery Beat in one container)
  • Dockerfile, .dockerignore, entrypoint script
  • Managed PostgreSQL (Neon)
  • Managed Redis (Upstash, TLS)
  • Render deployment via prebuilt Docker image
  • GitHub Actions CI/CD (test-gated deploys)
  • Sentry error monitoring (Django + Celery)

Tech Stack

Category Technology
Language Python 3.14
Framework Django 5.x
API Django REST Framework
Frontend React, Vite, TanStack Query
Database PostgreSQL (Neon)
Cache / Broker Redis (Upstash)
Task Queue Celery + Celery Beat
Authentication SimpleJWT
Email Twilio SendGrid
Payments Stripe
PDF Generation WeasyPrint
Production Server Gunicorn + Supervisord
Containerization Docker
CI/CD GitHub Actions
Backend Deployment Render
Frontend Deployment Vercel
Monitoring Sentry
API Testing Bruno / Postman

Architecture

Halt API Architecture

The backend and Celery worker/beat processes run together in a single Render web service (managed by Supervisord), sharing one Docker image built and pushed by CI/CD. Neon provides managed PostgreSQL, Upstash provides managed Redis (used for caching, the Celery broker/backend, and seat-hold locks), and Stripe/Twilio SendGrid handle payments and transactional email respectively.


Project Structure

halt-api/
├── src/
│   ├── authentication/
│   ├── bookings/
│   │   ├── holds.py          # Redis seat-hold logic
│   │   ├── stripe_client.py  # Stripe Checkout Session creation
│   │   ├── webhooks.py       # Stripe webhook handling
│   │   ├── tickets.py        # PDF ticket generation
│   │   └── tasks.py          # Celery email tasks
│   ├── buses/
│   ├── cities/
│   ├── operators/
│   ├── routes/
│   ├── trips/
│   │   └── management/commands/
│   │       └── sync_available_seats.py
│   ├── core/
│   │   ├── cache/
│   │   ├── constants/
│   │   ├── email/
│   │   ├── settings/
│   │   │   ├── base.py
│   │   │   ├── development.py
│   │   │   └── production.py
│   │   ├── celery.py
│   │   └── urls.py
│   │
│   └── manage.py
│
├── docs/
├── Dockerfile
├── .dockerignore
├── entrypoint.sh
├── supervisord.conf
├── requirements.txt
├── .env.example
├── .gitignore
└── README.md

The application is organized into Django apps based on domain responsibility, not technical layer. Business logic lives in services.py files, kept separate from views and serializers, for testability and maintainability.


Authentication Flow

Register
   │
   ▼
Generate OTP
   │
   ▼
Send OTP via Twilio SendGrid (Celery task)
   │
   ▼
Verify OTP
   │
   ▼
Activate Account
   │
   ▼
Login
   │
   ▼
Generate JWT Tokens
   │
   ▼
Protected APIs

Password Reset Flow

Forgot Password
      │
      ▼
Generate Reset Token
      │
      ▼
Send Reset Email
      │
      ▼
Validate Token
      │
      ▼
Set New Password

Booking Flow

Search Trip
    │
    ▼
Select Seats (deck-aware seat map)
    │
    ▼
Select Boarding / Drop Point
    │
    ▼
Enter Passenger Details (per seat)
    │
    ▼
Initiate Booking
    │
    ▼
Validate Seats + Place Redis Hold (10 min TTL)
    │
    ▼
Create Stripe Checkout Session
    │
    ▼
Stripe Payment
    │
    ▼
Stripe Webhook (checkout.session.completed)
    │
    ▼
Re-validate Seats Still Available
    │
    ▼
Confirm Booking + Release Hold
    │
    ▼
Send Confirmation Email with PDF Tickets (Celery)

If a webhook confirms payment but the held seats were lost to a race condition, the booking is automatically marked EXPIRED and the payment is refunded — a paying customer is never left charged without a seat.


Concurrency & Seat Locking

Two mechanisms work together to prevent two users from booking the same seat:

  1. Redis atomic holds (SET NX EX) — the moment a booking is initiated, each selected seat is atomically locked in Redis with a 10-minute TTL. A second user attempting to select an already-held seat is rejected immediately, before ever reaching Stripe.
  2. Database-level locking (select_for_update()) inside an atomic transaction — used when the booking is finally confirmed via webhook, ensuring the seat count and SeatBooking rows are updated consistently even under concurrent load.

This was verified with a real two-browser concurrency test: one user holds a seat mid-checkout, a second user attempting to book the same seat is correctly rejected with a clear error.


API Documentation

Interactive API documentation is available through Swagger/OpenAPI.

Swagger

https://halt-api.onrender.com/api/docs/

Documentation is also maintained in the docs/ directory, covering:

  • Authentication
  • Cities, Operators, Routes, Buses
  • Trips
  • Bookings (initiate, confirm, cancel)
  • Payments & Webhooks

Environment Variables

Create a .env file in the project root based on .env.example.

# Django
SECRET_KEY=
DEBUG=True
DJANGO_SETTINGS_MODULE=core.settings.development
ALLOWED_HOSTS=127.0.0.1,localhost

# Database
DB_NAME=
DB_USER=
DB_PASSWORD=
DB_HOST=
DB_PORT=

# Redis (cache, Celery broker/backend, seat holds)
REDIS_URL=

# JWT
ACCESS_TOKEN_LIFETIME=15
REFRESH_TOKEN_LIFETIME=1440

# Email (Twilio SendGrid)
SENDGRID_API_KEY=
DEFAULT_FROM_EMAIL=

# Stripe
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=

# CORS / CSRF
CORS_ALLOWED_ORIGINS=http://localhost:5173
CSRF_TRUSTED_ORIGINS=http://localhost:5173

# Frontend (used for Stripe redirect URLs)
FRONTEND_URL=http://localhost:5173

# Monitoring (production only)
SENTRY_DSN=

Never commit .env or production secrets to Git. In production, all secrets are managed through Render's environment variable dashboard.


Installation

Clone the Repository

git clone <repository-url>
cd halt-api

Create Virtual Environment

python -m venv .venv

Activate Virtual Environment

Linux / macOS

source .venv/bin/activate

Windows

.venv\Scripts\activate

Install Dependencies

pip install -r requirements.txt

Configure Environment Variables

cp .env.example .env

Fill in the required Django, PostgreSQL, Redis, Twilio SendGrid, and Stripe values.

Run Migrations

cd src
python manage.py migrate

Start Development Server

python manage.py runserver

Start Celery Worker (separate terminal)

celery -A core worker --loglevel=info

Start Celery Beat (separate terminal, for scheduled trip seeding)

celery -A core beat --loglevel=info --scheduler django_celery_beat.schedulers:DatabaseScheduler

The development server will be available at:

http://127.0.0.1:8000/

Docker

Halt includes Docker configuration for containerized deployment, running gunicorn, the Celery worker, and Celery Beat together under Supervisord.

Docker Files

Dockerfile
.dockerignore
entrypoint.sh
supervisord.conf

Build Image

docker build -t halt-api .

Run Container

docker run --env-file .env -p 8000:8000 halt-api

The entrypoint runs database migrations and collectstatic before handing off to Supervisord, which starts and supervises gunicorn, the Celery worker, and Celery Beat — automatically restarting any of the three if they crash.


Testing

API testing can be performed using:

  • Bruno
  • Postman
  • Insomnia
  • cURL

Run Django tests with:

cd src
python manage.py test

The project also runs automated tests through GitHub Actions on every push, using ephemeral Postgres and Redis service containers.


CI/CD

GitHub Actions automates testing and deployment. The deploy pipeline only runs after tests pass on main, enforced via a workflow_run dependency (not just a race between two independent triggers).

Push to main
     │
     ▼
Test Workflow
(ephemeral Postgres + Redis, migrations, test suite)
     │
     ▼  (on success)
Deploy Workflow
     │
     ▼
Build Docker Image
     │
     ▼
Push to Docker Hub
     │
     ▼
Trigger Render Deploy Hook
     │
     ▼
Render pulls image and redeploys

The frontend (separate repository) deploys independently via Vercel's native Git integration — every push to main triggers an automatic build and deploy, with preview deployments for pull requests.


Monitoring

Halt uses Sentry for production error monitoring and application observability.

Sentry captures unhandled exceptions from both the Django API and Celery background tasks, providing:

  • Production error tracking with full stack traces
  • Request and transaction context
  • Environment tagging (production only — not active in local development)
  • Celery task failure monitoring

Known Limitations

These are conscious, documented trade-offs rather than oversights — each is explained below.

  • Render free-tier cold starts. The backend spins down after periods of inactivity. The first request after idle time can take 30–60 seconds while the service wakes up. Subsequent requests are fast. This is a hosting-tier constraint, not an application performance issue.

  • Email deliverability without domain authentication. Transactional email (via SendGrid) uses Single Sender Verification rather than full Domain Authentication, since this demo project does not have a custom domain provisioned. Single Sender Verification allows sending to any recipient (unlike sandbox-only modes some providers default to), but without domain-level SPF/DKIM alignment, deliverability is weaker than a fully authenticated domain — emails may occasionally land in spam/junk folders depending on the recipient's mail provider. In a production deployment with a verified domain, Domain Authentication would be configured instead, improving inbox placement with no code changes required — only a provider-side configuration change.

  • Celery worker and Celery Beat run alongside gunicorn in a single container, managed by Supervisord, rather than as an independently scaled service. This is a deliberate choice to stay within Render's free-tier resource limits; Render's paid Background Worker tier would be the natural next step for a service under real production load, and would require only a small configuration change (splitting the Supervisord entries into separate services), not an architectural rewrite.

  • No pagination yet on list endpoints (trips, routes, bookings). Not a problem at current data volumes; a straightforward addition once dataset size warrants it.

  • Search filters by city name rather than city ID. Functionally correct, but not the most index-efficient approach at large scale — a documented, low-priority optimization.

  • No cache invalidation on admin-side edits to trips, routes, or buses — changes become visible once the relevant Redis cache TTL expires, rather than instantly.


Roadmap

  • Authentication with email OTP verification
  • Redis integration (cache, Celery broker, seat holds)
  • Celery integration
  • Celery Beat scheduled trip seeding
  • Twilio SendGrid email integration with attachments
  • Deck-aware seat selection (seater + sleeper)
  • Boarding/drop point selection
  • Per-passenger booking
  • Stripe Checkout + webhook-confirmed payments
  • Redis-backed seat holds (concurrency-safe booking)
  • PDF ticket generation
  • Per-passenger cancellation with partial refunds
  • Gunicorn + Supervisord production process management
  • Docker configuration
  • CI/CD (test-gated deploys)
  • Production deployment (Render + Vercel)
  • Sentry error monitoring
  • Pagination
  • Filtering
  • Rate limiting on public search endpoints
  • Automated test coverage for booking/payment flow
  • Cancel-by-ticket-reference (no login required)

Author

Arun

Software Engineer

Full-stack developer working with Python, Django, React, PostgreSQL, Redis, Docker, CI/CD, and cloud deployment.


Built with Django & Django REST Framework

About

Bus Booking REST API built with Django REST Framework

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages