Update readme, move frontend build to build phase in dockerfile

This commit is contained in:
Jon Staab
2026-06-02 10:50:30 -07:00
parent 430f33383b
commit b331a806ca
16 changed files with 371 additions and 38 deletions
+14 -15
View File
@@ -16,17 +16,16 @@ Rust backend for Caravel. It manages tenants, relays, invoices, and background w
backend/
migrations/
0001_init.sql
spec/ # Module-by-module design notes
src/
main.rs # App bootstrap: load Env, build services, serve + spawn workers
env.rs # Configuration from the environment (+ NIP-44 encryption, NIP-98 signing)
api.rs # Shared Api state, router, NIP-98 auth + authorization helpers
web.rs # HTTP response envelope + helpers
routes/ # HTTP route handlers (identity, plans, tenants, relays, invoices, stripe)
routes/ # HTTP route handlers (identity, plans, tenants, relays, invoices)
models.rs # Domain models + sqlite rows
query.rs # Database reads
command.rs # Database writes + activity broadcast
pool.rs # SQLite pool + migrations
db.rs # SQLite pool, migrations, activity broadcast channel
billing.rs # Stripe subscription reconciliation + Lightning collection worker
stripe.rs # Thin Stripe REST client
wallet.rs # NWC wallet handle (NIP-47)
@@ -43,10 +42,11 @@ All configuration is read from the environment by `Env::load()` at startup. **Ev
| Variable | Description |
| ---------------------- | ------------------------------------------------------------------- |
| `SERVER_HOST` | API bind host; also the value the NIP-98 `u` tag must contain |
| `SERVER_PORT` | API bind port |
| `SERVER_URL` | Public API base URL; the value the NIP-98 `u` tag must equal exactly |
| `SERVER_PORT` | API bind port (the server always binds to `127.0.0.1`) |
| `SERVER_ADMIN_PUBKEYS` | Comma-separated admin pubkeys (hex) |
| `SERVER_ALLOW_ORIGINS` | Comma-separated allowed CORS origins |
| `APP_URL` | Frontend base URL; used to build links in DMs and invoices |
| `DATABASE_URL` | SQLite URL; relative `sqlite://` paths are resolved under `backend/` |
**Robot identity**
@@ -84,18 +84,15 @@ All configuration is read from the environment by `Env::load()` at startup. **Ev
**Billing (Stripe)**
| Variable | Description |
| ----------------------- | ----------------------------------------------------------------------- |
| `STRIPE_SECRET_KEY` | Stripe API secret key used for billing API operations |
| `STRIPE_WEBHOOK_SECRET` | Stripe webhook signing secret used to verify `Stripe-Signature` headers |
| `STRIPE_PRICE_BASIC` | Stripe price ID (`price_...`) backing the Basic plan |
| `STRIPE_PRICE_GROWTH` | Stripe price ID (`price_...`) backing the Growth plan |
| Variable | Description |
| ------------------- | ----------------------------------------------------- |
| `STRIPE_SECRET_KEY` | Stripe API secret key used for billing API operations |
Comma-separated list variables are split on commas and trimmed; empty entries are dropped.
## Schema and Architecture
See [spec](spec) for more details
The database schema lives in [migrations](migrations); see the module-level doc comments in `src/` for architecture details.
## API Routes
@@ -105,7 +102,6 @@ Public exceptions:
- `GET /plans`
- `GET /plans/:id`
- `POST /stripe/webhook` (validated with Stripe signatures)
- `GET /identity` — get auth identity (`pubkey`, `is_admin`); side-effect-free
- `GET /tenants` — list tenants (admin)
@@ -122,16 +118,19 @@ Public exceptions:
- `POST /relays/:id/deactivate` — deactivate relay (admin or relay tenant)
- `POST /relays/:id/reactivate` — reactivate relay (admin or relay tenant)
- `GET /tenants/:pubkey/invoices` — list tenant invoices (admin or same tenant)
- `GET /tenants/:pubkey/invoices/draft` — get the tenant's synthetic draft invoice for the current period, or `null` if nothing is due (admin or same tenant)
- `GET /tenants/:pubkey/invoices/draft/items` — list the draft invoice's line items (admin or same tenant)
- `GET /invoices/:id` — get invoice (admin or same tenant)
- `GET /invoices/:id/bolt11` — get invoice bolt11 (admin or same tenant)
- `GET /tenants/:pubkey/stripe/session` — create Stripe customer portal session (admin or same tenant)
- `GET /invoices/:id/items` — list invoice line items (admin or same tenant)
- `GET /tenants/:pubkey/stripe/session` — create Stripe customer portal session (same tenant only)
## API Auth Model
Caravel intentionally uses a session-style variant of NIP-98 for client-to-backend API auth.
- Frontend signs one kind `27235` event with `u = VITE_API_URL` and caches that header for about 10 minutes.
- Backend verifies event kind, signature, and that `u` contains configured `SERVER_HOST`.
- Backend verifies event kind, signature, and that `u` equals configured `SERVER_URL`.
- Backend intentionally does not bind auth to exact request URL/method/query, and does not enforce payload hash, timestamp freshness window, or replay cache.
- Goal: reduce repeated wallet signing prompts and avoid cookie-based sessions.
- Tradeoff: this is weaker request-intent binding than strict NIP-98 semantics.
+2 -1
View File
@@ -32,7 +32,7 @@ use crate::models::{Relay, Tenant};
use crate::query;
use crate::robot::Robot;
use crate::routes::identity::get_identity;
use crate::routes::invoices::{get_invoice, get_invoice_bolt11, list_invoice_items};
use crate::routes::invoices::{get_invoice, get_invoice_bolt11, list_invoice_items, list_invoices};
use crate::routes::plans::{get_plan, list_plans};
use crate::routes::relays::{
create_relay, deactivate_relay, get_relay, list_relay_activity, list_relay_members,
@@ -87,6 +87,7 @@ impl Api {
.route("/relays/:id/activity", get(list_relay_activity))
.route("/relays/:id/deactivate", post(deactivate_relay))
.route("/relays/:id/reactivate", post(reactivate_relay))
.route("/invoices", get(list_invoices))
.route("/invoices/:id", get(get_invoice))
.route("/invoices/:id/bolt11", get(get_invoice_bolt11))
.route("/invoices/:id/items", get(list_invoice_items))
+9 -1
View File
@@ -131,7 +131,15 @@ pub async fn get_invoice(invoice_id: &str) -> Result<Option<Invoice>> {
)
}
pub async fn list_invoices(tenant_pubkey: &str) -> Result<Vec<Invoice>> {
pub async fn list_invoices() -> Result<Vec<Invoice>> {
Ok(
sqlx::query_as::<_, Invoice>("SELECT * FROM invoice ORDER BY created_at DESC")
.fetch_all(pool())
.await?,
)
}
pub async fn list_invoices_for_tenant(tenant_pubkey: &str) -> Result<Vec<Invoice>> {
Ok(sqlx::query_as::<_, Invoice>(
"SELECT * FROM invoice WHERE tenant_pubkey = ? ORDER BY created_at DESC",
)
+9
View File
@@ -6,6 +6,15 @@ use crate::api::{Api, AuthedPubkey};
use crate::query;
use crate::web::{ApiResult, internal, not_found, ok};
pub async fn list_invoices(
State(api): State<Arc<Api>>,
AuthedPubkey(auth): AuthedPubkey,
) -> ApiResult {
api.require_admin(&auth)?;
ok(query::list_invoices().await.map_err(internal)?)
}
pub async fn get_invoice(
State(api): State<Arc<Api>>,
AuthedPubkey(auth): AuthedPubkey,
+3 -1
View File
@@ -174,7 +174,9 @@ pub async fn list_tenant_invoices(
.await
.map_err(internal)?;
let invoices = query::list_invoices(&pubkey).await.map_err(internal)?;
let invoices = query::list_invoices_for_tenant(&pubkey)
.await
.map_err(internal)?;
ok(invoices)
}