Skip to content

vela-widget

Embeddable checkout and subscription UX layer that merchants include on their own pages.

Purpose and Role

The product layer that turns protocol capabilities into an understandable subscriber flow. Merchants embed the widget to offer:

  • Checkout flow for new subscriptions.
  • <vela-pricing-table> component for plan selection.
  • Wallet-native mandate UX that would be unclear or unsafe without guided flow.

The widget exists because wallet-native mandate UX cannot be assumed to be clear or safe enough on its own.

Tech Stack

TechnologyVersionPurpose
React19.xCheckout UI in iframe
TypeScript5.8+Language
@vela/sdklatestCheckout transaction building (browser-safe barrel)
Tailwind CSS4.2.xStyling
Shadow DOMStyle isolation from parent page
postMessage APIWallet ↔ iframe communication
Cloudflare WorkersHosting for loader script and iframe bundle

Directory Structure

vela-widget/
├── loader/                    # Small script injected on merchant pages
│   ├── src/
│   │   ├── index.ts           # Loader entry — discovers wallet, creates iframe
│   │   ├── wallet-detect.ts   # Parent page wallet discovery
│   │   └── postmessage.ts     # Typed postMessage relay
│   └── package.json           # Published as separate bundle (~5kb)
├── checkout/                  # Full iframe checkout app
│   ├── src/
│   │   ├── app/
│   │   │   ├── components/
│   │   │   │   ├── CheckoutFlow.tsx    # Main checkout flow
│   │   │   │   ├── PlanSelector.tsx    # Plan selection UI
│   │   │   │   ├── WalletConnect.tsx   # Wallet connection (via postMessage)
│   │   │   │   └── Confirmation.tsx    # Transaction confirmation
│   │   │   └── hooks/
│   │   │       ├── useCheckout.ts      # Checkout session management
│   │   │       └── useWalletSign.ts    # Wallet signing via postMessage
│   │   └── index.tsx           # iframe app entry
│   ├── tailwind.css
│   └── package.json
├── shared/                    # Shared types for postMessage protocol
│   ├── messages.ts            # Typed postMessage definitions
│   └── events.ts              # Widget event types
├── vite.config.ts             # Build config (separate bundles for loader + iframe)
└── package.json

Deployment Target

  • Cloudflare Workers: Static assets for loader script and iframe bundle.
  • CDN: Loader script served from widget.velapay.com/loader.js.
  • iframe: Checkout app loaded from widget.velapay.com/checkout/.

Dependencies

What It Depends On

DependencyTypePurpose
@vela/sdknpm package (browser-safe)Checkout transaction building, PDAFactory

What Depends on It

Nothing. Merchants embed the widget; no other repo depends on it.

Current Status

  • v1.1 complete: Loader script, iframe checkout, postMessage relay.
  • v1.5 updates: <vela-pricing-table> component, Shadow DOM with DSD, theming.
  • v1.8 updates: Multi-token plan rendering via Phase 46 TokenConfig resolution.

Notable Design Decisions

Wallet-in-Parent, UI-in-Iframe Architecture

Wallets inject into the parent page (merchant's website), not the iframe. The loader handles wallet discovery on the parent page, and the iframe handles checkout UX. Typed postMessage bridges signing requests between the two contexts.

This split exists because:

  • Wallet extensions (Phantom, Solflare) inject into the top-level window.
  • iframes have a different origin — wallet injection doesn't propagate.
  • The loader (on parent page) can access window.solana or window.phantom.
  • The iframe (VelaPay origin) renders checkout UI with strict CSP.

Shadow DOM with Declarative Shadow DOM (DSD)

Widget rendering uses Shadow DOM for style isolation from the parent page. DSD support enables SSR-compatible theming — the widget renders correctly before JavaScript loads.

Typed postMessage Protocol

All communication between parent and iframe uses a typed protocol:

typescript
// Parent → iframe
type ParentMessage =
  | { type: "WALLET_AVAILABLE"; wallet: WalletAdapter }
  | { type: "SIGNATURE_RESULT"; signature: string }
  | { type: "SIGNATURE_ERROR"; error: string };

// iframe → Parent
type iframeMessage =
  | { type: "SIGN_REQUEST"; transaction: Buffer }
  | { type: "CHECKOUT_COMPLETE"; mandateAddress: string }
  | { type: "CHECKOUT_CANCELLED" };

Strict CSP in iframe

The iframe has a strict Content Security Policy that prevents access to parent page DOM, cookies, or storage. Only postMessage communication is allowed.

~5kb Loader Script

The loader is intentionally tiny — merchants won't embed a large script on their pages. All heavy UI loads lazily in the iframe after the merchant page has rendered.

Browser-Safe SDK

The widget uses @vela/sdk/browser barrel which excludes Node.js-only dependencies. This keeps the iframe bundle small and compatible with browser environments.

Internal knowledge base for the Vela Labs workspace.