Appearance
Dashboard & Admin Stack
VelaPay has two operational app surfaces: the merchant dashboard and the admin dashboard. They share a common architectural pattern but serve distinct user roles and have separate deployment targets.
Merchant Dashboard (vela-dashboard)
The primary product surface for merchants. Plan creation, subscriber management, revenue analytics, and Arcium-encrypted billing insights.
| Area | Current Choice |
|---|---|
| UI Framework | React 19 |
| Routing | TanStack Router (file-based, fully type-safe) |
| Data Fetching | TanStack Query (server state management) |
| API Layer | Hono (mounted as /api/* routes) |
| Authentication | Better Auth (email-first, SIWS for wallet linking) |
| Database | Drizzle ORM + Cloudflare D1 |
| Build Tooling | Vite |
| Deployment | Cloudflare Worker (Worker-compatible backend) |
| Infra Touchpoints | Helius webhooks, D1, KV (sessions), R2 (exports/assets), Queues |
Key Features
- Plan builder: create subscription plans with pricing, frequency, trial periods, billing mode (fixed/streaming/usage)
- Subscriber management: active, churned, past-due status with real-time updates via Helius webhooks
- Revenue analytics: MRR, churn, ARPU computed inside Arcium MXE, decrypted client-side for the merchant only
- Invoice management: auto-generated invoices, PDF delivery, failed payment handling with grace periods
- Payment links: shareable URLs (
pay.velapay.com/link/{slug}) with conversion analytics - Webhook integration: idempotent event processing for
pull_executed,subscription_created,subscription_cancelled
Admin Dashboard (vela-admin)
The protocol operator surface. Protocol configuration, audit logging, and cross-merchant visibility.
| Area | Current Choice |
|---|---|
| UI Framework | React 19 |
| Routing + Data | TanStack Router + TanStack Query |
| API/Runtime | Hono on Cloudflare Worker |
| Signing Model | Browser wallet signing (SIWS) |
| Audit Model | Server-side audit logging for all admin actions |
| Cross-Service Pattern | Service binding into the dashboard backend |
Key Features
- Protocol config management (pause/unpause, fee structure, keeper settings)
- Admin-level mandate inspection
- Audit trail for all operator actions
- Cross-merchant analytics (aggregate, not per-merchant detail)
Shared Architectural Pattern
Both apps use the same stack and differ only in deployment target and data access patterns:
┌─────────────────────────────────────────┐
│ React 19 + TanStack Router/Query │
│ (type-safe routing, server state) │
├─────────────────────────────────────────┤
│ Hono API Layer │
│ (middleware, routes, validation) │
├─────────────────────────────────────────┤
│ Drizzle ORM → D1 │
│ (type-safe queries, batch API) │
├─────────────────────────────────────────┤
│ Cloudflare Worker │
│ (edge deployment, bindings) │
└─────────────────────────────────────────┘The dashboard owns the D1 database. The admin does not have its own database — it accesses the dashboard's data through a service binding.
Auth Stack
Better Auth: Email-First with Wallet Linking
The auth system is built on Better Auth, which provides:
- Email + password as the primary authentication method
- Magic link fallback (email-based, no password required)
- SIWS (Sign-In With Solana) for wallet-based authentication
- Session management via Cloudflare KV (session tokens stored in KV, not D1)
Why Better Auth
| Better Auth | Alternatives |
|---|---|
| Native D1 + Drizzle adapter | Prisma — no D1 support |
| Hono middleware | NextAuth — Next.js-centric |
| Self-hosted, no vendor lock-in | Clerk/Auth0 — vendor lock-in, usage pricing |
batch() API for D1 atomicity | D1 doesn't support interactive transactions |
Auth Flow
- Merchant signs up via email → Better Auth creates user in D1
- Merchant links wallet via SIWS → wallet address stored in D1, associated with user
- Session created in KV → API middleware validates on every request
- Wallet operations (signing transactions) happen client-side via Wallet Standard
Portal Auth (vela-portal)
The customer portal uses a different auth pattern:
- SIWS standard — domain-bound signing (
portal.velapay.com), phishing-resistant - Magic link fallback — subscriber can auth via email if they provided one at checkout
- Multi-wallet linking — subscriber can link multiple wallets to see all mandates
- Session tiers — 4-hour browsing session, fresh SIWS for destructive actions (cancel, switch plan)
Data Layer
Drizzle ORM + Cloudflare D1
| Aspect | Detail |
|---|---|
| ORM | Drizzle 0.45.x, first-class D1 support |
| Dialect | SQLite (D1 is SQLite-compatible) |
| Migrations | drizzle-kit generate for SQL migrations, drizzle-kit push for dev |
| Atomicity | D1 batch() API — no interactive transactions |
| Migration path | Drizzle's dialect abstraction enables Postgres migration at PMF without schema rewrite |
D1 Batch API for Atomicity
D1 does not support interactive transactions (BEGIN/COMMIT). Instead, Drizzle uses D1's batch() API, which groups multiple statements into a single atomic operation. This is the correct pattern for:
- Invoice creation + line item insertion
- Session creation + user metadata update
- Payment event processing + subscriber state update
Key Database Tables
| Table | Purpose |
|---|---|
users | Better Auth user records |
sessions | Auth sessions |
plans | Subscription plan definitions |
checkout_sessions | Hosted checkout session state |
payment_links | Shareable payment link metadata |
invoices | Invoice records with PDF references |
billing_events | Helius webhook event log (idempotent) |
subscriber_snapshots | Denormalized subscriber state for dashboard queries |
Service Binding: Admin → Dashboard
The admin dashboard does not duplicate the backend. Instead, it uses Cloudflare service bindings to call the dashboard's API:
toml
# vela-admin/wrangler.toml
[[services]]
binding = "DASHBOARD"
service = "vela-dashboard-api"This means:
- Admin's Hono routes proxy to the dashboard backend via
env.DASHBOARD.fetch() - Admin authenticates with its own auth token (
DASHBOARD_PROXY_AUTH_TOKEN) - No public API exposure between admin and dashboard — internal Worker-to-Worker communication
- Admin can access all dashboard data without exposing a separate database connection
This pattern also applies to vela-checkout and vela-portal, which both bind to the dashboard for session and invoice data.
Why They're Separate
The split is a real product boundary, not just a code organization choice:
| Aspect | Dashboard | Admin |
|---|---|---|
| Users | Merchants | Protocol operators |
| Data ownership | Per-merchant isolation | Cross-merchant aggregate |
| Auth model | Email + wallet | Wallet-only (admin keypair) |
| Deployment coupling | Owns D1 database | Service binding, no own DB |
| Audit requirements | Standard | Enhanced (all actions logged) |
| Scaling | Per-merchant traffic | Low traffic, high privilege |
The v1.2 planning and audit trail show this split is now a real operational boundary, not a future design note. The admin surface handles protocol-level operations that should never be exposed to merchant traffic, and the service-binding pattern ensures the admin never needs direct database access.