MailSage
Stack
Type
Duration
Year

TL;DR
MailSage is a web application that connects to Gmail, and uses AI to turn email threads into concise summaries and action items. I built it solo in ~11 weeks to assist me in a former software-based role, where I could expect to receive in excess of 10 email threads per day. These lengthy threads typically had at least 2 other people on them, and pertained to things such as software bugs and product features. MailSage offered me an easier way to keep track of the flow. The product I would eventually ship, is not the one I started out building however.
MailSage initially began as a more complex Gmail add-on, supported by a database-backed architecture. However, when a persistent infrastructure issue with PostgreSQL threatened my intended delivery deadline, I re-assessed the scope of the project, retained the product’s central value proposition — and with only 3 weeks remaining; I rebuilt it as a much leaner user experience: a standalone Next.js application (whilst leveraging Vite for quick prototyping).
Product | AI-assisted Gmail summarisation application |
My role | Product definition, technical planning, interface design, development, QA testing/debugging and deployment |
Project period | Approximately 11 weeks |
Core tech | Next.js, React, JavaScript, Gmail API, Google OAuth, OpenAI API and Vercel |
Prototyping tools | Vite and HTML |
Initial technologies explored | PostgreSQL, Prisma, Supabase and Resend |
Development support | Developer documentation, ChatGPT, Gemini, Cursor, Stack Overflow and developer communities |
Final outcome | A deployed application that retrieved Gmail content and generated summaries and action items |
Section 001: The motivation
Important decisions, questions and next steps can become buried inside lengthy email conversations. Returning to an older thread often means rereading several messages simply to reconstruct what happened. I wanted to explore whether AI could reduce that effort in my workflow, by producing a concise overview of the conversation and grasping its context in order to identify actionable items/steps.

Section 002: The original concept
My initial intention was to build the experience directly into Gmail itself, supported by a more complete backend architecture. Before implementation, I wrote detailed pseudocode, identified the functions it would need, and mapped reusable elements from an existing TypeScript project, to the new JavaScript-based frontend.
Original architecture
Section 003: The constraints
A persistent PostgreSQL and Prisma schema issue, prevented the database layer from functioning as intended. I initially approached the problem through code review, documentation research, community resources and AI-assisted troubleshooting. When the investigation began producing repeated or destructive suggestions, I introduced a written test log to track hypotheses and avoid repeating previous attempts.
Section 004: Pivoting
The Wall (Context)
To elaborate further on the constraints, by week 5 of the project, the backend looked to be taking shape — App Router routes, a Prisma schema, a Postgres database. But then the console errors started: Prisma was referencing tables within the database's schemas in a way I couldn't seem to get past. What looked like a day of debugging became three weeks (as I was working on this at the end of my workday). I worked through the official Supabase docs, Stack Overflow, and Reddit, plus ChatGPT — the latter of which (at one stage) kept looping me toward 'fixes' that would have deleted a key component of the database. To stop circling, I began logging every test I ran, so that I wouldn't inadvertently repeat a step. By week 7 of the project however, I was now two weeks behind, and no closer to resolving the backend issues.
The Options
Keep digging — unbounded time risk against a fixed deadline.
Cut features, but keep the add-on architecture — a smaller product on the same broken foundation.
Or move the idea out of Gmail entirely — lose the add-on form factor, keep the core value, drop the layer causing the problem.
Continue debugging | Reduce scope and pivot |
|---|---|
Preserve the original architecture | Preserve the central user value |
Risk missing the deadline | Increase the likelihood of shipping |
Potentially solve the database problem | Remove a non-essential dependency |
Leave less time for UX and QA | Create time for an end-to-end prototype |
The Call
With only 3 weeks remaining, I took a risk and decided to pivot. The reframe that made it possible: the database wasn't the product — the summaries were. Removing the architectural constraints, preserved the core premise behind the project.
The Rebuild
Three weeks to deliver on a working build, ended up being sufficient time for me, only because of two decisions:
I prototyped in Vite for its fast feedback loop, but wrote standard .jsx throughout (so that ~90% of the code would port straight into Next.js for production).
And I reused my earlier OAuth and Gmail API work, rather than rebuilding from zero.
Eight prototype versions later, I migrated the codebase, QA-tested multiple OpenAI models to balance speed against summary quality, and deployed to Vercel on deadline.
The pivot removed architectural complexity without removing the product’s central proposition.
Planned vs Shipped Architecture
Section 005: Minimum viable workflow
Given the time constraints, I temporarily deprioritized visual design, and focused my efforts on proving the smallest complete technical path. The more challenging (and thus time consuming) aspects at this stage of the project were:
Setting up the OAuth credentials correctly in Google Cloud
Plugging these into NextAuth
Fetching email data via the Gmail API
This was the stage at which I decided to introduce additional tools (i.e. Cursor) into the debugging process. Primarily as an AI-assisted development environment, rather than as an autonomous builder. For example, it quickly became a useful aid in understanding errors and helping with refactoring smaller pieces of code.
Core Loop
High-level system flow:
Breakdown:
The intended API route would essentially:
Take the logged-in user's credentials
Call Gmail
Gmail then returns a simplified list of emails (subject line, snippet, date, thread ID), outputting them initially in the following JSON format:
And then eventually in the following example format:
I opted to display the fetched Gmail data in raw JSON initially, because my primary focus was on building out the backend infrastructure. And this was the quickest way to iterate on the app's core functionality.



MVP Version: MailDeck
The initial dev version of the product (which I called 'MailDeck' at the time) was able to successfully authenticate with Google, retrieve Gmail content, send that content to the OpenAI API, and subsequently return summaries and action items through two manual controls.



Section 006: LLM selection
Deciding which large language model would power the summary generation feature, was a process comprised of reading up on OpenAI's existing models and their respective spec sheets, pulling feedback online from other developers, and then running experiments to better understand each model's limitations.
Criteria
At a high-level, I screened each model against the following benchmarks:
Summary quality
Ability to identify relevant actions
Response speed
Consistency
Potential operating costs
Tested Models
gpt-3.5
gpt-4o
gpt-5-mini
gpt-5-nano
gpt-5.1
The reason I included a legacy model like gpt-3.5, was that it allowed me to set a baseline for testing. Being able to compare multiple models of varying levels, made the the experiment more comprehensive.
Considerations (Trade-offs)
While a faster model may occasionally produce less nuanced summaries, a more capable model could make the interaction feel unnecessarily slow. Speed was a crucial factor for me.
Decision
I opted for a model which provided an acceptable quality-to-latency balance, rather than defaulting to the most seemingly-capable, the fastest, or the most expensive option. The model I selected is gpt-4o.
From the tests I ran, it seemed to fare better than some of the newer models at grasping the necessary context. Particularly as it pertained to pulling action items.
Section 007: Vite → Next.js
Rather than immediately rebuilding the complete web application in Next.js, I used Vite as a lower-friction environment for interface development and debugging. And because both environments use React and standard JSX, most of the prototype's code could later be ported over without having to be rewritten.


Section 008: AI-assisted development
AI tools accelerated parts of the research and debugging process, but they were not reliable substitutes for documentation, controlled testing or architectural judgement.
At one stage, I recall ChatGPT suggesting a fix which would have removed an important database component. As a counteractive measure, I decided to compare outputs across various LLMs and tools, validating their suggestions against official developer documentation, and maintaining a written record/log of attempted fixes.
Section 009: Final Week
I completed and deployed a working version of MailSage that connected to Gmail, retrieved email content and used the OpenAI API to generate summaries and action items. I migrated the prototype into Next.js, stored the project in GitHub, configured the required Google Cloud settings and deployed the application through Vercel’s GitHub integration.
The final week also included:
UI clean-up
Further feature trimming (search functions)
Model QA
Google Cloud amendments
Multiple deployment tests
Last-minute API and UX fixes
Automatic Vercel updates following the final GitHub push

Project Timeline
____________________________________________________________________
Product Demo
Reflections
As highlighted in 'Section 004' above, with approximately three weeks remaining until my intended delivery date, I had to decide whether to continue investigating the database layer, or protect the project’s core outcome. I determined that the likely investigation cost, would exceed the remaining project window. This meant that I had to accept that the database was part of the implementation approach, but not the central user value. I therefore removed it from the critical path, and reframed the the app around one end-to-end workflow: retrieve email content → and generate useful summaries and action items.
The compressed schedule came with defect frequency risks. And I found myself trying to rush through certain tasks. Rather than this approach accelerating my progress; it actually led to syntax errors, increased the likelihood of bugs, and it made debugging more stressful for myself. Thus counteractive measures included narrowing the project scope/build, streamlining my workflow (ref. Section 005 above), and introducing additional QA.
Database Issue
I reviewed the relevant log data, as part of a retrospective into the initial stages of the project (i.e. pre-pivoting). Returning to the errors with a fresh perspective, I was able probe the console errors in ways I hadn't thought of doing, previously. And through this process, I realized that the persistent console errors were due to the port I was trying to use. My database URL ended in:
And that (6543) port meant I was attempting to connect to Supabase via a Connection/Transaction-mode Pooler.
A connection pooler functions like an efficient receptionist in a busy office. When an app needs a quick piece of data, the receptionist assigns an available worker to the task. The worker procures the data for you, and then the receptionist immediately sends the worker back into the 'pool' to assist another application with their query.
This dynamic is great for quick/short-lived application queries, such as fetching a product by its 'slug' for a page render on an e-commerce website, looking up a user's session upon logging in, inserting a comment on a website, changing a view counter etc. Connection poolers are therefore, best suited for single round trips that finish in milliseconds and leave nothing behind on the connection. But it is not suited for what I was attempting to do: a Prisma migration.
A Prisma/database migration is akin to a major home or office renovation. And for such tasks, Prisma needs a dedicated 'contractor' to stay on-site in order to carry out the heavy structural work, without any interruptions. This is the reason for the persistent console errors I was getting: the connection pooler was attempting the migration, but then returning to the 'pool' before the process was complete. It left the migration 'hanging'. But this is to be expected, given that connection poolers are hard-coded for quick tasks/queries.
However, Supabase's Session Pooler (on port 5432) is much better suited for such workflows.
Further Takeaways
Product
The core user value should be distinguishable from the original technical implementation.
Removing features can improve the likelihood that the central experience is delivered.
A working vertical slice is more valuable than several partially-completed system layers.
Deadlines require active scope management rather than simply working faster.
Engineering
Authentication, API retrieval and AI processing should always be validated independently.
A prototype environment can reduce debugging complexity.
Shared framework foundations can make later migrations less costly (in both time and resources).
Troubleshooting should be logged systematically.
AI-generated fixes need independent verification.
Workflow
Detailed planning helped me begin, but excessive preparation also consumed scarce implementation time.
Compressed schedules increased syntax errors and made debugging more stressful.
Trying to complete architecture, interface, functionality and QA concurrently created unnecessary risk.
Future projects should establish a minimal end-to-end proof at a much earlier stage.
Overall, the main takeaway is not that the original architecture was wrong. It was that I had committed to too much of it before proving the project’s smallest valuable workflow. It's a lesson that I will carry into all future projects: validate backend architecture first (such as authentication, retrieval systems, AI outputs etc) — and then introduce persistence or additional features, only after stress-testing stability.