HALO Knowledge Docs
Generated knowledge spine · research/HALOCRM-ANALYSIS-2026-08-29.md

01 · Synthesis

The merged read across all seven research lanes.

Generated from research/HALOCRM-ANALYSIS-2026-08-29.md · regenerate with /opt/homebrew/opt/node@24/bin/node site/scripts/generate-docs.mjs

HALO CRM — analysis and 10x research

Observed 2026-08-29 · repo pinned at 347329a · read-only clone at clients/halocrm/repo Private mirror: github.com/sisodias/halocrm-mirror

Method: seven parallel Opus lanes (domain map · OSS replacements · Lovable/block extraction · security · vertical gaps · platform multipliers · competitor teardown), each required to verify counts from source and every GitHub repo via gh api before citing it. Lane outputs are in lanes/. Findings I verified personally are in lanes/VERIFIED-BY-MAIN-AGENT.md. Where a lane corrected me, the correction is recorded rather than quietly absorbed.


What it is

A creator-talent-agency CRM, built solo by Camron Kellman with Claude Code over roughly one month, running live on halocrm.vercel.app.

StackVite + React 18 + TS + shadcn/Tailwind, Convex backend, Vercel
Scale622 files · src/ 68,706 LOC · convex/ 11,274 LOC · 57 tables · 39 pages · 37 Convex modules
OriginA Lovable project — lovable-tagger still wired at vite.config.ts:4, README still stock
StateMid-migration off Supabase (39 files still import the Supabase client; 12 in components/payroll)
TestsNone. No vitest/jest, no test files

Domains: creator onboarding + intake queue · contracts/e-sign · multi-currency invoicing (creator + referral) · payment log · payroll/attendance · referrals · Google-Drive-backed content pipeline with taxonomy/scan/index/thumbnails · gamification for the chatter team · AI voice cloning · credential vault · messaging · RBAC · dashboard analytics · customs tracker.


Part 1 — The security position

Convex functions are public by default. Anything exported as query/mutation/action is callable by anyone holding the deployment URL, which ships inside the client bundle. The React app is not a security boundary.

ctx.auth / getUserIdentity appears 0 times across all 37 Convex modules. There is no server-side authentication anywhere in the backend.

PublicInternal
query738
mutation1329
action165
Total22122

Correction on the record: I first reported "456 public" by counting the substring query(, which also matches ctx.db.query( database calls. The security lane caught it. The correct figure is 221 (91% of the surface). Direction unchanged, number fixed.

The three that matter

C1 — Creator platform credentials, plaintext and unauthenticated. socialMediaLogins.password is a plain string (convex/schema.ts:502). listByCreatorEmail (convex/socialMediaLogins.ts:20) is a public query taking only an email, and the module contains zero identity checks. Creators' OnlyFans/Instagram/TikTok passwords are retrievable by an unauthenticated caller. The Secure Area gate in front of it is cosmetic: convex/secureArea.ts:7 publicly returns the gate's own password hash, and savePasswordHash (:17) lets anyone overwrite it. Assume already exposed → encrypt at rest and rotate.

C2 — resetAndSeed is a public mutation that wipes the business. convex/resetHalo.ts:84 — no args, no auth check, iterates 42 tables calling ctx.db.delete on every document. Six more public seed/demo mutations mutate or clear live data. Fix is one word each: mutationinternalMutation. Highest value-per-keystroke item in this document.

C3 — Identity is self-asserted from localStorage. src/context/SupabaseAuthContext.tsx:320-324 parses haloLocalAuthSession and trusts role/roles verbatim; haloAuth.login issues no token. Edit the value in devtools → full admin. Underneath, password hashing is single-round salted SHA-256 (convex/haloAuth.ts:4-9,28), no stretching, no rate limiting, and five employee hashes

  • salts are committed in cleartext at convex/resetHalo.ts:6-38.

Correctly done — credit where due

Contract and invite tokens use crypto.getRandomValues at 160/144 bits (convex/contracts.ts:6, convex/creatorInvitations.ts:6) and signing is single-use. Real secrets are properly server-side (ElevenLabs, currency API, Drive credentials). VITE_RUNPOD_API_KEY sits in .env.example but is read nowhere in src/, so Vite never inlined it — the key is not burned. No secrets in any commit in history.


Part 2 — The money problem

Every monetary field is v.number() — float64. No integer minor units, no decimal type. Across 11,274 lines of Convex there are 2 Math.round/toFixed calls, and none in invoicing.ts, referrals.ts, referralInvoicing.ts or payroll.ts.

Three consequences, all verified at source:

  1. Catastrophic cancellation on nearly-settled invoices.

convex/referralInvoicing.ts:138Math.max(0, amount - paid) * conversionRate. Subtracting two accumulated floats can yield 1.8e-12 rather than 0; Math.max(0,…) doesn't catch a positive residue. A fully-paid invoice can show a non-zero balance.

  1. The same commission computed two contradictory ways. convex/referrals.ts:534

accumulates creatorShare += gross * (percentage/100) per entry; :546 computes (gross * percent) / 100 on the summed gross. Float arithmetic guarantees these disagree sometimes. The app displays both, and no ledger arbitrates.

  1. A business term hidden in a null-coalesce. entry.percentage ?? 50 at

invoicing.ts:34, :65, :250 — any creator whose rate was never set is silently billed at 50/50. This is a commercial question, not a code style one.

There is also no audit trail: no append-only history anywhere in the 57 tables (customStatusHistory is customs-only). updatedAt records that a row changed, never what, from what, or by whom — in an app where staff edit revenue records daily.

Fix path: dinerojs/dinero.js (6,793★, MIT, v2.0.0 stable since 2026-03) for integer minor units and allocate(), which splits a total across ratios and distributes the remainder deterministically so parts always sum to the whole. A full double-entry engine (Formance, TigerBeetle) is out of proportion for a small agency; correct representation plus an append-only journal table in Convex is the right size.


Part 3 — The 10x list

Ranked by leverage. Every repo verified via gh api on 2026-08-29.

Install first

WhatRepo / actionWhy
Unified deployConvex preview deployments (config, no dep)Fixes the exact incident in his CLAUDE.md — frontend shipping ahead of backend. npx convex deploy --cmd 'npm run build' makes the failure structurally impossible. ~30 min.
Authget-convex/better-auth 765★ Apache-2.0First-party Convex component; kills C1/C3 at the root. Engine: better-auth/better-auth 29,744★ MIT.
Testsget-convex/convex-test 22★ Apache-2.0The only way to unit-test 57 tables of Convex functions. Zero-tests problem starts here.
Durable jobsget-convex/workflow 80★ Apache-2.0Drive provisioning is scheduler.runAfter fire-and-forget with no retry (googleDrive.ts:344-380); a Drive 500 becomes silent manual work.
Dashboard costget-convex/aggregate 31★ Apache-2.0166 .collect() calls; dashboard.ts:6 scans the whole creators table then filters seven times in JS. Gets slower and pricier per creator, then throws.
Error visibilityConvex→Sentry native integrationOne dev, no tests, 221 public functions. Currently learns of backend errors from user complaints.
Authorizationstalniy/casl 7,063★ MITRole ladder + per-resource flags is exactly its shape; same rules run server- and client-side.
Secretsjedisct1/libsodium.js ISC / FiloSottile/age BSD-3Don't adopt a vault product — envelope-encrypt with a KMS-held key. ~200 lines, not a service.

Safety item worth an hour

convex/driveScan.ts:252-253 fetches Drive thumbnails and calls ctx.storage.store(blob) unprocessed. Phone photos routinely carry GPS EXIF; Convex file URLs are unguessable but public. For a creator agency that is a physical-safety issue, not a compliance checkbox. Audit ten stored thumbnails for GPS tags before building anything — Google may already strip them on thumbnailLink, in which case the work is unnecessary. If not, lovell/sharp (32,613★ Apache-2.0, needs a Node action) or mattiasw/ExifReader (997★ MPL-2.0, pure JS).

Traps — verified, do not adopt

lucia-auth/lucia repositioned as a learning resource, not a library · osohq/oso deprecated per its own README (last push 2025-02) · crater-invoice-inc/crater stale ~2yr · hashicorp/vault now BSL (use openbao/openbao MPL-2.0) · get-convex/convex-auth has no LICENSE file at all + 154 open issues · invoiceninja is ELv2 (hosting forbidden) · Vaultwarden/Passbolt are human password managers, wrong shape for app-level secrets.

Licence note: flagged as metadata, never used to eliminate — selection is on engineering quality per the standing rule. The e-sign lane is uniformly AGPL (Documenso, DocuSeal, OpenSign); Documenso is TypeScript and therefore the one whose patterns transfer.


Part 4 — What this repo teaches the Action Model programme

This is a rare natural experiment: a Lovable project driven by a competent developer until it became a real app. It is an honest record of where prompt-to-app stops.

The Lovable/human boundary is not recoverable from git — history begins at 6bf902e with 631 files already tracked, including 24 Convex modules. Any git-derived percentage would be invented, so the lane declined to offer one. What survives is artefact evidence: stock README, lovable-tagger still wired, public/lovable-uploads/ still serving load-bearing brand assets 14 files deep.

The most telling single fact: git diff 6bf902e HEAD -- src/components/ui/ is empty. 5,283 LOC across 57 shadcn components, untouched through a month that rewrote half the app. Only 4 of 57 were ever restyled. The generated floor was good enough to build a business on — the ceiling is elsewhere.

Where the ceiling is — each with a structural reason, not a quality complaint: a server-side scheduler (crons.ts:16-21 — creators upload via share link, "polling is the only way to notice"); OAuth under a restricted scope (Workspace-Internal consent because External needs CASA verification for auth/drive); a taxonomy whose source of truth lives outside the code (driveStructure.ts:23 — the folder is USED : POSTED, spaces around the colon, and a mismatch silently creates a duplicate); idempotent failure-tolerant provisioning; an index with a deletion invariant (delete the thumbnail blob before the row, or orphan it forever); frozen FX rates so a sent invoice can't silently change value; a temporal-ownership model for referral links, because "a single pointer cannot answer what we were owed in September."

None of those are code a generator writes badly. They are decisions with nowhere to live in a prompt-to-app model, each one learned from a production failure.

The block finding. Creator and referral invoicing are one concept implemented twice — 8,385 LOC across both. The repo says so itself three times (referralInvoicing.ts:5-9 "the money-out counterpart"; schema.ts:181 "mirrors the creator invoicing workflow"; RemittancePdf.ts:5-14 "two house styles would read as two different businesses"). They share a settlement spine — running paid total, receipt storage, frozen FX rate, send-status tracked separately from payment, 0.01 settle epsilon, derived-not-stored status, compound index on (subject, period) — and differ on six axes that map cleanly onto a block contract's parameters: direction, period strategy, amount function, settlement currency, send-state set, and partial-payment hooks.

That is the shape of the argument for assembly over generation: not "a model can write a CRUD page," but "these 8,385 lines are one parameterised block written twice by a competent developer who knew it."


Part 5 — The market gap: what the vertical considers table-stakes

Verified against live vendor product pages (Infloww, Supercreator, CreatorHero, OnlyMonster, Scrile Connect), not marketing summaries.

**The one-sentence version: HALO is an excellent agency back-office — money, people, contracts, content storage. The commercial products are revenue-floor operating systems — fans, conversations, attribution. He built the half nobody sells, and is missing the half everybody sells.**

The structural gap: HALO's CRM object is the creator. The market's is the fan.

Missing capabilitySeverityNote
Fan CRM — per-fan record, tags, lifecycle, spend historyCriticalThe biggest structural gap
Per-chatter revenue attribution ($ per chatter per shift)CriticalHe pays chatters but cannot rank them; commission pay is impossible without it
Shift management + handover (he has attendance only)Critical
Mass-message / PPV campaign tooling with segmentationCritical
Content "already sent to this fan" trackingHighOne join table away from his existing Drive index — highest ROI-per-hour item
Whale / top-spender surfacingHigh
Traffic-source link attribution to revenueHigh
Compliance word-blocking on outbound messagesHigh
1099 / contractor tax-form trackingHigh
Credential-free delegated accessHighThe market shares access; HALO shares secrets — architecturally weaker, and it is the same finding as C1 from the other end

Two of these converge with findings reached independently by other lanes — the credential model (C1) and per-chatter attribution (which the missing audit trail also blocks). When three lanes reach the same conclusion from different directions, it is worth weighting.

Deliberately out of scope: competitors market ban-evasion and detection-avoidance features. Noted as market context only; not researched, not recommended.


Open questions for Shaan

  1. Camron noteFOR-CAMRON-security-note.md is drafted, not sent. C1 (plaintext

credentials) and C2 (public wipe) are live-production issues on a friend's app. My read: send the short version soon, separately from this research.

  1. Fan-layer question — Part 5 says the market sells the half he lacks. Is that a

direction he wants, or is HALO deliberately a back-office and the fan layer someone else's product? That is a business call, not a technical one.

  1. Scope — is this a study asset for Action Model block extraction, or is there a

build engagement behind it? That changes what happens next.

Canonical source remains research/HALOCRM-ANALYSIS-2026-08-29.md. This HTML is a generated projection; edit the source, then run generate-docs.mjs.