Trizlink: one React codebase as a web app, an Android…
A link manager that ships to three surfaces from one repository. What actually got shared, what could not be, and the free-tier limits that shaped the data…
Trizlink started as a URL shortener and stopped being one fairly quickly. It now does branded short links, a link-in-bio page, click analytics, custom domains, team workspaces, and a suite of fifty-plus utilities — QR and barcode generators, colour tools, PDF tools, text and SEO and code tools.
The part worth writing about is not the feature list. It is that the same React 19 codebase ships as a web app, a Capacitor Android app, and a Chrome and Firefox extension built on WXT — and how much of that is genuinely shared.
What is shared, honestly
Almost all of it, and that surprised me. The data layer, the domain logic, the validation schemas, the design system and the entire authenticated application are one implementation. The differences are at the edges:
- The shell. The web app has a header and a footer; the Android app has native back-button handling and safe-area insets; the extension has a popup with a fixed size and no routing to speak of.
- Storage. Three different persistence surfaces behind one interface, so nothing above it knows which one it is on.
- Auth. This is the one that genuinely cannot be shared, and it is the subject of half of this post.
The extension is a different product wearing the same clothes
Manifest V3 forbids remotely-hosted code. Not discourages — forbids, and enforcement is a rejected submission rather than a console warning.
That single rule removes: the Firebase Auth web SDK, Google Analytics, any CDN script, any analytics library that fetches its own payload, and eval in every form including the ones a bundler generates if you let it.
So the extension gets a different authentication path — the browser's own identity API for a token — and ownership on the data side is verified against a user-id field on the document rather than against the request's auth context. Everything else is bundled at build time.
The practical lesson: check this on day one. Discovering it at submission means unpicking your auth layer with a rejection already on record, and an extension's identity is permanent once it is published.
The free tier shaped the data model, and that was good for it
The whole product runs on Firebase's free tier plus my own FilesHub platform for uploads. Click analytics is the interesting constraint: every redirect is a write, and every analytics screen wants to aggregate them.
The naive design is to store one document per click and count them when someone opens the dashboard. That works beautifully with two hundred clicks and exhausts a daily read quota with fifty thousand — which is a thing I have watched happen on a different product of mine, where one unbounded list query took production down for a day until the quota reset.
So the aggregates are maintained as the clicks arrive rather than computed on read, and the dashboard reads a handful of summary documents instead of a collection. It is what you would do anyway at scale; the free tier just makes "anyway" arrive on day one.
Every list read is paginated, twenty by default and fifty at most, and the database does the counting. Not a guideline — a helper function whose limit is a required argument.
Custom domains are where the real complexity lives
Nothing about a short link is hard. Letting somebody point their own domain at your service is hard, and it is entirely operational rather than algorithmic: verification records, certificate issuance, the window where DNS has propagated for some resolvers and not others, and what the product shows during it.
The thing I would tell anyone building this: the states are not "working" and "broken". They are unverified, verified but no certificate, certificate issued but DNS still propagating, and live — and a user staring at a domain that is in state three needs to be told that specifically, or they will delete it and start again.
The link-in-bio page is a different product with different rules
Worth separating, because it is the part that behaves least like the rest. Short links are a redirect: no rendering, no SEO, no design, just speed. A link-in-bio page is a public landing page somebody puts their name on, and it inherits every concern the redirect does not have.
It has to load fast on a phone on mobile data, because that is where every single visit comes from — a social profile on a phone. It has to be themeable enough that people feel it is theirs, without becoming a page builder. And it has to be crawlable, because a person's link page is frequently the most-linked page they own.
The tension is that the two halves want opposite architectures. The redirect wants to be as close to nothing as possible. The landing page wants prerendered HTML with real content in it. Building them as one thing meant the redirect carried weight it had no use for, and the fix was to stop pretending they were the same feature.
What I would build differently, in one sentence each
- Extension first, because every constraint it imposes is one the other surfaces can live with, and discovering them last meant the shared layer had already assumed otherwise.
- Analytics aggregated on write from day one, rather than computing on read and retrofitting it once the numbers got real.
- The utilities behind a lazy boundary, so somebody who came to shorten one link does not download fifty tools first.
- Custom-domain states named explicitly in the interface, because "not working yet" and "propagating" look identical to a user and only one of them is worth waiting through.
What I would do differently
I would build the extension first. It is the most constrained surface, and every constraint it imposes — no remote code, a tiny bundle, no routing — is one the other two can live with comfortably. Building it last meant discovering those constraints after the shared layer had already assumed otherwise.
And I would put the utilities behind a lazily loaded boundary from the start rather than retrofitting it. Fifty tools is a lot of code for someone who came to shorten one link.
https://aoneahsan.com/blog/case-study-trizlink