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) β β
β βββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββ-
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.
-
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.
-
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.
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 layersThis 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 throughinfrastructure/.infrastructure/handles external I/O: database connections, CRM API calls, LLM requests, Slack delivery.features/contains React components and hooks. They fetch data frompages/api/using HTTP requests β they never import domain services directly.- Nothing goes backward.
infrastructure/never imports fromdomain/.domain/never imports frompages/.
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 $3The $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-renderReact 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.