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 at83ab4ba. Its 39-page/56-table/routes and recommended moves must be replaced or reverified usingsite/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 isv.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:534vs:546), 2 rounding calls in 11k LOC. - MOVE → TRANSPLANT. Port a real double-entry engine —
flash-oss/medici(MIT, Node) orpgr0ss/pgledger(MIT, Postgres) as the model, or studyformancehq/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 ?? 50silently 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.authcalls across 36 modules. 221 public functions. Identity is self-asserted from localStorage — editrolein 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-helperscustom-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.passwordis a PLAINTEXT string;listByCreatorEmailis 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) orlibsodium.js(ISC), key in Convex env (neverVITE_). 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) vssrc/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 —
trashDriveItemtakes any fileId, no ownership check (highest blast radius; escapes even Convex backups). Uploads useupsert:true(silent overwrite). EXIF/GPS not stripped — creator-safety issue./upload/:idnot behindProtectedRoute, 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) orExifReader(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 (
expiresAtunset). 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/aggregatefor 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_URLin a Vite app —process.envdoesn'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 usecrypto.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:useCustomAttachmentsreturns 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-suppliedchatterId/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/onboarding4,052) — KEEP + FIX the invite expiry./dashboard(55) — FIX.dashboard.ts:6scans the whole creators table + 7 JS filter passes.get-convex/aggregatefixes 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 rails → BUY (Deel, Trolley, Yoti) — don't build.
- Chatter QA scorecards → study MaestroQA patterns — the measurement layer under gamification.
THE ORDER (physical next steps)
- This week: tell Camron the 3 live holes — plaintext creds, public wipe mutation, public Drive handle. One friendly message.
- Foundation:
better-auth+convex-helpers→ makes 221 functions safe. Everything below needs this. - Money: trust-liability double-entry ledger (medici/pgledger) → the thing his business runs on.
- 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.