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.