Earlier this year I set out to build a SaaS platform for domain name research — a tool that helps investors find newly registered domains and expiring auctions worth buying, backed by real business signals instead of gut feeling. Four months and a hundred-plus commits later, it's live. Here's what the journey actually looked like.
What the platform does
The core idea: a domain name is more valuable if real companies are already using that name. So the platform cross-references millions of domain candidates against company datasets — LinkedIn company data, Crunchbase, funding records — and surfaces metrics like "how many companies use this keyword" and "does the .com resolve to a funded startup." Users can filter new registrations and live auctions by dozens of these signals, save filter sets, and even schedule automatic bids on auctions.
Tech stack: React 19 + Vite on the frontend (deployed to Azure Static Web Apps), ASP.NET Core 8 API on the backend, SQL Server for the data.
Lesson 1: At 13 million rows, every query is a performance problem
My biggest recurring theme was SQL performance. The candidates table holds over 13 million rows, and innocent-looking filters would take tens of seconds. Some things that saved me:Denormalize aggressively. Joining a 13M-row table against per-keyword metrics on every page load was never going to work. Copying the hot metric columns onto the candidate rows (updated in batch) turned multi-second queries into instant ones.
Filtered indexes have sharp edges. I had a filtered index that looked perfect but the optimizer couldn't use it for a scan pattern in a sync procedure — that one anti-join was reading 13 GB off the clustered index. Adding a plain non-filtered index cut reads by 10x.
Cap your counts. "Showing 1 of 4,381,022 results" is a nice flex, but computing that exact count on every filter change is expensive. Capping the count query made the filter UI feel instant.
Lesson 2: Third-party data is always staler than you think
The auction feature schedules bids near an auction's end time. Sounds simple — until a user's bid gets rejected with "auction already ended" on an auction that's clearly still live. The cause: the end times I'd cached from the auction provider had drifted. Auctions get extended, removed, and rescheduled constantly.
The fix was layered: re-check the live end time from the provider's API at the moment a bid is scheduled, run a recurring background job to refresh end times for anything with a pending bid, and track an explicit auction status so removed auctions don't masquerade as active ones. The lesson generalizes: any cached copy of someone else's real-time data needs a freshness strategy, not just a sync job.
Related sub-lesson: store everything in UTC, convert at the edges, and let users pick an IANA timezone (falling back to the browser's). I've yet to regret a UTC decision; I've regretted every alternative.
Lesson 3: Background jobs beat polling
Auto-bidding started as scheduled tasks checking "is it time to bid yet?" — wasteful and imprecise. Moving to Hangfire with event-driven, per-auction scheduled jobs made bids fire at exactly the right moment and made the whole thing observable: each auction stores its job ID, so I can see, cancel, or reschedule any pending bid.
Lesson 4: The cloud will hurt you in ways the docs don't mention
Two incidents worth sharing:Azure Static Web Apps just... stopped accepting deployments. Server-side rejections with no useful error, for days. I ended up building a manual deploy recipe as a workaround. Sometimes the fix is a runbook, not a root cause.
ASP.NET Core's DataProtection key ring silently regenerated on App Service Linux, which meant every encrypted secret in the database (users' API credentials for the bidding integrations) became undecryptable overnight. The default key storage is ephemeral in containers. Persisting keys to the database via PersistKeysToDbContext fixed it — but only after users had to re-enter credentials. If you encrypt anything with DataProtection, decide where the keys live before production.
Lesson 5: Multi-tenancy is a hundred small decisions
Turning an internal tool into a licensable product meant feature entitlements: internal users see everything, external users see what their plan includes, and premium data sources get gated field-by-field in API responses — not just hidden in the UI. Doing this properly took eight phases of work and touched nearly every endpoint. My advice: design the entitlement model early, even if you launch with one plan. Retrofitting gating onto finished endpoints is slow, careful work.
What's next
Subscription billing with Stripe, and a data pipeline to generate my own TLD zone statistics from ICANN zone files instead of depending on third-party counts. But that's the next post.
No comments:
Post a Comment