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
Design the Firestore data model for routes and tickets
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.
Build the route selection and ticket purchase flow
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.
Process payment via Stripe and generate the ticket
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.
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();67const HMAC_SECRET = functions.config().tickets.hmac_secret;89export 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 }1516 const session = event.data.object;17 const { routeId, ticketType, userId } = session.metadata;18 const now = admin.firestore.Timestamp.now();1920 const validityHours: Record<string, number> = {21 single: 2, day: 24, week: 168, month: 72022 };23 const validUntil = new Date(now.toDate().getTime() +24 validityHours[ticketType] * 3600000);2526 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}`;3334 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 });4243 res.status(200).send('ok');44});Expected result: After Stripe payment, a ticket is created in Firestore with a cryptographically signed QR code payload.
Display the ticket QR code and build a ticket wallet
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.
Build the station validation scanner for ticket checking
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.
Auto-expire tickets with a scheduled Cloud Function
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
1FIRESTORE DATA MODEL:2 routes/{routeId}3 name: String (e.g. 'Downtown Express Line')4 stops: [String] (ordered stop names)5 scheduleUrl: String6 fares: {7 single: 2.50,8 day: 8.00,9 week: 25.00,10 month: 80.0011 }1213 tickets/{ticketId}14 userId: String15 routeId: String16 routeName: String17 ticketType: "single" | "day" | "week" | "month"18 validFrom: Timestamp19 validUntil: Timestamp20 qrCodeData: String (ticketId:hmacHash)21 status: "active" | "used" | "expired"22 purchasedAt: Timestamp23 stripePaymentId: String2425QR CODE DATA FORMAT:26 "{ticketId}:{hmac16chars}"27 HMAC = SHA256(ticketId + userId + ticketType, secret).substring(0,16)28 Prevents forgery: scanner verifies HMAC server-side2930VALIDITY DURATIONS:31 single: 2 hours from purchase32 day: until 23:59 of purchase day33 week: 7 days from purchase34 month: 30 days from purchase3536WIDGET TREE — Ticket Wallet:37 Column38 ├── Text ('My Tickets', Headline Small)39 ├── SizedBox (16)40 └── ListView (tickets orderBy purchasedAt desc)41 └── Container (card, greyed if used/expired)42 Row43 ├── Column44 │ ├── Text (routeName, bold)45 │ ├── Container (ticketType badge)46 │ └── Text ('Valid until ' + validUntil)47 ├── Spacer48 ├── Container (status badge: green/grey/red)49 └── IconButton (QR icon → TicketDetail)5051WIDGET 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)6162WIDGET TREE — Validation Scanner:63 Column64 ├── Expanded65 │ Custom Widget: MobileScanner → onDetect callback66 └── 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.
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.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation