Org Signal Feed
The per-org layer on top of the shared signals store. One immutable intel_signals row is an objective fact with no org and no score; this is where it becomes relevance. Each org scores the same row by its own ICP fit. A user's sonar pull surfaces every signal it finds; an automated push must clear a threshold (and a competitor-ICP check) first. What survives materializes into intel_org_signal_feed — the only org-scoped, RLS-per-org table of the signal stack, the door the launchpad, inbox and digest all read through.
System Design
where the feed sits The store and the feed are two layers with opposite access models. intel_signals is shared, append-only and RLS deny-all — one objective row per event, no organization_id, no score. Scoring sits on the boundary and fans each row out to every org whose universe it touches; survivors materialize into intel_org_signal_feed, the only RLS-per-org table of the stack, which the launchpad, the inbox, the digest and downstream agents read. Signals reach the store by two ingest classes — a user's sonar pull or an automated push — and that class decides whether the gate applies (next section). Click any node to spotlight the paths it touches.
Ingest paths
pull vs push A signal reaches intel_signals by one of two classes, and the class — not the score — decides how it is treated. A pull is user-initiated: someone ran a sonar search, so every signal it finds is returned and shown, ungated. A push is automated: nightly agents and scrapers surface signals nobody asked for, so they are gated — only the strong ones get pushed, and a brief competitor-ICP check keeps a hot lead from going to rival agencies at once. Every discovery, either way, is logged in intel_signal_ingests — one row per find, many per fact — so a re-find never doubles the signal yet is still attributed to the run that found it.
- Trigger
- company-sonar · people-sonar on-demand search
- Provenance
ingest_path = 'sonar_search' · workflow_run_id set- Gate
- No threshold gate — the user asked, so every scored signal returns in the results and materializes a feed row, however weak.
- Feed origin
origin = 'sonar'- Surfaces
- Sonar search results · sonar app data table (all of them) · launchpad
Both classes write the same intel_signals fact and the same scored intel_org_signal_feed row — only the gate and the recorded origin differ. The launchpad reads the union; the sonar app's data table reads the pull lane alone (ingest_path = 'sonar_search'), so an automated find never clutters the search view.
Data flow
survivor pipeline The path from one shared signal to a ranked launchpad row, for one org. Ingest + ICP pair the objective fact with this org's relevance; Score collapses it to a scalar; the path-aware gate drops weak pushes (but never a pull); Materialize writes the intel_org_signal_feed row with its origin; Rank orders it; Deliver renders the launchpad, inbox and digest. Tap a stage to read what goes in, the transform, and what comes out.
Score
scoreA (signal, org) candidate
score = signal-type weight × icp_fit × recency decay(observed_at), evaluated now — never stored on the signal.
A scalar 0..10 for this org
{ score, decay, weight }Fan-out
one signal, many feeds The idea the whole layer turns on. One intel_signals row is scored separately for every org. The signal-type weight and the recency decay are the same for all of them — only icp_fit differs — so the same fact lands as three different scores and crosses different thresholds. Pick a signal, then age it to watch decay drag every score down through the same two gates.
tight ICP match — exact industry + size band
partial match — adjacent vertical
weak match — out of core segment
One shared intel_signals row · 2 of 3 orgs materialize a feed row · the rest see nothing, though the signal still exists in the store underneath them.
Scoring model
weight × ICP × decay Three factors, multiplied, on a 0–10 scale so the canonical thresholds (feed_threshold 4, digest_threshold 7) read directly. Weight is the intrinsic importance of the signal type; ICP fit is the per-org relevance; decay is a 90-day half-life applied to observed_at at read time — the store row is never rewritten, so the same signal scores lower simply by getting older.
funding_roundcapital0.9executive_changepeople moves0.85job_changepeople moves0.7hired_growth_rolehiring0.6scaled_meta_adsad spend0.5new_tech_stackbrand + tech0.45rebrand_or_relaunchbrand + tech0.4thought_leadershipengagement0.3 Decay is evaluated when the feed is materialized and again when the inbox reads — never stored on intel_signals. A correction or a fresher sighting arrives as a new store row, not an update.
Schema design
the org-scoped table One table — intel_org_signal_feed — holds the activation copy. It is the only org-scoped table of the signal stack: organization_id and the score live here, never on the shared signal. Every row points back at one immutable intel_signals row, so many orgs' feed rows fan from one fact.
One intel_signals row fans to one intel_org_signal_feed row per org (1 ──< many); each feed row joins back to its organizations row (many >── 1). organization_id and the score live only inside the activation boundary — the shared store carries neither.
| Column | Type | Role |
|---|---|---|
idPK | uuid | one row per (org, signal) survivor |
organization_idFK | uuid | → organizations — the per-org owner; the RLS subject. The store carries no org_id at all. |
signal_idFK | uuid | → intel_signals — many feed rows point back at one shared, immutable signal. |
score | numeric | weight × icp_fit × decay, captured at materialization — a snapshot, not a live recompute. |
icp_fit | numeric | this org's relevance 0..1 — the one factor that differs per org and explains the spread. |
decay | numeric | the recency multiplier applied when this row was scored, from the signal's observed_at. |
tierENUM | enum 'feed' | 'digest' | which gate it cleared — feed_threshold 4 for the inbox, digest_threshold 7 for the digest. |
originENUM | enum 'sonar' | 'proactive' | how this row entered the feed — a user's sonar pull or an automated push. Sonar wins when a signal arrives by both, so the launchpad label stays honest. The full discovery log lives in intel_signal_ingests. |
rank | int | position in this org's ranked feed, ordered by score. |
surfaced_at | timestamptz | when it entered this org feed. |
seen_at | timestamptz · null | inbox read state — null until the user opens it. |
(organization_id, signal_id)UNIQUE | unique | one feed row per signal per org — re-materialization upserts the score in place. |
No signal_type, payload or subject_id is copied down — the feed row carries only relevance (score, icp_fit, tier, rank, origin) and joins through signal_id for the fact. How the signal was discovered (which run, which agent, when) lives in the separate intel_signal_ingests log, not here. The append-only store stays the single source of truth; the feed is a disposable, rebuildable projection.
Delivery surfaces
launchpad · inbox · digest Three surfaces read the one feed table. The launchpad shows the hottest rows ranked by score — a mix of both origins, each tagged sonar or proactive so the user knows whether they pulled it or the system pushed it. The always-on signal inbox renders every feed-tier row; the periodic digest batches only the strongest (≥ digest_threshold 7). seen_at tracks read state per row, so what is new stays marked.
- 1Acme
funding_roundproactive2ddigest8.6 - 2Beacon
executive_changesonar4ddigest7.4 - 3Northwind
hired_growth_roleproactive11dinbox5.7 - 4Vertex
scaled_meta_adssonar6dinbox4.6 - 5Lumen
new_tech_stackproactive20dinbox4.1
The hottest mix of both origins, ranked by score — the dashboard widget. sonar = the org pulled it; proactive = an agent pushed it.
Access + RLS
deny-all store, allow-per-org feed The two layers invert their access model. The shared store is RLS deny-all — no client touches it, only the ac-python-api service role writes and reads. The feed is RLS allow-per-org: each org reads exactly its own rows, scoped by organization_id, and the same service role is the only writer (the materializer). One fact, isolated per tenant at the feed boundary. Pick an actor to watch RLS resolve every store and feed row, with the feed predicate evaluating live.
An authenticated member of Halo. auth.org_id() resolves to org_halo, so the feed policy admits only Halo rows.
intel_signals · sig_7b20intel_signals has no policy → deny-all to every client roleDENYfeed · sig_7b20 → Halo · 8.6organization_id (org_halo) = auth.org_id() (org_halo) ✓ALLOWfeed · sig_7b20 → Northstar · 5.4organization_id (org_north) ≠ auth.org_id() (org_halo) ✗DENY1 of 3 rows readable as Halo user.