Layers, data flow, tech stack, development commands
Architecture
Clean Architecture with strict layer boundaries — domain logic has zero external dependencies.
src/
├── domain/ # Entities, enums and ports — no external imports
├── application/ # Use cases and orchestration
├── infrastructure/ # External integrations
│ ├── db/ # The Postgres pool and the migration runner
│ ├── queue/ # pg-boss: queues, workers, schedules
│ ├── cache/ # Vulnerability, repository and organization persistence
│ ├── feeds/ # One file per source, listed in feedRegistry.js
│ ├── parsers/ # One file per ecosystem, listed in parserRegistry.js
│ ├── notifiers/ # Slack, Teams, email
│ ├── auth/ # Passkey ceremonies, sessions, recovery codes, audit
│ └── llm/ # Provider catalog and adapters
├── interface/
│ ├── index.js # The API: HTTP server, REST routes, Slack callbacks
│ ├── worker.js # The worker: no port, takes jobs off the queue
│ ├── http/ # Routes, one file per resource
│ └── mcp/ # MCP tools for agents, served at /mcp by the API
└── cli/ # Ink terminal client (TypeScript), an HTTP client
ui/ # Console service — no imports from src/
├── server/ # BFF: holds the session cookie, injects the API key
└── src/ # React client
docs/ # This documentation, and the site built from it
└── site/ # The renderer: Markdown in, static HTML out
config/ # Technology filter, vendor mappings, database catalog
db/migrations/ # SQL migrations, applied on boot behind an advisory lock
scripts/atalaia.sh # Launcher: Docker Compose or Apple container
Three processes, one database. The API serves requests and enqueues work. The worker runs it. The console talks only to the API. One Postgres holds the data, the queue and the schedules — which is what makes "is a scan running?" a question with one answer no matter how many containers are up.
One source per file under infrastructure/feeds/, listed in feedRegistry.js — the single list the monitoring cycle and the health check both read, so a source can never be collected but invisible to health checks, or the reverse. The same pattern holds for dependency parsers (infrastructure/parsers/parserRegistry.js), LLM providers (infrastructure/llm/llmProviders.js) and email providers (infrastructure/notifiers/emailProviders.js).
How it works
┌─────────────┐ ┌──────────────┐ ┌────────────┐ ┌───────────┐
│ CVE Feeds │────▶│ Filter by │────▶│Deduplicate │────▶│ Notify │
│ (enabled │ │ Tech Stack │ │ & Merge │ │ Slack + │
│ sources) │ │ │ │ (Postgres) │ │ Email │
└─────────────┘ └──────────────┘ └────────────┘ └───────────┘
NVD config/ Source Block Kit
CISA KEV technologies.json priority buttons
MITRE / EUVD ranking + weekly
GHSA / OpenCVE reports
Snyk / VulDB
…and the rest,
off by default
- A schedule fires — a cron row in the database, so exactly one job is created however many services are running
- The worker takes the job and fetches all enabled sources concurrently — one feed failure never blocks others
- Tech filter matches CVEs against your stack (case-insensitive, configurable at runtime)
- Deduplication checks what is already stored; merges multi-source CVEs using source priority
- Notification sends Slack/Teams alerts with interactive buttons + optional LLM explanation
- Weekly digest emails a severity-grouped report to stakeholders
Nothing runs at API startup any more: a cycle happens when a schedule fires or someone asks for one. See queues.md.
Vulnerability identity is cve_id (a unique key). Status lifecycle is
OPEN → ACKNOWLEDGED → RESOLVED, changed from Slack buttons, the API, the
console or the CLI.
Tech stack
| Layer | Technology |
|---|---|
| Runtime | Node.js 24 LTS (ES Modules) |
| Database | Postgres 13+, whichever one you point DATABASE_URL at. Nothing host-specific is used |
| Queue | pg-boss, in the same Postgres — no Redis |
| Package manager | pnpm 11 workspaces (root + ui) |
| Framework | Express 5 |
| Logging | Pino |
| Notifications | Slack Block Kit, Teams Adaptive Cards, nodemailer |
| Intelligence | OpenAI-compatible providers, Anthropic, Ollama |
| Scraping | axios, cheerio, rss-parser |
| Console | React 19, Vite |
| CLI | Ink, commander, TypeScript — an HTTP client of the API |
| Testing | Jest, supertest |
| Deployment | Containers only: Docker Compose or Apple container |
Development
pnpm install # root + ui workspaces
pnpm run dev # API with hot-reload
pnpm run dev:worker # worker with hot-reload
TEST_DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:5432/postgres pnpm test
pnpm --filter atalaia-console run typecheck
./scripts/atalaia.sh doctor # runtime, database and configuration
Tests live in tests/unit/ and tests/integration/. Each integration suite takes
its own Postgres schema — and its own pg-boss schema — through search_path, so
suites cannot see each other's rows and none of them can see a developer's
running worker. Without TEST_DATABASE_URL they skip themselves and say why, so
the unit suites still run anywhere.