logicspike/docs

Internal Ops

Internal Ops Dashboard — Backlog & Scorecard

Last updated: 2026-06-28 Analyst: Codebase audit (apps/internal-api + apps/internal-dashboard, full source review)


Scorecard

# Parameter Score Notes
1 Authentication 9.5 Dual-gate: Cloudflare Access JWT (cryptographically verified via JWKS) + STAFF_EMAILS allowlist; dev bypass is fail-closed (must still pass allowlist); audience pinning (ACCESS_AUD) optional but not yet set
2 Security 9.5 Sensitive columns explicitly excluded from all reads (no databaseConfig, no Razorpay/Stripe IDs); ops DB separate from core DB; workers_dev = false + preview_urls = false (no .workers.dev backdoor); read-only on core DB
3 Business Logic 8.5 Net spend formula correctly handles HOLD/CAPTURE/VOID three-way ledger; MRR handles monthly vs yearly billing cycles; credit consolidation collapses hold/capture/void pairs in transaction history
4 API Design 8.5 Consistent Hono routes; keyset pagination on customer list (stable across concurrent inserts); parallel DB queries in getCustomerDetail; DaysToggle reactive (7/30/90d)
5 Data Model 8.5 Clean ops DB schema: accessLog (accountability with IP + UA), customerNotes, three rollup tables; all core DB reads on carefully allowlisted columns
6 Audit Trail 8.0 access_log written on every GET /customers/:id and POST /customers/:id/notes; includes staff email, action, target tenant, IP, user-agent, timestamp
7 Frontend Quality 8.0 Next.js 16 App Router; reusable primitives (Card, StatCard, Badge, Table, Skeleton, EmptyState, ErrorState); useApi hook with cancellation; recharts LineTrend + custom BarList
8 Feature Completeness 7.5 6 pages complete (Overview, Customers, Revenue, Credits, Growth, Audit); keyset customer pagination; read-only (no mutations yet); audit log viewer
9 Error Handling 7.5 safe() wrapper for optional DB tables; API throws with message on non-OK; frontend shows ErrorState with retry; credit consolidation handles edge cases
10 Infrastructure 8.5 ops.vlozi.app/api/* Worker route + Vercel Next.js; Cloudflare Access gates entire hostname; dual Neon DBs (core read-replica + ops write DB)
11 Observability 6.5 accessLog provides staff accountability; no metrics, alerts, or structured logging on the ops layer itself; no health endpoint on internal-api
12 Performance 5.5 All analytics reads are LIVE (rollup cron disabled); no index on credit_transactions(created_at) WHERE amount < 0; heavy queries will timeout at scale
13 Documentation 5.5 READMEs in both apps; memory doc exists; no formal spec docs under docs/internal-ops/ until now
14 Testing 2.0 Zero test files in either apps/internal-api or apps/internal-dashboard

Overall: 7.5 / 10


Bugs

B1 — Live-only queries will timeout at scale (rollup cron disabled)

Severity: High Location: apps/internal-api/src/services/spend.ts, wrangler.toml

The rollup cron (0 2 * * *) is commented out in wrangler.toml (Cloudflare account is at the 5-cron limit). All analytics reads — especially netConsumed() and netSpendByService() — perform live scans of credit_transactions filtered by created_at. At even a few million rows, these queries take 5–10 seconds and will hit the 30-second Worker CPU limit. There is also no index on (created_at) WHERE amount < 0 to speed up debit-only scans.

Recommended index:

CREATE INDEX CONCURRENTLY credit_tx_created_at_debit_idx
  ON credit_transactions (created_at) WHERE amount < 0;

B2 — Note add triggers second view_customer audit entry via refetch

Severity: Low Location: apps/internal-dashboard/src/app/customers/[id]/page.tsx

After a staff member adds a note, the page calls refetch() which re-calls GET /api/admin/customers/:id. The route handler writes view_customer to accessLog on every call. A staff member who adds 5 notes generates 6 audit entries (1 initial view + 5 refetch-triggered views). The audit trail overstates how many times the profile was independently accessed.

Fix: Only log view_customer on GET when the request does NOT include a ?refetch=notes marker (or separate the notes endpoint from the full profile).


B3 — Keyset cursor encodes Date object: timezone-dependent serialisation

Severity: Low Location: apps/internal-api/src/services/admin-analytics.tslistCustomers()

The pagination cursor is base64(JSON.stringify({ c: createdAt, id })). createdAt is a JavaScript Date object. JSON.stringify(new Date()) produces an ISO-8601 string in UTC — but if the DB driver returns a Date already adjusted to a non-UTC timezone, the comparison >= cursor.c on subsequent pages uses a different reference time. This could produce duplicate or skipped rows in rare environments where the Neon driver or Node.js runtime uses local timezone for Date construction.


B4 — listAccessLog limit param not validated; accepts any integer

Severity: Low Location: apps/internal-api/src/routes/admin.tsGET /audit

The route reads limit = Number(c.req.query("limit") ?? "200") and passes it directly to listAccessLog(opsDb, limit). There is no cap validation — a caller can request limit=1000000 and force a full table scan. The listAccessLog service has a documented max of 500 but doesn't enforce it in code.


B5 — safe() silently hides missing tables on Growth page

Severity: Low Location: apps/internal-api/src/services/admin-analytics.tsgetGrowth()

Waitlist and feedback queries are wrapped in safe() which returns null on error. If the waitlist or feedback tables don't exist (e.g., a staging DB), the Growth page renders empty sections with no explanation. Staff see no waitlist data and may believe there are zero signups, when actually the table is absent. A null result should surface a visible warning in the UI.


B6 — STAFF_EMAILS allowlist not whitespace-trimmed

Severity: Low Location: apps/internal-api/src/middleware/staff-auth.ts

The allowlist is parsed with STAFF_EMAILS.split(",") but .trim() is not applied to each entry. If the secret is set as "founder@vlozi.app, hello@vlozi.app" (space after comma), " hello@vlozi.app" with a leading space will never match the JWT email claim. The failure mode is silent 403s for valid staff members.


B7 — No access log retention policy

Severity: Low Location: apps/internal-api/src/db/schema.tsaccessLog

The accessLog table has no TTL, cleanup job, or partitioning strategy. At 50 customer-profile views per day (conservative for a growing team), it accumulates 18,000+ rows/year. Within 3–4 years this degrades audit query performance. No retention decision is enforced at the schema or infrastructure level.


Areas of Improvement

A1 — Re-enable rollup cron + add credit_tx index

The single most important performance improvement. Free a cron slot (or upgrade account tier), uncomment [triggers] in wrangler.toml, and add the index. This drops overview/credits/growth query time from seconds to milliseconds once rolled up.


A2 — Add customer mutations (suspend, adjust credits)

Current state: read-only. Phase 2 scope includes suspending a tenant and issuing credit adjustments. With credit helpers already in the service and the audit trail built, the infrastructure is ready — only the route and UI components are missing.


A3 — Add test coverage

Both apps/internal-api and apps/internal-dashboard have zero tests. At minimum:

  • admin-analytics.ts service functions (spend computation, MRR formula, keyset cursor)
  • Staff auth middleware (JWT rejection, allowlist miss)
  • spend.ts net spend formula (hold/capture/void combinations)

A4 — Move STAFF_EMAILS to ops DB table

Managing staff access via an env var requires a Worker redeployment every time someone joins or leaves. A staffAccess table in ops DB with (email, active, addedBy, addedAt) rows would allow runtime management and produce an audit trail of its own.


A5 — Extract credit helpers to @repo/core-billing

apps/internal-api/src/services/credit-helpers.ts duplicates logic from the manager app. A shared @repo/core-billing package (already referenced in TODOs) would eliminate the drift risk.


A6 — Enable ACCESS_AUD in production

The ACCESS_AUD env var is defined and the JWT validation code supports it, but it's not set in production. Audience pinning binds the JWT to this specific Cloudflare Access application — a leaked JWT from another Access app cannot be replayed here. Set ACCESS_AUD to the Application Audience (AUD) tag from the Cloudflare Access dashboard.


A7 — Log all page views for full accountability

Currently only view_customer and add_note are logged. Revenue, Credits, and Growth pages that show aggregate sensitive data (MRR, top consumers) are not logged. For a complete audit trail, every authenticated page request should write an entry — even if the target is null for non-customer-specific pages.


Known Deferred Items (Not Bugs)

Item Status
One-time PIN login via Email Routing Needs CF Email Routing for @vlozi.app + OTP enabled in Access
Rollup cron Disabled (5-cron limit); re-enable when slot freed
Feedback/Waitlist optional tables safe() wrapper is the current mitigation
Customer mutations (suspend, refund) Phase 2 scope
Per-service deep activity drill-down Phase 2 scope
Credit helpers extracted to @repo/core-billing Technical debt, tracked
Internal Ops