I built my own here.now

✦ 5 min read

My agents publish things all day: a prototype, a figure for review, a report I want to open on my phone. That used to mean here.now. I say "publish this", the agent uploads a folder, and I get a link. I wanted the same thing on my own domain, in my own storage. here.now's agent skill is open source, but its server isn't, and there's no way to bring your own infra. So I had an agent build one. It's called ownnow, and it's open source now.

Why

Ownership. Every link I shared lived on someone else's domain, in someone else's bucket. If here.now changed its pricing or went away, two dozen links went with it.

Cost I could see. I already pay for a server. Files on Cloudflare R2 have no egress fees. I didn't want a third bill for serving static files.

Exploring. I wanted to try a stack I don't write every day, so I asked for Go or Rust, and Bun. The agent picked Go for the server and Bun for a Cloudflare Worker, and skipped Rust: the Worker was about 120 lines of fetch logic, where Rust buys nothing.

How a publish works

ownnow keeps here.now's flow because it's a good one. A publish is three calls:

  1. Create. The server reserves a slug and hands back one signed upload URL per file.
  2. Upload. With R2 or any S3 storage, the files go straight there over those URLs and never pass through the server. On local disk, the server takes them itself.
  3. Finalize. One atomic switch makes the new version live.
With R2, the files skip the server. Finalize flips a pointer, so a rollback is the same flip back.

Visitors never see a half-uploaded site, because nothing changes until finalize succeeds. Old versions stay in storage, so a rollback is a pointer flip with no re-upload.

The server is one static Go binary with no dependencies outside the standard library. Even the S3 request signing for R2 is hand-rolled. It also handles passwords, expiring sites, SPA fallback, custom 404 pages and Wasm headers.

Where it runs

This part took three tries.

Plan A: all Cloudflare. A Worker serves {slug}.harssha.design straight from R2 at the edge, for free. But a Worker can only claim a hostname in a zone on Cloudflare, and my domain's DNS lives at Vercel. Moving it meant moving the DNS for my portfolio too.

Plan B: Vercel. The agent made the case well: Vercel already runs my DNS, so adding a domain is one command, and Go runs as a function. It even created the project. Then I remembered who pays for serving the files: every byte served through a function counts against Vercel's bandwidth. I wasn't going to pay Vercel egress on files I'd stored in R2 to avoid egress. We deleted the project.

What shipped. DNS stays at Vercel with one new A record, now, pointing at my VPS. On the server, Caddy handles TLS and passes requests to the Go binary, which reads from R2. Uploads go straight to R2, R2 doesn't charge egress, and serving runs on the bandwidth my server already includes. The Worker is still in the repo for anyone whose DNS is on Cloudflare.

What it costs. No edge cache, for now. Every request makes a round trip through my server to R2. At my scale I don't notice.

Publishing without handling a key

Publishing needs one key, a bearer token I generated myself. It lives in the Infisical instance on my server, and each machine already has a read-only identity for that project. The ownnow skill fetches the key when it needs it, so an agent on my Mac or the server can publish without me pasting anything:

python3 ~/.agents/skills/ownnow/scripts/publish.py ./site --slug demo

The agent tested it from both machines, then moved the old here.now skill into an archive folder on each. No agent of mine reaches for here.now anymore.

Moving 24 sites

My here.now account had 24 live sites: 299 files, about 578 MB. The agent wrote a resumable script that downloads each site and republishes it through the same three calls, keeping every slug. One site crashed it on a file with no content type. It fixed that, resumed, and skipped the 23 that were done.

The old {slug}.here.now links don't redirect. Anything I'd shared before needs the new link.

Bring your own infra

Once it worked for me, the obvious question was the one I'd started with: why can't anyone else run this? So we made the repo public under MIT, removed my domain, IP and Infisical details from the code, and squashed the history so the personal version never went public.

There are three ways to run it, from the same code:

  • Docker anywhere. Clone it, set your domain and key in .env, docker compose up, and point one DNS record at the server. Caddy gets the certificate.
  • Cloudflare. A Worker serves R2 at the edge with no egress fees. The Go binary only publishes, and can run on a laptop.
  • One binary. Download a release, run it, and publish to localhost on local disk.
Same code, three ways to run it. You bring the domain and the storage.

Auth is deliberately boring: one key you generate. No accounts, no telemetry, nothing calls home. Release v0.1.0 ships binaries for Linux, macOS and Windows, and ownnow.dev explains it in one page.

What doesn't work yet

  • No edge cache on the VPS setup. Large files stream through one server.
  • No rate limiting on the publish API. Fine with one key, but needed before I share access.
  • Less than here.now. It has no email allowlists, analytics, teams or custom domains per site. ownnow covers publishing, versions and rollback, plus the basics above.

Directing it

The agent wrote nearly all of the code. My job was the decisions: no Vercel egress, DNS stays where it is, reuse the read-only identity I already had, make it public. Each of those changed the architecture more than any line of Go did.

Where it struggled was taste. The demo GIF for the README took five rounds, mostly because I said "the demo site is wrong" and it heard "the GIF is wrong". The fix was to be exact about which thing I meant.

If you run ownnow on your own infra and something breaks, tell me on X or open an issue on GitHub.