Update env template

This commit is contained in:
Jon Staab
2026-03-26 11:09:09 -07:00
parent 087405b1ac
commit c4a63b18af
4 changed files with 90 additions and 119 deletions
+16 -13
View File
@@ -1,26 +1,29 @@
# Server
HOST=127.0.0.1
PORT=3000
ALLOW_ORIGINS= # Optional comma-separated allowed CORS origins; empty = permissive
# Auth
ADMINS= # Comma-separated hex pubkeys with admin access
# Database
DATABASE_URL=sqlite://data/caravel.db
# Nostr
PLATFORM_ADMIN_PUBKEYS= # Comma-separated hex pubkeys with super admin access
PLATFORM_SECRET= # Nostr private key (hex) used to send NIP-17 DMs and sign events
# Robot identity (published as kind 0)
ROBOT_SECRET= # Nostr private key (hex)
ROBOT_NAME=
ROBOT_DESCRIPTION=
ROBOT_PICTURE=
ROBOT_OUTBOX_RELAYS=relay.damus.io,relay.primal.net,nos.lol
ROBOT_INDEXER_RELAYS=purplepag.es,relay.damus.io,indexer.coracle.social
ROBOT_MESSAGING_RELAYS=auth.nostr1.com,relay.keychat.io,relay.ditto.pub
# Zooid
ZOOID_API_URL=http://127.0.0.1:8032
ZOOID_API_URL=http://127.0.0.1:3334
RELAY_DOMAIN=spaces.coracle.social
LIVEKIT_URL=
LIVEKIT_API_KEY=
LIVEKIT_API_SECRET=
# Billing
NWC_URL= # Nostr Wallet Connect URL for generating Lightning invoices
# Indexer
NOSTR_INDEXER_RELAYS=wss://purplepag.es/,wss://relay.damus.io/,wss://relay.nostr.band/
# Platform identity (published as kind 0)
PLATFORM_NAME=
PLATFORM_DESCRIPTION=
PLATFORM_PICTURE=
PLATFORM_MESSAGING_RELAYS= # Comma-separated relay URLs published in kind 10050
+46 -100
View File
@@ -1,30 +1,29 @@
# Backend
Rust backend for the Nostr relay hosting platform. This service manages tenants, relays, invoices, and billing, and provisions relays by calling the zooid HTTP API. It authenticates requests using NIP-98.
Rust backend for Caravel. It manages tenants, relays, invoices, and background workers for relay provisioning and billing.
## Tech Stack
- Rust (Edition 2024)
- Axum (HTTP server)
- SQLx + SQLite (persistence)
- Nostr SDK (NIP-98 verification)
- Axum (HTTP API)
- SQLx + SQLite
- Tokio (async runtime + workers)
- Nostr SDK (NIP-98 auth, NIP-17 DMs, NIP-47 wallet connect)
## Layout
```
backend/
migrations/ # SQL migrations
migrations/
0001_init.sql
src/
auth.rs # NIP-98 verification helper
billing.rs # Billing loop + invoice generation
config.rs # Env-based configuration
db.rs # SQLite pool + migrations
main.rs # Axum server entrypoint
models.rs # Data models
notifications.rs # NIP-17 DM sender + relay discovery
platform.rs # Startup kind 0/10050 publishing
provisioning.rs # Zooid provisioning worker
repo.rs # Data access layer
api.rs # Axum routes + NIP-98 auth checks
billing.rs # Invoice generation + collection worker
infra.rs # Zooid sync worker
main.rs # App bootstrap
models.rs # DB models
repo.rs # Data access layer
robot.rs # Nostr robot identity + DM sending
```
## Configuration
@@ -33,95 +32,42 @@ Environment variables:
| Variable | Description | Default |
|---|---|---|
| `DATABASE_URL` | SQLite database URL | `sqlite://data/caravel.db` |
| `HOST` | Bind host | `127.0.0.1` |
| `PORT` | Bind port | `3000` |
| `ZOOID_API_URL` | Zooid API base URL | `http://127.0.0.1:8032` |
| `PLATFORM_SECRET` | Platform Nostr secret key for NIP-98 auth | _required_ |
| `RELAY_DOMAIN` | Relay base domain for subdomains | `spaces.coracle.social` |
| `NWC_URL` | Platform NWC URL for invoice generation | _required for billing_ |
| `NOSTR_INDEXER_RELAYS` | Comma-separated relays to fetch kind `10050` DM relays | _required for notifications_ |
| `PLATFORM_NAME` | Platform display name for kind `0` metadata | _optional_ |
| `PLATFORM_DESCRIPTION` | Platform description for kind `0` metadata | _optional_ |
| `PLATFORM_PICTURE` | Platform picture URL for kind `0` metadata | _optional_ |
| `PLATFORM_MESSAGING_RELAYS` | Comma-separated relays published in kind `10050` | _optional_ |
| `DATABASE_URL` | SQLite URL. Relative paths are resolved under `backend/`. | `sqlite://<backend>/data/caravel.db` |
| `HOST` | API bind host (also used for NIP-98 `u` host check) | `127.0.0.1` |
| `PORT` | API bind port | `3000` |
| `ADMINS` | Comma-separated admin pubkeys (hex) | _optional_ |
| `ALLOW_ORIGINS` | Comma-separated CORS origins. If empty, CORS is permissive. | _optional_ |
| `ZOOID_API_URL` | Zooid API base URL used by infra worker | _required for infra sync_ |
| `RELAY_DOMAIN` | Base domain appended to relay subdomains | empty |
| `LIVEKIT_URL` | LiveKit URL sent to zooid when relay livekit is enabled | _optional_ |
| `LIVEKIT_API_KEY` | LiveKit API key sent to zooid | _optional_ |
| `LIVEKIT_API_SECRET` | LiveKit API secret sent to zooid | _optional_ |
| `NWC_URL` | Platform NWC URL used to generate BOLT11 invoices | _required for invoice generation_ |
| `ROBOT_SECRET` | Robot Nostr secret key | _required_ |
| `ROBOT_NAME` | Robot display name (kind `0`) | _optional_ |
| `ROBOT_DESCRIPTION` | Robot description (kind `0`) | _optional_ |
| `ROBOT_PICTURE` | Robot picture URL (kind `0`) | _optional_ |
| `ROBOT_OUTBOX_RELAYS` | Comma-separated relays published as kind `10002` | _required_ |
| `ROBOT_INDEXER_RELAYS` | Comma-separated relays used for recipient relay discovery | _required_ |
| `ROBOT_MESSAGING_RELAYS` | Comma-separated relays published as kind `10050` | _required_ |
The database directory is created automatically if it doesnt exist.
Relay list env vars are comma-separated and trimmed. If a relay has no `ws://` or `wss://` scheme, `wss://` is prepended.
## Database Schema
## Schema and Architecture
Created via `migrations/0001_init.sql`:
- `tenants`
- `relays`
- `invoices`
- `invoice_items`
## Running
```bash
cd backend
cargo run
```
Health check:
```
GET /healthz
```
## NIP-98 Authentication
NIP-98 verification is implemented in `auth.rs` using the Rust Nostr SDK. It verifies:
- Authorization header format
- Event signature and kind
- URL + HTTP method tags
- Timestamp validity
This is ready to be used by API routes.
## Billing Jobs
The backend runs an in-process billing loop that:
- Generates monthly invoices (using `NWC_URL`)
- Uses the tenants `nwc_url` for recurring pull payments (if set)
- Sends NIP-17 DMs with invoices when recurring is off
- Sends NIP-17 DMs on successful payment when recurring is on
NIP-17 relay discovery:
- Uses `NOSTR_INDEXER_RELAYS` to fetch kind `10050` for each tenant
- Cached for a short period
- If no relays are found, no DM is sent
On startup, the backend publishes:
- Kind `0` metadata (name/description)
- Kind `10050` relay list for DMs
These are published to the relays listed in `NOSTR_INDEXER_RELAYS`.
See [spec](spec) for more details
## API Routes
Tenant routes (all require NIP-98 auth; pubkey is inferred from the token):
All routes are NIP-98 protected.
- `GET /tenant`fetch (or create) tenant
- `GET /tenant/relays` — list tenant relays
- `POST /tenant/relays` — create relay
- `GET /tenant/relays/:id` — get relay
- `PUT /tenant/relays/:id`update relay
- `DELETE /tenant/relays/:id`deactivate relay
- `GET /tenant/invoices` — list invoices
- `PUT /tenant/billing` — update tenant billing (NWC URL)
Admin routes (all require NIP-98 auth; pubkey must be in `PLATFORM_ADMIN_PUBKEYS`):
- `GET /admin/tenants` — list tenants
- `GET /admin/tenants/:pubkey` — tenant detail (includes relays)
- `PUT /admin/tenants/:pubkey` — update tenant status
- `GET /admin/relays` — list relays
- `GET /admin/relays/:id` — get relay
- `PUT /admin/relays/:id` — update relay
- `DELETE /admin/relays/:id` — deactivate relay
- `GET /tenants`list tenants (admin)
- `POST /tenants` — create current auth pubkey as tenant
- `GET /tenants/:pubkey` — get tenant (admin or same tenant)
- `PUT /tenants/:pubkey/billing` — update tenant `nwc_url` (admin or same tenant)
- `GET /relays`list relays (`?tenant=<pubkey>` allowed for admin only)
- `POST /relays`create relay (admin or relay tenant)
- `GET /relays/:id` — get relay (admin or relay tenant)
- `PUT /relays/:id` — update relay (admin or relay tenant)
- `POST /relays/:id/deactivate` — deactivate relay (admin or relay tenant)
- `GET /invoices` — list invoices (`?tenant=<pubkey>` allowed for admin only)
+1 -1
View File
@@ -15,7 +15,7 @@ Members:
## `pub fn new() -> Self`
- Reads environment and populates members
- Reads environment and populates members. Relay urls should be split and normalized.
- Publishes a `kind 0` nostr profile, a `kind 10002` relay list, and `kind 10050` relay selections using `client`
## `pub async fn send_dm(&self, recipient: &str, message: &str) -> Result<()>`
+27 -5
View File
@@ -35,9 +35,19 @@ impl Robot {
let name = std::env::var("ROBOT_NAME").unwrap_or_default();
let description = std::env::var("ROBOT_DESCRIPTION").unwrap_or_default();
let picture = std::env::var("ROBOT_PICTURE").unwrap_or_default();
let outbox_relays = split_env("ROBOT_OUTBOX_RELAYS");
let indexer_relays = split_env("ROBOT_INDEXER_RELAYS");
let messaging_relays = split_env("ROBOT_MESSAGING_RELAYS");
let outbox_relays = split_relays("ROBOT_OUTBOX_RELAYS");
let indexer_relays = split_relays("ROBOT_INDEXER_RELAYS");
let messaging_relays = split_relays("ROBOT_MESSAGING_RELAYS");
if outbox_relays.is_empty() {
return Err(anyhow!("ROBOT_OUTBOX_RELAYS is required"));
}
if indexer_relays.is_empty() {
return Err(anyhow!("ROBOT_INDEXER_RELAYS is required"));
}
if messaging_relays.is_empty() {
return Err(anyhow!("ROBOT_MESSAGING_RELAYS is required"));
}
let keys = Keys::parse(&secret)?;
let client = Client::new(keys);
@@ -190,15 +200,27 @@ impl Robot {
}
}
fn split_env(key: &str) -> Vec<String> {
fn split_relays(key: &str) -> Vec<String> {
std::env::var(key)
.unwrap_or_default()
.split(',')
.map(|v| v.trim().to_string())
.map(|v| normalize_relay_url(v.trim()))
.filter(|v| !v.is_empty())
.collect()
}
fn normalize_relay_url(url: &str) -> String {
if url.is_empty() {
return String::new();
}
if url.starts_with("ws://") || url.starts_with("wss://") {
url.to_string()
} else {
format!("wss://{url}")
}
}
async fn indexer_client(secret: &str, indexer_relays: &[String]) -> Result<Client> {
let keys = Keys::parse(secret)?;
let client = Client::new(keys);