aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-16 12:58:17 -0700
committerAdam Soltys <[email protected]>2026-10-16 12:58:17 -0700
commit2fe542686dde91f424ecb019c2886e467e9aebd8 (patch)
tree3b5931450301779a31c98935ff03b67de38a0d85
parenta3d2dc2e05a4f7c5ab0d6e7689498da9ca63de7b (diff)
downloadcoinosv3-server-2fe542686dde91f424ecb019c2886e467e9aebd8.tar.gz
coinosv3-server-2fe542686dde91f424ecb019c2886e467e9aebd8.zip
README, example config and the deploy hook
Co-Authored-By: Claude Opus 5.5 <[email protected]>
-rw-r--r--README.md118
-rwxr-xr-xdeploy/post-receive61
-rw-r--r--names/config.example.json24
-rw-r--r--package.json6
4 files changed, 208 insertions, 1 deletions
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..c2c7d5f
--- /dev/null
+++ b/README.md
@@ -0,0 +1,118 @@
+# coinosv3-server
+
+The server-side services behind [coinos v3](https://v3.coinos.io), the
+bitcoin, Ark, Lightning and nostr wallet whose app lives in
+[coinosv3](https://git.coinos.io/coinosv3.git). The wallet itself is
+self-custodial and runs in the browser. These services hold no keys to user
+funds; they give a wallet a payment address, wake it when a request arrives,
+and store its encrypted sync data.
+
+| Directory | Service | Public at |
+| ---------- | ------- | --------- |
+| `names/` | **Payment-name registrar.** Gives each wallet a `[email protected]` address: BIP-353 DNS records (written through Cloudflare's API), LNURL-pay, and a BOLT 12 offer per name on the Ark server's Lightning node. Settled payments are forwarded to the name's Ark address from a small float wallet. Also serves zap receipts, point-of-sale invoices, CLINK, payment webhooks, link previews and translation. Writes are authorized with NIP-98. | `names.coinos.io` |
+| `nwcpush/` | **NWC push notifier.** Watches relays for Nostr Wallet Connect requests addressed to registered wallet-service keys and wakes the device with a Web Push, so a closed wallet can answer. | (internal, behind the app) |
+| `blossom/` | **Blossom server for sync envelopes.** Stores only the wallet's encrypted cross-device state blobs, which can outgrow a relay's event size. Refuses anything else. | `blossom.coinos.io` |
+| `feed/` | **Public feed snapshot** (`/api/feed`) and server-rendered note and profile pages. | `v3.coinos.io` |
+| `vmedia/` | **Media helper** for the feed: video posters, H.264 fallbacks and resized pictures. | `v3.coinos.io/vmedia/` |
+| `bridge/` | Ark to Lightning swap bridge. **Retired**: the app uses the Ark server's Lightning. `bridge/ln.js` (a minimal CLN RPC client) is still used by `names/`. | — |
+
+## Shared code: the `coinosv3` submodule
+
+The services use the wallet's own code for Ark, BOLT 12 and nostr helpers,
+so the server speaks exactly the wire format and policy the app does. That
+code comes from the `coinosv3/` submodule, **pinned to a specific commit**.
+Moving the pin is a deliberate, reviewable commit of its own:
+
+```sh
+git -C coinosv3 fetch && git -C coinosv3 checkout <commit>
+git add coinosv3 && git commit -m "coinosv3 → <commit>"
+```
+
+`names/vendor/` holds two text helpers, `lang-guess.js` and
+`translate-split.js`, at the versions the server runs. They are newer than the
+current pin, and both go once the pin moves past them.
+
+## Running
+
+You need [Bun](https://bun.sh) (the deployment pins `oven/bun:1.3.14`) and,
+for `names/`, a Core Lightning node's RPC socket and a Cloudflare zone.
+
+```sh
+git clone --recurse-submodules https://git.coinos.io/coinosv3-server.git
+cd coinosv3-server
+bun install
+cp names/config.example.json names/config.json # fill it in
+NAMES_CONFIG=names/config.json bun names/server.js
+```
+
+Each service reads a JSON config. `names` uses `NAMES_CONFIG` and `nwcpush`
+uses `NWCPUSH_CONFIG`; `blossom` reads `BLOSSOM_*` environment variables
+(see the header of `blossom/server.js`). Example configs:
+`names/config.example.json`, `nwcpush/config.example.json`,
+`bridge/config.example.json`.
+
+**Configs hold secrets and are never committed.** They include the DNS
+provider's API key, admin tokens, the forwarder float's mnemonic, VAPID keys
+and nostr secret keys. `.gitignore` excludes every `*config*.json` except the
+examples, and all runtime state.
+
+### With Docker
+
+`compose.yml` runs `names`, `nwcpush` and `blossom` from the checkout,
+mounted read-only. Configs and state live outside it, under a directory you
+name in a `.env` file:
+
+```sh
+# .env
+STATE=/srv/coinosv3-state
+CLN_RPC=/path/to/lightning-rpc
+```
+
+```
+$STATE/names/config.json $STATE/names/data/ (names.db)
+$STATE/nwcpush/config.json $STATE/nwcpush/data/
+ $STATE/blossom/data/
+```
+
+```sh
+docker network create net # once; the services share it with the reverse proxy
+docker compose up -d
+```
+
+## State
+
+`names` keeps its state in SQLite (`names.db`, see `names/store.js`): one row
+per name, offer, invoice and so on. Each change is a single transaction that
+writes only the rows that changed. On its first start it imports the older
+JSON state file named by `stateFile`, and a corrupt file stops startup rather
+than starting empty. For a consistent backup while it runs, take a snapshot
+through SQLite, not by copying the file:
+
+```sh
+sqlite3 names.db "VACUUM INTO 'names-backup.db'"
+```
+
+## Deploying
+
+coinos deploys by pushing to a bare repository on the server.
+`deploy/post-receive` is its hook. A push to `prod` checks the commit out,
+installs dependencies, restarts the services whose files changed, and mirrors
+the commit to [git.coinos.io](https://git.coinos.io) and gittr. Don't copy
+files onto the server by hand: what runs should always be a commit.
+
+```sh
+git push prod HEAD:prod && git push origin HEAD:master
+```
+
+## Tests
+
+```sh
+bun test names/store.test.js
+```
+
+`tools/` holds the older end-to-end and unit scripts for the registrar, feed
+and translation. They are run directly with `bun tools/<file>`.
+
+## License
+
+MIT, see [LICENSE](LICENSE).
diff --git a/deploy/post-receive b/deploy/post-receive
new file mode 100755
index 0000000..78e4ba2
--- /dev/null
+++ b/deploy/post-receive
@@ -0,0 +1,61 @@
+#!/bin/sh
+# Deploy hook for cs:~/coinosv3-server.git. Install it as hooks/post-receive.
+#
+# A push to `prod` checks the commit out to $CHECKOUT, installs dependencies,
+# restarts the services whose files changed, then mirrors the commit to
+# git.coinos.io and gittr. What runs is always a commit: never copy files
+# into the checkout by hand.
+set -u
+CHECKOUT=/home/adam/coinosv3-server
+LOG=/home/adam/coinosv3-server-deploy.log
+MIRROR=/home/adam/git/coinosv3-server.git
[email protected]:npub1w27mc4aa6m0ufe3xs5z3m6qyr52gc0rglept7vqlwx4xeaf72tasnqz70r/coinosv3-server.git
+BARE=$(pwd)
+SERVICES="names nwcpush blossom"
+
+while read -r oldrev newrev ref; do
+ [ "$ref" = refs/heads/prod ] || continue
+ {
+ echo "== $(date -u +%FT%TZ) deploy $newrev"
+ mkdir -p "$CHECKOUT"
+ git --git-dir="$BARE" --work-tree="$CHECKOUT" checkout -f "$newrev" || { echo "checkout failed"; exit 1; }
+ cd "$CHECKOUT" || exit 1
+ # the pinned coinosv3 commit comes from the coinosv3 repo on this host
+ env -u GIT_DIR git -C "$CHECKOUT" --git-dir="$BARE" --work-tree="$CHECKOUT" \
+ -c submodule.coinosv3.url=/home/adam/coinosv3.git -c protocol.file.allow=always \
+ submodule update --init --force || { echo "submodule update failed"; exit 1; }
+ docker run --rm -v "$CHECKOUT":/app -w /app oven/bun:1.3.14 bun install --frozen-lockfile --production \
+ || { echo "bun install failed"; exit 1; }
+
+ # which services to restart: their own directory changed, or something
+ # they all share did (the pin, dependencies, compose); everything on a
+ # first deploy
+ if [ "$oldrev" = 0000000000000000000000000000000000000000 ]; then
+ restart=$SERVICES
+ else
+ changed=$(git --git-dir="$BARE" diff --name-only "$oldrev" "$newrev")
+ if echo "$changed" | grep -qE '^(coinosv3|package\.json|bun\.lock|compose\.yml)$'; then
+ restart=$SERVICES
+ else
+ restart=""
+ for s in $SERVICES; do echo "$changed" | grep -q "^$s/" && restart="$restart $s"; done
+ # names imports bridge/ln.js
+ echo "$changed" | grep -q '^bridge/ln\.js$' && case "$restart" in *names*) ;; *) restart="$restart names";; esac
+ fi
+ fi
+ if [ -n "$restart" ]; then
+ echo "restarting:$restart"
+ docker compose -f "$CHECKOUT/compose.yml" --env-file /home/adam/coinosv3-state/.env up -d --force-recreate $restart \
+ || echo "compose up failed"
+ else
+ echo "no service files changed"
+ fi
+
+ # mirrors; a failure here never fails the deploy
+ git --git-dir="$BARE" push --quiet "$MIRROR" "$newrev:refs/heads/master" "refs/tags/*:refs/tags/*" \
+ || echo "mirror push to git.coinos.io failed"
+ GIT_SSH_COMMAND="ssh -i /home/adam/.ssh/gittr_ed25519 -o IdentitiesOnly=yes -o BatchMode=yes -o ConnectTimeout=15" \
+ timeout 120 git --git-dir="$BARE" push --quiet "$GITTR" "$newrev:refs/heads/master" "refs/tags/*:refs/tags/*" \
+ || echo "mirror push to gittr failed"
+ } >> "$LOG" 2>&1
+done
diff --git a/names/config.example.json b/names/config.example.json
new file mode 100644
index 0000000..163dd0a
--- /dev/null
+++ b/names/config.example.json
@@ -0,0 +1,24 @@
+{
+ "port": 8798,
+ "domain": "coinos.io",
+ "domains": {
+ "coinos.io": { "zoneId": "REPLACE: Cloudflare zone id" }
+ },
+ "publicBase": "https://names.example.com",
+ "cfEmail": "REPLACE: Cloudflare account email",
+ "cfKey": "REPLACE: Cloudflare API key",
+ "stateFile": "/data/names.json",
+ "dbFile": "/data/names.db",
+ "ln": { "kind": "socket", "path": "/cln/lightning-rpc" },
+ "forwarder": {
+ "mnemonic": "REPLACE: the float wallet's BIP-39 mnemonic",
+ "ark": "https://ark.example.com",
+ "esplora": "https://esplora.example.com/api",
+ "network": "mainnet"
+ },
+ "push": { "url": "http://nwcpush:8797", "token": "REPLACE: shared with nwcpush's notifyToken" },
+ "welcome": { "sk": "REPLACE: hex nostr secret key for welcome DMs", "message": "Welcome!" },
+ "adminToken": "REPLACE: random string",
+ "migrateToken": "REPLACE: random string, shared with coinos-server",
+ "rateExempt": []
+}
diff --git a/package.json b/package.json
index 6267263..85fc10f 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,11 @@
"description": "Server-side services for coinos v3: the payment-name registrar, NWC push, Blossom sync storage, the public feed and media proxy",
"license": "MIT",
"scripts": {
- "test": "bun test names/store.test.js"
+ "test": "bun test names/store.test.js",
+ "names": "bun names/server.js",
+ "nwcpush": "bun nwcpush/server.js",
+ "blossom": "bun blossom/server.js",
+ "feed:serve": "bun run feed/server.js"
},
"dependencies": {
"@noble/curves": "1.9.2",