11:43 PM. Two engineers. Two laptops. One shared frustration that finally ran out of patience.
Ankit messaged me at 11:47 PM on a Tuesday. Not "hey, what's up" — just a screenshot. A 404 page. Our internal deployment link, the one we'd been sharing across four Slack threads and two Confluence pages for the past month, had vanished. The GoLinks tool our team had been quietly depending on since 2022 had finally died, and nobody had noticed until that exact moment, when Ankit needed it most.
He followed up with three words: "I'm done, yaar."
I laughed. Then I thought about it. Then I stopped laughing.
Because it wasn't just the dead link. It was everything. The Jira ticket URLs we pasted into Slack that broke when the project got renamed. The Notion pages full of links that pointed to pages that no longer existed. The marketing campaign links that went to a landing page we'd redesigned six months ago — still circulating in emails, still showing up in print materials, still redirecting to a page with the old product name. Two years of accumulated link debt, and all of it invisible until the moment it wasn't.
We talked until 1 AM that night — not about the broken link, but about what a proper solution would actually look like. By the time I closed my laptop, we had a name scribbled in my notes app: SmartURL. And a completely unrealistic timeline.
// 01 The Problem Ankit and I Couldn't Stop Complaining About
Here's what's funny about working in tech: we build incredibly sophisticated systems for our users, and then we manage our own internal workflows with a shared Google Sheet and good intentions. The link problem is a perfect example of this. Every engineer I've talked to has their own version of the same story.
Ankit was working on a healthcare product at the time. Their team had a Jira board with 300+ active tickets. Their PM would share ticket URLs in standup. Those URLs were 90 characters long. Half the time the project key had changed and the URL was dead. The workaround was a Notion page maintained by whoever cared enough that week — which meant it was maintained by nobody, consistently.
I was dealing with a different flavour of the same rot. At my day job, marketing was sending branded campaign links through Bit.ly. Every click event, every referrer, every geo-location — all of it living on Bit.ly's servers, outside our control, outside our analytics stack. When leadership wanted to know "how many engineers from Germany clicked the product launch link on mobile?" — the answer was: call Bit.ly's customer support, maybe.
The link is never just a link. It's a contract between the person who created it and every person who will ever click it. Break that contract once, and you've lost something harder to get back than uptime: trust.
What we needed wasn't a URL shortener. What we needed was a routing platform — something that
could handle internal developer shortcuts like go/deploy and go/staging, marketing
campaign links with real analytics, and pattern-based dynamic routing like b/4821 pointing to Jira
ticket 4821, all under one roof, all under our own domain, with no third party owning our data.
The combination simply didn't exist at any quality level. Every company was reinventing this, badly, in a spreadsheet. So we decided to build it properly.
Two tools solving the same problem for different teams — no shared data, no unified view, no ownership.
// 02 Why We Chose Go — and Why That Decision Almost Broke Us
Ankit and I are both JavaScript engineers by trade. Node, React, TypeScript — that's home. Choosing Go for SmartURL was a deliberate bet, and it made the first three weeks genuinely painful.
But the architecture demanded it. The core requirement we'd written down on night one was non-negotiable: the redirect must never wait for the analytics. When someone clicks a short link, they expect to land somewhere in milliseconds. They don't care about Kafka, or Redis, or database writes. They care about speed. And if our analytics pipeline hiccupped, none of that could touch the redirect response time.
Go's goroutines made this clean. A click event fires into a buffered channel — capacity 10,000 — and the redirect is already done. That channel feeds a Kafka producer goroutine that batches events in groups of 500 or every 500 milliseconds, whichever comes first. A separate consumer enriches each event with geo-location and device data, then batch-inserts into Postgres. The redirect never sees any of this. It's fire-and-forget by design, not by accident.
The redirect is done in under 100ms. The analytics pipeline never touches that clock.
The stat we designed toward was brutal:
We're not serving millions of requests yet. But the architecture already supports it. Ankit called this "building a runway before you know what plane is landing." I think that's exactly right.
// 03 The Postgres Migration That Nearly Destroyed Our Weekend
Four weeks into the build, on a Saturday afternoon, I pushed what I thought was a simple schema change. I needed to add a new role value to a PostgreSQL enum type. I added the migration, ran it, wrote a follow-up migration that referenced the new value, and watched the whole thing explode in a way I didn't understand at all.
Two hours later, Ankit found a GitHub issue from 2019 buried in the golang-migrate repo. That was
the moment everything clicked.
golang-migrate wraps every .sql file in a BEGIN/COMMIT
transaction. PostgreSQL does not allow a statement that uses a newly added enum value in the same
transaction as the ALTER TYPE ... ADD VALUE that created it. The solution: put
ADD VALUE alone in migration N. Put any UPDATE or INSERT referencing it
in migration N+1. Two files. Two transactions. Problem gone. This is now carved into the SmartURL contributor
guide.
It sounds minor. But it forced a discipline that made everything else better. We wrote a rule: never edit an already-applied migration. Schema changes are additive only. Breaking changes use expand-contract — add the new column, backfill, update the code, then remove the old column in a future migration. Production databases stay alive while the world changes around them. This is how you earn the right to sleep at night.
// 04 The Feature That Turned a Tool into a Platform
Static short links were straightforward. What made SmartURL genuinely different from everything else we'd tried was the routing rule engine — and it was Ankit's idea.
He framed it like this: "What if I never have to create a link for a Jira ticket again? What if I just create
one rule — /b/{id} maps to Jira ticket {id} — and then go/b/4821 just
works forever?" That's GoLinks functionality. That's the thing engineers type from muscle memory without
thinking. And we built it.
Three rules. Infinite links. One rule for all Jira tickets, one for all search queries, one for all pull requests.
The engine supports four match types. Exact matching for specific slugs. Prefix matching where
/b/{id} captures any segment after /b/ and injects it into the destination template.
Query passthrough for search-style redirects. And regex matching for complex captures. Rules are evaluated in
priority order, and a static link always wins over a routing rule for the same slug — no ambiguity, no
surprises.
One detail I'm genuinely proud of: if you try to create a static link whose slug collides with an active
routing rule, you get a 409 Conflict that explicitly names the conflicting rule. The system
protects its own integrity. You can't accidentally break your routing rules by creating a link with the wrong
name. It sounds obvious in hindsight. It almost didn't make it into v1.0.
// 05 Multi-Tenancy: The Problem Nobody Teaches You in Tutorials
From the beginning, Ankit and I wanted SmartURL to be something other teams could deploy for themselves. That meant multi-tenancy — and that meant solving a problem that every tutorial glosses over: how do you make data isolation impossible to forget, not just easy to remember?
The naive approach is to add WHERE tenant_id = ? to your queries and trust developers. They won't
always remember. Not under deadline pressure at 11 PM. One missed clause and tenant A reads tenant B's data. In
a URL management platform where links might be sensitive, proprietary, or internal — that's not acceptable.
Our solution is structural. Every authenticated request has its tenant_id injected into the Go
context by middleware. Every store function — every single database access function — receives
tenant_id as a required parameter. You cannot call the store without providing it. The Go compiler
enforces this. Not code review. Not the developer's memory. The compiler.
// Every store function signature requires tenantID func (s *LinkStore) GetBySlug( ctx context.Context, tenantID uuid.UUID, // required — compiler won't let you skip it slug string, ) (*model.Link, error) { return s.db.QueryRow(ctx, `SELECT * FROM links WHERE tenant_id = $1 AND slug = $2 AND is_deleted = false`, tenantID, slug, ) }
When correctness matters more than cleverness, clarity wins every time.
// 06 What Testing Looks Like When You Actually Care
Ankit and I both had scar tissue from past projects where testing was an afterthought — a coverage badge on a README that nobody trusted. SmartURL was where we decided to be honest with ourselves about what testing actually means.
It's three phases. Phase one is systematic manual verification via curl — a complete playbook that walks every user journey from tenant registration through live redirects, including the edge cases that usually only get discovered in production. What happens when you redirect a soft-deleted link? What about a slug containing pattern-injection characters? What about recreating a deleted slug with the same name?
Phase 1.1 is live. Unit tests cover handlers, stores, and services. Phase 1.5 E2E is next.
Phase two — now shipped — is unit tests using Go's testing package, testify for
assertions, and pgxmock for database mocking. All test files live in a root /test
directory, completely separate from production code. Tests are written as external test packages to verify the
public API only — no accidental coupling to implementation details you shouldn't depend on.
The target is 80% coverage on the right 80%. Not 100% — 100% is a vanity metric. 80% on the paths that actually matter catches the bugs that actually hurt.
// 07 Three Things I'd Tell Myself on Night One
Every project teaches you things you couldn't have known at the start. SmartURL gave me three lessons I'll carry into every system I build from here.
Design your error taxonomy before you write your first handler. We built a centralized error
framework with typed categories, machine-readable error codes like AUTH_001 and
BIZ_021, and matching operation codes for success paths. It sounds like over-engineering until the
day a frontend asks "how do I know which toast to show?" and you hand them a contract instead of telling them to
parse a string.
Separate your redirect service from your management API on day one. Not when you need to scale. Day one. They have different performance contracts, different scaling curves, and completely different failure modes. Bundling them is a false economy that costs you exactly when it hurts most.
Document the gap between your PRD and your implementation. The super admin role existed in the code before it existed in the product docs. That's normal — requirements evolve — but undocumented decisions are technical debt of a different kind. They live in someone's head until that person is unavailable at 11 PM on a Tuesday.
The best systems aren't the ones with the most features. They're the ones where every decision has a reason, every boundary has a name, and every failure has a plan.
— Something Ankit said at 1 AM that I immediately wrote down// 08 Where SmartURL Is Now — and Where It's Going
SmartURL v1.0 is live. The redirect engine is running. Auth works. The routing rule engine handles prefix,
exact, query passthrough, and regex patterns. Analytics flows through Kafka without touching the hot path. Every
endpoint is documented in Swagger UI at /docs. You can spin the entire stack locally with
make dev in under three minutes.
v1.5 brings custom branded domains, full RBAC with owner/admin/editor/viewer roles, geo and device analytics, link expiration, and bulk CSV imports. v2.0 is where A/B split routing, retargeting pixel injection, SSO/SAML, and ClickHouse analytics live.
But here's what I keep coming back to: the architecture is already ready for all of it. The foundations are solid. The contracts are documented. The test suite exists. When it's time to build the next layer, the next contributor — or future me, six months from now, exhausted and confused — won't be starting from chaos. They'll be starting from a platform.
Ankit got that on the first night, I think. I got it slowly, across every late night and Saturday debugging session that followed. That's the difference between building a feature and building a system.
And it started because a link broke at 11:43 PM on a Tuesday.
Prakash Mandal is a Lead Experience Engineer with 10+ years building enterprise-scale systems. SmartURL is his personal side project, built with his friend Ankit over late nights and weekends.
github.com/bytecode-agency · linkedin.com/company/bytecode-agency-uk · bytecodeagency.com