Skip to main content
RapidDev - Software Development Agency
flutterflow-tutorials

How to Create a Ticketing System for Public Transport in FlutterFlow

Build a public transport ticketing app where passengers select a route, choose a ticket type (single, day, week, month), pay via Stripe, and receive a QR code ticket validated at stations. Routes and fares are stored in Firestore. After Stripe payment, a Cloud Function generates a ticket with a cryptographic QR code containing the ticket ID and an HMAC hash to prevent forgery. A ticket wallet shows all purchased tickets sorted by status. Station staff validate tickets by scanning the QR code against a Cloud Function that checks authenticity and validity period.

What you'll learn

  • How to build a route and ticket type selection flow for transit
  • How to generate secure QR code tickets with HMAC authentication
  • How to create a ticket wallet showing active, used, and expired tickets
  • How to build a QR scanner for station-side ticket validation
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner9 min read25-30 minFlutterFlow Pro+ (Cloud Functions required for Stripe and QR validation)March 2026RapidDev Engineering Team
TL;DR

Build a public transport ticketing app where passengers select a route, choose a ticket type (single, day, week, month), pay via Stripe, and receive a QR code ticket validated at stations. Routes and fares are stored in Firestore. After Stripe payment, a Cloud Function generates a ticket with a cryptographic QR code containing the ticket ID and an HMAC hash to prevent forgery. A ticket wallet shows all purchased tickets sorted by status. Station staff validate tickets by scanning the QR code against a Cloud Function that checks authenticity and validity period.

Building a Public Transport Ticketing System in FlutterFlow

Paper tickets and physical cards are expensive to produce and easy to counterfeit. A mobile ticketing app lets passengers buy tickets instantly, display them as QR codes, and validate at stations with a scanner. This tutorial builds the full flow: route and ticket selection, Stripe payment, secure QR generation with HMAC authentication, a ticket wallet, and a validation scanner for station staff.

Prerequisites

  • A FlutterFlow project with Firestore and Cloud Functions configured
  • Stripe account connected for payment processing
  • Understanding of QR code Custom Widgets and Cloud Functions
  • Firebase Storage or a secret key stored in Cloud Function environment for HMAC

Step-by-step guide

1

Design the Firestore data model for routes and tickets

Create a routes collection with fields: name (String, e.g. 'Downtown Express Line'), stops (String Array listing all stops in order), scheduleUrl (String, link to schedule PDF or page), fares (Map of ticket type to price: single 2.50, day 8.00, week 25.00, month 80.00). Create a tickets collection: userId (String), routeId (String), routeName (String), ticketType (String: single, day, week, month), validFrom (Timestamp), validUntil (Timestamp), qrCodeData (String, the secure QR payload), status (String: active, used, expired), purchasedAt (Timestamp), stripePaymentId (String).

Expected result: Firestore has routes with fare tiers and a tickets collection tracking purchases with validity periods.

2

Build the route selection and ticket purchase flow

Create a BuyTicket page. Display available routes in a ListView, each row showing route name, number of stops, and a brief description. When a user taps a route, show ticket type options in a BottomSheet or new section: four Container cards showing Single Ride (2h valid), Day Pass, Weekly Pass, and Monthly Pass with prices from the route's fares map. Each card shows the ticket type name, validity duration, and price. On selection, update Page State with selectedRoute and selectedTicketType.

Expected result: Passengers select a route and ticket type, seeing clear pricing and validity information for each option.

3

Process payment via Stripe and generate the ticket

On the Confirm Purchase button tap, call a Cloud Function that creates a Stripe Checkout Session with the ticket price and metadata (routeId, ticketType, userId). The function returns a checkout URL. Launch the URL using a Launch URL action. After successful payment, a Stripe webhook calls another Cloud Function (checkout.session.completed) that creates the ticket document in Firestore: sets validFrom to now, calculates validUntil based on ticket type, generates the QR code data with HMAC, and sets status to 'active'. The passenger is redirected back to the app showing their new ticket.

handle_ticket_purchase.ts
1// Cloud Function: createTicket (called by Stripe webhook)
2import * as functions from 'firebase-functions';
3import * as admin from 'firebase-admin';
4import * as crypto from 'crypto';
5admin.initializeApp();
6
7const HMAC_SECRET = functions.config().tickets.hmac_secret;
8
9export const handleTicketPurchase = functions.https.onRequest(async (req, res) => {
10 const event = req.body;
11 if (event.type !== 'checkout.session.completed') {
12 res.status(200).send('ignored');
13 return;
14 }
15
16 const session = event.data.object;
17 const { routeId, ticketType, userId } = session.metadata;
18 const now = admin.firestore.Timestamp.now();
19
20 const validityHours: Record<string, number> = {
21 single: 2, day: 24, week: 168, month: 720
22 };
23 const validUntil = new Date(now.toDate().getTime() +
24 validityHours[ticketType] * 3600000);
25
26 const ticketRef = admin.firestore().collection('tickets').doc();
27 const ticketId = ticketRef.id;
28 const hmac = crypto.createHmac('sha256', HMAC_SECRET)
29 .update(ticketId + userId + ticketType)
30 .digest('hex')
31 .substring(0, 16);
32 const qrCodeData = `${ticketId}:${hmac}`;
33
34 await ticketRef.set({
35 userId, routeId, ticketType, qrCodeData,
36 validFrom: now,
37 validUntil: admin.firestore.Timestamp.fromDate(validUntil),
38 status: 'active',
39 purchasedAt: now,
40 stripePaymentId: session.payment_intent,
41 });
42
43 res.status(200).send('ok');
44});

Expected result: After Stripe payment, a ticket is created in Firestore with a cryptographically signed QR code payload.

4

Display the ticket QR code and build a ticket wallet

Create a TicketWallet page. Query tickets where userId equals current user, ordered by purchasedAt descending. Group tickets visually: active tickets at the top with full color, used and expired tickets below with greyed styling. Each ticket card shows: route name, ticket type badge, validity period (Valid until date and time), status badge (green for active, grey for used, red for expired), and a View QR button. On tap, navigate to a TicketDetail page showing a large QR code rendered with a qr_flutter Custom Widget displaying the qrCodeData string. Add a brightness slider or white background container around the QR for scanner readability.

Expected result: A ticket wallet shows all purchased tickets organized by status, with QR codes accessible for active tickets.

5

Build the station validation scanner for ticket checking

Create a ValidateTicket page for station staff. Add a mobile_scanner Custom Widget that reads QR codes from the camera. When a QR is scanned, extract the ticket data (ticketId and hmac from the colon-separated string). Call a Cloud Function that: looks up the ticket by ID, recomputes the HMAC from the ticket's fields and the secret, compares it to the scanned HMAC, checks that status is 'active', and checks that the current time is before validUntil. If valid, return success and optionally mark single-use tickets as 'used'. Display a green checkmark with ticket details for valid tickets, or a red X with the rejection reason for invalid or expired tickets.

Expected result: Station staff scan ticket QR codes and see instant validation results with clear pass or fail indicators.

6

Auto-expire tickets with a scheduled Cloud Function

Create a scheduled Cloud Function that runs every hour. It queries all tickets where status equals 'active' and validUntil is less than or equal to the current timestamp. For each expired ticket, update status to 'expired'. This ensures the ticket wallet always reflects accurate status even if the user has not opened the app. On the TicketWallet page, also add a client-side check: if validUntil has passed but status is still 'active', display it as expired locally while the Cloud Function catches up.

Expected result: Expired tickets automatically update their status, keeping the wallet and validation system accurate.

Complete working example

FlutterFlow Transit Ticketing Setup
1FIRESTORE DATA MODEL:
2 routes/{routeId}
3 name: String (e.g. 'Downtown Express Line')
4 stops: [String] (ordered stop names)
5 scheduleUrl: String
6 fares: {
7 single: 2.50,
8 day: 8.00,
9 week: 25.00,
10 month: 80.00
11 }
12
13 tickets/{ticketId}
14 userId: String
15 routeId: String
16 routeName: String
17 ticketType: "single" | "day" | "week" | "month"
18 validFrom: Timestamp
19 validUntil: Timestamp
20 qrCodeData: String (ticketId:hmacHash)
21 status: "active" | "used" | "expired"
22 purchasedAt: Timestamp
23 stripePaymentId: String
24
25QR CODE DATA FORMAT:
26 "{ticketId}:{hmac16chars}"
27 HMAC = SHA256(ticketId + userId + ticketType, secret).substring(0,16)
28 Prevents forgery: scanner verifies HMAC server-side
29
30VALIDITY DURATIONS:
31 single: 2 hours from purchase
32 day: until 23:59 of purchase day
33 week: 7 days from purchase
34 month: 30 days from purchase
35
36WIDGET TREE Ticket Wallet:
37 Column
38 Text ('My Tickets', Headline Small)
39 SizedBox (16)
40 ListView (tickets orderBy purchasedAt desc)
41 Container (card, greyed if used/expired)
42 Row
43 Column
44 Text (routeName, bold)
45 Container (ticketType badge)
46 Text ('Valid until ' + validUntil)
47 Spacer
48 Container (status badge: green/grey/red)
49 IconButton (QR icon TicketDetail)
50
51WIDGET TREE Ticket QR Display:
52 Column (centered)
53 Text (routeName, Headline Small)
54 Text (ticketType + ' Pass')
55 SizedBox (24)
56 Container (white background, padding: 16)
57 Custom Widget: QrImageView(data: qrCodeData, size: 250)
58 SizedBox (16)
59 Text ('Valid until ' + validUntil)
60 Text ('Scan at station entrance', grey)
61
62WIDGET TREE Validation Scanner:
63 Column
64 Expanded
65 Custom Widget: MobileScanner onDetect callback
66 Container (result display)
67 Conditional:
68 valid green Container (checkmark + ticket details)
69 invalid red Container (X + rejection reason)

Common mistakes when creating a Ticketing System for Public Transport in FlutterFlow

Why it's a problem: Generating QR codes with just the ticket ID and no cryptographic signature

How to avoid: Include an HMAC hash in the QR data computed from the ticket ID, user ID, and a server-side secret. The validation endpoint recomputes and compares the hash.

Why it's a problem: Validating tickets only client-side by checking the status field

How to avoid: Always validate through a Cloud Function that reads the ticket from Firestore, verifies the HMAC, and checks the validity period server-side.

Why it's a problem: Not auto-expiring tickets with a scheduled Cloud Function

How to avoid: Run a scheduled Cloud Function hourly to update expired tickets. Also always check validUntil in the validation logic as a defense-in-depth measure.

Best practices

  • Sign QR codes with HMAC to prevent ticket forgery
  • Validate tickets server-side via Cloud Function, never client-side only
  • Auto-expire tickets with a scheduled Cloud Function for accurate status tracking
  • Show clear validity periods on every ticket so passengers know when it expires
  • Use a white container with padding around QR codes for optimal scanner readability
  • Store Stripe payment IDs on ticket documents for refund and audit purposes
  • Sort the ticket wallet with active tickets at the top for quick access

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I want to build a public transport ticketing app in FlutterFlow. Passengers select a route, choose single/day/week/month tickets, pay via Stripe, and receive QR code tickets. QR codes include an HMAC hash for forgery prevention. Station scanners validate tickets via Cloud Function. Show me the data model, Stripe webhook Cloud Function, QR generation, and validation flow.

FlutterFlow Prompt

Create a ticket purchase page with a list of transit routes, ticket type selection cards with prices, and a confirm purchase button. Add a ticket wallet page showing ticket cards with QR code buttons.

Frequently asked questions

How do I handle offline ticket display when there is no internet?

Cache the ticket data and QR code locally in App State with persistence. The QR code is a static string that does not need internet to display. The scanner validates online, but the ticket can be shown offline.

Can I support multi-route passes that work across all lines?

Add a routeId value of 'all' for multi-route passes. During validation, check if routeId equals 'all' or matches the station's route, accepting the ticket in either case.

How do I handle refunds for unused tickets?

Call a Cloud Function that checks the ticket status is 'active' and not yet used. If valid for refund, create a Stripe refund using the stripePaymentId and update ticket status to 'refunded'.

Can I add NFC tap-to-validate instead of QR scanning?

Flutter supports NFC via the nfc_manager package as a Custom Widget. Write the qrCodeData to an NFC tag or emit it from the phone. The validator reads it the same way as a QR scan and passes it to the same validation Cloud Function.

How do I prevent screenshots of QR codes being shared?

Add a timestamp or rotating element to the ticket display screen: show the current time in large text next to the QR code. Station staff verify the time is current. For stronger protection, generate time-based one-time QR codes via Cloud Function that rotate every 30 seconds.

Can RapidDev help build a ticketing or transit app?

Yes. RapidDev can build transit ticketing systems with secure QR validation, multi-route passes, NFC support, usage analytics, and integration with existing transit infrastructure.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.