Build a coupon engine using a Firestore coupons collection storing code, type (percentage or fixed), value, minimum order amount, max uses, current uses, and expiration date. Users enter a coupon code in a TextField, a Cloud Function validates it against all rules, and the discount is applied to the checkout total via Page State. An admin CRUD page lets you create, edit, and deactivate coupons with real-time usage statistics.
Coupon engine with server-side validation and admin management
Discount codes drive conversions, but a poorly built coupon system is easy to exploit. This tutorial builds a secure coupon engine in FlutterFlow: a Firestore collection stores coupon rules (type, value, limits, expiry), a Cloud Function validates codes server-side so users cannot bypass checks, and an admin panel lets you create, edit, and monitor coupon usage. The system supports both percentage and fixed-amount discounts with minimum order thresholds.
Prerequisites
- A FlutterFlow project with Firebase/Firestore connected
- A checkout or cart page where the discount will be applied
- Basic understanding of Action Flows and Page State variables
- Firebase Cloud Functions enabled for server-side validation
Step-by-step guide
Create the Firestore coupons collection
Create the Firestore coupons collection
In Firestore, create a coupons collection with fields: code (String, uppercase, unique), type (String: 'percentage' or 'fixed'), value (Double: e.g., 15.0 for 15% or 10.0 for $10 off), minOrderAmount (Double: minimum cart total required), maxUses (Integer: total redemptions allowed, 0 for unlimited), currentUses (Integer, default 0), expiresAt (Timestamp), isActive (Boolean, default true), and applicableCategories (List of Strings, empty means all categories). Set Firestore rules: read allowed for authenticated users (needed for admin panel), write restricted to admin role users only. The code field must be stored uppercase and matched uppercase during validation.
Expected result: A coupons collection is ready in Firestore with all validation fields defined.
Build the coupon input UI on the checkout page
Build the coupon input UI on the checkout page
On your checkout page, add a Row below the order summary containing a TextField (hint text: 'Enter coupon code', text capitalization: Characters) and a Button labeled 'Apply'. Add three Page State variables: appliedCouponCode (String), discountAmount (Double, default 0), and couponError (String). Below the Row, add a Text widget for couponError (red color, Conditional Visibility: couponError is not empty) and a Container showing the applied discount (green background, showing 'Coupon SAVE20 applied: -$15.00', Conditional Visibility: discountAmount > 0). Include a small 'Remove' IconButton in the discount Container that resets all three Page State variables to their defaults. Update the order total Text to subtract discountAmount from the cart subtotal.
Expected result: The checkout page has a coupon code input field, Apply button, error message area, and a discount display that updates the total.
Create a Cloud Function to validate coupon codes
Create a Cloud Function to validate coupon codes
Create a callable Cloud Function named validateCoupon that receives the coupon code and the current order total. The function performs these checks in order: (1) look up the coupon document by code (document ID), return error if not found; (2) check isActive is true; (3) check expiresAt is in the future; (4) check currentUses < maxUses (or maxUses == 0 for unlimited); (5) check order total >= minOrderAmount. If all checks pass, calculate the discount: for percentage type, multiply order total by value/100 and cap at the order total; for fixed type, use the value directly but cap at the order total. Return the discount amount and coupon details. This server-side validation prevents users from bypassing checks by manipulating the client.
1// Cloud Function: validateCoupon2const functions = require('firebase-functions');3const admin = require('firebase-admin');45exports.validateCoupon = functions.https.onCall(async (data, context) => {6 const { code, orderTotal } = data;7 if (!context.auth) throw new functions.https.HttpsError('unauthenticated', 'Login required');89 const couponRef = admin.firestore().collection('coupons').doc(code.toUpperCase());10 const couponSnap = await couponRef.get();1112 if (!couponSnap.exists) throw new functions.https.HttpsError('not-found', 'Invalid coupon code');1314 const coupon = couponSnap.data();15 if (!coupon.isActive) throw new functions.https.HttpsError('failed-precondition', 'Coupon is no longer active');16 if (coupon.expiresAt.toDate() < new Date()) throw new functions.https.HttpsError('failed-precondition', 'Coupon has expired');17 if (coupon.maxUses > 0 && coupon.currentUses >= coupon.maxUses) throw new functions.https.HttpsError('resource-exhausted', 'Coupon usage limit reached');18 if (orderTotal < coupon.minOrderAmount) throw new functions.https.HttpsError('failed-precondition', `Minimum order $${coupon.minOrderAmount} required`);1920 let discount = coupon.type === 'percentage'21 ? Math.min(orderTotal * (coupon.value / 100), orderTotal)22 : Math.min(coupon.value, orderTotal);2324 discount = Math.round(discount * 100) / 100;25 return { discount, type: coupon.type, value: coupon.value, code: code.toUpperCase() };26});Expected result: The Cloud Function validates all coupon rules server-side and returns the calculated discount amount or an error message.
Wire the Apply button to the Cloud Function and update checkout totals
Wire the Apply button to the Cloud Function and update checkout totals
On the Apply button's On Tap Action Flow: first, convert the TextField value to uppercase. Then call the validateCoupon Cloud Function using a Backend Call action, passing the coupon code and the current cart subtotal. On success: set Page State appliedCouponCode to the returned code, discountAmount to the returned discount value, and clear couponError. On error: set couponError to the error message from the Cloud Function response and clear discountAmount. Update the order total display: bind it to a Custom Function that calculates subtotal minus discountAmount. On successful order completion (after Stripe Checkout or payment), call a second action to increment the coupon's currentUses by 1 using a Cloud Function or direct Firestore Update Document with FieldValue.increment(1).
Expected result: Applying a valid coupon updates the discount display and recalculates the order total. Invalid codes show an error message.
Build the admin coupon management panel
Build the admin coupon management panel
Create an AdminCouponsPage restricted to admin users (check user role on page load). Add a ListView bound to a Backend Query on the coupons collection ordered by expiresAt descending. Each row shows: code (bold), type badge (percentage or fixed), value, usage as 'currentUses / maxUses', expiresAt formatted date, and an isActive toggle Switch. The Switch On Change action updates the coupon's isActive field. Add a FloatingActionButton that opens a BottomSheet form for creating new coupons with fields: code TextField, type DropDown (percentage/fixed), value TextField (number keyboard), minOrderAmount TextField, maxUses TextField, expiresAt DateTimePicker, and applicableCategories ChoiceChips. The Save button creates the coupon document using the uppercase code as the document ID.
Expected result: Admins can view all coupons with usage stats, toggle them active/inactive, and create new coupons from a form.
Complete working example
1Firestore Data Model:2└── coupons/{CODE} (document ID = uppercase coupon code)3 ├── code: String ("SAVE20")4 ├── type: String ("percentage" | "fixed")5 ├── value: Double (20.0 for 20% or 10.0 for $10)6 ├── minOrderAmount: Double (50.0)7 ├── maxUses: Integer (100, 0 = unlimited)8 ├── currentUses: Integer (43)9 ├── expiresAt: Timestamp10 ├── isActive: Boolean (true)11 └── applicableCategories: List<String> (["electronics", "clothing"])1213Checkout Page — Coupon Section:14├── Row15│ ├── TextField (coupon code input, capitalize: Characters)16│ └── Button ("Apply")17│ └── On Tap → Call validateCoupon Cloud Function18│ ├── On Success → Set Page State: discountAmount, appliedCouponCode19│ └── On Error → Set Page State: couponError20├── Text (couponError, red) [Cond. Vis: error not empty]21├── Container (green, applied coupon display) [Cond. Vis: discount > 0]22│ ├── Text ("SAVE20 applied: -$15.00")23│ └── IconButton (Remove → reset Page State)24└── Order Summary25 ├── Text ("Subtotal: $75.00")26 ├── Text ("Discount: -$15.00") [Cond. Vis: discount > 0]27 └── Text ("Total: $60.00", bold)2829Admin Coupons Page:30├── ListView (coupons, orderBy expiresAt DESC)31│ └── Row32│ ├── Text (code, bold)33│ ├── Badge (type: percentage/fixed)34│ ├── Text (value)35│ ├── Text ("43/100 used")36│ ├── Text (expires date)37│ └── Switch (isActive toggle)38└── FAB → BottomSheet (create coupon form)39 ├── TextField (code)40 ├── DropDown (type: percentage/fixed)41 ├── TextField (value, number)42 ├── TextField (minOrderAmount, number)43 ├── TextField (maxUses, number)44 ├── DateTimePicker (expiresAt)45 └── Button (Save → create doc with code as ID)Common mistakes when building a Custom Coupon and Discount System in FlutterFlow
Why it's a problem: Validating coupon only on the client side in the Action Flow
How to avoid: Validate all coupon rules in a Cloud Function. The client sends the code and order total; the server checks every rule and returns the discount or an error. Never trust client-calculated discounts.
Why it's a problem: Incrementing currentUses when the coupon is applied instead of after payment
How to avoid: Increment currentUses only inside the payment success handler (Stripe webhook Cloud Function or post-payment Action Flow). This ensures only completed orders consume coupon uses.
Why it's a problem: Storing coupon codes in mixed case without normalization
How to avoid: Store all coupon codes as uppercase in Firestore. Convert user input to uppercase before lookup using toUpperCase() in the Cloud Function and text capitalization Characters on the TextField.
Best practices
- Use the coupon code as the Firestore document ID for guaranteed uniqueness and fast lookups
- Validate all coupon rules server-side in a Cloud Function to prevent client bypass
- Normalize coupon codes to uppercase for case-insensitive matching
- Increment currentUses only after confirmed payment, not on coupon application
- Cap discount at the order total to prevent negative amounts on small orders with large fixed discounts
- Round discount calculations to two decimal places to avoid floating-point penny errors
- Add rate limiting on the validate endpoint to prevent brute-force code guessing
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
Write a Firebase Cloud Function that validates a coupon code against a Firestore coupons collection. Check if the coupon exists, is active, not expired, under max uses, and meets the minimum order amount. Calculate the discount for both percentage and fixed types.
Create a checkout coupon section with a TextField for code entry, an Apply button, an error text, and a green container showing the applied discount. Add Page State variables for discountAmount and couponError.
Frequently asked questions
How do I support both percentage and fixed-amount discounts?
Store a type field (percentage or fixed) and a value field on the coupon document. In the validation logic, check the type: for percentage, calculate orderTotal * (value / 100); for fixed, use the value directly. Always cap the discount at the order total to prevent negative totals.
How do I prevent users from guessing coupon codes?
Use random alphanumeric codes (8-12 characters) generated in Cloud Functions instead of predictable words. Add rate limiting to the validate function: reject requests if a user tries more than 5 invalid codes in one minute.
Can I restrict coupons to specific product categories?
Yes. Add an applicableCategories list field to the coupon. During validation, compare the cart items' categories against this list. If the list is empty, the coupon applies to all categories. Otherwise, only items in matching categories receive the discount.
How do I create single-use-per-user coupons?
Add a usedBy list field on the coupon document or create a coupon_redemptions subcollection. During validation, check if the current user's UID is already in the usedBy list. On redemption, add their UID. This allows the coupon to be used globally but only once per user.
Should I show the discount in the Stripe Checkout total or apply it before?
Apply the discount before creating the Stripe Checkout session. Pass the discounted total as the amount. This way the customer sees the correct price on the Stripe payment page, and you do not need to configure Stripe-side discounts.
Can RapidDev help build an advanced promotion engine?
Yes. Advanced coupon systems need stackable discounts, tiered pricing, automatic coupon suggestions, A/B testing of offers, and analytics dashboards. RapidDev can build the full promotion engine with Cloud Functions and integrate it with your payment flow.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation