Introduction
I built PreMeder because the existing tools for tracking a pre-med journey were embarrassingly bad. Spreadsheets with 40 tabs. Sticky notes on monitors. GroupMe threads where someone asks for the third time which schools accept AP credit. The entire pre-health application process, AMCAS, AACOMAS, TMDSAS, is a logistics problem, and nobody had built a proper tool to solve it.
PreMeder started as a personal project to track my own clinical hours and school list. It now has over 12,000 active users. This is the story of how it got there, what I built, and what I learned about shipping a product for a niche community that desperately needed one.

The Problem
If you have never applied to medical school, the scope of the tracking problem is hard to appreciate. A typical applicant manages the following simultaneously:
Pre-meds are some of the most organized people I know, and they were still drowning in spreadsheets. The problem was not discipline. It was the absence of a tool designed for the specific workflow.
Technical Stack
I chose the stack based on two constraints: I was the only developer, and I needed to ship fast. Every technology choice was filtered through the question: does this let me move faster without creating debt I cannot pay back later?
The Stack
| Layer | Technology | Why |
|---|---|---|
| Framework | Next.js (App Router) | Server components, API routes, and static generation in one framework |
| Database | PostgreSQL + Prisma | Relational data with type-safe queries and zero migration headaches |
| Auth | NextAuth.js | Google OAuth for frictionless sign-up, session management handled |
| Hosting | Vercel + Supabase | Edge deployment, managed Postgres, no DevOps overhead |
| Styling | Tailwind CSS | Rapid iteration on UI without context-switching to CSS files |
The decision to use PostgreSQL over a NoSQL option was critical. Application tracking is inherently relational. A school has many secondaries. A user has many schools. An activity has many hours entries. Trying to model this in a document store would have created join nightmares within the first month.
Prisma gave me type-safe database queries that caught schema mismatches at compile time. When you are the only developer and there is no code review, the compiler is your code reviewer.
Core Features
PreMeder has three pillars. Each one addresses a specific pain point that I experienced personally and validated with dozens of conversations in pre-med communities.
Application Tracker
The tracker models the full application lifecycle as a state machine. Each school-application pair moves through defined states: not started, primary submitted, secondary received, secondary submitted, under review, interview invited, interview completed, and finally one of accepted, waitlisted, or rejected. Transitions are validated server-side to prevent impossible state jumps.
// Simplified state machine for application status
const VALID_TRANSITIONS: Record<AppStatus, AppStatus[]> = {
NOT_STARTED: ['PRIMARY_SUBMITTED'],
PRIMARY_SUBMITTED: ['SECONDARY_RECEIVED'],
SECONDARY_RECEIVED: ['SECONDARY_SUBMITTED'],
SECONDARY_SUBMITTED:['UNDER_REVIEW'],
UNDER_REVIEW: ['INTERVIEW_INVITED', 'WAITLISTED', 'REJECTED'],
INTERVIEW_INVITED: ['INTERVIEW_COMPLETED'],
INTERVIEW_COMPLETED:['ACCEPTED', 'WAITLISTED', 'REJECTED'],
WAITLISTED: ['ACCEPTED', 'REJECTED'],
ACCEPTED: [],
REJECTED: [],
}Hours Logger
The hours logger tracks activities across AMCAS categories with running totals, supervisor information, and per-entry reflections. The hard part was not logging hours. The hard part was making it fast enough that people would actually use it instead of defaulting back to their spreadsheet. Every interaction, adding an entry, editing a total, switching categories, needed to feel instant.
I implemented optimistic updates on the client side. When a user logs hours, the UI updates immediately while the write propagates to the database asynchronously. If the write fails, the UI rolls back. This pattern cut perceived latency from 400ms to under 50ms.
School List Builder
The school list builder aggregates admissions data, average MCAT, GPA ranges, mission statements, and secondary prompts, and lets users build a balanced list of reach, target, and safety schools. The data layer pulls from a curated dataset that I maintain manually, cross-referencing MSAR data with publicly available admissions statistics.
Growing to 12,000 Users
I did not run ads. PreMeder grew entirely through organic distribution in pre-med communities. The growth curve had three phases.
r/premed and in pre-med Discord servers. The initial reception was skeptical, as it should have been. Pre-meds had seen tools come and go. What converted the first users was the hours logger. It solved an immediate, daily pain point. Once someone logged their first week of clinical hours, they were hooked.You do not need a marketing strategy for a niche product. You need a product so good that the niche markets it for you. Every hour I spent on product quality returned more users than any hour I could have spent on promotion.

Technical Challenges
Scaling from a personal tool to 12,000 users surfaced problems I had not anticipated.
Real-time sync across devices. Many users log hours on their phone during a shift and review their application tracker on a laptop later. The data needed to be consistent across devices within seconds. I implemented this with a combination of SWR for stale-while-revalidate caching and WebSocket-based invalidation for critical state changes like application status updates.
Mobile responsiveness under load. The application tracker displays a dense grid of schools and statuses. On a phone screen, this became unusable. I rebuilt the mobile layout as a card-based interface with swipe gestures for status transitions, which actually turned out to be faster than the desktop grid for quick updates.
Data integrity during the cycle. During peak secondary season (July-August), users are making dozens of updates per day. Race conditions between optimistic updates and server-side validation could produce inconsistent states. I added idempotency keys to every mutation and implemented server-side conflict resolution that always favored the most recent user intent.
// Idempotent mutation with conflict resolution
async function updateApplicationStatus(
userId: string,
schoolId: string,
newStatus: AppStatus,
idempotencyKey: string
) {
return await prisma.$transaction(async (tx) => {
const existing = await tx.mutation.findUnique({
where: { idempotencyKey }
});
if (existing) return existing.result; // Already processed
const app = await tx.application.findUnique({
where: { userId_schoolId: { userId, schoolId } }
});
if (!VALID_TRANSITIONS[app.status].includes(newStatus)) {
throw new InvalidTransitionError(app.status, newStatus);
}
return await tx.application.update({
where: { id: app.id },
data: { status: newStatus },
});
});
}What I Learned
Building PreMeder taught me things that no computer science course covers.
Conclusion
PreMeder exists because I was frustrated, and I could code. That is the entire origin story. The technical decisions were important, but the product instinct came from being the user. I did not need to interview customers to understand the problem. I was the customer.
12,000 users later, the most meaningful metric is not the number. It is the messages I get from applicants who say PreMeder helped them stay organized during the most stressful year of their lives. That is worth more than any technical achievement in the stack.
If you belong to a community with an unsolved problem, build the solution. You already have the domain expertise. You already have the distribution channel. The only thing between you and a product people love is the willingness to start.