Most founders find out which features matter by asking users directly. That works until you have 50 accounts and genuinely do not know if anyone has clicked the export button you spent three days building. You could run a survey, or you could just know.
Amplitude solves this, but it starts at $0 and quickly climbs past $200 a month once you have real traffic and want real answers. Mixpanel is similar. These tools are worth the cost at scale. Before you reach scale, you are paying for enterprise reporting when what you actually need is "did users reach step 3 of onboarding?"
Here is how to build that without a third-party tool.
A metrics dashboard (MRR, churn, signups) tells you the health of your business. Product analytics tells you what users do inside your product -- which features they use, where they get stuck, and whether they complete key actions like inviting a teammate or upgrading to a paid plan.
The output is not a single number. It is answers to questions like:
You do not need Amplitude for this. You need one table and one function.
In your schema file, add:
export const eventTable = pgTable('events', {
id: uuid('id').defaultRandom().primaryKey(),
userId: uuid('user_id').references(() => userTable.id, { onDelete: 'set null' }),
name: varchar('name', { length: 100 }).notNull(),
properties: jsonb('properties').default({}),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
name is the event name -- feature_used, onboarding_completed, export_clicked. properties holds any extra context as JSON: plan name, resource type, count, whatever helps you answer the question later.
Run npm run db:migrate to apply it.
Add a trackEvent function in your service layer:
export async function trackEvent(
userId: string | null,
name: string,
properties?: Record<string, unknown>
) {
await db.insert(eventTable).values({ userId, name, properties });
}
Call it from your services, never from API routes. If a user exports a CSV, the export service calls trackEvent(userId, 'csv_exported', { recordCount: rows.length }). That is the whole integration.
You do not need a dashboard to get value from this. A few queries cover most early questions.
Feature adoption -- how many unique users have ever triggered a specific action:
SELECT COUNT(DISTINCT user_id) FROM events WHERE name = 'csv_exported';
Onboarding funnel -- what percentage of users who completed onboarding also invited a teammate:
SELECT
COUNT(DISTINCT e1.user_id) AS completed_onboarding,
COUNT(DISTINCT e2.user_id) AS also_invited
FROM events e1
LEFT JOIN events e2
ON e1.user_id = e2.user_id AND e2.name = 'teammate_invited'
WHERE e1.name = 'onboarding_completed';
Run these in Drizzle Studio (npm run db:studio) or surface them in your admin panel as saved queries. No charting library required to find the answers -- the numbers tell you what to do.
This setup has limits. Once you track hundreds of event types across tens of thousands of users, querying raw events slows down and you need indexes. Complex cohort analysis -- users who did X then Y within 7 days -- is possible but verbose to write.
Those are good problems to have. They mean you have enough users that a real analytics tool is worth the cost. For now, the cost of this approach is one migration, one function, and the habit of calling it from your services.
Event data becomes more useful alongside the business numbers you already track. The SaaS metrics dashboard shows MRR, churn, and LTV -- add event queries to it and you can see whether churned users ever used your core feature at all. If they did not, that is a retention problem. If they did, that is a pricing problem. That distinction shapes everything you build next.
You can also layer this on top of A/B testing: run a test on your onboarding flow, then use event tracking to measure not just conversion but what converted users actually do in week two. That combination -- experiment then observe -- tells you whether a change is genuinely better or just produces a different kind of disengaged user.
Next.js SaaS Boilerplate includes Drizzle ORM with Neon DB wired up and ready to extend. Add the events table, drop in the tracking call, and you go from "I think users like this" to "I know users use this" -- before you spend another week building a feature no one will click. Get the boilerplate and start shipping with data this weekend.