# s12.in: I shipped a short domain that tracks clicks and hosts files

Source: https://www.michaelchurley.com/blog/s12-in-url-shortener-file-hosting  
Published: 2026-08-08  
Author: Michael C. Hurley  
Tags: s12.in, url-shortener, file-hosting, convex, clerk, nextjs, analytics, vercel, chrome-extension, martech

![s12.in landing. Short links, powerful results](/blog/s12-in-url-shortener-file-hosting/screenshots/landing.png)

## Who

I got tired of leasing short links from tools that treat a redirect like a subscription upsell.

Operators (marketers, founders, agencies, anyone pasting campaign URLs into SMS and decks) need a domain they own, a dashboard that lists what they created, and click facts that survive the redirect. Developers already on Convex and Clerk do not need a fifth SaaS login for "paste URL, get code."

s12.in is for that seat: MarTech folks, Next.js-on-Vercel shippers, and anyone who wants `s12.in/abc` instead of a twenty-character tracking URL.

## What

I shipped a URL shortener and file host at [s12.in](https://s12.in).

Stack facts: **Next.js 16.1.6** (App Router, Turbopack) on Vercel, **React 19.2**, **Convex** for links/files/clicks/users plus `_storage` uploads, **Clerk** for auth, **Tailwind CSS v4**, Radix primitives, Resend in the lockfile, Biome + oxlint + tsgo. Package name `s12`, version **0.1.0**, packageManager **bun@1.3.1**. Repo is **private** under HurleyUS.

![Dashboard. Shorten, upload, manage](/blog/s12-in-url-shortener-file-hosting/screenshots/dashboard.png)

Paste a long URL on the homepage or dashboard. Convex `links.create` issues a short code (optional `customCode` / `password` / `expiresAt` exist on the schema; the public form only sends `url` + optional `userId`). `app/[code]/route.ts` resolves the code, parses user-agent, reads Vercel geo headers, hashes the IP with SHA-256 + a salt (16 hex chars), fires `recordClick` without blocking, then **302**s. Files take the same path: viewable MIME types redirect to the Convex storage URL; others return an attachment stream.

Dashboard tabs list your links and files with copy/delete. Click and download counters are denormalized on the row. `getAnalytics` can roll up by day/country/browser/device/referrer. Charts are not on the dashboard yet.

A Manifest V3 Chrome extension scaffold lives under `extension/` and posts to `/api/shorten` with CORS. Icon PNGs are still marked `ICONS_NEEDED.md` (SVG only in tree).

Honesty operators can smell: the hero still prints **10M+ / 50K+ / 99.9%** as static JSX. The live Clerk publishable key I hit was **pk_test** on `*.clerk.accounts.dev`. README still names PostHog and Sentry; those packages were removed Feb 21. Footer GitHub still points at `michaelmonetized/s12.in` while the working private remote is **HurleyUS/s12.in**. Schema `users.plan` free|pro|team is ahead of any billing UI.

![Redirect pipeline](/blog/s12-in-url-shortener-file-hosting/screenshots/redirect-pipeline.png)

## Where

Product: [s12.in](https://s12.in) (apex redirects toward www). Vercel project alias on the GitHub homepage field: [s12-in.vercel.app](https://s12-in.vercel.app). Routes that matter: `/`, `/dashboard`, `/sign-in`, `/sign-up`, `/privacy`, `/terms`, `/refunds`, `/api/shorten`, `/[code]`.

Code: [github.com/HurleyUS/s12.in](https://github.com/HurleyUS/s12.in). Private, no topics, no license file, no tagged releases. Deploy path is Vercel continuous with a Blacksmith `ship.yml` gate. Local path: `bun install`, `bunx convex dev`, `bun dev`.

Audience sits next to every "just use Bitly" thread and every Convex starter that never grew a redirect route.

![Chrome extension scaffold](/blog/s12-in-url-shortener-file-hosting/screenshots/extension.png)

## When

**2026-01-08.** Initial commit: Next.js, Convex, Tailwind. Same day: OPPORTUNITIES.md and homepage improvements; PLAN.md improvement list. Early docs still daydream about a CDN. The product that shipped is the shortener + files.

**2026-01-31 to 02-05.** Sync. Tailwind v4 `@import`. Prod build ready.

**2026-02-06.** The spine. Core URL shortening. Upgrade to Next.js 16 and React 19; middleware renamed toward `proxy.ts` (and briefly back during Next's naming war). Convex backend configured and deployed. Clerk auth keys landed.

**2026-02-11.** Dashboard UI improvements: links/files mental model.

**2026-02-15–21.** Docs say `proxy.ts`. Footer gets real social URLs. Unused `posthog-js` and `@sentry/nextjs` leave the lockfile.

**2026-02-27.** Security headers in `next.config.ts`. `sitemap.ts` / `robots.ts`. Error boundaries. Auth checks on links and files mutations.

**2026-03-23.** PR #23 strips `console.error` noise.

**2026-05-14–15.** Blacksmith CI standardized. Providers tolerate missing Clerk keys so builds do not die. Deploy URL / health-check fixes.

**2026-08-08.** HEAD `d6c074f`. `vercel.json` sets `X-Robots-Tag: index, follow`. Thirty-eight commits on the clock.

## Why

A short link you do not control is a tax with a dashboard skin. I wanted the domain, the Convex tables, and the redirect that writes analytics before the visitor leaves, on the same Clerk identity I already use everywhere else.

Shortening and file hosting landed on `s12.in`. Clicks record geo and device fields. Shipping continued until CI and robots headers stopped being the embarrassment.

It is still 0.1.0. Custom codes, password gates, plan limits, and analytics charts sit in schema or queries waiting for UI. Clerk on the observed deploy is still test-mode. That is fine to say out loud. The redirect path and the dashboard list already do the job a rented shortener charges monthly for.

What would you put on `s12.in/yourcode` first: the campaign URL, the PDF, or the deck you keep resending as a thirty-line Google Drive link?
