Skip to content

Hosted Billing Surfaces

Overview

VelaPay has three billing surface architectures that serve different merchant integration patterns:

  1. vela-widget: Embedded checkout for merchant pages (iframe overlay).
  2. vela-checkout: Hosted checkout at pay.velapay.com (standalone page).
  3. vela-portal: Subscriber self-service at portal.velapay.com (management portal).

Why vela-checkout and vela-portal Are Separate Repos

From vela-dashboard

Both vela-checkout and vela-portal were extracted from vela-dashboard in v1.5 because they have fundamentally different runtime concerns:

Dimensionvela-dashboardvela-checkoutvela-portal
PurposeMerchant managementPayment collectionSubscriber self-service
Auth modelEmail-primary + orgPublic (Turnstile-gated)Magic-link + SIWS
AudienceMerchants (authenticated)Anyone with a payment linkSubscribers (authenticated)
RuntimeRailway SSR + CF Worker APICF Worker onlyCF Worker only
Domainapp.velapay.compay.velapay.comportal.velapay.com
Deploy cadenceMerchant feature changesCheckout UX changesPortal feature changes
Security boundaryInternal — merchant dataPublic — payment flowPublic — subscription management

Why Separate from Each Other

Checkout and portal are also separate from each other because:

  • Different user flows: Checkout is a linear payment flow. Portal is a management interface.
  • Different auth requirements: Checkout needs no auth (just Turnstile bot protection). Portal needs subscriber auth (magic-link or SIWS).
  • Different scaling patterns: Checkout has bursty traffic during payment campaigns. Portal has steady traffic from subscriber management.
  • Different failure modes: A checkout outage blocks new revenue. A portal outage blocks self-service (less urgent).

Widget vs Hosted Checkout Architecture

vela-widget: Embedded Checkout

The widget is an iframe overlay that merchants embed on their own pages:

┌─────────────────────────────────────────────────┐
│  Merchant's Page (merchant.com)                  │
│                                                  │
│  ┌────────────────────────────────────────────┐  │
│  │  <vela-pricing-table> or checkout widget   │  │
│  │  ┌──────────────────────────────────────┐  │  │
│  │  │  vela-widget iframe                  │  │  │
│  │  │  (checkout UI, plan selection)       │  │  │
│  │  └──────────────────────────────────────┘  │  │
│  └────────────────────────────────────────────┘  │
│                                                  │
│  Wallet injection happens HERE (parent page)     │
│  postMessage bridges wallet ↔ iframe             │
└─────────────────────────────────────────────────┘

Key architectural decisions:

  • Loader script: Small (~5kb) script that merchants include on their page. Handles wallet discovery and iframe creation.
  • iframe isolation: Checkout UI runs in an iframe with strict CSP. No access to parent page DOM.
  • Wallet access: Wallets inject into the parent page (not the iframe). The loader handles wallet discovery and bridges signing requests via postMessage.
  • Shadow DOM with DSD: Widget rendering uses Shadow DOM with Declarative Shadow DOM support for SSR-compatible theming.
  • Theming: CSS custom properties flow from the merchant's page into the iframe via postMessage.

vela-checkout: Hosted Checkout

Hosted checkout is a standalone page at pay.velapay.com:

┌─────────────────────────────────────────────────┐
│  pay.velapay.com/checkout/:sessionId             │
│                                                  │
│  ┌────────────────────────────────────────────┐  │
│  │  Full checkout page                        │  │
│  │  • Plan details                            │  │
│  │  • Wallet connection (direct)              │  │
│  │  • Payment confirmation                    │  │
│  │  • Turnstile bot protection                │  │
│  └────────────────────────────────────────────┘  │
│                                                  │
│  Wallet connects directly (same page)            │
│  No iframe boundary                              │
└─────────────────────────────────────────────────┘

Key architectural decisions:

  • Direct wallet connection: No iframe boundary. Wallet connects directly to the page using standard wallet adapter.
  • Full URL routes: Each checkout session has a unique URL (pay.velapay.com/checkout/:sessionId). Shareable via payment links.
  • Payment links: Merchants create checkout sessions and share the URL via email, social media, or QR code.
  • Invoice pages: Extended checkout flow with line items, tax calculation, and PDF generation.

Architecture Comparison

DimensionWidget (Embedded)Checkout (Hosted)
Wallet connectionParent page → postMessage → iframeDirect connection on the page
DomainMerchant's domainpay.velapay.com
CSP scopeMerchant's CSP + widget CSPVelaPay CSP only
BrandingMerchant's page wraps widget iframeVelaPay-branded checkout
SharingNot shareable (embedded in page)Shareable via URL
Payment linksNot applicableYes — unique URL per session
InvoicesNot applicableYes — line items + PDF
Integration effortMedium (embed loader script)Low (share a link)
CustomizationLimited (theming via CSS vars)Minimal (VelaPay-branded)
SEO/marketingNonePayment link sharing, QR codes, UTM

Shared Checkout-Session Creation Pattern

Both the widget and hosted checkout use the same checkout-session creation pattern:

typescript
// Shared pattern (used by widget, checkout, and portal switch-plan)
interface CheckoutSession {
  id: string;                    // Unique session ID
  merchantId: string;            // Merchant org ID
  planId: string;                // Plan to subscribe to
  tokenMint: string;             // Token mint for payment
  amount: string;                // Billing amount (raw units)
  returnUrl?: string;            // Where to redirect after checkout
  metadata?: Record<string, string>; // Merchant-defined metadata
  expiresAt: number;             // Session expiry timestamp
}

Session Creation Flow

1. Merchant creates checkout session via API or dashboard

2. Session stored in D1 with unique ID

3a. Widget: Loader script fetches session, renders iframe
3b. Checkout: Payment link URL includes session ID
3c. Portal: Switch-plan flow creates new session and redirects

4. Subscriber completes checkout (wallet sign + mandate creation)

5. Session marked as completed in D1

6. Webhook fires to merchant's endpoint

Why a Shared Pattern

  • Consistency: Widget checkout and hosted checkout produce identical on-chain state.
  • Testability: Same session creation logic can be tested once and used everywhere.
  • Analytics: Checkout sessions are tracked uniformly regardless of surface.
  • Security: Same session validation and Turnstile verification across all surfaces.

Public Billing Apps Proxy to Dashboard Internal Routes

Checkout and portal are public-facing applications that proxy some operations to internal dashboard API routes:

vela-checkout (public)                  vela-dashboard (internal)
    │                                       │
    │  POST /api/checkout/create-session    │
    │──────────────────────────────────────→│
    │                                       │  Creates session in D1
    │  ← session ID                         │
    │                                       │
    │  GET /api/checkout/:sessionId         │
    │──────────────────────────────────────→│
    │                                       │  Reads session from D1
    │  ← session details                    │
    │                                       │
    │  POST /api/checkout/:sessionId/complete│
    │──────────────────────────────────────→│
    │                                       │  Marks complete, fires webhook
    │  ← success                            │

Why Proxy (Not Direct D1 Access)

  • Schema coordination: Dashboard owns the D1 schema. Checkout and portal access data through the dashboard API.
  • Business logic centralization: Session validation, webhook firing, and analytics are handled by the dashboard.
  • Auth boundary: Dashboard authenticates the request as coming from a known billing app, not from a random caller.

vela-portal Switch-Plan Flow

The portal's switch-plan flow reuses the checkout-session pattern:

Subscriber clicks "Switch Plan" in portal


Portal creates checkout session via dashboard API
(new plan, same subscriber, same merchant)


Portal redirects to checkout flow
(either embedded widget or hosted checkout)


Subscriber completes checkout
(creates upgrade instruction via SDK)


Session marked complete, webhook fires


Subscriber returns to portal with updated plan

Integration Patterns for Merchants

Merchant creates a checkout session in the dashboard and shares the URL:

pay.velapay.com/checkout/cs_abc123

No code required. Share via email, social media, or embed in a button.

Pattern 2: Embedded Widget (Medium)

Merchant includes the loader script on their page:

html
<script src="https://widget.velapay.com/loader.js"
        data-merchant="merchant_123"
        data-plan="plan_456">
</script>
<vela-pricing-table></vela-pricing-table>

Checkout happens in an iframe overlay. Wallet access bridges from the parent page.

Pattern 3: SDK Integration (Advanced)

Merchant uses @vela/sdk to build custom checkout flows:

typescript
import { VelaClient } from "@vela/sdk";

const client = new VelaClient(connection);
const subscribeIx = await client.createSubscription({
  plan: planAddress,
  subscriber: wallet.publicKey,
});

Full control over UX. Merchants build their own checkout experience using the SDK.

Pattern 4: Webhook Integration (Backend)

Merchant's backend receives billing events:

typescript
import { verifyWebhookSignature, parseEvent } from "@vela/webhook";

app.post("/webhooks/vela", async (req, res) => {
  const payload = verifyWebhookSignature(req.headers, req.body, secret);
  const event = parseEvent(payload);
  // Handle: mandate.created, pull.executed, subscription.cancelled, etc.
});

No frontend integration required. Backend-driven billing management.

Internal knowledge base for the Vela Labs workspace.