Appearance
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)
- Auth isolation: Admin auth (SIWS) is completely separate from merchant auth (email-primary). No shared session logic.
- Single schema path: All D1 schema changes go through the dashboard Worker. No risk of admin and dashboard using different schema versions.
- Traffic isolation: Admin traffic never hits merchant-facing endpoints. No shared load, no shared rate limits.
- 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:
- Browser signs: Admin user's wallet signs the transaction in the browser (via SIWS or direct wallet interaction).
- 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 chainOn-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
| Operation | Source of Truth | Why |
|---|---|---|
| Admin wallet list | ProtocolConfig.admin (on-chain) | If D1 is compromised, attacker can't change admin |
| Protocol paused state | ProtocolConfig.paused (on-chain) | Pause state must be verifiable by keepers and transfer hook |
| Transfer hook program ID | ProtocolConfig.transfer_hook_program_id (on-chain) | Transfer hook reads this at validation time |
| Keeper authority | ProtocolConfig.keeper_authority (on-chain) | Keepers verify authority at execution time |
| Token configuration | TokenConfig 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
| Severity | Operations | Confirmation UX | 2FA Required |
|---|---|---|---|
| Normal | update_keeper_config, update_config (non-breaking) | Single confirm button | No |
| High | pause_protocol, admin_cancel | Type-to-confirm (enter operation name) | Yes |
| Critical | unpause_protocol (after incident), update_transfer_hook | Type-to-confirm + second admin approval | Yes |
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:
- Admin initiated
admin_cancelfor a mandate. - Dashboard read mandate state from D1 cache.
- D1 cache was stale (mandate had been upgraded since last sync).
- Cancel transaction was submitted with wrong parameters.
- 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
| Dimension | vela-dashboard | vela-admin |
|---|---|---|
| Primary auth | Email/password or Google SSO | SIWS (wallet-gated) |
| Identity source | Better Auth + D1 | ProtocolConfig.admin on-chain |
| Session model | Persistent cookies (7 days) | Per-request SIWS verification |
| Access control | Org membership + roles | Single admin wallet list |
| 2FA | TOTP (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.