Update env template
This commit is contained in:
+16
-13
@@ -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
@@ -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 doesn’t 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 tenant’s `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)
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user