A Claude Code Prompt

Run your own community

Stand up a private, invite-only community on your own domain, self-hosted on a $24/month server. Built on Buzz — open source group communication over Nostr.

This was written by actually doing it. Every trap listed at the bottom cost real time on the first pass — the prompt exists so it doesn't cost you any.

What you end up with: wss://relay.yourdomain.com — your own relay, real TLS, invite-only, nobody else's servers. About 30 minutes.

1. Before you start

2. Create the droplet

DigitalOcean → CreateDroplets

SettingValue
ImageUbuntu 24.04 LTS — plain, not a Marketplace image
TypeBasic → Regular SSD
Size$24/mo — 4 GB RAM / 2 vCPU / 80 GB
RegionClosest to you
AuthenticationSSH key — paste your ~/.ssh/id_ed25519.pub
Hostnamecommunity-relay

Three of these matter more than they look:

Copy the droplet's IP when it's ready.

3. The prompt

Fill in the three bracketed values and paste the whole block into Claude Code.

Paste into Claude Code
Set up a private, invite-only Buzz community on my own domain, self-hosted on my
DigitalOcean droplet. Build it and verify it actually works — don't just tell me
it should.

## My details

- Domain: [YOURDOMAIN.com]
- Droplet IP: [IP] — Ubuntu 24.04, root SSH with my key
- DNS is managed at: [Squarespace / Cloudflare / Namecheap / ...]

## What I'm building

A self-hosted Buzz relay (https://github.com/block/buzz) using its
`deploy/compose` bundle, reachable at `relay.[YOURDOMAIN.com]`, with TLS
terminated by Caddy and Let's Encrypt.

It is five long-lived containers: a Rust WebSocket relay, Postgres, Redis,
MinIO, and Caddy. This cannot run on serverless hosting. If I suggest putting it
somewhere like that, correct me instead of agreeing.

## Order of work — verify each step before starting the next

### 1. Provision the droplet

SSH in and do these in order:

1. **Firewall first.** `ufw allow 22`, then 80 and 443, THEN `ufw enable`.
   Enabling before allowing 22 locks you out of my server.
2. **Wait for apt.** A fresh droplet runs cloud-init and unattended-upgrades on
   first boot and holds the dpkg lock. Poll until it's free — check
   `fuser /var/lib/dpkg/lock-frontend` and run `cloud-init status --wait` —
   rather than failing on a confusing dpkg error.
3. Install Docker from **Docker's official apt repo**, not Ubuntu's, so the
   Compose plugin is current. Confirm `docker compose version` is >= 2.24.4.
4. Shallow-clone https://github.com/block/buzz to `/root/buzz`.

### 2. Get my identity

I need a Nostr keypair to be the relay owner.

Tell me to install the Buzz desktop app from
https://github.com/block/buzz/releases/latest, create an identity, and send you
the `npub`. I need the app anyway.

Then decode it yourself: it's bech32 — verify the checksum and confirm it's 32
bytes. `RELAY_OWNER_PUBKEY` needs **64-character hex**, and an `npub` pasted
straight in will not work.

**Never ask me for an `nsec` or a private key.** `npub` is public and safe to
share; `nsec` is secret and must never leave my machine. If I paste an `nsec` by
accident, tell me to abandon that identity and generate a fresh one.

### 3. Configure

Write `deploy/compose/.env` from `.env.example`:

- Generate every secret with `openssl rand -hex 32` — Postgres password, Redis
  password, relay private key, git hook HMAC, S3 access and secret keys.
  **Generate them on the droplet or my machine. Never invent them in your own
  output** — a private key in a chat transcript is compromised.
- Set the domain to `relay.[YOURDOMAIN.com]`.
- Set `RELAY_OWNER_PUBKEY` to my decoded hex.
- Set `BUZZ_REQUIRE_RELAY_MEMBERSHIP=true` — this is what makes it invite-only.
- `chmod 600` the file.

`run.sh` refuses to start while any `CHANGE_ME` remains. Confirm zero are left.

### 4. DNS

Have me add one record:

| Type | Name    | Data            |
|------|---------|-----------------|
| A    | `relay` | my droplet IP   |

Two warnings to give me:

- It must be a **plain DNS record, not proxied through Cloudflare.** Caddy needs
  port 80 reachable for the Let's Encrypt HTTP-01 challenge, and a proxy also
  complicates the WebSocket upgrade.
- Some DNS panels reject pasted values carrying a trailing period or whitespace,
  with a misleading "invalid" error that blames the value. Tell me to type it by
  hand.

Confirm it resolves before continuing.

### 5. Start it

`BUZZ_COMPOSE_TLS=true ./run.sh start`

Then verify from the public internet and show me the actual output:

- all containers healthy
- Let's Encrypt cert issued — print the issuer and expiry
- `https://relay.[DOMAIN]/_liveness` returns 200
- the NIP-11 document is served
- **a WebSocket connection returns HTTP 101 and the relay replies with a NIP-42
  AUTH challenge** — this is the real test, not the liveness endpoint
- `./run.sh list-members` shows me as `owner`
- **port-scan from outside**: 5432, 6379, 9000 and 3000 must all be closed

One expected result that looks like a failure: in TLS mode `compose.caddy.yml`
uses `!reset` to unpublish port 3000, so `curl 127.0.0.1:3000` **on the droplet**
fails by design. Only Caddy is exposed. Don't chase it.

## How you should work

- **Verify, don't assert.** Every claim about DNS, TLS, or ports gets a command
  and real output. "Should work" is not acceptable.
- Read `deploy/compose/README.md` and `NOSTR.md` before acting — they contain
  constraints that aren't obvious from the code.
- Tell me when something I asked for is wrong, and why, before building it.
- Never print a secret back to me in full.

4. Adding people

One command per member, from your machine:

ssh root@YOUR_DROPLET_IP 'cd /root/buzz/deploy/compose && ./run.sh add-member <npub>'

Takes an npub or raw hex. Add them before they try to connect — a non-member hitting the relay gets an auth failure with no useful explanation.

Then they install Buzz, choose Join a community, and enter wss://relay.yourdomain.com.

Everything else, from /root/buzz/deploy/compose:

./run.sh list-members       # who's in
./run.sh remove-member <k>  # revoke
./run.sh logs               # follow relay logs
./run.sh status             # container health
./run.sh backup-hint        # what to back up
Do these on day one

Back up deploy/compose/.env. Losing BUZZ_RELAY_PRIVATE_KEY changes your relay's identity and every member has to re-trust it. It's the one irreplaceable file.

Pin BUZZ_IMAGE. It defaults to ghcr.io/block/buzz:main — a moving tag, so an upgrade could pull a breaking change. Pin a digest or release tag once you're happy.

Traps this prompt already handles

TrapWhat happens without the guardrail
2 GB dropletSwaps, OOM-kills the relay
Docker Marketplace imageCompose older than 2.24.4; the TLS override's !reset tag fails
ufw enable before allowing 22Locked out of your own server
apt lock on first bootInstall fails with a confusing dpkg error
Relay subdomain proxied by CloudflareLet's Encrypt challenge fails, cert never issues
Pasted DNS valuesTrailing period rejected with an error that blames the value
npub used as RELAY_OWNER_PUBKEYNeeds 64-char hex; the owner never bootstraps
curl 127.0.0.1:3000 on the dropletFails by design in TLS mode — looks like a broken deploy
Adding a member after they connectAuth failure with no useful explanation
Approving members with SQLUnnecessary — ./run.sh add-member exists

Or skip the setup and apply to join ours →