Design
Routing on the Host header
Every domain in the portfolio points at the same app. Middleware normalises the request's Host header (lower-case, no www., no port) and looks it up in the inventory:
| Host is… | Renders |
|---|---|
| the portfolio's own domain | the catalog: every name for sale, with search and a category filter |
a domain marked for_sale | its lander: price or minimum offer, lease-to-own terms, the offer form |
a domain marked sold or hold | a simple status page |
| anything else | "not listed" |
Adding a domain to the portfolio takes two steps: add it in the admin panel, and point its DNS at the tunnel. The lander appears on the next request. Cloudflare's edge provides HTTPS for each domain, so the app never handles a certificate.
Hosting
The same pattern as everything else here
It runs in docker compose as two containers: the app, published on loopback only, and a cloudflared sidecar that dials out to Cloudflare. As with this site and the Aegis API, nothing on the home network accepts an inbound connection. SQLite lives on a bind mount so it can be backed up as a single file. The data module sits behind a small interface, so it can move to Postgres without touching the pages.
Security
Small surface, on purpose
- The admin only exists on one host.
/adminand the admin API answer only on the portfolio's own domain and on loopback. On any lander domain they return404, so a for-sale name never exposes a login page. - The offer form is the only public write path. It validates the email and amount, caps the message length, has a honeypot field and allows 5 offers per hour per IP.
- Email can't break it. A new offer is stored first, then emailed, with the buyer as
Reply-To. If the mail fails, the offer is still in the inbox and the buyer still gets a success response. - Secrets stay out of the image. A
.dockerignorekeeps.envand the database out of the build. The tunnel token lives in its own git-ignored file.
Bugs found in production
Redirects to 0.0.0.0
After logging in, saving a domain or updating an offer, the browser was sent to https://0.0.0.0:3000/…. The admin routes built their redirects from request.url. Behind the tunnel, that's the address the standalone server is bound to, not the address the visitor used.
◆ Fix
A small seeOther() helper sends a relative Location, so the browser resolves it against the URL it actually used. Behind any proxy, the app can't know its public address unless it's told, so it shouldn't build absolute URLs from what it sees.
▲ And a typo that went on sale
The admin form was "add or update": re-entering an existing domain blanked its price and tagline, and a typo created a stray for-sale listing for a misspelled name. The form is now add-only. An existing domain is rejected with a pointer to its edit page.
Selling
Reaching buyers who aren't looking yet
A lander only reaches people who already type the name. To show up when someone searches for a domain at GoDaddy and its partner registrars, the names are also listed on Afternic, with a written playbook:
- Keep my own nameservers, so the Domainfolio landers stay up. That costs a 25% commission instead of 15%.
- Boost off, because it adds another 5%.
- Lease-to-own terms set per domain on each lander, to match the Afternic terms instead of a fixed 12-month split.
- Close through escrow. The buyer pays escrow, the domain is pushed to their registrar account, escrow releases the money. Never take card payments for a domain directly.
Status
What's next
The visit counts per domain will start to say which names draw traffic, and that's what pricing should follow. A CAPTCHA has a marked slot on the offer form if the honeypot and rate limit stop being enough.
