Skip to content

Admin Architecture

Overview

vela-admin is a separate Cloudflare Worker that provides internal operations tooling for the VelaPay protocol team. It is never exposed to merchant traffic and uses a fundamentally different auth model from the merchant dashboard.

Service Binding Pattern

┌──────────────────┐     Service Binding      ┌──────────────────┐
│   vela-admin      │ ──────────────────────→  │  vela-dashboard   │
│   (CF Worker)     │    (internal RPC)        │  (CF Worker)      │
│                   │                          │                   │
│  • SIWS auth      │                          │  • Email auth     │
│  • Admin ops      │                          │  • D1 binding     │
│  • Audit logging  │                          │  • Helius webhooks│
└──────────────────┘                          └────────┬──────────┘

                                                       │ D1 binding

                                              ┌──────────────────┐
                                              │   D1 Database     │
                                              │  (merchant data,  │
                                              │   sessions,        │
                                              │   analytics)       │
                                              └──────────────────┘

Why Service Binding (Not Direct D1 Access)

  1. Auth isolation: Admin auth (SIWS) is completely separate from merchant auth (email-primary). No shared session logic.
  2. Single schema path: All D1 schema changes go through the dashboard Worker. No risk of admin and dashboard using different schema versions.
  3. Traffic isolation: Admin traffic never hits merchant-facing endpoints. No shared load, no shared rate limits.
  4. Audit trail: All admin actions are logged through the dashboard API, which adds the admin identity and timestamp.

Service Binding Implementation

typescript
// vela-admin Worker
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    // Admin auth: SIWS verification against ProtocolConfig PDA admin field
    const adminWallet = await verifySIWS(request);
    if (!isAdminWallet(adminWallet, env)) {
      return new Response("Unauthorized", { status: 403 });
    }

    // Service binding to dashboard
    const dashboardResponse = await env.DASHBOARD_SERVICE.fetch(
      new Request(new URL("/api/admin/...", request.url), {
        method: request.method,
        headers: {
          "X-Admin-Wallet": adminWallet,
          "X-Admin-Timestamp": new Date().toISOString(),
          ...request.headers,
        },
        body: request.body,
      }),
    );

    return dashboardResponse;
  },
};

Browser-Signs, Server-Logs Model

The admin architecture follows a browser-signs, server-logs pattern:

  1. Browser signs: Admin user's wallet signs the transaction in the browser (via SIWS or direct wallet interaction).
  2. Server logs: Admin Worker validates the signature, logs the action, and proxies to the dashboard Worker.

Why This Model

  • Key management: Admin private keys never leave the browser. The server only sees verified signatures.
  • Auditability: Every admin action has a wallet signature attached. Non-repudiation is built in.
  • No server-side key storage: The admin Worker doesn't store private keys. No key rotation needed.
  • Familiar UX: Admin users sign with their wallet, which they already know how to do.

Admin Action Flow

Admin browser

    │  1. Admin initiates action (e.g., pause protocol)


vela-admin Worker

    │  2. Verify SIWS signature
    │  3. Verify wallet is in ProtocolConfig.admin
    │  4. Log action to immutable audit trail


Service binding → vela-dashboard Worker

    │  5. Execute action (e.g., call pause_protocol instruction)
    │  6. Record action in D1 audit log


Solana (on-chain)

    │  7. Transaction submitted to Solana
    │  8. ProtocolConfig.paused = true


Result returned through the chain

On-Chain Source of Truth for Critical Paths

For critical admin operations, the on-chain state is the source of truth, not D1.

ProtocolConfig PDA

The ProtocolConfig PDA is the on-chain global config:

rust
#[account]
pub struct ProtocolConfig {
    pub admin: Pubkey,                    // Admin wallet address
    pub transfer_hook_program_id: Pubkey, // Current transfer hook program
    pub keeper_authority: Pubkey,         // Keeper authorization
    pub paused: bool,                     // Emergency pause flag
    pub bump: u8,                         // PDA bump
}

Why On-Chain Source of Truth

OperationSource of TruthWhy
Admin wallet listProtocolConfig.admin (on-chain)If D1 is compromised, attacker can't change admin
Protocol paused stateProtocolConfig.paused (on-chain)Pause state must be verifiable by keepers and transfer hook
Transfer hook program IDProtocolConfig.transfer_hook_program_id (on-chain)Transfer hook reads this at validation time
Keeper authorityProtocolConfig.keeper_authority (on-chain)Keepers verify authority at execution time
Token configurationTokenConfig PDAs (on-chain)Billing rails must be verifiable by all participants

D1's Role

D1 is used for:

  • Analytics and indexing: Aggregated billing data, merchant metrics, subscriber counts.
  • Session management: Admin sessions, merchant sessions.
  • Webhook state: Endpoint configurations, delivery history, retry state.
  • Audit trail: Human-readable logs of admin actions (supplementary to on-chain state).

D1 is not the source of truth for critical protocol state. It's a cache and index that can be rebuilt from on-chain data.

Typed Confirmation Gates

Admin operations that modify protocol state require typed confirmation gates:

Confirmation Gate Design

typescript
interface ConfirmationGate {
  // What operation is being confirmed
  operation: "pause_protocol" | "unpause_protocol" | "admin_cancel" | "update_config" | "update_keeper_config";
  // Parameters that will be submitted on-chain
  params: Record<string, unknown>;
  // Display text shown to admin for confirmation
  displayText: string;
  // Severity level (determines confirmation UX)
  severity: "normal" | "high" | "critical";
  // Whether 2FA is required
  requires2FA: boolean;
}

Severity Levels

SeverityOperationsConfirmation UX2FA Required
Normalupdate_keeper_config, update_config (non-breaking)Single confirm buttonNo
Highpause_protocol, admin_cancelType-to-confirm (enter operation name)Yes
Criticalunpause_protocol (after incident), update_transfer_hookType-to-confirm + second admin approvalYes

Why Typed Confirmation

  • Prevent fat-finger disasters: Pausing the protocol affects all merchants. Type-to-confirm ensures it's intentional.
  • Audit trail: Every confirmation gate logs the admin's intent, the parameters, and the result.
  • Two-person rule (critical ops): Most destructive operations require a second admin to approve within a time window.

Credential Derivation at Transaction Time

For admin operations that interact with on-chain accounts (e.g., admin_cancel), credentials are derived at transaction time from on-chain state, not from D1 cache.

Why Not D1 Cache

The v1.2 incident where D1 cache coupling caused a brittle EmergencyCancel path:

  1. Admin initiated admin_cancel for a mandate.
  2. Dashboard read mandate state from D1 cache.
  3. D1 cache was stale (mandate had been upgraded since last sync).
  4. Cancel transaction was submitted with wrong parameters.
  5. Transaction failed on-chain but D1 showed it as cancelled.

Fix: On-Chain Derivation

typescript
async function adminCancel(mandateAddress: PublicKey, env: Env) {
  // Step 1: Read current mandate state FROM CHAIN (not D1)
  const mandateAccount = await connection.getAccountInfo(mandateAddress);
  const mandate = deserializeMandate(mandateAccount.data);

  // Step 2: Derive all required PDAs from on-chain state
  const credentialPDA = PDAFactory.credential({ subscriber: mandate.subscriber, merchant: mandate.merchant });
  const planPDA = PDAFactory.plan({ planId: mandate.plan });

  // Step 3: Build transaction with current on-chain state
  const ix = await program.methods
    .adminCancel()
    .accounts({
      mandate: mandateAddress,
      plan: planPDA,
      credential: credentialPDA,
      // ... all derived from on-chain state
    })
    .instruction();

  // Step 4: Submit and verify
  const tx = await sendAndConfirm(connection, ix, [adminKeypair]);
  return tx;
}

This ensures admin operations always use the current on-chain state, regardless of D1 cache freshness.

Why Admin is a Separate Worker

Different Auth Model

Dimensionvela-dashboardvela-admin
Primary authEmail/password or Google SSOSIWS (wallet-gated)
Identity sourceBetter Auth + D1ProtocolConfig.admin on-chain
Session modelPersistent cookies (7 days)Per-request SIWS verification
Access controlOrg membership + rolesSingle admin wallet list
2FATOTP (optional)Not applicable (wallet is 2FA)

Never Exposed to Merchant Traffic

The admin Worker is:

  • Not on the same domain as the merchant dashboard.
  • Not discoverable from merchant-facing URLs.
  • Rate-limited independently with different thresholds.
  • Logged separately with full audit trail.

Independent Deployment

Admin can be deployed, updated, or rolled back without affecting merchant dashboard availability. This is critical for:

  • Emergency fixes: Admin tooling can be updated during an incident without touching merchant-facing code.
  • Feature development: Admin features can be developed and tested independently.
  • Security patches: Admin security updates don't require merchant dashboard redeployment.

Operational Separation

The admin Worker handles:

  • Protocol monitoring: TVL, mandate counts, merchant counts, volume.
  • Keeper monitoring: Keeper health, success rates, failure alerts.
  • Merchant management: Merchant lookup, subscription inspection.
  • Emergency controls: Pause, unpause, admin cancel.
  • Observability: Event stream, reconciliation, cost tracking.
  • Immutable audit trail: Every admin action logged with wallet, timestamp, parameters, and result.

None of these concerns belong in the merchant-facing dashboard. They're operational, not product.

Internal knowledge base for the Vela Labs workspace.