Skip to content

Architecture

Stellaroid Earn is a Next.js 15 (App Router) frontend deployed on Vercel that talks directly to a Soroban smart contract on Stellar testnet. There is no traditional backend — the chain is the system of record.

System overview

The system has two deployed pieces: a Next.js 15 (App Router) + React 19 frontend on Vercel, and a Soroban smart contract (Rust, soroban-sdk 22.0.0, compiled to wasm32v1-none) on Stellar testnet. All state — issuer records, certificate records, payment links, admin config — lives in contract storage. Reads reach the chain through Soroban RPC simulateTransaction; writes are signed in the user's wallet and submitted through the same RPC. The only server-side code is Next.js itself: React Server Components render proof pages, and four API routes: GET /api/health, GET /api/events and its /stream variant (read-only public event aggregation), and POST /api/fee-bump — an optional, bearer-token-gated endpoint that signs fee-bump transactions server-side with a dedicated testnet sponsor key.

Users (issuer / student / employer)
        |
        v
Next.js 15 frontend (Vercel)
  config layer -> wallet layer (Freighter | Albedo) -> contract client
        |                                  |
        | reads: simulateTransaction      | writes: signTransaction -> sendTransaction
        v                                  v
Soroban RPC  --  Stellar testnet
  stellaroid_earn contract
    - Issuer Registry      (persistent storage)
    - Certificate Store    (persistent storage)
    - Payment Links        (persistent storage)
    - Admin Config         (instance storage)
  XLM Stellar Asset Contract (SAC) - native asset

Access control is enforced in the contract, not the frontend: init, approve_issuer, suspend_issuer, and reward_student are admin-only; register_certificate, verify_certificate, revoke_certificate, and suspend_certificate require an approved issuer; register_issuer, link_payment, get_certificate, and get_issuer are public. Failures surface as a typed #[contracterror] enum with 17 variants.

Component breakdown

ComponentLocationResponsibility
Config layerfrontend/src/lib/config.tsReads NEXT_PUBLIC_* env vars (RPC URL, network, passphrase, contract ID, read address). The passphrase must match the network the contract is deployed on.
Wallet layerfrontend/src/lib/wallet/ + hooks/Provider registry with two wallets — Freighter (browser extension) and Albedo (web-based signer). Manages connection state, public key, network validation, and signTransaction.
Contract clientfrontend/src/lib/contract-client.ts, contract-read-server.tsBuilds transactions with @stellar/stellar-sdk; handles ScVal argument serialization, return-value decoding, and error normalization. Server-side reads have a dedicated module for RSC use.
UIfrontend/src/app/ + components/App Router routes (/app, /issuer, /proof/[hash], /proof/[hash]/embed, /about, …). Wallet-touching components are "use client" because wallet APIs are browser-only; proof pages render server-side.
Smart contractcontract/src/lib.rs (separate deploy)Issuer registry, certificate lifecycle, payment links. Persistent storage with 518,400–1,036,800 ledger TTLs; admin config in instance storage.
Soroban RPChttps://soroban-testnet.stellar.org (default)Simulation for reads, submission and polling for writes, getEvents for recent contract events.
Stellar Experthttps://stellar.expert/explorer/testnetExplorer links, plus a public contract-event index used to supplement RPC's limited event retention.
Friendbothttps://friendbot.stellar.orgFunds testnet accounts, including the read-only simulation address.

Data flow: the three core actions

All writes follow one pipeline in contract-client.ts: build the invocation → simulateTransaction to assemble it → sign in the connected wallet → sendTransaction → poll getTransaction until a terminal status. Reads stop after simulation and never submit anything.

1. Credential issuance and verification (issuer signs)

  1. An issuer calls register_issuer (public, signed by the issuer's own wallet); the admin then calls approve_issuer. These emit iss_reg and iss_appr events.
  2. The approved issuer registers a certificate via register_certificate — the transaction is simulated, signed in Freighter or Albedo, submitted, and polled to confirmation. Emits a cert_reg event carrying the proof hash.
  3. The issuer attests the credential with verify_certificate, which emits cert_ver. The student can now share the /proof/<hash> URL.

2. Public proof verification (nobody signs)

  1. Anyone opens /proof/<hash>. The route validates the 64-character hex format before making any RPC call.
  2. A React Server Component calls get_certificate through simulateTransaction, using the funded account in NEXT_PUBLIC_STELLAR_READ_ADDRESS as the transaction source. The simulation is never submitted — no signature, no fee, no on-chain footprint, and no events.
  3. The page renders certificate status, issuer trust evidence, and a verification breakdown, and is CDN-cached with revalidate=60. Client-side dashboard components use the same simulation mechanism directly for real-time state.

3. Employer payment (employer signs)

  1. An employer viewing a proof calls link_payment (public), signing with their own wallet through the same simulate → sign → submit → poll pipeline.
  2. XLM moves through the native Stellar Asset Contract (SAC) — no custom token, so graduates receive actual XLM with no trustline setup.
  3. The contract records a PaymentRecord (payer, amount, linked certificate hash) and emits a payment event with the amount. Admin-initiated reward_student calls emit a separate reward event.

Emitted events (init, iss_reg, iss_appr, iss_susp, cert_reg, cert_ver, reward, payment) are surfaced as public evidence via /api/events, a short-lived Server-Sent Events stream at /api/events/stream, and /status#metrics. Because RPC event retention only covers recent ledgers, the frontend supplements getEvents with Stellar Expert's public contract-event index and labels each item by source (rpc, stellar_expert, or e2e). This is display-grade evidence, not an audit-grade analytics store.

Deployment

ComponentPlatformDetail
FrontendVercelstellaroid.tech
ContractStellar testnetCDMUOHMARNVOJZM3IVOCJUPGBHDTHFBMZCCZXEZPQDVJGILH3NIKTTW3
Source verificationStellar ExpertFresh security-hardened deploy; source re-verification pending
Testnet only. Every RPC endpoint, contract ID, and payment flow in this stack targets Stellar testnet. There is no mainnet deployment, and no flow in this documentation should be pointed at mainnet.

Configuration is entirely env-var driven from lib/config.ts. The app refuses to operate meaningfully without NEXT_PUBLIC_SOROBAN_CONTRACT_ID and an RPC URL. Two test-only variables (NEXT_PUBLIC_E2E_MODE, NEXT_PUBLIC_PLAYWRIGHT) also flow through lib/config.ts but are gated to be inert outside local test runs, and NEXT_PUBLIC_CANONICAL_URL feeds the SEO/security layers.

VariableDefaultPurpose
NEXT_PUBLIC_STELLAR_RPC_URLhttps://soroban-testnet.stellar.orgSoroban RPC endpoint
NEXT_PUBLIC_STELLAR_NETWORKTESTNETNetwork name, mapped to the expected passphrase for wallet network checks
NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASETest SDF Network ; September 2015Must match the network the contract is deployed on
NEXT_PUBLIC_SOROBAN_CONTRACT_ID(none — required)Deployed contract address
NEXT_PUBLIC_SOROBAN_ASSET_ADDRESS(none)XLM SAC contract address
NEXT_PUBLIC_SOROBAN_ASSET_CODE / _DECIMALSXLM / 7Display metadata for the payment asset
NEXT_PUBLIC_STELLAR_READ_ADDRESS(none)Funded testnet G... account used only as the source for read-only simulations (fund via Friendbot)
NEXT_PUBLIC_STELLAR_ADMIN_ADDRESS(none)Admin public key, used for role detection in the UI
NEXT_PUBLIC_FEE_SPONSOR_ADDRESS(none)Fee-sponsorship account; sponsor signing sits behind server auth with contract/method/fee validation
NEXT_PUBLIC_STELLAR_EXPLORER_URLhttps://stellar.expert/explorer/testnetExplorer link base

One build-time value is set automatically in next.config.ts: NEXT_PUBLIC_SW_BUILD is derived from VERCEL_GIT_COMMIT_SHA (first 12 characters, with a timestamp fallback) and stamps the service worker registration URL per deploy.

Notable technical decisions

Reads via simulation with a dedicated read address

Public proof verification must not require a wallet — that is critical for employer adoption. Read calls are built as normal contract invocations but only ever passed to simulateTransaction, sourced from the funded read-only account. Nothing is signed or submitted, so verification is free, anonymous, and cacheable (revalidate=60 on proof pages).

Nonce-based CSP assembled in middleware

Static security headers (X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy, Permissions-Policy, and 2-year preloaded HSTS) come from next.config.ts. The Content-Security-Policy itself is built per-request in src/middleware.ts: a fresh nonce is generated with crypto.getRandomValues, passed to the app via an x-nonce header, and embedded in script-src — so production scripts run without 'unsafe-inline'. connect-src is restricted to 'self' and https://*.stellar.org. frame-ancestors is 'none' everywhere except /proof/<hash>/embed (matched against the 64-hex pattern), which allows framing so proofs can be embedded; CSP frame-ancestors takes precedence over X-Frame-Options in modern browsers.

default-src 'self';
script-src 'self' 'nonce-<per-request>' https://va.vercel-scripts.com;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
font-src 'self' https://fonts.gstatic.com;
img-src 'self' data: blob:;
worker-src 'self';
manifest-src 'self';
connect-src 'self' https://*.stellar.org;
frame-src 'none';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none'   (frame-ancestors * on /proof/<hash>/embed only)

A deliberately conservative service worker

The PWA service worker (frontend/public/sw.js) is scoped narrowly because the app's data is on-chain and staleness is dangerous:

  • Navigations: network-first, falling back to a cached copy of that page, then /offline.html (page cache capped at 30 entries).
  • /_next/static/*: cache-first — these assets are content-hashed and immutable.
  • Same-origin images, fonts, and icons: stale-while-revalidate.
  • /api/* and all cross-origin requests (Soroban RPC, fonts CDN): untouched — always network.
  • Framed navigations (the /proof/<hash>/embed route) are skipped entirely, because a cached fallback cannot reproduce the route's frame-ancestors headers.

The worker is registered as /sw.js?v=<build id> and names its caches with that version, so each deploy installs a new worker, refreshes the precache, and purges the previous deploy's caches. next.config.ts additionally serves /sw.js with Cache-Control: no-cache, no-store, must-revalidate so the new worker takes over quickly.

Verification pages are never served from cache

Pages under /proof, /talent, and /opportunity are explicitly excluded from the service worker's page cache. The comment in sw.js states the rationale: a stale on-chain verdict is worse than no page — a revoked credential replayed from cache as "verified" would invert the product's core guarantee. If the network is unavailable, these routes fall through to the offline page rather than a cached verdict.

Other decisions

DecisionRationale
Soroban over classic StellarThe issuer trust layer and credential lifecycle states need custom logic that classic offers/payments cannot express
XLM via SAC, not a custom tokenGraduates receive actual XLM; no trustline to a custom asset required
Persistent storage with long TTLsCredentials should outlive short-term contract state; 518,400–1,036,800 ledger TTLs provide months of persistence
Typed #[contracterror] enumClear, actionable errors instead of opaque integer codes
Fee sponsorship behind server authBearer authorization plus contract/method/fee validation prevents arbitrary public XDR from being sponsor-signed
Public indexer fallback for event metricsRPC event retention covers only recent ledgers; Stellar Expert's index keeps older public evidence visible without claiming first-party analytics

Frequently asked questions

Does Stellaroid Earn have a backend server?
No traditional backend. The Soroban contract on Stellar testnet is the system of record. The only server-side code is the Next.js app itself: React Server Components that render proof pages via read-only simulation, and four API routes: GET /api/health, GET /api/events and its /stream variant (read-only public event aggregation), and POST /api/fee-bump — an optional, bearer-token-gated endpoint that signs fee-bump transactions server-side with a dedicated testnet sponsor key.
How do proof pages verify credentials without a wallet?
The /proof/[hash] route validates the 64-character hex hash, then calls the contract's get_certificate function through Soroban RPC simulateTransaction, using a dedicated funded read-only address as the transaction source. Simulations are never signed or submitted, so verification requires no wallet, no login, and no fee.
Which wallets are supported for signing?
Two providers are registered in the wallet layer: Freighter (browser extension) and Albedo (web-based signer, which enables mobile signing). All writes go through the connected provider: build transaction, simulate, sign in the wallet, submit via sendTransaction, then poll getTransaction until confirmed.
Is anything deployed to mainnet?
No. This is an early-access pilot on Stellar testnet only. The contract (CDMUOHMARNVOJZM3IVOCJUPGBHDTHFBMZCCZXEZPQDVJGILH3NIKTTW3) is deployed to testnet, the default RPC endpoint is soroban-testnet.stellar.org, and all payment flows use testnet XLM.
Why doesn't the service worker cache proof pages?
Because a stale on-chain verdict is worse than no page. If a revoked credential were replayed from cache as verified, it would invert the product's core guarantee. Pages under /proof, /talent, and /opportunity always hit the network, and /api/* plus cross-origin RPC requests bypass the service worker entirely.

Run it yourself on testnet

Everything documented here is live in the early-access pilot — free, testnet-only, and auditable on stellar.expert.

Architecture — Next.js frontend, Soroban contract | Stellaroid Earn