Skip to content

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 ProductRole in VelaPayRepos Using It
WorkersCompute for all app surfacesDashboard, admin, web, docs, widget, checkout, portal, webhook, synthetic
D1SQLite database for operational state and audit dataDashboard, checkout, portal
QueuesAsync event processing (billing events, webhook fan-out, invoice generation)Dashboard, webhook
KVSession storage, cachingDashboard (auth sessions), portal (SIWS sessions)
R2File storage (invoice PDFs, merchant assets, exports)Dashboard, checkout
TurnstileBot protection on public-facing surfacesCheckout (checkout page), web (auth pages)
DNSDomain routing for velapay.com and subdomainsAll
Web AnalyticsPrivacy-aware analytics (no cookies, GDPR-compliant)Web
Service BindingsInternal Worker-to-Worker communicationAdmin → 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-worker

This 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 generate for SQL, drizzle-kit push for 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

TableRowsPurpose
usersAuth recordsBetter Auth managed
plansSubscription plansMerchant-defined
checkout_sessionsSession stateHosted checkout
payment_linksShareable linksPayment link metadata
invoicesInvoice recordsAuto-generated from billing events
billing_eventsWebhook event logIdempotent processing
subscriber_snapshotsDenormalized stateDashboard query optimization

Queues: Async Event Processing

QueuePurposeProducerConsumer
BILLING_QUEUEBilling event processing, invoice generationWebhook workerDashboard worker
WEBHOOK_FANOUTWebhook delivery to merchant endpointsDashboard workerDashboard 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:

  1. Drizzle's dialect abstraction: Switch from sqlite dialect to pg dialect in Drizzle config. Most queries are portable.
  2. Migration tooling: drizzle-kit generate produces SQL migrations for either dialect.
  3. Runtime change: Swap D1 binding for a Postgres connection string. Drizzle's query builder is dialect-agnostic.
  4. What changes: D1 batch() calls become standard Postgres transactions. SQLite-specific features (like RETURNING behavior) may need adjustment.
  5. 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

DomainServiceRole
velapay.comvela-webLanding page
docs.velapay.comvela-docsDeveloper documentation
app.velapay.comvela-dashboardMerchant dashboard
admin.velapay.comvela-adminProtocol admin
pay.velapay.comvela-checkoutHosted checkout
portal.velapay.comvela-portalCustomer self-service
js.velapay.comvela-widgetEmbeddable 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

ProductFree TierPaid
Workers100K requests/day$0.50/million requests
D15M rows read/day, 100K rows written/day$0.75/million rows read, $0.001/million rows written
KV100K reads/day, 1K writes/day$0.50/million reads, $5/million writes
R210 GB storage, 10M reads/month$0.015/GB/month, $0.36/million reads
Queues10K ops/day$0.40/million ops
TurnstileUnlimitedFree
DNSFreeFree
Web AnalyticsFreeFree

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-lib in 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.

Internal knowledge base for the Vela Labs workspace.