Skip to content

Fluxora-Org/Fluxora-Backend

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

324 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Fluxora Backend

Express + TypeScript API for the Fluxora treasury streaming protocol. Today this repository exposes a minimal HTTP surface for stream CRUD and health checks. It now documents both the decimal-string serialization policy for chain/API amounts and the consumer-facing webhook signature verification contract the team intends to keep stable when delivery is enabled.

Quick Start with Docker Compose

The fastest way to run Fluxora Backend with all dependencies:

# 1. Clone and navigate to the repository
git clone <repository-url>
cd Fluxora-Backend

# 2. Copy and configure environment variables
cp .env.example .env
# Edit .env with your secrets (JWT_SECRET, API_KEYS, etc.)

# 3. Start with PostgreSQL only
docker-compose up -d

# 4. Or start with PostgreSQL + Redis (full stack)
docker-compose --profile redis up -d

# 5. Check service health
curl http://localhost:3000/health

# 6. View logs
docker-compose logs -f app

Docker Compose Services

Service Description Default URL
app Fluxora Backend API http://localhost:3000
postgres PostgreSQL 16 database localhost:5432
redis Redis 7 cache (optional) localhost:6379

Configuration Profiles

  • Default (docker-compose up): App + PostgreSQL
  • With Redis (--profile redis): App + PostgreSQL + Redis
  • Full Stack (--profile full): All services

Health Checks

All services include health checks:

  • PostgreSQL: pg_isready every 10s
  • Redis: redis-cli ping every 10s
  • App: HTTP health endpoint every 30s

The app waits for PostgreSQL to be healthy before starting.

Database Initialization

PostgreSQL automatically initializes on first run using scripts in init-db/:

  • 01-schema.sql: Creates tables, indexes, and initial data
  • Streams table for treasury protocol state
  • Indexer state tracking
  • Audit logs for chain-derived changes
  • Webhook delivery tracking (future)

Troubleshooting

# Reset everything (destroys data)
docker-compose down -v

# Rebuild after code changes
docker-compose up -d --build

# Check database logs
docker-compose logs postgres

# Connect to database
docker-compose exec postgres psql -U fluxora -d fluxora

# Scale app instances (with external load balancer)
docker-compose up -d --scale app=3

Current status

  • Implemented today:
    • REST endpoints for API info, health, and in-memory stream CRUD
    • decimal-string validation for amount fields
    • indexer freshness classification for healthy, starting, stalled, and not_configured
    • consumer-side webhook signing and verification helpers in src/webhooks/signature.ts
    • WebSocket channel for real-time stream updates at ws://<host>/ws/streams
  • Explicitly not implemented yet:
    • live webhook delivery endpoints
    • durable delivery logs or replay store
    • persistent database-backed stream/indexer state
    • automated restart orchestration
    • request rate limiting middleware

If a feature in this README is described as a webhook contract, treat it as the documented integration target for consumers and operators, not as proof that the live service already emits webhooks from this repository.

Decimal String Serialization Policy

All amounts crossing the chain/API boundary are serialized as decimal strings to prevent precision loss in JSON.

Amount Fields

  • depositAmount - Total deposit as decimal string (for example "1000000.0000000")
  • ratePerSecond - Streaming rate as decimal string (for example "0.0000116")

Validation Rules

  • Amounts must be strings in decimal notation
  • Native JSON numbers are rejected to prevent floating-point precision issues
  • Values exceeding safe integer ranges are rejected with DECIMAL_OUT_OF_RANGE

Error Codes

Code Description
DECIMAL_INVALID_TYPE Amount was not a string
DECIMAL_INVALID_FORMAT String did not match decimal pattern
DECIMAL_OUT_OF_RANGE Value exceeds maximum supported precision
DECIMAL_EMPTY_VALUE Amount was empty or null

Webhook signature verification for consumers

Scope and guarantee

For consumer-side verification of Fluxora webhook deliveries, Fluxora aims to guarantee:

  • each delivery carries a stable set of verification headers
  • the signature is computed over the exact raw request body, not parsed JSON
  • consumers can reject stale, oversized, tampered, or duplicate deliveries with predictable outcomes
  • operators have a written checklist for diagnosing delivery failures without relying on tribal knowledge

This repository currently provides the canonical algorithm and the expected outcomes. It does not yet provide a live webhook sending service.

Verification contract

Fluxora webhook deliveries are expected to use these headers:

Header Meaning Validation
x-fluxora-delivery-id Stable id for a single delivery attempt chain; use it for deduplication Required; non-empty string
x-fluxora-timestamp Unix timestamp in seconds Required; positive integer string
x-fluxora-signature Hex-encoded HMAC-SHA256(secret, timestamp + "." + rawBody) Required; 64-char hex (case-insensitive, whitespace trimmed)
x-fluxora-event Event name such as stream.created or stream.updated Informational

Canonical signing payload:

${timestamp}.${rawRequestBody}

Canonical verification rules:

  • use the raw request bytes exactly as received
  • reject payloads larger than 256 KiB
  • reject timestamps outside a 300 second tolerance window
  • compare signatures with a constant-time equality check
  • deduplicate on x-fluxora-delivery-id

Reference implementation lives in src/webhooks/signature.ts.

Consumer verification example

import { verifyWebhookSignature } from './src/webhooks/signature.js';

const verification = verifyWebhookSignature({
  secret: process.env.FLUXORA_WEBHOOK_SECRET,
  deliveryId: req.header('x-fluxora-delivery-id') ?? undefined,
  timestamp: req.header('x-fluxora-timestamp') ?? undefined,
  signature: req.header('x-fluxora-signature') ?? undefined,
  rawBody,
  isDuplicateDelivery: (deliveryId) => seenDeliveryIds.has(deliveryId),
});

if (!verification.ok) {
  return res.status(verification.status).json({
    error: verification.code,
    message: verification.message,
  });
}

Trust boundaries

Actor Trusted for Not trusted for
Public clients Valid request shape only Payload integrity, replay prevention
Authenticated partners / webhook consumers Possession of shared webhook secret and endpoint ownership Skipping signature checks, bypassing replay controls
Administrators / operators Secret rotation, incident response, delivery diagnostics Reading secrets from logs or bypassing audit trails
Internal workers Constructing signed payloads, retry scheduling, durable delivery state once implemented Silently mutating or dropping verified deliveries

Failure modes and expected behavior

Condition Expected result Suggested HTTP outcome
Missing secret in consumer config Treat as configuration failure; do not trust the payload 500 internally, do not acknowledge
Missing delivery id / timestamp / signature Reject as unauthenticated 401 Unauthorized
Non-numeric or stale timestamp Reject as replay-risk / invalid input 400 for malformed timestamp, 401 for stale timestamp
Signature mismatch Reject as unauthenticated 401 Unauthorized
Payload larger than 256 KiB Reject before parsing JSON 413 Payload Too Large
Duplicate delivery id Do not process the business action twice 200 OK after safe dedupe or 409 Conflict
Consumer overloaded Ask sender to retry later 429 Too Many Requests

WebSocket API

Endpoint

ws://<host>/ws/streams

No authentication is required to connect (auth is a documented non-goal for this release — see follow-up below).

Message schema

All messages are JSON. The server only sends; clients may send subscription messages (currently ignored beyond abuse checks).

Stream event:

{
  "event": "stream.created" | "stream.updated" | "stream.cancelled",
  "streamId": "stream-abc123",
  "payload": { ... },
  "timestamp": "2026-01-01T00:00:00.000Z"
}

Degraded state notification:

{
  "event": "service.degraded",
  "reason": "Stellar RPC unreachable",
  "timestamp": "2026-01-01T00:00:00.000Z"
}

Abuse prevention

Control Limit Client-visible outcome
Max client message size 4 KiB Connection closed with code 1009
Per-connection rate limit 10 messages / 10 s Connection closed with code 1008
Heartbeat (ping/pong) Every 30 s Unresponsive clients are terminated

Operator observability

  • GET /health includes wsConnections — the count of currently connected clients.
  • Connect/disconnect events are logged natively to standard output as structured JSON.
  • Connection Lifecycle Logs: - On connection, a ws_connect event is emitted containing the assigned connectionId and the client's ip.
    • On termination, a ws_disconnect event is emitted containing the connectionId, connection durationMs, closure code, closure reason, and a metrics payload.
  • Per-Connection Metrics: Included in the ws_disconnect log, the metrics object tracks:
    • messagesReceived / messagesSent
    • bytesReceived / bytesSent (measured purely as utf-8 string buffers to preserve decimal-string serialization guarantees).

Manual verification

# Install wscat if needed
npm install -g wscat

# Connect and watch events
wscat -c ws://localhost:3000/ws/streams

Authentication (optional, backward-compatible)

JWT authentication on the WebSocket upgrade is supported via two environment variables:

Variable Default Description
WS_AUTH_REQUIRED false Set to true to reject unauthenticated upgrade requests with HTTP 401
JWT_SECRET HS256 secret used to verify tokens

Token delivery (first match wins):

  1. Authorization: Bearer <token> header on the upgrade request
  2. ?token=<jwt> query-string parameter

When WS_AUTH_REQUIRED is absent or false, all connections are accepted regardless of whether a token is present. This enables a zero-downtime rollout: deploy first, issue tokens to clients, then flip the flag.

# Connect with a token
wscat -c "ws://localhost:3000/ws/streams" \
  --header "Authorization: Bearer <your-jwt>"

# Or via query string
wscat -c "ws://localhost:3000/ws/streams?token=<your-jwt>"

Non-goals and follow-up

  • Message replay / durable event log — events are fire-and-forget; clients that disconnect miss events. Follow-up: add a replay store.
  • Per-stream subscription filtering — all clients receive all events. Follow-up: add a subscription message protocol.

Health and observability

  • GET /health returns service status and indexer freshness classification
  • request IDs enable correlation across logs
  • structured JSON logs are expected for diagnostics
  • if indexer.status = "stalled", treat that as an operational signal that chain-derived views would be stale if the real indexer were enabled in this service

Security headers: helmet middleware

Service-level outcomes

  • every HTTP response carries a predictable baseline of browser-facing security headers
  • the service does not advertise Express internals through the X-Powered-By header
  • operators can verify the header policy with a simple GET /health or GET / check during rollout and incident response
  • failures in downstream dependencies do not disable the security-header baseline because the middleware is applied before route handling

Trust boundaries

Actor May do May not do
Public internet clients Call public routes and observe the documented response headers Weaken or negotiate a lower security-header policy
Authenticated partners Use partner/admin routes once enabled and receive the same baseline headers Bypass the default browser-hardening behavior
Administrators / operators Verify header presence through health checks, logs, and smoke tests Treat the presence of headers as a substitute for auth, input validation, or TLS termination controls
Internal workers Reach internal HTTP surfaces through the same Express stack when applicable Disable header emission on a per-worker basis

Failure modes and expected behavior

Condition Expected behavior
Invalid input or route error Return the normal error status/body and still emit the security headers
Dependency outage /health may report degraded or unavailable state, but the header baseline remains present on the HTTP response
Partial data or missing resources Client receives the documented 404/409/5xx behavior with the same header policy intact
Duplicate delivery or replayed request Business logic decides 200/409 behavior; the security headers are unchanged because they are orthogonal to idempotency

Operator observability and diagnostics

  • smoke check with curl -I http://127.0.0.1:3000/health and confirm content-security-policy, strict-transport-security, x-content-type-options, and x-frame-options
  • use structured request logs and request IDs to correlate header checks with the request path under investigation
  • if a proxy or CDN strips headers, compare direct-app responses with edge responses to identify where the policy is being altered

Verification evidence

  • automated regression coverage lives in tests/helmet.test.ts
  • manual verification: curl -I http://127.0.0.1:3000/ and curl -I http://127.0.0.1:3000/health

Non-goals and audit notes

  • this issue adds baseline browser-facing security headers only; it does not replace TLS, authentication, authorization, rate limiting, or CSP tuning for a future browser UI
  • residual risk: intermediaries can still overwrite or strip headers, so production verification should include at least one edge-facing probe

Local setup

Prerequisites

  • Node.js 20+
  • pnpm 9+

Install and run

pnpm install
pnpm dev

API runs at http://localhost:3000.

Scripts

  • pnpm dev - run with tsx watch
  • pnpm build - compile to dist/
  • pnpm test - run the full test suite (vitest, single run)
  • pnpm test:coverage - run tests and enforce 95% coverage threshold
  • pnpm test:watch - run tests in watch mode during development
  • pnpm lint - check for ESLint violations
  • pnpm lint:fix - auto-fix ESLint violations where possible
  • pnpm format - format all files with Prettier
  • pnpm format:check - verify formatting without writing (used in CI)
  • pnpm start - run compiled dist/index.js
  • pnpm docker:build - build a production container image
  • pnpm docker:run - run the production container locally
  • pnpm docker:smoke - run a quick container health smoke check

Code quality

ESLint and Prettier are configured to enforce consistent style and prevent common bugs:

  • amount and balance fields must stay as decimal strings — parseFloat, Number(), and unary + on these variables are lint errors.
  • All exported functions and class methods require explicit return types.
  • Prettier settings are locked (endOfLine: lf, singleQuote, trailingComma: all) to avoid format-war diffs in PRs.

The CI pipeline runs pnpm lint, pnpm format:check, pnpm audit, and pnpm test:coverage on every push and PR. Coverage below 95% fails the build.

Production Docker Image (Issue #30)

Service-level outcomes

  • The backend can be packaged and started in a reproducible production image with a single command.
  • The container runs as a non-root user and exposes only one HTTP port (3000 by default).
  • Operators get a built-in container health signal via Docker HEALTHCHECK against GET /health.
  • Startup behavior is explicit: the process fails fast if the app cannot boot.

Trust boundaries for containerized runtime

  • Public internet clients: may call read/write API routes (/, /health, /api/streams/**) and receive normalized JSON responses.
  • Authenticated partners: currently same HTTP capabilities as public clients (authentication is intentionally deferred and documented).
  • Administrators/operators: may configure runtime through environment variables (PORT, LOG_LEVEL, INDEXER_*) and observe health/log output.
  • Internal workers: represented by indexer health classification in /health; workers are not container-exposed endpoints.

Failure modes and client-visible behavior

  • Invalid input: returns 400 error envelopes from validation middleware.
  • Dependency outage or stale worker checkpoint: /health reports degraded when indexer is starting or stalled.
  • Partial data / missing stream: stream lookups return 404 (NOT_FOUND) when absent.
  • Duplicate delivery/conflicting transitions: stream cancel path returns 409 (CONFLICT) for already-cancelled/completed streams.
  • Process-level failure (boot error/panic): container exits non-zero so orchestrators can restart or alert.

Operator observability and diagnostics

  • Health endpoint: GET /health for liveness/degraded state, includes indexer freshness summary.
  • Container health: Docker health status reflects HTTP health response.
  • Logs: structured console logs include request metadata and request/correlation IDs for incident correlation.
  • Triage flow:
    • Check docker ps health status.
    • Query /health and confirm indexer.status, lagMs, and summary.
    • Inspect container logs for request/error context.

Verification evidence for this issue

Run the following commands:

npm run docker:build
npm run docker:smoke

Optional manual verification:

docker run --rm -p 3000:3000 fluxora-backend:local
curl -sS http://127.0.0.1:3000/health

Non-goals and follow-up tracking

  • This issue does not introduce authentication/authorization for containerized endpoints.
  • Follow-up recommendation: add CI job that builds the image and runs /health smoke checks on every PR.

Reorg handling: chain tip safety for indexer

The Fluxora indexer implements strict chain tip safety and reorg handling to ensure the durability and accuracy of chain-derived state.

Service-level outcomes

  • Chain Tip Safety: The indexer reports a lastSafeLedger which lags the current ingested tip by a safety margin (default 1 ledger for Stellar finality).
  • Reorg Detection: If an incoming batch contains a ledger number that has already been indexed but with a different ledgerHash, the service detects a chain reorg.
  • Automatic Rollback: Upon reorg detection, the service automatically rolls back its internal state to the ledger before the reorg point and re-indexes the new chain branch.
  • Operator Observability: Reorgs and safety metrics are exposed via GET /health and high-visibility logs.

Trust boundaries

Actor Trusted for Not trusted for
Public internet clients Reading safe ledger state Determining chain finality
Authenticated partners / Indexers Providing valid ledger hashes Forcing rollbacks on final ledgers
Administrators / Operators Manual state resets Mutating individual event records
Internal Workers Detecting reorgs via RPC Suppressing reorg alerts

Failure modes and client-visible behavior

Condition Indexer Behavior Client-visible outcome
Chain Reorg detected Trigger rollback and set reorgDetected: true GET /health reports degraded during rollback
Duplicate delivery ON CONFLICT (event_id) DO NOTHING 200 OK (idempotent)
Invalid input (missing hash) Reject batch with 400 Bad Request Error envelope with validation details
Database outage Return 503 Service Unavailable API reports temporary unavailability

Indexer operator observability

  • Health Snapshot: GET /health includes lastSafeLedger and reorgDetected.
  • Logs: Reorgs are logged as WARN with existingHash and incomingHash for triage.
  • Triage: If reorgDetected is true, operators should monitor the lastSafeLedger to ensure the indexer is making forward progress on the new chain branch.

Verification evidence

  • Unit Tests: src/indexer/reorg.test.ts simulates reorg scenarios and verifies rollback logic.
  • Manual Check: Observe lastSafeLedger in /health increases during ingestion.

Local setup with Stellar testnet

This section covers everything needed to run Fluxora locally against the Stellar testnet.

What is the Stellar testnet?

The Stellar testnet is a public test network that mirrors mainnet behaviour but uses test XLM with no real value. It resets periodically (roughly every 3 months). Horizon testnet endpoint: https://horizon-testnet.stellar.org.

Additional prerequisites

  • Stellar CLI — optional, useful for account inspection
  • A Stellar testnet keypair (see below)

1. Copy environment file

cp .env.example .env

.env.example ships with the testnet defaults already set:

Variable Default value Required
PORT 3000 No
HORIZON_URL https://horizon-testnet.stellar.org Yes
NETWORK_PASSPHRASE Test SDF Network ; September 2015 Yes

Do not commit .env — it is listed in .gitignore.

2. Generate a testnet keypair

You can generate a keypair and fund it with Friendbot in one step:

# Using Stellar CLI
stellar keys generate --network testnet dev-account

# Or using curl (replace with any new keypair)
curl "https://friendbot.stellar.org?addr=<YOUR_PUBLIC_KEY>"

Alternatively, generate a keypair at Stellar Laboratory — click Generate Keypair, then fund it via the Friendbot button.

Keep the secret key out of version control. Store it only in .env or your local secrets manager.

3. Verify the testnet account

curl "https://horizon-testnet.stellar.org/accounts/<YOUR_PUBLIC_KEY>" | jq .

A successful response includes "id", "balances", and "sequence". An HTTP 404 means the account is not yet funded — run Friendbot first.

4. Install and start the API

npm install
npm run dev

Confirm the server is running:

curl http://localhost:3000/health
# {"status":"ok","service":"fluxora-backend","timestamp":"..."}

5. Create a test stream

Sender and recipient must be valid Stellar public keys (G…).

curl -X POST http://localhost:3000/api/streams \
  -H "Content-Type: application/json" \
  -d '{
    "sender": "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN",
    "recipient": "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGZCP2J7F1NRQKQOHP3OGN",
    "depositAmount": "100",
    "ratePerSecond": "0.001",
    "startTime": 1700000000
  }'

6. Query streams

# List all streams
curl http://localhost:3000/api/streams

# Get a specific stream
curl http://localhost:3000/api/streams/<stream-id>

Trust boundaries

Client type Allowed Not allowed
Public internet Read health, list/get/create streams Admin operations, raw DB access
Authenticated partner Future: write operations with JWT
Internal workers Future: Horizon sync, event processing Direct DB writes bypassing API

Failure modes

Condition Expected behaviour
Missing required body fields 400 with a descriptive error message
Stream ID not found 404 { "error": "Stream not found" }
Horizon unreachable Future: health check returns 503; streams degrade gracefully
Invalid Stellar address Future: 400 once address validation is added
Server crash / restart In-memory streams are lost (expected until DB is added)

Observability

  • GET /health — returns { status, service, timestamp }; use this as the liveness probe in any deployment
  • Console logs via tsx watch show all request activity in development
  • Future: structured JSON logging and a /metrics endpoint

Database Backups and Restore Runbook (Issue #52)

Service-level outcomes

  • The backend guarantees a durable, restorable view of chain-derived state using PostgreSQL custom-format dumps.
  • Backup operations execute without halting read/write API availability.
  • Restore operations execute with a --clean flag, guaranteeing the database state exactly matches the backup snapshot, avoiding partial data overlaps.
  • Dumps can be streamed directly to/from an S3 bucket — no temporary file is written to the container filesystem when an S3 target is configured.
  • All monetary amount fields remain as decimal strings throughout; this module never coerces them to numbers.

S3 streaming target (optional)

Install the AWS SDK v3 packages to enable S3 streaming:

npm install @aws-sdk/client-s3 @aws-sdk/lib-storage

Configure credentials via standard AWS environment variables:

Variable Purpose
AWS_ACCESS_KEY_ID AWS access key
AWS_SECRET_ACCESS_KEY AWS secret key
AWS_REGION / AWS_DEFAULT_REGION Target region (default: us-east-1)

Backup to S3

import { backupDatabase } from './src/scripts/db-ops.js'

const result = await backupDatabase(
  process.env.DATABASE_URL!,
  '',   // outputPath is ignored when s3Target is set
  { bucket: 'my-backups', key: `fluxora/${new Date().toISOString()}.dump` },
)
// { success: true, message: 'Backup successfully streamed to s3://my-backups/fluxora/...' }

Restore from S3

import { restoreDatabase } from './src/scripts/db-ops.js'

const result = await restoreDatabase(
  process.env.DATABASE_URL!,
  '',   // inputPath is ignored when s3Source is set
  { bucket: 'my-backups', key: 'fluxora/2026-04-23T00:00:00.000Z.dump' },
)
// { success: true, message: 'Restore successfully completed from s3://my-backups/...' }

Local backup / restore (no S3)

// Backup to local file
await backupDatabase(process.env.DATABASE_URL!, './backup.dump')

// Restore from local file
await restoreDatabase(process.env.DATABASE_URL!, './backup.dump')

Security notes

  • DATABASE_URL is validated before any subprocess is spawned. Empty strings and non-postgres schemes are rejected immediately.
  • All subprocess arguments are passed as an array via execFile / spawn — never interpolated into a shell string — preventing command injection.
  • Output/input paths are checked for shell metacharacters (`, $, |, ;, &, <, >) before use.
  • AWS credentials are consumed from environment variables only; they are never logged or included in error messages.
  • The error field in DbOperationResult contains raw pg_dump/pg_restore stderr — review before logging to ensure no credentials appear in your log pipeline.

Trust boundaries

Actor Allowed Not allowed
Public internet clients No access Cannot trigger, view, or detect backup/restore operations.
Authenticated partners No access Cannot trigger or download backups.
Internal workers (Cron) Execute backupDatabase securely via local FS or S3 stream Cannot execute restores or drop tables directly.
Administrators / operators Execute restoreDatabase during incident response, download dumps from S3 cold storage Leaving unencrypted dumps on public web servers.

Failure modes and expected behavior

Condition Expected result System Behavior
DATABASE_URL missing or malformed Immediate failure before subprocess spawns success: false returned, no partial files created.
DATABASE_URL is not a postgres:// URL Immediate failure before subprocess spawns success: false with descriptive message.
Output/input path contains shell metacharacters Immediate failure before subprocess spawns success: false — injection attempt blocked.
DB credentials invalid/revoked Subprocess fails with authentication error Returns Backup failed with stderr detail for logs.
Disk out of space during local backup pg_dump panics mid-stream Incomplete file remains; operation returns error. Operators must monitor FS capacity.
S3 upload fails mid-stream Upload promise rejects Returns Backup failed with error detail.
S3 object body is empty on restore Detected before spawning pg_restore Returns Restore failed with descriptive message.
Corrupted or invalid dump file provided to restore pg_restore rejects the archive format Returns Restore failed. Database state remains unchanged (clean drop does not execute).
AWS SDK v3 not installed Lazy-load fails with clear message Returns Backup/Restore failed with install instructions.

Operator observability and diagnostics

Operators can diagnose backup/restore health without relying on tribal knowledge:

  • Routine Backups: The backup routine outputs structured JSON containing { success: boolean, message: string, error?: string }.
  • Triage Flow (Backup Failure):
    1. Check disk space on the volume mapped to the output path (local mode) or S3 bucket permissions (S3 mode).
    2. Verify DATABASE_URL validity using psql "$DATABASE_URL" -c '\l'.
    3. Check the error string in the logs for pg_dump specific stderr (e.g., FATAL: connection limit exceeded).
    4. For S3 mode: verify AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and bucket write permissions.
  • Triage Flow (Restore Failure):
    1. Ensure the target DB has active connections terminated before running a --clean restore.
    2. Verify the input file was generated using custom format (pg_dump --format=custom), as plain SQL dumps will fail pg_restore.
    3. For S3 mode: verify the object key exists and the IAM role has s3:GetObject permission.

Verification evidence

Automated unit tests (tests/db-ops.test.ts) assert the boundaries of the pg_dump and pg_restore wrappers, covering:

  • Local file backup and restore success paths
  • S3 streaming backup and restore success paths
  • Input validation (missing URL, invalid scheme, shell metacharacters in paths)
  • Error propagation from subprocess stderr
  • AWS SDK not installed error path
  • DbOperationResult shape guarantees

Non-goals and follow-up tracking

  • Intentionally deferred: Automated scheduling (e.g., node-cron) is deferred until persistent volume claims (PVCs) are provisioned in the deployment orchestration.
  • Intentionally deferred: Backup encryption at rest — use S3 server-side encryption (SSE-S3 or SSE-KMS) at the bucket level.

GET /api/streams/:id backed by database (Issue #15)

Service-level outcomes

  • The /api/streams/:id endpoint provides a durable, highly available read path for chain-derived stream state.
  • Values returned respect the Decimal String Serialization Policy to prevent precision loss.

Trust boundaries

Actor Allowed Not allowed
Public internet clients Can query any known stream ID and receive normalized JSON. Cannot modify stream state or execute unbounded DB queries (e.g., table scans).
Internal workers / Indexer Trusted to write accurate chain-derived data to the underlying streams table.
Administrators / Operators Monitor DB latency and connection pool health.

Failure modes and client-visible behavior

Condition Expected result System Behavior
Valid stream ID exists 200 OK Returns JSON payload with decimal strings.
Stream ID does not exist 404 Not Found Returns {"error": "NOT_FOUND"}. No DB locks held.
Database connection drops 503 Service Unavailable Returns {"error": "SERVICE_UNAVAILABLE"}. Logs the underlying pg error for operator triage.

Operator observability and diagnostics

  • Health Checks: A 503 from this endpoint indicates pool exhaustion, network partition, or DB credentials failure. Cross-reference with GET /health.
  • Diagnostics: Look for [GET /api/streams/:id] Database error: in standard out. This will contain the raw pg driver stack trace.

GET /api/streams filters: status, recipient, sender (Issue #14)

Service-level outcomes

  • Integrators and finance reviewers can deterministically filter the stream index by status, sender, and recipient addresses.
  • Filtering is applied prior to cursor-based pagination to guarantee consistent, traversable result sets.

Trust boundaries

Actor Allowed Not allowed
Public internet clients Can filter public streams by valid addresses and statuses. Cannot bypass pagination limits or execute wildcard/regex searches.
Internal workers / Operators Full access to filter across all streams for reconciliation.

Failure modes and client-visible behavior

Condition Expected result System Behavior
Invalid Stellar Address Format 400 Bad Request Fails fast with VALIDATION_ERROR indicating exactly which field failed the Regex check.
Invalid Status Enum 400 Bad Request Rejects unknown statuses (e.g., pending) to prevent cache poisoning or DB errors.
Valid filters match no records 200 OK Returns an empty streams: [] array, not a 404, preserving API list semantics.

Operator observability and diagnostics

  • Filter parameters are logged alongside requestId to help diagnose user reports of "missing streams" (usually caused by a typo in the recipient query).

API overview

Method Path Description
GET / API info
GET /health Health check
GET /api/streams List streams
GET /api/streams/:id Get one stream
POST /api/streams Create stream

Project structure

src/
  routes/          # health, streams
  webhooks/        # canonical webhook signing and verification contract
  index.ts         # Express app and server
k6/
  main.js          # k6 entrypoint — composes scenarios
  config.js        # thresholds, stage profiles, base URL
  helpers.js       # shared metrics and payload helpers
  scenarios/       # per-endpoint load scenarios

Load testing (k6)

The k6/ directory contains a load-testing harness for critical endpoints.

Common commands:

npm run dev
npm run k6:smoke
npm run k6:load
npm run k6:stress
npm run k6:soak

Chain-First Model

Environment

Optional:

  • PORT - server port, default 3000
  • FLUXORA_WEBHOOK_SECRET - shared secret for webhook signature verification once delivery is enabled

Likely future additions:

  • DATABASE_URL
  • REDIS_URL
  • HORIZON_URL
  • JWT_SECRET

WebSocket API — Stream Updates (#49)

The backend exposes a WebSocket endpoint for real-time stream update notifications. Clients subscribe to individual streams and receive push events as the indexer processes on-chain activity.

Endpoint

ws://<host>/ws/streams

Protocol

All messages are JSON text frames. Binary frames are rejected.

Client → Server

Message Description
{ "type": "subscribe", "streamId": "<id>" } Subscribe to updates for a stream
{ "type": "unsubscribe", "streamId": "<id>" } Stop receiving updates for a stream

Server → Client

Message Description
{ "type": "stream_update", "streamId": "<id>", "eventId": "<id>", "payload": {...} } Stream state change event
{ "type": "error", "code": "<CODE>", "message": "<text>" } Protocol or policy error

Error codes

Code Cause
PAYLOAD_TOO_LARGE Inbound message exceeds 4 096 bytes
RATE_LIMIT_EXCEEDED More than 30 messages in a 10-second window
BINARY_NOT_SUPPORTED Binary frame received
INVALID_JSON Message is not valid JSON
INVALID_MESSAGE Missing or invalid streamId field
UNKNOWN_TYPE Unrecognised type field

Operational notes

  • Rate limiting: 30 inbound messages per 10-second window per connection. Excess messages receive a RATE_LIMIT_EXCEEDED error; the connection stays open.
  • Duplicate delivery prevention: Each event is identified by (streamId, eventId). If the indexer or RPC layer replays an event, the hub delivers it exactly once. The dedup cache holds up to 10 000 entries (LRU eviction).
  • Oversized payload rejection: Inbound messages larger than 4 096 bytes are rejected with PAYLOAD_TOO_LARGE. The connection stays open.
  • RPC failure isolation: The WS hub operates independently of the Stellar RPC circuit breaker. Clients remain connected during RPC outages; events resume automatically when the indexer recovers.
  • Graceful shutdown: StreamHub.close() is called during server shutdown to drain connections cleanly.

Example (browser)

const ws = new WebSocket('ws://localhost:3000/ws/streams');

ws.onopen = () => {
  ws.send(JSON.stringify({ type: 'subscribe', streamId: 'stream-abc123' }));
};

ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  if (msg.type === 'stream_update') {
    console.log('Update for', msg.streamId, msg.payload);
  }
};

Implementation

File Description
src/ws/hub.ts StreamHub class: connection tracking, rate limiting, dedup, broadcast
tests/ws.test.ts Integration tests: lifecycle, dedup, rate limiting, RPC failure modes

WebSocket API — Stream Updates (#49)

The backend exposes a WebSocket endpoint for real-time stream update notifications. Clients subscribe to individual streams and receive push events as the indexer processes on-chain activity.

Endpoint

ws://<host>/ws/streams

Protocol

All messages are JSON text frames. Binary frames are rejected.

Client → Server

Message Description
{ "type": "subscribe", "streamId": "<id>" } Subscribe to updates for a stream
{ "type": "unsubscribe", "streamId": "<id>" } Stop receiving updates for a stream

Server → Client

Message Description
{ "type": "stream_update", "streamId": "<id>", "eventId": "<id>", "payload": {...} } Stream state change event
{ "type": "error", "code": "<CODE>", "message": "<text>" } Protocol or policy error

Error codes

Code Cause
PAYLOAD_TOO_LARGE Inbound message exceeds 4 096 bytes
RATE_LIMIT_EXCEEDED More than 30 messages in a 10-second window
BINARY_NOT_SUPPORTED Binary frame received
INVALID_JSON Message is not valid JSON
INVALID_MESSAGE Missing or invalid streamId field
UNKNOWN_TYPE Unrecognised type field

Operational notes

  • Rate limiting: 30 inbound messages per 10-second window per connection. Excess messages receive a RATE_LIMIT_EXCEEDED error; the connection stays open.
  • Duplicate delivery prevention: Each event is identified by (streamId, eventId). If the indexer or RPC layer replays an event, the hub delivers it exactly once. The dedup cache holds up to 10 000 entries (LRU eviction).
  • Oversized payload rejection: Inbound messages larger than 4 096 bytes are rejected with PAYLOAD_TOO_LARGE. The connection stays open.
  • RPC failure isolation: The WS hub operates independently of the Stellar RPC circuit breaker. Clients remain connected during RPC outages; events resume automatically when the indexer recovers.
  • Graceful shutdown: StreamHub.close() is called during server shutdown to drain connections cleanly.

Example (browser)

const ws = new WebSocket('ws://localhost:3000/ws/streams');

ws.onopen = () => {
  ws.send(JSON.stringify({ type: 'subscribe', streamId: 'stream-abc123' }));
};

ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  if (msg.type === 'stream_update') {
    console.log('Update for', msg.streamId, msg.payload);
  }
};

Implementation

File Description
src/ws/hub.ts StreamHub class: connection tracking, rate limiting, dedup, broadcast
tests/ws.test.ts Integration tests: lifecycle, dedup, rate limiting, RPC failure modes

Related repos

  • fluxora-frontend - dashboard and recipient UI
  • fluxora-contracts - Soroban smart contracts

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors