UTM Dashboard: Centralizing Marketing Link Management

Software Engineer
Next.js, React, TypeScript, Tailwindcss, PostgreSQL
Marketing Team, Project Manager
Private (Internal Tool)

(opens in new window)Behind The Project

Managing marketing campaigns across multiple channels is messy. You’re juggling dozens of links, each with different UTM (Urchin Tracking Module) parameters for tracking. Someone on the team forgets to add utm_source, another person misspells utm_campaign, and suddenly your analytics are unreliable.

Utm Dashboard Ga Example

At justfont, campaign links were scattered across Discord threads, Google Docs, Notion, and personal spreadsheets, with no consistency in naming conventions and no single place to find what already existed.

The marketing team was building UTM links by hand for every campaign, and typos were common. Parameters like utm_source=fb and utm_source=facebook would fragment the same traffic data in analytics, making it impossible to accurately measure channel performance. When a campaign needed a short link for social sharing, that was a separate manual step.

The result was marketing data the team couldn’t fully trust.

(opens in new window)Solution & Approach

I worked directly with the marketing team to understand their day-to-day workflow before designing anything. The goal was clear: a single source of truth for all campaign links, with enforced naming conventions, automatic URL shortening, and team-wide access through Discord authentication.

Utm Dashboard List

The core interface is a searchable, sortable data table showing all campaign links. Each entry stores the UTM parameters, the original URL, the auto-generated short link, and internal notes. Multi-column filtering lets teams narrow by traffic source, marketing medium, campaign name, or custom tag simultaneously. Table state syncs to URL search parameters, so filtered views can be bookmarked or shared with teammates directly.

Link creation enforces standardization at the point of entry. Traffic sources and mediums come from predefined dropdown lists, eliminating the possibility of variant spellings like facebook, fb, and Facebook that would fragment analytics data. Frequently-used source/medium combinations can be saved as presets.

Utm Dashboard Dialog Source

Choosing a preset fills the entire form in one click, cutting the time needed for common campaign types significantly. When a link is saved, the system automatically calls justfont’s internal URL shortening API to generate a compact ref.justfont.com/... URL alongside the full UTM-tagged version.

The dashboard also generates QR codes for any campaign link, with options for custom colors, embedded logos, and adjustable error correction levels. The marketing team uses these for print materials and social posts where a scannable code is more practical than a typed URL.

Utm Dashboard Detail Dialog

(opens in new window)Technical Challenges

(opens in new window)Type Safety Across the Full Stack

In a project touching form inputs, API routes, and database queries, type consistency tends to break down at each boundary. A field that’s required in the form might slip through as undefined at the API layer, causing silent data issues that are hard to trace. I define one schema per entity, then derive TypeScript types, form validation rules, and API request validation from that same schema. The frontend form validates user input against the Zod schema. API routes validate incoming requests against the same schema before anything touches the database. Drizzle ORM table definitions mirror the schema structure. Any mismatch becomes a TypeScript compile error rather than a runtime bug.

This pattern caught dozens of data integrity issues during development. When I added a new required field to the link model, TypeScript immediately surfaced every place in the codebase that needed updating, making the refactor safe rather than risky.

This approach proved its value again when I migrated from MongoDB to PostgreSQL several months into the project. I initially chose MongoDB for its flexible schema during early prototyping, but as the data structure stabilized, PostgreSQL’s relational model became a better fit for the fixed schema. Because all validation logic lived in Zod schemas rather than database-specific code, the migration was straightforward. I updated the Drizzle ORM table definitions and data layer, but the schemas, types, and validation logic required no changes.

Utm Dashboard Preset

(opens in new window)Discord Authentication with Role Verification

Discord OAuth handles authentication. Team members sign in with accounts they already have, and role verification happens through the Discord OAuth. Only users with the appropriate Discord server roles can access the dashboard. No separate account system to build or maintain; access management lives entirely in Discord.

Implementing Discord authentication required understanding the distinction between two separate authorization mechanisms. OAuth credentials prove who the user is, while a Discord Bot Token allows the server to query guild member roles. These serve different purposes and both are necessary.

The flow works like this: a user authenticates via Discord OAuth, establishing their identity. The server then uses a Bot Token to call the Discord API and fetch that user’s roles in the justfont server. If they hold the required role, a session is created; if not, access is denied. Adding or removing someone’s access becomes as simple as adjusting their Discord role, with no application changes needed.

The naive approach, loading all links and filtering on the client, would slow down with every new campaign the team created. After a few months of active use, fetching the full table on every page load would become noticeably sluggish.

I moved filtering and pagination entirely to the server. The API accepts filter parameters (source, medium, campaign, tag) and constructs targeted database queries using only the active criteria. Indexes on the most commonly filtered columns keep query times fast even as the table grows. The table displays one page of results at a time with the total count shown, so teams can understand the full scope without loading everything at once.

Table state syncs to URL search parameters rather than React component state. This makes filtered views persistent across page refreshes, shareable as direct links, and recoverable after navigation. A user can bookmark all instagram links from the spring campaign and return to exactly that view without reconstructing the filters manually.

Utm Dashboard Filter

(opens in new window)Impact & Results

Since launching in September 2024, the UTM Dashboard has become the standard tool for campaign link management at justfont. The marketing team uses it daily to create links, generate QR codes for print materials, and organize campaigns by tag.

The most significant improvement was eliminating UTM naming inconsistencies. With predefined source and medium lists, the fragmented analytics data that came from variant spellings (fb vs. facebook vs. Facebook) is no longer a problem. Campaign tracking is now reliable enough to inform real decisions about channel performance, something that wasn’t possible before.

The preset combinations feature turned out to be the highest-impact addition. For standard campaign types, link creation dropped from around 5 minutes to under 30 seconds, a change the team noticed immediately and cited consistently in feedback. Combined with automatic URL shortening, what used to be a multi-step manual process became a single form submission.

(opens in new window)Lessons Learned

The dashboard became the team’s first stop when starting any new campaign, not because they were required to use it, but because it was faster and less error-prone than any alternative they’d tried.

Building this tool clarified something I’ve come to apply across all internal projects: the most valuable features are almost always the ones that remove a specific, recurring friction point from someone’s daily routine. Features like preset combinations, enforced naming conventions, and shareable filtered views aren’t technically impressive, but each one saved real time for real people, every single day.