Appearance
Cloudflare Infrastructure
Cloudflare is the common infrastructure substrate across all non-protocol surfaces. Every app, worker, and static site in the workspace deploys to Cloudflare Workers.
Full Usage Map
| Cloudflare Product | Role in VelaPay | Repos Using It |
|---|---|---|
| Workers | Compute for all app surfaces | Dashboard, admin, web, docs, widget, checkout, portal, webhook, synthetic |
| D1 | SQLite database for operational state and audit data | Dashboard, checkout, portal |
| Queues | Async event processing (billing events, webhook fan-out, invoice generation) | Dashboard, webhook |
| KV | Session storage, caching | Dashboard (auth sessions), portal (SIWS sessions) |
| R2 | File storage (invoice PDFs, merchant assets, exports) | Dashboard, checkout |
| Turnstile | Bot protection on public-facing surfaces | Checkout (checkout page), web (auth pages) |
| DNS | Domain routing for velapay.com and subdomains | All |
| Web Analytics | Privacy-aware analytics (no cookies, GDPR-compliant) | Web |
| Service Bindings | Internal Worker-to-Worker communication | Admin → Dashboard, Checkout → Dashboard, Portal → Dashboard |
Service Binding Pattern
The key architectural pattern is service bindings for internal communication:
vela-admin ──binding──→ vela-dashboard-api
vela-checkout ──binding──→ vela-dashboard-api
vela-portal ──binding──→ vela-dashboard-workerThis means:
- No public API exposure between services — all internal traffic stays on Cloudflare's network
- Admin, checkout, and portal authenticate with the dashboard via
DASHBOARD_PROXY_AUTH_TOKEN - The dashboard owns the D1 database; other services access it through the binding
- No CORS, no rate limiting between internal services — just Worker-to-Worker
fetch()
This is a key v1.2 architectural outcome and should be preserved in future infra decisions.
D1: Database Layer
Current State
- Product: Cloudflare D1 (GA)
- Limit: 10 GB per database on paid plan
- ORM: Drizzle 0.45.x with SQLite dialect
- Migrations:
drizzle-kit generatefor SQL,drizzle-kit pushfor dev
D1 Batch API for Atomicity
D1 does not support interactive transactions (BEGIN/COMMIT/ROLLBACK). Instead, Drizzle uses D1's batch() API:
typescript
// Atomic: both succeed or both fail
await env.DB.batch([
db.insert(invoices).values({ ... }),
db.insert(invoiceLineItems).values([ ... ]),
]);This is the correct pattern for any multi-step write in the dashboard:
- Invoice creation + line item insertion
- Checkout session creation + metadata update
- Billing event processing + subscriber state update
- Payment link creation + analytics initialization
D1 Schema Health
The v1.2 planning artifacts record a migration risk around snapshot tables. The planning layer has identified gaps where snapshot migrations may have introduced inconsistencies. This means:
- D1 docs here should be read alongside the audit state in
.planning/ - Not every schema path is currently verified as healthy
- Future migration work should include a schema verification step
Key Tables
| Table | Rows | Purpose |
|---|---|---|
users | Auth records | Better Auth managed |
plans | Subscription plans | Merchant-defined |
checkout_sessions | Session state | Hosted checkout |
payment_links | Shareable links | Payment link metadata |
invoices | Invoice records | Auto-generated from billing events |
billing_events | Webhook event log | Idempotent processing |
subscriber_snapshots | Denormalized state | Dashboard query optimization |
Queues: Async Event Processing
| Queue | Purpose | Producer | Consumer |
|---|---|---|---|
BILLING_QUEUE | Billing event processing, invoice generation | Webhook worker | Dashboard worker |
WEBHOOK_FANOUT | Webhook delivery to merchant endpoints | Dashboard worker | Dashboard worker |
Queue pricing: 10K ops/day free. Paid: $0.40/million ops after 1M free.
Postgres Migration Path at PMF
D1 is the current database for all operational state. The migration path to Postgres at product-market fit is:
- Drizzle's dialect abstraction: Switch from
sqlitedialect topgdialect in Drizzle config. Most queries are portable. - Migration tooling:
drizzle-kit generateproduces SQL migrations for either dialect. - Runtime change: Swap D1 binding for a Postgres connection string. Drizzle's query builder is dialect-agnostic.
- What changes: D1
batch()calls become standard Postgres transactions. SQLite-specific features (likeRETURNINGbehavior) may need adjustment. - What doesn't change: Drizzle schema definitions, TypeScript types, query structure.
The trigger for migration is D1's per-database 10 GB limit or query performance at scale. Until PMF, D1 is the right choice — zero ops, built-in replication, and Cloudflare's 40-60% latency improvements.
Workers: Deployment Details
Domain Map
| Domain | Service | Role |
|---|---|---|
velapay.com | vela-web | Landing page |
docs.velapay.com | vela-docs | Developer documentation |
app.velapay.com | vela-dashboard | Merchant dashboard |
admin.velapay.com | vela-admin | Protocol admin |
pay.velapay.com | vela-checkout | Hosted checkout |
portal.velapay.com | vela-portal | Customer self-service |
js.velapay.com | vela-widget | Embeddable widget assets |
Static Assets
Workers now handles static assets natively (the former Pages model). Static asset requests are free. No need for a separate Pages deployment.
Worker Compatibility
All Workers use compatibility_date = "2026-04-01" and compatibility_flags = ["nodejs_compat"]. The nodejs_compat flag is required for Drizzle's D1 adapter and some Hono middleware.
Free Tier Limits and Pricing
| Product | Free Tier | Paid |
|---|---|---|
| Workers | 100K requests/day | $0.50/million requests |
| D1 | 5M rows read/day, 100K rows written/day | $0.75/million rows read, $0.001/million rows written |
| KV | 100K reads/day, 1K writes/day | $0.50/million reads, $5/million writes |
| R2 | 10 GB storage, 10M reads/month | $0.015/GB/month, $0.36/million reads |
| Queues | 10K ops/day | $0.40/million ops |
| Turnstile | Unlimited | Free |
| DNS | Free | Free |
| Web Analytics | Free | Free |
At current scale (pre-PMF), the free tier covers nearly everything. Costs become relevant at merchant volumes in the thousands.
KV: Session Management
Both dashboard and portal use KV for session storage:
- Dashboard: Better Auth sessions in KV. Faster than D1 for session lookups (edge-cached, no database query).
- Portal: SIWS sessions in KV with TTL-based expiry. 4-hour browsing sessions, 7-day "remember device" sessions.
KV is the right choice for session data because:
- Sub-millisecond reads (edge-cached)
- TTL-based expiry (no cron cleanup needed)
- No schema constraints (flexible session data)
R2: File Storage
Used for:
- Invoice PDFs: Generated by
pdf-libin Workers, stored in R2, referenced by URL - Merchant assets: Logos for branded checkout pages
- Exports: CSV downloads of billing data
R2 is S3-compatible but with zero egress fees. This is important for invoice PDFs — merchants and subscribers download them without generating bandwidth costs.