Appearance
Hosted Billing Surfaces
Overview
VelaPay has three billing surface architectures that serve different merchant integration patterns:
- vela-widget: Embedded checkout for merchant pages (iframe overlay).
- vela-checkout: Hosted checkout at pay.velapay.com (standalone page).
- 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:
| Dimension | vela-dashboard | vela-checkout | vela-portal |
|---|---|---|---|
| Purpose | Merchant management | Payment collection | Subscriber self-service |
| Auth model | Email-primary + org | Public (Turnstile-gated) | Magic-link + SIWS |
| Audience | Merchants (authenticated) | Anyone with a payment link | Subscribers (authenticated) |
| Runtime | Railway SSR + CF Worker API | CF Worker only | CF Worker only |
| Domain | app.velapay.com | pay.velapay.com | portal.velapay.com |
| Deploy cadence | Merchant feature changes | Checkout UX changes | Portal feature changes |
| Security boundary | Internal — merchant data | Public — payment flow | Public — 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
| Dimension | Widget (Embedded) | Checkout (Hosted) |
|---|---|---|
| Wallet connection | Parent page → postMessage → iframe | Direct connection on the page |
| Domain | Merchant's domain | pay.velapay.com |
| CSP scope | Merchant's CSP + widget CSP | VelaPay CSP only |
| Branding | Merchant's page wraps widget iframe | VelaPay-branded checkout |
| Sharing | Not shareable (embedded in page) | Shareable via URL |
| Payment links | Not applicable | Yes — unique URL per session |
| Invoices | Not applicable | Yes — line items + PDF |
| Integration effort | Medium (embed loader script) | Low (share a link) |
| Customization | Limited (theming via CSS vars) | Minimal (VelaPay-branded) |
| SEO/marketing | None | Payment 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 endpointWhy 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 planIntegration Patterns for Merchants
Pattern 1: Payment Links (Simplest)
Merchant creates a checkout session in the dashboard and shares the URL:
pay.velapay.com/checkout/cs_abc123No 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.