HALO Knowledge Docs
Historical / reusable evidence · research/00-PAGE-BY-PAGE.md

★ His app, page by page

Every page in Camron's app: what's on it, what we found, and the move — keep/transplant/add/fix/build. The join of his code + all research + the repos.

Generated from research/00-PAGE-BY-PAGE.md · regenerate with node site/scripts/generate-docs.mjs

HALO CRM — the whole app, page by page, with what to do to each

STALE-BASELINE: This page inventory describes 347329a, not the current client product at 83ab4ba. Its 39-page/56-table/routes and recommended moves must be replaced or reverified using site/docs/HALO-CLIENT-FRESHNESS-GAP.md.

One row per real page in Camron's app. For each: what's on it (his components), what it does (his backend), what our research found, and the move — KEEP / TRANSPLANT (a repo already does this, swap it in) / ADD (bolt on new capability) / FIX (security/correctness) / BUILD (novel, no donor exists).

Live app: halocrm.vercel.app · read-only clone: repo/ · 39 pages, 56 tables, 0 tests, 0 server-auth. This file is the join of his code + all 30 research reports + the repos we verified.

Legend for the "Move" column:

  • KEEP — works, leave it.
  • TRANSPLANT — a repo already IS this; lift or wrap it and preserve the complete functional specification. The donor UI may replace his when parity is proven.
  • ADD — new capability he asked for or clearly needs, bolt on.
  • FIX — broken or unsafe; correct before anything else.
  • BUILD — genuinely novel; no donor; real work.

MONEY (his biggest, most valuable, most broken area)

/payment-log — PaymentLog.tsx (871 LOC — his single largest page)

  • On it: merged ledger of creator + referral + manual payments, void/refund/restore, recurring expenses.
  • Backend: convex/paymentLog.ts (690 LOC). Money is v.number() (float). No account chart, no postings, no balance invariant.
  • Research found: this is a hand-rolled reconciliation ledger that isn't double-entry. Catastrophic-cancellation bug (referralInvoicing.ts:138), commission computed two contradictory ways (referrals.ts:534 vs :546), 2 rounding calls in 11k LOC.
  • MOVE → TRANSPLANT. Port a real double-entry engine — flash-oss/medici (MIT, Node) or pgr0ss/pgledger (MIT, Postgres) as the model, or study formancehq/ledger / tigerbeetle. Preserve every PaymentLog capability; use his UI or a stronger donor UI depending on which yields the simpler complete implementation. Model creator earnings as a trust liability (their money the agency holds), not agency revenue — the way real talent agencies do (Syngency/Reprtoir/Curve confirm this). This is the single highest-value change in the app.

/creator-invoicing — CreatorInvoicing.tsx (349) + /referral-invoicing — ReferralInvoicing.tsx (763)

  • On it: raise/settle invoices, multi-currency, partial payments, receipts, frozen FX.
  • Backend: convex/invoicing.ts (705), referralInvoicing.ts (579), currency.ts. percentage ?? 50 silently assumes 50/50 splits.
  • Research found: these are TWO implementations of ONE concept (2,396 LOC). Block-extraction lane proved it: same settlement spine, differ on 6 axes (direction, period, amount fn, currency, send-states, partial-payment hook).
  • MOVE → FIX + TRANSPLANT. They ride on the same ledger transplant above. Integer minor units via dinerojs/dinero.js (MIT, v2 stable). Collapse the two into one parameterised invoice block. Kill the silent 50/50 default (it's a business term hiding in a null-coalesce).

/payroll — Payroll.tsx (79)

  • On it: admin/chatter/attendance payroll views.
  • Backend: convex/payroll.ts (330) — orphaned: 0 frontend consumers, 15 of 21 components still on Supabase.
  • Research found: a complete Convex payroll module nothing calls; the UI still reads the old Supabase path.
  • MOVE → FIX (finish the migration) + ADD. Wire the existing Convex module to the UI. For real payroll/contractor pay, BUY (Deel/Trolley) rather than build — note 1099 threshold rose $600→$2,000 for 2026.

AUTH & ACCESS (the foundation — nothing is safe until this is done)

/login /register /forgot-password /reset-password — auth pages (~900 LOC)

  • Backend: convex/haloAuth.ts (49) — single-round SHA-256, session as unsigned JSON in localStorage.
  • Research found (verified): 0 ctx.auth calls across 36 modules. 221 public functions. Identity is self-asserted from localStorage — edit role in devtools → admin. 5 employee hashes committed in cleartext (resetHalo.ts:6-38).
  • MOVE → TRANSPLANT (this is the keystone). get-convex/better-auth (Apache-2.0, first-party for Convex) + get-convex/convex-helpers custom-function wrappers to enforce auth once across all 221 functions. Do this first. Every other port depends on it.

/secure-logins — SecureLogins.tsx (220) — the credential vault

  • On it: stores creators' third-party platform passwords.
  • Backend: convex/socialMediaLogins.ts, secureArea.ts. password is a PLAINTEXT string; listByCreatorEmail is a public query with no auth. The "secure gate" publicly returns its own password hash.
  • Research found: highest-severity finding. Creators' OnlyFans/IG/TikTok passwords readable by anyone on the internet right now.
  • MOVE → FIX (urgent) + TRANSPLANT. Envelope-encrypt with age (BSD) or libsodium.js (ISC), key in Convex env (never VITE_). Or study Vaultwarden patterns. Assume already exposed → encrypt AND rotate. Tell Camron this week.

/access-control + /users — AccessControlPanel.tsx (80), UserManagement.tsx (63)

  • Backend: convex/rolePermissions.ts (canonical) vs src/utils/permissionUtils.ts (Supabase) — two parallel permission systems.
  • Research found: permissions render-gate the UI only; no Convex function checks a role. Cosmetic.
  • MOVE → TRANSPLANT. stalniy/casl (MIT) — same rules server + client, collapses the two systems. Rides on the auth foundation.

CONTENT (his best work + a real safety gap)

/shared-drive — SharedDrive.tsx (293) + /upload/:id — CreatorUpload.tsx (148)

  • On it: Google-Drive content pipeline, taxonomy, scan/index, thumbnails, mark-used. Creator upload handoff.
  • Backend: 7 drive* modules. Genuinely well-engineered (markUsed move-before-delete, idempotent provisioning).
  • Research found: 4 public unauthenticated actions on the LIVE Drive — trashDriveItem takes any fileId, no ownership check (highest blast radius; escapes even Convex backups). Uploads use upsert:true (silent overwrite). EXIF/GPS not stripped — creator-safety issue. /upload/:id not behind ProtectedRoute, still on Supabase storage.
  • MOVE → FIX + KEEP. Scope the Drive actions to the CRM root + internalize (FIX, urgent). Strip EXIF with sharp (needs Node action) or ExifReader (FIX, after auditing 10 stored thumbnails). Otherwise his pipeline is good — study Immich patterns (perceptual dedupe) only if needed. Don't replace it.

/ai-voice — AIVoice.tsx (788)

  • On it: ElevenLabs voice cloning, upload sample, clone, TTS queue, emotions.
  • Backend: convex/voice.ts (628), API key correctly server-side.
  • Research found: works. Still split — one service calls a Supabase edge fn. No consent/expiry/revocation scope on voice records.
  • MOVE → KEEP + ADD. Finish the Supabase→Convex bit. Add a consent/rights scope to voice records (creator-safety). Provider stays ElevenLabs.

PEOPLE (creators, staff, roles)

/creators /creators/:id /creators-data — CreatorProfile (122), Creators (96), CreatorsData (45) + components/creators* (~5.4k LOC)

  • On it: the creator record — THE root entity every other page hangs off.
  • Research found: this was unnamed by the first domain sweep; it's the app's spine. Onboarding invite tokens never expire (expiresAt unset). Accept flow provisions a hardcoded temp password.
  • MOVE → FIX + KEEP + ADD. Fix invite expiry + hardcoded password (FIX). The "trust passport" idea from research (a ready(creator, capability) predicate gating each action on contract+consent+evidence+access+revoke-path) is the ADD that makes this world-class — but it's later, on the auth foundation.

/team /team/:id — Team (85), TeamMemberProfile (284) + components/team, employees

  • On it: staff records, A/B/C team assignment, member onboarding.
  • Research found: activity tracking stubbed ("coming soon"). No per-chatter revenue attribution — the market's #1 feature, and it's the measurement layer the gamification page sits on top of.
  • MOVE → KEEP + BUILD. The team model is fine. The ADD/BUILD is per-chatter revenue attribution (which chatter earned what per shift) — no clean donor; it's real work, but it's what makes gamification and payroll actually mean something.

THE FAKE PAGES (never worked — tells us where he ran out of runway)

/creators/:id/analytics — CreatorAnalytics.tsx (62)

  • Research found: reads mockEngagementStats["1"] — hardcoded to the first mock record. No backend, no tables. Never worked.
  • MOVE → BUILD / TRANSPLANT. Needs a real data source. get-convex/aggregate for the counts (kills the 166 .collect() full-scans), then a dashboard donor (Metabase / Tremor / Lightdash) behind the shape he already drew. First decide what analytics the business needs (research says: creator LTV, churn, funnel — the fan-CRM stuff).

/messages — Messages.tsx (66)

  • Research found: posts to process.env.N8N_WEBHOOK_URL in a Vite app — process.env doesn't exist, so it's ALWAYS an empty string. Every send POSTs to "". Never worked once.
  • MOVE → BUILD (the strategic fork). This is the missing HALF the whole market sells — the fan conversation layer. Study Chatwoot's data model (don't mount it). This is the "does HALO grow into a fan-revenue OS or stay back-office" decision. Biggest single fork in the whole app.

WORKS — LEAVE ALONE

  • /contracts + /contracts/sign/:token (Contracts 140, ContractSigning 167) — KEEP. Tokens use crypto.getRandomValues (160-bit), correct. For legal-grade audit trails later, study Documenso (AGPL) — but his works.
  • /customs-tracker (CustomsTracker 180) — KEEP. Real kanban, status history. Note: useCustomAttachments returns demo path strings (attachments don't actually store — small FIX).
  • /tasks-rewards (gamification, 187 + 5,886 in components) — KEEP + FIX. His largest domain, works. But quest completion accepts client-supplied chatterId/xpEarned (gamification.ts:911) — same "trusts the caller" disease; can't be authority for real rewards until auth foundation lands. Reward the quality/safety signal, never gross sales alone.
  • /onboard /onboarding-form (CreatorOnboarding 90 + components/onboarding 4,052) — KEEP + FIX the invite expiry.
  • /dashboard (55) — FIX. dashboard.ts:6 scans the whole creators table + 7 JS filter passes. get-convex/aggregate fixes it.

THE ADDITIONS he'll want (the Notion / Airtable / etc. layer)

These aren't in his app yet but the research says a creator agency needs them. Bolt-ons, not transplants:

  • Docs / SOPs / creator briefs ("a Notion") → study AFFiNE (mixed license) or Outline (BSL). The research flagged this as the category the first hunt missed.
  • Flexible tables / roster ("an Airtable") → NocoBase / Teable / Directus — but only if he outgrows Convex tables; likely not needed.
  • Durable jobs (his Drive scan is fire-and-forget, no retry) → get-convex/workflow + workpool.
  • Audit trail (none exists in 56 tables) → get-convex/table-history.
  • Contractor pay / KYC / payout railsBUY (Deel, Trolley, Yoti) — don't build.
  • Chatter QA scorecards → study MaestroQA patterns — the measurement layer under gamification.

THE ORDER (physical next steps)

  1. This week: tell Camron the 3 live holes — plaintext creds, public wipe mutation, public Drive handle. One friendly message.
  2. Foundation: better-auth + convex-helpers → makes 221 functions safe. Everything below needs this.
  3. Money: trust-liability double-entry ledger (medici/pgledger) → the thing his business runs on.
  4. Then decide the fork: back-office (what he built) or grow the fan layer (/messages → the market's half)? That picks what's next.

Every page above is KEEP / TRANSPLANT / ADD / FIX / BUILD. Most are keep or transplant. Very few are build. That's the whole app, and that's the plan.

Canonical source remains research/00-PAGE-BY-PAGE.md. This HTML is a generated projection; edit the source, then run generate-docs.mjs.