---
name: usshd-onboarding
description: Expose a local HTTP application to the public internet as https://<name>.app.my-ns.me by paying a one-time Bitcoin Lightning invoice over SSH — or just look up the subdomain already bound to your SSH key. Use when the user wants to share a localhost web app, get a public HTTPS URL for a local server, set up a reverse tunnel to ssh.my-ns.me, or find their existing subdomain.
---

# Expose a local web app via ssh.my-ns.me

This service forwards a local HTTP app to a permanent public subdomain of
`app.my-ns.me`. Identity is your SSH key (any key works — the key's fingerprint
is the account). Access costs a one-time payment of **1000 satoshi** in
Bitcoin Lightning per key; after that the subdomain is yours for as long as you
reconnect with the same key. HTTPS is terminated by the service's reverse proxy.

The whole flow is non-interactive (no PTY required), so you can drive it from a
script or agent. You can also just **look up the subdomain bound to your key
without serving anything** — see Step 1.

## Inputs you need

- An **SSH key** to connect with — always required. Any key is fine; whichever
  key you use is the identity that the payment (and the subdomain) is bound to.
  Reuse the same key to keep the same subdomain.
- The **local port** your HTTP app listens on (this guide calls it `<PORT>`) —
  needed **only if you actually want to serve an app** (Step 4). To check status,
  look up your subdomain, or pay, no port is needed.

## Step 0 — pin the host keys (once per environment)

The server's SSH host keys are published over HTTPS as a ready known_hosts
fragment at `https://app.my-ns.me/known_hosts`. Pin them before the first
connect, so every ssh command below verifies the host instead of trusting it on
first use:

```
mkdir -p ~/.ssh && curl -s https://app.my-ns.me/known_hosts >> ~/.ssh/known_hosts
```

If you cannot (or prefer not to) touch `~/.ssh`, save the fragment to any file
and add `-o UserKnownHostsFile=<file>` to the ssh commands below. Skip this step
if the keys are already pinned in your environment.

## Step 1 — check status: your subdomain, or an invoice

Connect once with stdin closed and read what the server prints. This sets up **no
forward** — it is a pure status check, so **no port is needed**:

```
ssh app@ssh.my-ns.me </dev/null
```

Interpret the output by looking for one of two **stable marker lines**:

- **`Your forwarded site: https://<name>.app.my-ns.me`** — this key is already
  admitted (already paid). That line contains your permanent subdomain. If you
  only wanted to find your subdomain, **you are done** — there is nothing else to
  do. To actually serve a local app on it, go to Step 4.
- **`Please pay: <bolt11>`** — this key is not admitted yet. Extract the bolt11
  invoice string from that line, e.g.:

  ```
  ssh app@ssh.my-ns.me </dev/null | grep 'Please pay:' | awk '{print $3}'
  ```

  (The `Please pay:` / `Your forwarded site:` markers are a stability contract —
  parse those exact prefixes, not the surrounding prose.)

Reconnecting the same key before paying returns the **same** invoice; it does
not create a new charge.

## Step 2 — pay the invoice (capability-agnostic)

You now have a bolt11 Lightning invoice for **1000 sat**, payable within
1 hour. How it gets paid depends on your capabilities:

- **If you can pay Lightning invoices yourself** (you have a wallet / CLI / API
  under your control): pay the bolt11 invoice now.
- **If you cannot pay autonomously**: present the bolt11 invoice to the user,
  state the amount (**1000 sat**) and that it is payable within
  1 hour, and **stop**. Continue only after the user confirms they
  have paid.

Do not invent a payment method you do not actually have — when in doubt, hand the
invoice to the user.

## Step 3 — confirm admission

After payment, reconnect with the **same key** and confirm the admitted marker
(still a status check, so no port needed):

```
ssh app@ssh.my-ns.me </dev/null | grep 'Your forwarded site:'
```

That line contains your permanent URL: `https://<name>.app.my-ns.me`.

## Step 4 — run the tunnel (only when serving an app)

**Do this step only if the user actually wants to expose a local application** —
that is, they have told you *which* app to serve and *which* local port it
listens on. If the user only wanted their subdomain, or has not said what to
serve or where, **stop after Step 1/3**: you already have the URL from the
`Your forwarded site:` line, and there is nothing to forward.

When you do have an app listening on `<PORT>`, bring the forward up and keep it
open for as long as you want the site reachable:

```
ssh -R80:localhost:<PORT> app@ssh.my-ns.me -N
```

The app is now live at `https://<name>.app.my-ns.me`. Notes:

- Port `80` in `-R80:...` is a convention (you forward plain HTTP; the proxy adds
  HTTPS). Keep it.
- One active tunnel per subdomain: reconnecting the same key overtakes the older
  connection.
- Reconnect any time with the same key to get the same subdomain back.

---

Powered by usshd — https://github.com/akovalenko/usshd
