API
Server actions and API routes for programmatic access.
API
ATHELOS is primarily used through its UI, but exposes server actions and API routes for automation and integration.
Two access paths
Server actions — called from within the app itself. Not exposed to the network. Type-safe, session-authenticated.
API routes — HTTP endpoints under /api. Callable by scripts,
webhooks, or external tools. Session-authenticated via cookies.
Neither is designed for public consumption. Both assume you're behind a VPN or on the local machine.
Server actions
Server actions live in apps/web/src/lib/*/actions.ts and are marked
"use server". They're called from client components via React's
server action mechanism.
Every action:
- Requires a valid session (
requireUser()) - Validates input with Zod
- Performs the operation
- Writes to the audit log
- Revalidates affected paths
- Returns
{ ok: true, ... }or{ ok: false, error }
Actions never throw across the boundary. Errors are returned, not raised.
Action groups
Knowledge base — lib/kb/actions.ts
| Action | Purpose |
|---|---|
createArticle | Create a KB article |
updateArticle | Update an existing article |
deleteArticle | Delete an article |
Playbooks — lib/playbooks/actions.ts
| Action | Purpose |
|---|---|
loadPlaybook | Attach a playbook to an engagement |
toggleTask | Mark a task complete or incomplete |
updateTaskNotes | Update notes on a task |
attachFindingToTask | Link a finding to a task |
attachEvidenceToTask | Link evidence to a task |
detachFindingFromTask | Unlink |
detachEvidenceFromTask | Unlink |
removePlaybook | Remove a playbook from an engagement |
Engagements — lib/engagements/actions.ts
| Action | Purpose |
|---|---|
createEngagement | Create a new engagement |
updateEngagement | Update fields |
advancePhase | Move the lifecycle forward |
revertPhase | Move the lifecycle back |
Findings — lib/findings/actions.ts
| Action | Purpose |
|---|---|
createFinding | Create a new finding |
updateFinding | Update fields |
deleteFinding | Delete |
setFindingStatus | Change status |
Evidence — lib/evidence/actions.ts
| Action | Purpose |
|---|---|
uploadEvidence | Attach a file to a finding |
deleteEvidence | Remove |
Reports — lib/reports/actions.ts
| Action | Purpose |
|---|---|
generateReport | Generate from engagement |
updateReportField | Update a text field |
updateReportSection | Update a JSONB section |
duplicateReport | Create a new version |
setReportStatus | Change status |
Deliveries — lib/deliveries/actions.ts
| Action | Purpose |
|---|---|
createDelivery | Create a delivery link |
revokeDelivery | Revoke a link |
recordView | Log a client view (public) |
Clients — lib/clients/actions.ts
| Action | Purpose |
|---|---|
createClient | Create a client |
updateClient | Update |
addContact | Add a contact |
logInteraction | Log a touchpoint |
Invoices — lib/invoices/actions.ts
| Action | Purpose |
|---|---|
createInvoice | Create an invoice |
addLineItem | Add a line item |
recordPayment | Record payment received |
cancelInvoice | Void |
AI — lib/ai/actions.ts
| Action | Purpose |
|---|---|
embedArticle | Generate an embedding |
removeArticleEmbedding | Remove an embedding |
embedAllArticles | Reindex everything |
chatWithKb | RAG chat with streaming |
draftExecSummary | Assistive action |
suggestRemediation | Assistive action |
mapToAttck | Assistive action |
Notifications — lib/notify/actions.ts
| Action | Purpose |
|---|---|
sendTestEmail | Send a test email |
sendTestWebhook | Send a test webhook |
updateNotificationSettings | Update preferences |
Auth — lib/auth/*
| Action | Purpose |
|---|---|
signIn | Login (email + password + TOTP) |
signOut | End session |
enrollTotp | Begin TOTP enrollment |
verifyTotp | Complete enrollment |
disableTotp | Disable TOTP |
regenerateRecoveryCodes | New codes |
Calling a server action
From a client component:
"use client";
import { createFinding } from "@/lib/findings/actions";
async function handleSubmit() {
const result = await createFinding({
engagementId: "uuid",
title: "SQL Injection",
severity: "high",
});
if (result.ok) {
// Success
} else {
// result.error has the message
}
}Server actions are called like regular functions. TypeScript knows the input and output types.
API routes
Routes live under apps/web/src/app/api/. They use Next.js route handlers.
Auth — /api/auth/[...nextauth]
Standard NextAuth handler. Handles login, logout, session, and CSRF.
Methods: GET, POST
Reports PDF — /api/reports/[id]/pdf
Generates and downloads a report PDF.
Methods: GET
Response: application/pdf
Deliveries — /api/deliveries/[token]/view
Records a view on a delivery link.
Methods: POST
Public — no session required. Used by the delivery page.
AI ping — /api/ai/ping
Checks whether Ollama is reachable.
Methods: GET
Response: { ok: true, model: "llama3.2:3b", host: "http://localhost:11434" }
AI search — /api/ai/search/kb
Semantic search over the KB.
Methods: POST
Body:
{
"query": "how to exploit delegation",
"limit": 10
}Response:
{
"results": [
{ "slug": "ad-delegation", "similarity": 0.82 },
...
]
}Authentication
API routes check the session cookie automatically via middleware. If no
valid session exists, the route returns 401.
For scripts that don't have a browser session, options:
- Cookie extraction — log in via the browser, copy the session cookie, use it in your HTTP client
- Direct server action call — if you have Node.js access, import the action and call it directly with the same session context
There's no API token system. The single-user model assumes you're the operator.
Audit logging
Every mutating server action writes one row to audit_log:
await db.insert(auditLog).values({
userId: user.id,
action: "finding.create",
entity: "finding",
entityId: finding.id,
meta: { severity: finding.severity },
});The audit log is append-only. It's the authoritative record of what happened in the system.
Error handling
Server actions return a structured result:
type ActionResult<T = {}> =
| ({ ok: true } & T)
| { ok: false; error: string };Check .ok before accessing fields. On error, .error has a
user-facing message.
API routes return standard HTTP errors:
| Status | Meaning |
|---|---|
| 200 | Success |
| 400 | Validation failed |
| 401 | No valid session |
| 403 | Session valid but not authorized |
| 404 | Not found |
| 429 | Rate limited |
| 500 | Server error |
Rate limiting
Only /api/auth/check-password is rate-limited (5 attempts per 15
minutes per IP+email). Other endpoints rely on the single-user model and
VPN-only access.
Webhooks
Outbound webhooks fire on events (see Event bus). Payload format and signature verification are covered in Notifications.
Using the API from scripts
Example — fetch all critical findings:
import { db, findings } from "@soc/db";
import { eq } from "drizzle-orm";
const critical = await db
.select()
.from(findings)
.where(eq(findings.severity, "critical"));Direct database access is often simpler than HTTP. The Drizzle schema is
importable from @soc/db.
Extending the API
To add a new API route:
- Create
apps/web/src/app/api/<path>/route.ts - Export
GET,POST, etc. - Use
auth()to check the session - Validate input with Zod
- Return a
Responseor JSON
Example:
import { auth } from "@/lib/auth";
import { NextResponse } from "next/server";
export async function GET() {
const session = await auth();
if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
return NextResponse.json({ ok: true });
}Where to go next
- Database — schema reference
- Event bus — how events trigger actions
- CLI reference — scripted operations