coinosv3-server
The server-side services behind coinos v3, the bitcoin, Ark, Lightning and nostr wallet whose app lives in coinosv3. 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 |
live/ |
Live streams sent from the app: the browser's recording over a WebSocket, out as HLS (a live window and the recording), for NIP-53 streams. Senders own a coinos name. | v3.coinos.io/live/ |
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:
git -C coinosv3 fetch && git -C coinosv3 checkout <commit>
git add coinosv3 && git commit -m "coinosv3 → <commit>"
Running
You need Bun (the deployment pins ghcr.io/coinos/bun:1.4.3, oven/bun with the distrusted emSign G3 root swapped out of its built-in store; see coinos-server docker/bun.Dockerfile) and,
for names/, a Core Lightning node's RPC socket and a Cloudflare zone.
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:
# .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/
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:
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 and gittr. Don't copy
files onto the server by hand: what runs should always be a commit.
git push prod HEAD:prod && git push origin HEAD:master
Rolling back
The previous deploy is the previous commit: git revert and push, or push an
older commit to prod (git push -f prod <commit>:prod). names reads its
SQLite state either way. Before the SQLite change, the JSON file it was
imported from is still in the data directory, but it stops at the moment of
the import.
Tests
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.
