Skip to Content
Part 1: FoundationsCh 1: The Big Picture

What Astrelo Actually Does

Before we look at any code, let’s understand the business problem.

The Problem: Sales teams waste 60%+ of their time on prospects that will never close. A rep has 200 companies in their pipeline. Which 20 should they focus on today? Which ones are about to churn? Which new prospects match their winning profile?

The Solution: Astrelo analyzes your closed deals to build a β€œwinning profile” β€” a mathematical fingerprint of the companies you’ve successfully sold to. Then it scores your entire pipeline against that profile, surfaces the highest-probability opportunities, and proactively alerts you when something needs attention.

Think of it as three systems working together:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Astrelo β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ Scoring β”‚ β”‚ Cosmo β”‚ β”‚ Proactive β”‚ β”‚ β”‚ β”‚ Engine β”‚ β”‚ (AI) β”‚ β”‚ Intelligence β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ "Who to β”‚ β”‚ "What to β”‚ β”‚ "When to act" β”‚ β”‚ β”‚ β”‚ target" β”‚ β”‚ do" β”‚ β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β–² β–² β–² β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ Your CRM β”‚ β”‚ β”‚ β”‚ (HubSpot / β”‚ β”‚ β”‚ β”‚ Salesforce) β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  1. The Scoring Engine pulls your deals, contacts, and companies from your CRM. It builds ML models (using embeddings and similarity math) to score every prospect on two dimensions: Fit (how similar to your past winners) and Intent (how actively they’re engaging). These combine into a Composite Score that ranks your entire pipeline.

  2. Cosmo is the AI chat assistant. It has 40+ tools β€” it can send emails, create tasks, log touches, generate pitch decks, and analyze deals. It’s not a chatbot that just answers questions; it’s an agent that executes real actions in your CRM.

  3. Proactive Intelligence monitors your CRM in real-time via webhooks. When a deal moves backward, a competitor enters, or a champion goes cold, it fires an alert with a pre-drafted action. It doesn’t wait for you to check β€” it pushes.

How a Request Flows Through Astrelo
1
Browser sends request to /api/ranking/calculateBrowser
2
requireAuth middleware validates JWT tokenAPI Route
3
Scoring orchestrator loads ICP profile and weightsDomain
4
Fit scorer computes NAICS + size similarityDomain
5
Intent scorer aggregates signals with recency decayDomain
6
Composite score combines fit and intentDomain
7
Results cached in Redis, persisted to scores tableDatabase

The File System: Where Everything Lives

Open the src/ directory and you’ll see this structure:

src/ β”œβ”€β”€ pages/ ← URL routes (Next.js convention) β”‚ β”œβ”€β”€ api/ ← Backend API endpoints β”‚ β”œβ”€β”€ app.tsx ← The main application page β”‚ β”œβ”€β”€ index.tsx ← The marketing landing page β”‚ └── login.tsx ← Login page β”‚ β”œβ”€β”€ contexts/ ← Global React state (auth, theme) β”œβ”€β”€ features/ ← Feature modules (UI components + hooks) β”œβ”€β”€ domain/ ← Business logic and database operations β”œβ”€β”€ infrastructure/ ← External connections (DB, auth, APIs) β”œβ”€β”€ ui/ ← Shared UI components β”œβ”€β”€ theme/ ← MUI theme configuration β”œβ”€β”€ config/ ← Environment and app configuration └── shared/ ← Utilities shared across layers

This isn’t random. It’s a deliberate layered architecture with strict dependency rules:

pages/api/ ──calls──► domain/ ──calls──► infrastructure/ (HTTP) (Logic) (External I/O) features/ ──calls──► pages/api/ (via HTTP fetch) (UI) (Server)

The rules:

  • pages/api/ (API routes) handle HTTP concerns: parse request, validate input, call domain services, format response.
  • domain/ contains the business logic: scoring algorithms, alert evaluation, prediction classification. It talks to the database through infrastructure/.
  • infrastructure/ handles external I/O: database connections, CRM API calls, LLM requests, Slack delivery.
  • features/ contains React components and hooks. They fetch data from pages/api/ using HTTP requests β€” they never import domain services directly.
  • Nothing goes backward. infrastructure/ never imports from domain/. domain/ never imports from pages/.

Why this matters: If you need to change how scoring works, you know exactly where to look: src/domain/scoring/. If the HubSpot API changes, you only touch src/infrastructure/providers/hubspot/. If a button looks wrong, it’s in src/features/. Each layer has one job.

How a Request Flows Through the System

Let’s trace what happens when a user opens the alert feed. This is the single most important mental model for understanding the entire codebase.

Step 1: The user opens the app.

The NotificationBell component renders in the top bar. Inside it, there’s a React Query hook:

// src/features/alerts/hooks/useAlerts.ts export function useAlerts(limit: number = 20) { const { token } = useAuth(); return useQuery({ queryKey: queryKeys.alerts.feed({ limit }), queryFn: async (): Promise<AlertFeedResponse> => { const res = await fetch(`/api/alerts?limit=${limit}`, { headers: { Authorization: `Bearer ${token}` }, }); if (!res.ok) throw new Error('Failed to fetch alerts'); return res.json(); }, enabled: !!token, staleTime: 15_000, }); }

React Query calls the queryFn β€” a plain fetch to /api/alerts. The JWT token is attached in the Authorization header.

Step 2: The request hits the API route.

Next.js maps the URL /api/alerts to the file src/pages/api/alerts/index.ts:

// src/pages/api/alerts/index.ts async function handler(req: AuthenticatedRequest, res: NextApiResponse) { if (req.method !== 'GET') { return res.status(405).json({ error: 'Method not allowed' }); } const userId = req.userId!; try { const limit = Math.min(parseInt(String(req.query.limit)) || 20, 50); const offset = parseInt(String(req.query.offset)) || 0; const [alerts, unviewedCount] = await Promise.all([ getAlertFeed(userId, limit, offset), getUnviewedCount(userId), ]); return res.status(200).json({ alerts, unviewedCount }); } catch (error) { console.error('[Alerts API] Error fetching feed:', error); return res.status(500).json({ error: 'Internal server error' }); } } export default requireAuth(handler);

Before handler runs, requireAuth extracts the JWT, verifies it, and attaches userId to the request. If the token is invalid, the user gets a 401 and handler never executes.

Step 3: The domain service queries the database.

getAlertFeed is a function in src/domain/alerts/services/alertPersistence.ts. It runs a SQL query:

SELECT * FROM realtime_alerts WHERE user_id = $1 AND dismissed_at IS NULL AND (expires_at IS NULL OR expires_at > NOW()) ORDER BY created_at DESC LIMIT $2 OFFSET $3

The $1 is the user’s ID β€” this is a parameterized query that prevents SQL injection. The database returns rows, which the service maps to TypeScript objects.

Step 4: The response flows back.

PostgreSQL β†’ alertPersistence.ts β†’ API route β†’ JSON response β†’ React Query β†’ Component re-render

React Query caches the result. For the next 15 seconds (staleTime: 15_000), if any other component calls useAlerts(), it gets the cached data instantly β€” no second network request.

Step 5: The UI updates.

The NotificationBell component reads data.unviewedCount from the query result and shows a badge. The AlertFeed component maps over data.alerts and renders each one as an AlertFeedItem.

This entire flow β€” from click to rendered pixels β€” takes roughly 50-200 milliseconds.

The Two Servers in One

Here’s something that confuses many developers new to Next.js: your frontend and backend are in the same project, but they run in very different environments.

src/pages/app.tsx ← Runs in the BROWSER (React, DOM, window) src/pages/api/alerts/ ← Runs on the SERVER (Node.js, database, fs)

When you deploy, Next.js bundles these into two separate outputs:

  • A client bundle (JavaScript sent to the browser) that contains your React components
  • A server runtime (Node.js process) that handles API routes

They communicate over HTTP, just like if they were separate applications. The convenience of Next.js is that you develop them in one codebase with shared TypeScript types β€” but at runtime, they’re completely separate worlds.

This is why you see a webpack.resolve.fallback in next.config.js that stubs out fs, dns, net, and pg for the browser bundle. Those are Node.js-only modules used by the server β€” the browser has no filesystem or database driver. Without the fallback, webpack would try to bundle them for the client and fail.

What We’ll Cover Next

Now that you have the mental model β€” layered architecture, request flow, two environments β€” we’ll start going deep. Chapter 2 covers the authentication system: how users log in, how JWTs work, and why every API route in the app starts with requireAuth.

Last updated on