Why I built My Coupons
I wanted a simple place to keep track of the coupons and offers I actually cared about without scattering them across screenshots, email threads, and random notes. The app is small, specific, and useful in a very everyday way: save a coupon, find it later, keep it active or inactive, and avoid re-entering the same details manually.
The underlying idea was not to build a huge consumer product. It was to make a useful personal utility fit naturally into the rest of my site. So the most practical path was to add My Coupons as an app under the existing site rather than creating a separate product shell or a new stack.
That also gave me a chance to keep the feature self-contained. The app has its own auth flow, its own route namespace, and its own storage logic. That made the feature easier to reason about and easier to keep isolated from the rest of the site.
What I built
My Coupons is a protected app under My Coupons. It gives a signed-in user a place to:
- save and manage coupons
- search through them quickly
- mark them active or inactive
- edit or delete inactive coupons
- refresh expired coupons
- paste coupon text or upload an image to speed up entry
- review promotional offers from Gmail
The core workflow is straightforward, but it is also shaped by the real constraints of the app. The feature is meant to be personal and practical, so it uses a private Google Drive app data folder instead of a conventional database. It also uses Google OAuth directly, with the My Coupons auth configured separately from the rest of the application.
The dashboard and the everyday workflow
The main app starts with a dashboard that loads the user’s saved coupons and shows them in a clean list. The first version is intentionally utility-driven: search, category metadata, status indicators, and quick edit actions are all designed to make the app useful without adding a lot of ceremony.
A coupon can include a title, brand, category, tags, discount type, discount value, coupon code, payment method, minimum spend, validity date, store URL, description, reminder setting, favorite flag, and active status. In practice, the app stores these fields in a structured object and validates them before saving.
I also kept a clear distinction between active and inactive states. The app displays status directly, and a refresh action checks validUntil values and moves expired active coupons to inactive. That means the dashboard stays useful without having to manually inspect every old coupon each time.
There is a simple rule for deletion too: only inactive coupons can be deleted. That is a practical decision rather than a broad product policy. It reduces accidental removal of live offers and makes the data model easier to maintain.
AI Quick Add: making manual entry less painful
One of the more interesting parts of the project is the AI Quick Add flow. The app lets a user paste coupon text or upload an image. If the content is an image, it runs OCR before sending the text to the backend. The OCR step uses Tesseract.js, which is a lightweight and practical choice for extracting text from screenshots or photos of offers.
Once the text is available, the app sends it to AI endpoint. It validates the request, checks the session, and calls the AI extraction helper. The extraction logic is implemented in the My Coupons library and uses the Google GenAI SDK with a Gemini model (gemini-3.6-flash). The prompt is designed to return a structured JSON object with fields like title, brand, category, discount type, valid until, and usage location.
The important part is that this is not a blind auto-save flow. The AI output is normalized and displayed in an editable form first. The user can review the extracted fields and fix anything that looks off before saving. That is a key engineering decision: treat AI as a time-saving assistant rather than an unverified source of truth.
This pattern is practical for real-world coupon text. Offer messages are often messy, inconsistent, and not formatted like clean structured data. The app accepts that and gives the user a chance to correct the output before persisting it.
Gmail promotional offers
The app also includes a Gmail promotional offers flow. On the promotional emails page, it loads saved offer metadata and gives the user a refresh button to fetch more recent emails. The route calls the Gmail API with a promotions query and filters the results using offer-like signals in the subject and snippet.
The filtering uses a set of simple rules to identify likely promotional emails. It looks for patterns such as discount language, cashback wording, coupon or promo wording, BOGO-style offers, and limited-time deal phrasing. The user sees a list of likely promotional offers rather than a raw dump of all messages. The filtered data is then saved in Google Drive so that it can be revisited later without re-fetching from Gmail every time.
This feature is useful because it turns an inbox full of marketing email noise into something closer to a reviewable offer list. It is not a full automation pipeline that creates final coupon records automatically. The app stops at surfacing likely candidates for review, which keeps the behavior honest and predictable.
Google OAuth and Google Drive storage
One of the biggest practical decisions in this project was how to authenticate and persist data. My Coupons uses the shared personal-app Google OAuth setup, with its auth endpoint under /apps/gmail/api/auth. The configuration prefers the shared Google client credentials and falls back to My Coupons-specific credentials. The permission set includes drive.appdata and gmail.readonly, which matches the actual feature needs.
The app uses Google Drive as the persistence layer for both coupons and saved promotional email data. It stores JSON in the private appDataFolder and reads or updates the files using the Drive API. The coupon store is versioned and validated before use, and the promotional email store keeps metadata such as sender, subject, date, and snippet.
This design is intentionally simple for a personal app. I did not add a separate database layer because the feature does not require one, and the user already has a valid Google account context. The tradeoff is that the app depends on Google access being granted and refreshed correctly. The code handles failures by surfacing clear messages and telling the user to sign out and sign in again when permission needs to be renewed.
Architecture and integration into bykrishnateja
From an architecture standpoint, this is a Next.js App Router app living inside a larger website. The My Coupons area uses standard Next.js pages, server actions, and API routes. The main dashboard is a client component because it needs to handle search, status toggles, editing flows, and interactive form states. The server side handles auth, storage, and external API calls.
This was important for the integration. I wanted to reuse the main bykrishnateja shell and navigation without letting the feature become entangled with unrelated application logic. The app keeps its own route namespace, auth configuration, storage helpers, and Google API access. That separation keeps the feature easier to reason about and easier to maintain.
The site shell remains shared, but the business logic stays local to My Coupons. It is a good example of building a focused app inside a broader project without turning the whole site into a single monolith.
Challenges and what I learned
There were a few real engineering challenges here.
The first was OAuth permission scoping. A personal utility like this needs Google access for Drive and Gmail, but it should not overreach into broader site-wide auth. I kept My Coupons isolated and explicit.
The second was data persistence. Saving to Google Drive works well for a private app data set, but it requires careful handling of missing files, invalid JSON, permission failures, and token refresh. I wanted the app to fail in a clear and understandable way instead of silently doing the wrong thing.
The third was AI extraction quality. The app does not assume that AI output is perfect. It expects messy input and treats the AI result as a draft for review. That is a key design decision and a practical lesson in using AI productively: reduce repetitive work without losing control.
The biggest takeaway is that everyday developer tools do not need to be massive or glamorous to be useful. A feature can be narrow, personal, and still be valuable if it solves a repeated problem well. My Coupons is a good example of that: it turns a real workflow into a focused app with clear boundaries and pragmatic engineering choices.
Future improvements
I have not implemented advanced notifications, analytics, deeper recommendation logic, or automated coupon conversion beyond the current review-based flows. Those are reasonable next steps, but they are not part of the current app. The feature is already useful in its current form: it stores offers, helps manage them, and reduces the effort of repetitive coupon entry.
That is what I think is most important about this project. It is not a grand platform story. It is a focused engineering story about building a useful tool inside the site I already had, and letting the app’s real needs drive the design decisions.