Relay Backend
The trustless middleman β it pays your gas, but it can't steal your funds.
Repository: relay-backend
Tech: NestJS Β· PostgreSQL (Neon) Β· Redis (Upstash) Β· Drizzle ORM
What Does the Relay Do?β
The relay backend is a NestJS API deployed on Render. It has five distinct responsibilities:
graph LR
RELAY["relay-backend"]
RELAY --> WA["WebAuthn\nRegistration & Login"]
RELAY --> RLY["Relay\nFee Sponsorship"]
RELAY --> SESS["Sessions\nSession Key Management"]
RELAY --> REC["Recovery\nGuardian Orchestration"]
RELAY --> IDX["Indexer\nTransaction History"]
Module Breakdownβ
WebAuthn Moduleβ
Handles passkey registration and login.
POST /api/webauthn/register/options β Generate registration challenge
POST /api/webauthn/register/verify β Verify attestation, store credential
POST /api/webauthn/assert/options β Generate authentication challenge
POST /api/webauthn/assert/verify β Verify assertion, return session
How challenges work:
- App requests a challenge from the relay
- Relay generates a random one-time challenge, stores it in Redis with a 5-minute TTL
- App uses the challenge to trigger the browser/device biometric
- App returns the signed response to the relay
- Relay verifies the signature and marks the challenge as used
Challenges cannot be reused β Redis automatically expires them.
Relay Moduleβ
This is the "gasless" magic. When a user submits a signed transaction:
sequenceDiagram
participant USER as User's Signed XDR
participant RELAY as Relay Module
participant LAUNCHTUBE as Launchtube (Stellar)
participant STELLAR as Stellar Network
USER->>RELAY: POST /api/relay/submit {signedXdr}
RELAY->>RELAY: Wrap in fee-bump transaction
Note over RELAY: Relay adds its own key to pay XLM gas
RELAY->>LAUNCHTUBE: Submit fee-bumped XDR
LAUNCHTUBE->>STELLAR: Broadcast to network
STELLAR-->>RELAY: Transaction hash
RELAY-->>USER: {txHash, status}
The relay cannot change the inner transaction β any modification would invalidate the user's passkey signature and get rejected by the blockchain.
Sessions Moduleβ
Manages session keys for reduced-friction interactions:
GET /api/sessions β List active session keys for a wallet
POST /api/sessions β Create a new session key (requires passkey sig)
DELETE /api/sessions/:id β Revoke a session key
Recovery Moduleβ
Orchestrates the guardian recovery flow:
POST /api/recovery/propose β Submit a recovery proposal
POST /api/recovery/approve β Guardian approves a proposal
GET /api/recovery/:proposalId β Get proposal status
GET /api/recovery/:proposalId/status β Poll for completion
When approval threshold is met and timelock expires, the relay calls execute_recovery on the Policy contract. The contract validates everything on-chain independently.
Indexer Moduleβ
Watches the Stellar network for events related to Rayos wallets and caches transaction history for fast retrieval by the frontend.
Well-Known Moduleβ
Serves the domain association files required for passkeys to work on real devices:
/.well-known/apple-app-site-association β iOS Universal Links
/.well-known/assetlinks.json β Android App Links
Passkeys silently fail without these β they are not optional.
Database Schemaβ
The relay uses PostgreSQL (via Neon in production, Docker locally) with Drizzle ORM.
Key tables:
credentialsβ Mapscredential_idtowallet_addressrecovery_proposalsβ Full proposal state with guardian approvalssession_keysβ Session key metadata and scopes
Redis is used only for ephemeral data:
- WebAuthn challenges (5-minute TTL, auto-expire)
- Rate limiting
Trust Modelβ
The relay is designed to be trustless:
| What the Relay Holds | Security Implication |
|---|---|
credential_id (public) | Same as publishing a public key β cannot sign anything |
wallet_address | Public on-chain anyway |
| Session scopes & expiry | Off-chain cache only β on-chain contract is the authority |
| Recovery proposals | Orchestration only β smart contract validates final execution |
| β Private keys | Never stored, never seen |
| β Seed phrases | Not applicable |
Bottom line: Even if someone hacked the relay server, they couldn't steal funds. They'd need to also compromise the user's secure hardware enclave.
Running Locallyβ
git clone https://github.com/Rayos-Org/relay-backend.git
cd relay-backend
pnpm install
cp .env.example .env
# Start PostgreSQL + Redis via Docker
docker-compose up -d
# Push database schema
pnpm db:push
# Start in dev mode
pnpm start:dev
| URL | Description |
|---|---|
http://localhost:3000/api | REST API root |
http://localhost:3000/api/docs | Interactive Swagger UI |
http://localhost:4983 | Drizzle Studio (visual DB browser) |