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 # Server
HOST=127.0.0.1 HOST=127.0.0.1
PORT=3000 PORT=3000
ALLOW_ORIGINS= # Optional comma-separated allowed CORS origins; empty = permissive
# Auth
ADMINS= # Comma-separated hex pubkeys with admin access
# Database # Database
DATABASE_URL=sqlite://data/caravel.db DATABASE_URL=sqlite://data/caravel.db
# Nostr # Robot identity (published as kind 0)
PLATFORM_ADMIN_PUBKEYS= # Comma-separated hex pubkeys with super admin access ROBOT_SECRET= # Nostr private key (hex)
PLATFORM_SECRET= # Nostr private key (hex) used to send NIP-17 DMs and sign events 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
ZOOID_API_URL=http://127.0.0.1:8032 ZOOID_API_URL=http://127.0.0.1:3334
RELAY_DOMAIN=spaces.coracle.social RELAY_DOMAIN=spaces.coracle.social
LIVEKIT_URL=
LIVEKIT_API_KEY=
LIVEKIT_API_SECRET=
# Billing # Billing
NWC_URL= # Nostr Wallet Connect URL for generating Lightning invoices 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 # 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 ## Tech Stack
- Rust (Edition 2024) - Rust (Edition 2024)
- Axum (HTTP server) - Axum (HTTP API)
- SQLx + SQLite (persistence) - SQLx + SQLite
- Nostr SDK (NIP-98 verification) - Tokio (async runtime + workers)
- Nostr SDK (NIP-98 auth, NIP-17 DMs, NIP-47 wallet connect)
## Layout ## Layout
``` ```
backend/ backend/
migrations/ # SQL migrations migrations/
0001_init.sql
src/ src/
auth.rs # NIP-98 verification helper api.rs # Axum routes + NIP-98 auth checks
billing.rs # Billing loop + invoice generation billing.rs # Invoice generation + collection worker
config.rs # Env-based configuration infra.rs # Zooid sync worker
db.rs # SQLite pool + migrations main.rs # App bootstrap
main.rs # Axum server entrypoint models.rs # DB models
models.rs # Data models repo.rs # Data access layer
notifications.rs # NIP-17 DM sender + relay discovery robot.rs # Nostr robot identity + DM sending
platform.rs # Startup kind 0/10050 publishing
provisioning.rs # Zooid provisioning worker
repo.rs # Data access layer
``` ```
## Configuration ## Configuration
@@ -33,95 +32,42 @@ Environment variables:
| Variable | Description | Default | | Variable | Description | Default |
|---|---|---| |---|---|---|
| `DATABASE_URL` | SQLite database URL | `sqlite://data/caravel.db` | | `DATABASE_URL` | SQLite URL. Relative paths are resolved under `backend/`. | `sqlite://<backend>/data/caravel.db` |
| `HOST` | Bind host | `127.0.0.1` | | `HOST` | API bind host (also used for NIP-98 `u` host check) | `127.0.0.1` |
| `PORT` | Bind port | `3000` | | `PORT` | API bind port | `3000` |
| `ZOOID_API_URL` | Zooid API base URL | `http://127.0.0.1:8032` | | `ADMINS` | Comma-separated admin pubkeys (hex) | _optional_ |
| `PLATFORM_SECRET` | Platform Nostr secret key for NIP-98 auth | _required_ | | `ALLOW_ORIGINS` | Comma-separated CORS origins. If empty, CORS is permissive. | _optional_ |
| `RELAY_DOMAIN` | Relay base domain for subdomains | `spaces.coracle.social` | | `ZOOID_API_URL` | Zooid API base URL used by infra worker | _required for infra sync_ |
| `NWC_URL` | Platform NWC URL for invoice generation | _required for billing_ | | `RELAY_DOMAIN` | Base domain appended to relay subdomains | empty |
| `NOSTR_INDEXER_RELAYS` | Comma-separated relays to fetch kind `10050` DM relays | _required for notifications_ | | `LIVEKIT_URL` | LiveKit URL sent to zooid when relay livekit is enabled | _optional_ |
| `PLATFORM_NAME` | Platform display name for kind `0` metadata | _optional_ | | `LIVEKIT_API_KEY` | LiveKit API key sent to zooid | _optional_ |
| `PLATFORM_DESCRIPTION` | Platform description for kind `0` metadata | _optional_ | | `LIVEKIT_API_SECRET` | LiveKit API secret sent to zooid | _optional_ |
| `PLATFORM_PICTURE` | Platform picture URL for kind `0` metadata | _optional_ | | `NWC_URL` | Platform NWC URL used to generate BOLT11 invoices | _required for invoice generation_ |
| `PLATFORM_MESSAGING_RELAYS` | Comma-separated relays published in kind `10050` | _optional_ | | `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`: See [spec](spec) for more details
- `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`.
## API Routes ## 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 /tenants`list tenants (admin)
- `GET /tenant/relays` — list tenant relays - `POST /tenants` — create current auth pubkey as tenant
- `POST /tenant/relays` — create relay - `GET /tenants/:pubkey` — get tenant (admin or same tenant)
- `GET /tenant/relays/:id` — get relay - `PUT /tenants/:pubkey/billing` — update tenant `nwc_url` (admin or same tenant)
- `PUT /tenant/relays/:id`update relay - `GET /relays`list relays (`?tenant=<pubkey>` allowed for admin only)
- `DELETE /tenant/relays/:id`deactivate relay - `POST /relays`create relay (admin or relay tenant)
- `GET /tenant/invoices` — list invoices - `GET /relays/:id` — get relay (admin or relay tenant)
- `PUT /tenant/billing` — update tenant billing (NWC URL) - `PUT /relays/:id` — update relay (admin or relay tenant)
- `POST /relays/:id/deactivate` — deactivate relay (admin or relay tenant)
Admin routes (all require NIP-98 auth; pubkey must be in `PLATFORM_ADMIN_PUBKEYS`): - `GET /invoices` — list invoices (`?tenant=<pubkey>` allowed for admin only)
- `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
+1 -1
View File
@@ -15,7 +15,7 @@ Members:
## `pub fn new() -> Self` ## `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` - 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<()>` ## `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 name = std::env::var("ROBOT_NAME").unwrap_or_default();
let description = std::env::var("ROBOT_DESCRIPTION").unwrap_or_default(); let description = std::env::var("ROBOT_DESCRIPTION").unwrap_or_default();
let picture = std::env::var("ROBOT_PICTURE").unwrap_or_default(); let picture = std::env::var("ROBOT_PICTURE").unwrap_or_default();
let outbox_relays = split_env("ROBOT_OUTBOX_RELAYS"); let outbox_relays = split_relays("ROBOT_OUTBOX_RELAYS");
let indexer_relays = split_env("ROBOT_INDEXER_RELAYS"); let indexer_relays = split_relays("ROBOT_INDEXER_RELAYS");
let messaging_relays = split_env("ROBOT_MESSAGING_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 keys = Keys::parse(&secret)?;
let client = Client::new(keys); 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) std::env::var(key)
.unwrap_or_default() .unwrap_or_default()
.split(',') .split(',')
.map(|v| v.trim().to_string()) .map(|v| normalize_relay_url(v.trim()))
.filter(|v| !v.is_empty()) .filter(|v| !v.is_empty())
.collect() .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> { async fn indexer_client(secret: &str, indexer_relays: &[String]) -> Result<Client> {
let keys = Keys::parse(secret)?; let keys = Keys::parse(secret)?;
let client = Client::new(keys); let client = Client::new(keys);