2026 · SECURITY SALES INTEL

SecuritySalesHub

A multi-industry signal platform with configurable recommendations; remote deployment was not yet wired as of the recorded snapshot.

IN PROGRESS Designed the platform generalization and its data-backed recommendation and trust controls.
On this page

CONTEXT

The work started here.

A GridSignals restack needed to support several regulated industries and vendor line cards without embedding product decisions in code.

BUILD

What was built.

A multi-industry, multi-vendor pre-sales signal intelligence platform — a restack of the Python/SQLite GridSignals prototype into Next.js 14 + Supabase. Monitors 29 free public sources (SEC EDGAR, CISA KEV, HHS OCR breach portal, FDIC enforcement, NERC/FERC pages, and 24 more) to detect buying signals for 61 security products across 47 vendors, covering energy, financial services, healthcare, and cross-industry regulated entities. The core architectural additions over GridSignals: industry-keyed scoring weights, a two-hop event→capability→product reasoning chain in config-as-data CSVs, watchlist entity resolution by regulatory identifier (CIK/ticker/LEI/Wikidata QID), and seven trust invariants enforced as schema CHECK constraints and tested against the live database. Built as a single-operator portfolio/demo product — 21 database migrations, 68 test files across two runtimes (Vitest + Deno). 565 commits · 113 PRs · 11 active days; remote Supabase and Netlify deploy not yet wired as of 2026-08-25.

PROJECT DESIGN

How it was shaped.

Built as an explicit generalization of GridSignals — a Python/SQLite/Streamlit prototype that covered only energy and a single vendor set. Every architectural decision was a deliberate generalization: industry-keyed scoring weights instead of a flat model, a CSV-seeded capability category layer between events and products instead of hardcoded mappings, and a seller-preference filter that enforces the vendor line card without touching classifier or scoring logic. The plan orchestrator — a custom SQLite-backed conductor at tools/orchestrator/ — managed 113 PRs through chunk lifecycle, gate verdicts, and seam locks. Cross-runtime discipline between Next.js (Node) and Supabase Edge Functions (Deno) was enforced by a generated-types hash sentinel that fails CI on any type drift, including Windows CRLF variants.

Five-layer pipeline — config seeds → ingestion → classification/resolution → scoring/recommendation → read surface
Config/domain
20 CSV seeds: 61 products, 14 capability categories, 199 watchlist entities29 source policies, 47 vendor preferences, industry-keyed scoring weightsAdmin-editable without code deploy
Ingestion
15+ Deno Edge Functions on pg_cron, one per public sourceShared runner: idempotent dedupe, TTL skip, cursor-resumable pagination, per-source error containmentRaw events stored verbatim in raw_events before any interpretation
Classification + resolution
5 typed classifiers (incident, company statement, leadership, ransomware, regulatory)Entity resolver: CIK/ticker/LEI/QID paths + difflib fuzzy fallback; ambiguous CIK → operator review queue
Scoring + recommendation
SQL decay formula in single authoritative migration (DN1, no TypeScript reimplementation)Two-hop recommend Edge Function: event_type → capability_category → productsAppend-only recommendation_snapshots (UPDATE/DELETE/TRUNCATE blocked by DB trigger)
Read surface
Next.js 14 App Router: /feed, /account/[id], /triage, /reviewAll reads from typed Supabase client; no LLM calls on any read pathLeaflet map over PostGIS with CARTO dark tiles, no API key

Key modules

Pipeline

Ingestion layer

15+ Deno Edge Functions (one per public source) on pg_cron, inheriting a shared runner with idempotent dedupe via ON CONFLICT DO NOTHING, TTL skip, cursor-resumable pagination, and per-source error containment.

Data

Entity resolver

Deterministic resolution by CIK, ticker, LEI, and Wikidata QID; difflib fuzzy fallback for names. An ambiguous CIK (two watchlist rows sharing one CIK) routes to the operator review queue, never auto-fires a signal.

Config

Multi-vendor catalog + seller filter

61 products across 47 vendors in CSV seeds; a per-vendor carried/not-carried preference layer enforces the seller's line card without touching recommendation logic. 35 vendors marked carried; 12 excluded for direct-only motion or partner-agreement conflicts.

Pipeline

Two-hop recommendation engine

Event-type-to-capability-category mapping in a CSV; capability-to-product mapping via the seeded products table filtered by seller preferences. No product name hardcoded in any classifier or scoring function.

Data

SQL scoring function

Decay formula (base_strength × 0.5^(age_days/half_life) × account_fit × scope_fit) lives in a single authoritative migration (0008); no TypeScript reimplementation. Industry and subsector weights are keyed rows in scoring_weights.csv.

Pipeline

Stack inference (infer-stack)

Reads each watchlist entity's EDGAR 10-K Item 1C cybersecurity filing to infer incumbent vendor stack using word-boundary regex — incumbent context for the recommendation engine, zero paid data sources.

Config

Config seeds

20 CSV files seeded via scripts/seed-loader.mjs. Products, capability categories, scoring weights, watchlist entities, and vendor preferences are admin-editable without a code deploy.

Key features

Per-industry scoring weights

The core v2 addition over GridSignals. Migration 0008 documents it explicitly: "ADDED for v2: the per-industry dimension (scoring_weights is keyed by industry since 0002), which the original had no concept of." Each row in scoring_weights.csv keys on industry + weight_kind + subsector — iou_electric (NERC CIP registered entities) scores at 1.10 for energy; midstream at 0.95 (TSA pipeline directives); equivalent rows exist for financial_services and healthcare subsectors. An event touching an unknown subsector falls back to a neutral 1.0 weight, never errors. The same scoring function handles all three industries.

Two-hop config-as-data recommendation

The multi-vendor reasoning path: event_type → capability_category → products. First hop lives in event_type_to_category.csv — an editable mapping, not code. Second hop is the seeded products table filtered by the seller's vendor preferences CSV. A healthcare entity triggering an HHS OCR breach enforcement event routes to identity_access and grc_compliance capability categories; the recommendation engine then surfaces the relevant vendor subset from the seller's carried line card, with a trigger class (reactive/compliance/opportunity) explaining the sales motion. Adding a vendor, swapping a product, or adjusting a capability mapping requires no code deploy and no classifier change — only a seed update.

SECURITY & OPS

The operating decisions.

  • Row-level security on all write paths (migration 0010_rls.sql); the anonymous role can SELECT only the four card-path tables — signals, watchlist_entities, signal_evidence, recommendation_snapshots.
  • Seven trust invariants locked as schema CHECK constraints and tested by a DB-level test harness: no price in the DOM, nothing surfaces unsourced, unconfirmed_early_warning cards never customer-facing, recommendation snapshots append-only, no paid or LLM call on any read path, market position shown only with a named analyst source and verified date.
  • Service-role leak scan (scripts/check-service-role-leak.mjs) and a generated-types content-hash sentinel run on every CI pass alongside TypeScript and Deno typechecks.
  • No paid data sources: 29 source policies are all free, read-only public APIs and pages with verified terms of service. The only recurring cost is a capped Claude API audit judge — a scheduled Edge Function, never in the read path.
Seven-trust-invariant signal path
Raw public events · 29 sources
Entity resolution: deterministic identifier paths (CIK/ticker/LEI/QID); ambiguous CIK routes to review queue, never auto-fires a signal
Classification: 5 typed classifiers; unconfirmed_early_warning cards never customer_facing_allowed — schema CHECK constraint, migration 0009
Scoring: SQL-only decay formula, no TypeScript reimplementation; recommendation_snapshots append-only (DB trigger blocks UPDATE/DELETE/TRUNCATE)
Read surface: zero LLM calls, zero paid data; market position displayed only with named analyst source + verified date (21 products downgraded to unranked/none after post-seed review, commit f22c5047)

BUILDER NOTES

What the build taught.

  • The plan orchestrator (tools/orchestrator/ — a custom SQLite-backed conductor, not shipped as part of the product) managed 113 PRs across 11 active days: chunk lifecycle, gate verdicts, seam locks, and reconcile passes. A 565-commit build with parallel tracks needs a conductor.
  • Cross-runtime parity between Next.js (Node) and Supabase Edge Functions (Deno) was enforced by shared types alone — a content-hash sentinel on generated Supabase TypeScript types fails CI on any drift, including CRLF variants on Windows (commit a7ec7b09). Shared import paths are a runtime error; shared types across a CI boundary are not.
  • 21 products were downgraded to unranked/none in commit f22c5047 after a post-seed review found market positions that couldn't be confirmed from their cited source. The market-position honesty gate (R4.10) is a schema guard and a marketPosition.ts runtime check — not documentation.

LESSONS

What changed afterward.

  • Score authority in one place is an architectural rule, not a style preference. Two implementations that agree on demo fixtures and diverge in production make a card whose rendered breakdown doesn't multiply to its own score. DN1 — SCORING LIVES HERE AND NOWHERE ELSE — locked scoring in SQL from day one.
  • Watchlist resolution by regulatory identifier, not name, is load-bearing. Company names are ambiguous and change; CIK/ticker/LEI/Wikidata QID are stable. The Python predecessor silently collapsed CIK collisions (dict last-write-wins); the Deno port surfaces them as an explicit operator review queue that blocks auto-fire.
  • Config-as-data compounds: when products, capability categories, scoring weights, and vendor preferences live in CSV seeds, a seller adjusts their line card without a code deploy — and the test suite asserts the seed invariants directly against the live schema.
  • Cross-runtime CI hygiene matters from the first commit. A content-hash sentinel on generated Supabase TypeScript types caught a Windows CRLF false-positive (commit a7ec7b09) before it could silently drift both runtimes out of alignment.

WHAT CHANGED

Product matching became a two-hop data path.

Industry weights, event-to-capability mappings, products, and seller preferences moved into CSV seeds, so the platform could change a line card without changing classifier or scoring code.

CONTROL

The constraint that held.

Scoring is authoritative in SQL, while schema constraints and append-only triggers enforce the documented trust invariants.

EVIDENCE

Where to inspect it next.

  • Row-level security on all write paths (migration 0010_rls.sql); the anonymous role can SELECT only the four card-path tables — signals, watchlist_entities, signal_evidence, recommendation_snapshots.
  • Seven trust invariants locked as schema CHECK constraints and tested by a DB-level test harness: no price in the DOM, nothing surfaces unsourced, unconfirmed_early_warning cards never customer-facing, recommendation snapshots append-only, no paid or LLM call on any read path, market position shown only with a named analyst source and verified date.
  • Service-role leak scan (scripts/check-service-role-leak.mjs) and a generated-types content-hash sentinel run on every CI pass alongside TypeScript and Deno typechecks.
  • No paid data sources: 29 source policies are all free, read-only public APIs and pages with verified terms of service. The only recurring cost is a capped Claude API audit judge — a scheduled Edge Function, never in the read path.
Build registerReturn to the build register

CARRIED FORWARD

What survived the project.

The two-hop config-as-data reasoning chain — event→capability→product with no hardcoded product names — and the principle that trust invariants belong in the schema (CHECK constraints + append-only triggers) rather than in application code.

WRITING

Field notes from this work.

No public field notes for this project yet.

Trust belongs in the schema, not the application.
The operating principle behind every project here. A rule the database enforces can't be forgotten in a hurry. Constraints, denied-by-default access, append-only logs — the controls that hold are the ones the system won't run without.