Skip to main content

Technical Architecture

Complete technical architecture documentation for Spritz, covering system design, protocols, and implementation details.

System Architecture

Spritz is built as a Next.js 16 application using the App Router, with a PostgreSQL backend for data persistence and real-time features.

┌─────────────────────────────────────────────────────────────┐
│ Client Layer │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Next.js │ │ React 19 │ │ TypeScript │ │
│ │ App Router │ │ Components │ │ Type Safe │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│ API Layer (Next.js) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Agents │ │ Streaming │ │ Auth/SIWE │ │
│ │ Endpoints │ │ Endpoints │ │ Endpoints │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘

┌───────────────────┼───────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Supabase │ │ Livepeer │ │ Google │
│ Database │ │ Streaming │ │ Gemini AI │
│ + pgvector │ │ + WebRTC │ │ + Embeddings│
└──────────────┘ └──────────────┘ └──────────────┘


┌─────────────────────────────────────────────────────────────┐
│ External Services │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │Logos │ │ Huddle01 │ │ x402 │ │
│ │ Messaging │ │ Video Calls │ │ Payments │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘

Messaging Protocol

Overview

Spritz uses Logos Messaging (formerly Waku) for decentralized, peer-to-peer messaging with end-to-end encryption.

Protocol Stack

LayerTechnologyPurpose
TransportLogos Messaging Light NodeP2P message relay
EncryptionECDH P-256 + AES-256-GCMEnd-to-end encryption
SerializationProtocol BuffersMessage encoding
PersistenceSupabase + localStorageMessage storage

ECDH Key Exchange

Security Model: Keys are derived using Elliptic Curve Diffie-Hellman (ECDH), not from wallet addresses alone. This prevents attackers who know both wallet addresses from computing the encryption key.

┌─────────────────────────────────────────────────────────────┐
│ ECDH Key Exchange Flow │
├─────────────────────────────────────────────────────────────┤
│ │
│ User A User B │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Generate P-256 │ │ Generate P-256 │ │
│ │ Keypair (local) │ │ Keypair (local) │ │
│ │ - privateKeyA │ │ - privateKeyB │ │
│ │ - publicKeyA │ │ - publicKeyB │ │
│ └─────────────────┘ └─────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ Store publicKeyA in Store publicKeyB in │
│ Supabase (for B to fetch) Supabase (for A to fetch) │
│ │ │ │
│ └────────────┬───────────────────┘ │
│ ▼ │
│ Both derive same shared secret: │
│ sharedSecret = ECDH(myPrivate, theirPublic) │
│ │ │
│ ▼ │
│ AES-256-GCM key = sharedSecret (256 bits) │
│ │
└─────────────────────────────────────────────────────────────┘

Key Generation (P-256):

const keyPair = await crypto.subtle.generateKey(
{ name: "ECDH", namedCurve: "P-256" },
true, // extractable
["deriveBits"]
);

Shared Secret Derivation:

const sharedBits = await crypto.subtle.deriveBits(
{ name: "ECDH", public: theirPublicKey },
myPrivateKey,
256 // 256 bits = 32 bytes
);

Message Encryption (AES-256-GCM)

All messages are encrypted before transmission:

// Generate random IV for each message
const iv = crypto.getRandomValues(new Uint8Array(12));

// Encrypt with AES-GCM
const encrypted = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
cryptoKey,
messageData
);

// Output: IV (12 bytes) + Ciphertext + Auth Tag (16 bytes)

Security Properties:

  • Confidentiality: AES-256 encryption
  • Integrity: GCM authentication tag
  • Replay Protection: Unique IV per message

PIN-Based Key Derivation

For users without wallet signing or passkey PRF support (email, Alien ID, World ID, Solana), Spritz provides PIN-based deterministic key derivation as an alternative:

ComponentDetails
PIN6+ digits (numbers only)
Key DerivationPBKDF2-SHA256, 600,000 iterations
OutputDeterministic X25519 keypair
Cross-deviceSame PIN + same address = same key on any device

See PIN-Based Messaging Encryption for the full technical implementation.

Key Backup (Optional)

Users can opt-in to cloud backup with PIN protection:

ComponentDetails
Recovery Phrase12 words (96 bits entropy)
PIN6 digits
Key DerivationPBKDF2-SHA256, 100,000 iterations
EncryptionAES-256-GCM
// Key derivation from phrase + PIN
const derivedBits = await crypto.subtle.deriveBits(
{
name: "PBKDF2",
salt: saltBuffer,
iterations: 100000,
hash: "SHA-256",
},
keyMaterial,
256
);

Message Format (Protobuf)

message ChatMessage {
uint64 timestamp = 1; // Unix timestamp (ms)
string sender = 2; // Sender address
string content = 3; // Encrypted message text
string messageId = 4; // UUID v4
string messageType = 5; // "text", "pixel_art", "system"
}

Content Topics

Messages are routed via content topics:

TypeTopic FormatExample
DMs/spritz/1/dm-{sorted-addresses}/proto/spritz/1/dm-0xabc-0xdef/proto
Groups/spritz/1/group-{groupId}/proto/spritz/1/group-abc123/proto

Hybrid Persistence

Messages are stored in multiple layers for reliability:

LayerRetentionEncryptionPurpose
Logos Messaging Store~30 daysE2EReal-time delivery
SupabasePermanentE2E (same key)Long-term backup
localStoragePermanentE2EOffline access

Legacy Key Compatibility

For backwards compatibility with messages sent before ECDH migration:

// Old deterministic key (DEPRECATED - kept for decryption only)
const seed = `spritz-dm-key-v1:${address1}:${address2}`;
const legacyKey = await crypto.subtle.digest("SHA-256", seed);

// Decryption tries ECDH key first, falls back to legacy
if (decryptWithECDH(message) fails) {
return decryptWithLegacy(message);
}

Video Calls Protocol

Overview

Spritz uses Huddle01 for decentralized video calls with WebRTC.

Architecture

┌─────────────────────────────────────────────────────────────┐
│ Video Call Flow │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. Room Creation │
│ ┌──────────┐ POST /api/v2/sdk/rooms/create-room │
│ │ Spritz │ ─────────────────────────────────────────► │
│ │ Server │ { roomLocked: false, metadata: {...} } │
│ └──────────┘ ◄───────────────────────────────────────── │
│ │ { roomId: "abc-def-ghi" } │
│ │ │
│ 2. Token Generation (per participant) │
│ │ │
│ ▼ │
│ ┌──────────┐ AccessToken.toJwt() │
│ │ Huddle01│ - roomId, role: HOST │
│ │ SDK │ - permissions: cam, mic, screen, data │
│ └──────────┘ - metadata: displayName, walletAddress │
│ │ │
│ 3. WebRTC Connection │
│ │ │
│ ▼ │
│ ┌──────────┐ ┌──────────┐ │
│ │ Client A │ ◄─────► │ Client B │ (via Huddle01 SFU) │
│ │ (WebRTC) │ │ (WebRTC) │ │
│ └──────────┘ └──────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

Room Creation

// API: POST /api/huddle01/room
const response = await fetch(
"https://api.huddle01.com/api/v2/sdk/rooms/create-room",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": HUDDLE01_API_KEY,
},
body: JSON.stringify({
roomLocked: false,
metadata: {
title: "Spritz Call",
hostWallets: [hostWallet],
},
}),
}
);
// Returns: { roomId: "xyz-abc-123" }

Token Generation

import { AccessToken, Role } from "@huddle01/server-sdk/auth";

const accessToken = new AccessToken({
apiKey: HUDDLE01_API_KEY,
roomId: roomId,
role: Role.HOST,
permissions: {
admin: true,
canConsume: true, // Receive media
canProduce: true, // Send media
canProduceSources: {
cam: true,
mic: true,
screen: true,
},
canRecvData: true,
canSendData: true,
canUpdateMetadata: true,
},
options: {
metadata: {
displayName: displayName,
walletAddress: userAddress,
},
},
});

const token = await accessToken.toJwt();

Call Types

TypeImplementationMax Participants
1:1 CallSingle room, both as HOST2
Group CallSingle room, all as HOST10+
Voice OnlyCamera disabled client-sideUnlimited

WebRTC Configuration

  • ICE Servers: Managed by Huddle01
  • Codec: VP8/VP9 for video, Opus for audio
  • Topology: Selective Forwarding Unit (SFU)
  • Encryption: DTLS-SRTP (WebRTC standard)

Livestreaming Protocol

Overview

Spritz uses Livepeer for decentralized livestreaming with WebRTC ingestion and HLS playback.

Architecture

┌─────────────────────────────────────────────────────────────┐
│ Livestream Flow │
├─────────────────────────────────────────────────────────────┤
│ │
│ Broadcaster │
│ ┌──────────┐ │
│ │ Camera │ │
│ │ +Mic │ │
│ └────┬─────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ WebRTC/WHIP │ https://livepeer.studio/webrtc/{key} │
│ │ Ingestion │ │
│ └────────┬─────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ Livepeer │ Transcoding: │
│ │ Processing │ - 720p @ 2 Mbps │
│ │ │ - 480p @ 1 Mbps │
│ │ │ - 360p @ 0.5 Mbps │
│ └────────┬─────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ Livepeer CDN │ HLS Manifest: │
│ │ (HLS Delivery) │ livepeercdn.studio/hls/{id}/index.m3u8│
│ └────────┬─────────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Viewer 1 │ │ Viewer 2 │ │ Viewer N │ (Adaptive HLS) │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

Stream Creation

// Create stream on Livepeer
const response = await fetch("https://livepeer.studio/api/stream", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${LIVEPEER_API_KEY}`,
},
body: JSON.stringify({
name: streamTitle,
record: true, // Enable VOD recording
profiles: [
{
name: "720p",
bitrate: 2000000,
fps: 30,
width: 1280,
height: 720,
},
{
name: "480p",
bitrate: 1000000,
fps: 30,
width: 854,
height: 480,
},
{ name: "360p", bitrate: 500000, fps: 30, width: 640, height: 360 },
],
}),
});

// Returns:
// {
// id: "stream-id",
// streamKey: "xxxx-xxxx-xxxx",
// playbackId: "yyyy-yyyy",
// rtmpIngestUrl: "rtmp://rtmp.livepeer.com/live/{streamKey}"
// }

Ingestion (WHIP Protocol)

WebRTC-HTTP Ingestion Protocol (WHIP) enables low-latency browser-based streaming:

// WebRTC ingest URL
const ingestUrl = `https://livepeer.studio/webrtc/${streamKey}`;

// Using @livepeer/react Broadcast component
<Broadcast.Root ingestUrl={ingestUrl}>
<Broadcast.Container>
<Broadcast.Video />
</Broadcast.Container>
</Broadcast.Root>;

Playback (HLS)

// HLS playback URL
const playbackUrl = `https://livepeercdn.studio/hls/${playbackId}/index.m3u8`;

// Using hls.js for playback
import Hls from "hls.js";

const hls = new Hls();
hls.loadSource(playbackUrl);
hls.attachMedia(videoElement);

Stream States

StateDescription
idleStream created, not broadcasting
liveCurrently broadcasting
endedStream terminated

Recording (VOD)

Streams are automatically recorded when record: true:

// Get recordings for a stream
const assets = await fetch(
`https://livepeer.studio/api/stream/${streamId}/assets`,
{ headers: { Authorization: `Bearer ${LIVEPEER_API_KEY}` } }
);

// Asset structure:
// {
// id: "asset-id",
// playbackId: "playback-id",
// playbackUrl: "https://livepeercdn.studio/hls/{id}/index.m3u8",
// downloadUrl: "...",
// status: { phase: "ready", progress: 100 },
// videoSpec: { duration: 1234 }
// }

Authentication & Smart Wallets

Overview

Spritz supports multiple authentication methods, all providing access to a Safe Smart Account for on-chain transactions.

Two Address System

AddressPurposeSource
Spritz IDSocial identity (profile, friends, messages)Auth-dependent
Spritz WalletOn-chain funds (Safe Smart Account)Derived from signer

Authentication Methods

MethodSpritz IDWallet SignerImmediate Wallet?
EVM WalletWallet addressWallet EOA✅ Yes
PasskeyHash of credential IDWebAuthn P-256✅ Yes
EmailDerived or existingPasskey (must create)❌ No
World IDnullifier_hashPasskey (must create)❌ No
Alien IDalienAddressPasskey (must create)❌ No
SolanaSolana addressPasskey (must create)❌ No
Alien ID Dual-Flow Authentication

Alien ID supports two authentication flows: SSO (browser-based via @alien_org/sso-sdk-react) and Mini App (embedded inside the Alien app via @alien_org/bridge). Both flows validate a JWT token and create a session with wallet_type = "alien_id". Alien ID users are automatically joined to the official "alien" channel on first login. See Alien Integration for the complete technical guide.

Safe Smart Account Architecture

┌─────────────────────────────────────────────────────────────┐
│ Safe Smart Account │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ Safe Proxy v1.4.1 │ │
│ │ Address: Deterministic across all chains│ │
│ └─────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ Owner(s) │ │
│ │ - EOA Wallet Address │ │
│ │ OR │ │
│ │ - WebAuthn Signer (P-256 passkey) │ │
│ │ + Optional Recovery Signer │ │
│ └─────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ ERC-4337 Integration │ │
│ │ - EntryPoint v0.7 │ │
│ │ - Pimlico Bundler │ │
│ │ - Pimlico Paymaster (sponsorship) │ │
│ └─────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

Supported Chains

ChainIDGas PaymentPimlico Network
Ethereum1USDC (ERC-20 paymaster)ethereum
Base8453Sponsoredbase
Arbitrum42161Sponsoredarbitrum
Optimism10Sponsoredoptimism
Polygon137Sponsoredpolygon
BNB Chain56Sponsoredbinance
Unichain130Sponsoredunichain
Avalanche43114Sponsoredavalanche

WebAuthn/Passkey Signing

For passkey users, transactions are signed using WebAuthn:

// Create WebAuthn account from passkey
const webAuthnAccount = toWebAuthnAccount({
credential: {
id: credentialId, // Base64url credential ID
publicKey: `0x${x}${y}`, // P-256 public key (64 bytes)
},
rpId: "spritz.chat", // Relying party ID
});

// Create Safe with WebAuthn owner
const safeAccount = await toSafeSmartAccount({
client: publicClient,
owners: [webAuthnAccount],
version: "1.4.1",
entryPoint: { address: entryPoint07Address, version: "0.7" },
saltNonce: BigInt(0),
// WebAuthn verification contracts
safeWebAuthnSharedSignerAddress:
"0x94a4F6affBd8975951142c3999aEAB7ecee555c2",
safeP256VerifierAddress: "0xA86e0054C51E4894D88762a017ECc5E5235f5DBA",
});

Gas Sponsorship

L2 Chains: Transactions are sponsored via Pimlico's sponsorship policy.

// Get paymaster context for sponsored chains
const paymasterContext = { sponsorshipPolicyId: policyId };

const client = createSmartAccountClient({
account: safeAccount,
chain,
bundlerTransport: http(pimlicoBundlerUrl),
paymaster: pimlicoClient,
paymasterContext,
});

Ethereum Mainnet: Users pay gas in USDC via ERC-20 paymaster.

// ERC-20 paymaster for mainnet
const paymasterContext = {
token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
};

AI Agents

RAG Architecture

┌─────────────────────────────────────────────────────────────┐
│ RAG Pipeline │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. Knowledge Indexing │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ URL │ ─► │ Fetch │ ─► │ Chunk │ │
│ │ Input │ │ Content │ │ (~500 │ │
│ └──────────┘ └──────────┘ │ tokens) │ │
│ └────┬─────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Generate │ │
│ │ Embeddings │ │
│ │ (768 dims) │ │
│ └──────┬───────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ pgvector │ │
│ │ Storage │ │
│ └──────────────┘ │
│ │
│ 2. Query Processing │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ User │ ─► │ Embed │ ─► │ Vector │ │
│ │ Query │ │ Query │ │ Search │ │
│ └──────────┘ └──────────┘ │ (cosine) │ │
│ └────┬─────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Top-K chunks │ │
│ │ + Context │ │
│ └──────┬───────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Gemini │ │
│ │ Generation │ │
│ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

Embedding Model

  • Model: Google text-embedding-004
  • Dimensions: 768
  • Index: IVFFlat (approximate nearest neighbor)
CREATE INDEX idx_knowledge_chunks_embedding
ON shout_knowledge_chunks
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
CREATE FUNCTION match_knowledge_chunks(
p_agent_id UUID,
p_query_embedding vector(768),
p_match_count INT DEFAULT 5,
p_match_threshold FLOAT DEFAULT 0.3
)
RETURNS TABLE (id UUID, content TEXT, similarity FLOAT, metadata JSONB)
AS $$
SELECT
kc.id,
kc.content,
1 - (kc.embedding <=> p_query_embedding) AS similarity,
kc.metadata
FROM shout_knowledge_chunks kc
WHERE
kc.agent_id = p_agent_id
AND 1 - (kc.embedding <=> p_query_embedding) > p_match_threshold
ORDER BY kc.embedding <=> p_query_embedding
LIMIT p_match_count;
$$ LANGUAGE sql;

x402 Payments

Protocol Flow

┌─────────────────────────────────────────────────────────────┐
│ x402 Payment Flow │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. Client requests agent chat │
│ ┌──────────┐ POST /api/public/agents/{id}/chat │
│ │ Client │ ────────────────────────────────────────► │
│ └──────────┘ │
│ │
│ 2. Server returns 402 Payment Required │
│ ◄────────────────────────────────────────────────────── │
│ { │
│ "error": "Payment Required", │
│ "paymentRequirements": { │
│ "x402Version": 1, │
│ "accepts": [{ network: "base", amount: "10000" }] │
│ } │
│ } │
│ │
│ 3. Client pays via x402-fetch │
│ ┌──────────┐ Automated payment │
│ │ x402- │ ─────────────────────────────────────────► │
│ │ fetch │ X-Payment header with signed proof │
│ └──────────┘ │
│ │
│ 4. Server verifies payment, processes request │
│ ◄────────────────────────────────────────────────────── │
│ { "message": "Agent response..." } │
│ │
└─────────────────────────────────────────────────────────────┘

Supported Networks

  • Base (mainnet): Production payments
  • Base Sepolia: Testing

Integration

import { wrapFetch } from "x402-fetch";

const paidFetch = wrapFetch(fetch, wallet);
const response = await paidFetch(
"https://app.spritz.chat/api/public/agents/{id}/chat",
{
method: "POST",
body: JSON.stringify({ message: "Hello!" }),
}
);

Security Architecture

Authentication Security

LayerImplementation
SIWE/SIWSCryptographic signature verification
JWT SessionsHTTP-only cookies (7 days)
CSRF ProtectionOrigin validation
Rate LimitingUpstash Redis + sliding window

Data Security

Data TypeProtection
MessagesE2E encrypted (ECDH + AES-GCM)
Passkey KeysDevice-only storage (WebAuthn)
API KeysServer-side only (never exposed)
Session TokensHTTP-only, secure, same-site

Smart Wallet Security

RiskMitigation
Passkey LossRecovery signer option
Gas GriefingSponsorship policies with limits
PhishingDomain-bound passkeys

Tech Stack Summary

CategoryTechnology
FrameworkNext.js 16 (App Router)
LanguageTypeScript (strict)
StylingTailwind CSS 4
AnimationsMotion (Framer Motion)
3D GraphicsThree.js + React Three Fiber
Web3 (EVM)viem, wagmi, permissionless.js
Web3 (Solana)@solana/wallet-adapter
Account AbstractionPimlico, Safe v1.4.1
Wallet ConnectionReown AppKit
Video CallsHuddle01 SDK
LivestreamingLivepeer (WHIP + HLS)
MessagingLogos Messaging (Waku)
AI/LLMGoogle Gemini 2.0 Flash
EmbeddingsGoogle text-embedding-004
Vector SearchPostgreSQL pgvector
DatabaseSupabase (PostgreSQL + Realtime)
Token DataThe Graph Token API
Push NotificationsWeb Push API
Paymentsx402 Protocol
Digital IdentityWorld ID, Alien ID

Deployment

Production Stack

  • Hosting: Vercel (Edge Runtime)
  • Database: Supabase (managed PostgreSQL)
  • CDN: Vercel Edge + Livepeer CDN
  • Monitoring: Vercel Analytics

Environment Variables

See Developer Installation Guide for complete configuration.


Next Steps