HomeArchitecture

Architecture

A detailed walk through how the Client Tool Emulator is built: the request path, the mock engine, the adapter platform, the correlation store, and the honest answer to whether the data is real. This page is public and needs no sign-in.

23
Adapters
198
Endpoints
15
With discovery
100
Fleet assets

System overview

One Next.js application that plays two roles at once.

The Client Tool Emulator is a single Next.js 14 application. It emulates the cybersecurity tools a client runs, and on top of those mocks it runs an adapter platform that behaves like an asset-inventory product such as Axonius. There is no separate backend service: the same deployment serves the browser dashboard, the mock vendor APIs, the gateway, and the background jobs. State that has to survive a restart lives in a Postgres database hosted on Supabase.

The point of the product is to let an AI agent, or any integration, exercise a realistic security stack end to end without touching a real client environment. An agent can call a mock API directly, or it can go through a configured connection. Either way it gets a believable, vendor-shaped response, and every call is recorded in a request trace.

A request travels through a fixed set of layers. The edge middleware decides whether a route is public or needs a session. Route handlers under the API folder do the HTTP work. The domain code under the lib folder holds the real logic: the mock engine, the tool registry, the adapter machinery, and the correlation store. The database sits at the end of that chain.

The request path, from any consumer down to the database.

Two ideas make the rest of this document easier to follow. First, the code registry is the source of truth for the catalog: the list of 23 tools and their 198 endpoints comes from TypeScript files, not from the database, so the dashboard still renders the catalog when the database is offline. Second, the database holds only runtime state: connections, sessions, fetch history, correlated assets, logs, and keys.

Technology stack

ConcernChoiceNotes
FrameworkNext.js 14 (App Router)Server components, route handlers, and edge middleware, all in one app.
LanguageTypeScriptShared types across the API, the domain layer, and the UI.
UIReact 18, Tailwind CSSA calm enterprise design system with light and dark themes, tuned for WCAG AA contrast.
Motion and iconsFramer Motion, LucideRestrained animation, disabled under reduced-motion preferences.
DiagramsMermaidRendered client-side and themed from the same CSS tokens, so they match light and dark.
DatabasePostgreSQL on SupabaseReached with node-postgres. Everything lives in an emulator schema, never public.
IdentityAutoX SSO (OIDC)Single sign-on via Authorization Code + PKCE; the app mints its own signed session cookie after the callback.
HostingVercel, or a long-lived Node serverThe code detects which one it runs on and adapts pooling and scheduling.

There are deliberately few dependencies. The mock responses, the correlation logic, and the scheduling are all plain TypeScript. That keeps behaviour predictable and the system easy to reason about.

The two layers

An emulator core, and an adapter platform built on top of it.

Layer one: the emulator core

Every tool is hand-authored to mirror a real vendor API: the same URL paths, the same authentication scheme, and the same response field names. Responses are deterministic, which means the same input always produces the same output. Inventory endpoints do not invent random data. They project a shared, canonical fleet, so the same laptop shows up in CrowdStrike, Qualys, Tenable, and Intune with matching serial numbers and MAC addresses.

  • A mock engine that matches a request to an endpoint, checks the tool's auth, applies latency and fault scenarios, logs the call, and can emit an event.
  • A request trace that records every call, direct or through the gateway, with method, path, status, latency, and redacted headers and bodies.
  • Publish and subscribe webhooks: an agent can subscribe a URL to a tool's events, and deliveries are signed with HMAC and logged.
  • Generators: background jobs that emit tool events on a schedule so the environment stays alive without anyone opening the dashboard.

Layer two: the adapter platform

The platform turns each mock tool into an adapter you can connect to. You create a credentialed connection, that connection maintains a live status through scheduled heartbeats, scheduled discovery fetches pull inventory through a single gateway endpoint, and the pulled records are normalized and correlated into one unified asset inventory. Of the 23 adapters, 15 run discovery fetches; the rest are enrichment-only tools that answer lookups but do not carry inventory.

Request routing and the edge

The edge middleware runs before every non-static request. It is the first line of defense, but not the only one: route handlers and layouts check permissions again on the server against the database, so bypassing the edge alone cannot grant access. This is defense in depth.

Public surfaces (no session required)

  • The public landing page and this architecture page.
  • The SSO endpoints under /api/auth/sso for login, the OIDC callback, and the cold-start health probe, plus logout.
  • The mock APIs under /api/mock, because agents authenticate with a per-tool API key rather than a browser session.
  • The gateway under /api/gateway, because the connection itself embodies the credential.
  • The inbound webhook receiver under /api/consumer, which accepts server-to-server delivery.
  • The scheduler trigger at /api/cron/tick, which is protected by a shared secret instead of a session.

Protected and admin-only surfaces

Everything else needs a valid signed session cookie. If the session is missing on a page route the user is redirected to the login page; on an API route the response is a 401. A smaller set of routes, such as key management and user management, additionally requires the administrator role. A signed-in user without that role is redirected away from a page, or receives a 403 from an API.

The mock engine

The one place a tool call is actually resolved.

Every call to a tool, whether it arrives at a mock API route or through the gateway, is resolved by the same engine. Sharing this path is what makes gateway traffic indistinguishable from direct traffic in the logs. The engine performs the same steps in the same order every time.

  1. 01Match the method and path against the tool's endpoint templates, filling path parameters such as an id in the URL.
  2. 02Check authentication using the tool's own scheme: a bearer token, basic auth, an API key header, or an API key query parameter.
  3. 03Apply any active scenario for that tool: added latency, a forced error rate, or a forced status code.
  4. 04Build the deterministic response body from the canonical fleet or the tool's seeded data.
  5. 05Log the call into the request trace with redacted secrets.
  6. 06If the call was a successful mutation, publish an event to any subscribers, without blocking the response.
Why determinism matters

Because responses are seeded rather than random, a test that passed yesterday passes today with the same inputs. It also lets inventory line up across tools: the same seed produces the same serial number in two different vendor formats, which is the precondition for correlation.

Tool registry and canonical fleet

The tool registry is a set of TypeScript modules that define each vendor: its endpoints, auth scheme, and response shapes. Alongside it, a metadata file adds the adapter-grade details used by the platform, such as the connection form fields, the list of asset types the adapter fetches, the specific fetch steps it runs, the heartbeat probe, and the vendor permissions the documentation lists. Together these two files are the catalog. The dashboard reads them at request time, so the catalog is always available even without a database.

The canonical fleet is one invented organization that every inventory-bearing tool projects into its own schema. It is generated deterministically from a seeded pseudo-random generator, so the same identifier appears every time. The fleet holds 60 devices and 40 users, each with a stable hostname, MAC address, serial number, and email. Because CrowdStrike, Qualys, Meraki, Entra, and the other adapters all draw from this one fleet, cross-adapter correlation on serial, MAC, hostname, and email genuinely works from end to end.

The key consequence

Correlation is not pre-computed or faked. Two adapters independently report the same machine because they both project the same fleet. When their records meet in the asset store, they merge for a real reason: the identifiers actually match.

Adapters and connections

A connection is one configured account, backed by a real credential.

Creating a connection provisions a real credential

This is the design decision that makes the platform behave like a real system rather than a mock-up. When you create a connection, three things happen. A connection row is written with status pending. A random secret is generated and stored on the server side, never returned to the browser. And an actual API key row is inserted for that tool, whose secret is exactly that stored secret.

From then on, when a heartbeat, a fetch, or a gateway call runs, the platform injects that secret using the tool's own auth scheme, and the engine validates it with the same auth check any request hits. So the credential is genuine. Disable the connection, or simulate revoked credentials, and the key is deactivated, at which point the engine returns a real 401 for that connection's traffic. Of the 23 adapters, 23 define a connection form with credential fields; the validator rejects unknown fields and names any missing required field.

The lifecycle is driven by real probes

Each connection has a heartbeat, a liveness probe that calls the adapter's designated read endpoint through the gateway. The outcome moves the connection through a state machine. The status you see in the dashboard is therefore a record of probes that actually ran, not a scripted animation.

Connection lifecycle. Every transition is written to the event trail.
StateMeaningHow it is reached
pendingJust created, not yet probed.Set at creation.
connectingRevalidating after a change.Credentials or configuration changed, or the connection was re-enabled.
connectedHealthy and authorized.A heartbeat got an authorized answer from the vendor mock.
degradedOne or two recent transient failures.A 5xx or a simulated outage, with a failure streak below three.
errorUnhealthy.Three or more consecutive transient failures, or a hard 401 on any probe or fetch step.
disabledTurned off by a user.The enabled flag was set to false; its credential is deactivated.

Sessions and observable reuse

A real client SDK authenticates once and reuses that session across many calls. The platform models this directly. The first call on a connection mints a session with a time to live, commonly thirty minutes. Every later call within that window reuses the same session and increments a use counter. Changing credentials, disabling the connection, or simulating revocation kills the live sessions.

Reuse is made visible rather than merely claimed. The connection tracks how many sessions it has issued and how many times it has reused one, and the gateway returns a header that states whether the current call reused a session. This is a deliberate step beyond the product it imitates, which re-authenticates on every fetch and keeps no session alive between cycles.

The gateway

One URL per connection, any endpoint on the underlying tool.

The gateway gives each connection a single base URL of the form /api/gateway/<connection>/<tool path>. It is the one choke point that the public gateway route, the heartbeats, and the fetch steps all pass through, which keeps behaviour consistent. For each call it does the following.

  1. 01Load the connection and resolve which tool it points at.
  2. 02Inject the connection's provisioned credential in that tool's real auth scheme.
  3. 03Apply connection-level fault injection: a simulated outage returns a 502 after a short pause, and a slow simulation adds delay.
  4. 04Run the real mock engine, so path matching, auth, scenarios, latency, logging, and events all apply.
  5. 05Record the call in the same request trace used for direct calls.

The gateway is public on purpose. The connection carries the credential, so no browser session is needed to call it. That mirrors how an integration in the field would reach a vendor: through the credential, not through a human login.

Discovery fetches

A discovery fetch is a real multi-step run against the adapter. It opens one session for the whole run, which is where session reuse becomes visible, then it works through the adapter's fetch steps in order. For each step it calls the endpoint through the gateway, reads the records array out of the response, normalizes each record, and correlates it into the asset store.

One discovery run. The dashed edge is the history written for every run.

Record extraction is tolerant of vendor quirks. It follows a dotted path to the array, and it understands common oddities such as a text envelope that prefixes JSON, or an XML-derived wrapper that nests the list one level deeper and collapses a single record into an object.

When the run finishes it writes a full history row: whether it succeeded, partially succeeded, or failed, its duration, the per-step results, the number of records by asset type, and whether the session was reused. If any step's credential is rejected, the run records an auth failure and moves the connection to error, so the fetch history and the connection status always agree.

Run statusMeaning
successEvery step completed without error.
partialSome steps succeeded and some failed.
failedEvery step failed.

Normalization and correlation

Vendor records in, one explainable asset inventory out.

Normalizing

Each inventory tool has a normalizer that turns a vendor record into a common shape. There are 6 tool-specific normalizers plus a generic fallback, so adapters added later still normalize without new code. A normalized record carries the correlation keys pulled out to the top level: the asset type, a stable external id, a display name, and where present the hostname, MAC, serial, and email, along with a summary and the original raw evidence.

Correlating

The asset store merges records into unified assets using a fixed, ordered set of rules. The first rule that matches wins. Keys are lowercased before comparison so formatting differences do not prevent a match. The asset types currently produced are: alert, device, saas_app, user, vulnerability.

The same device reported by three tools, merged on different keys.
Asset typeCorrelation rule, in order
deviceserial, then MAC, then hostname
useremail
vulnerabilitya unique combination of the CVE or QID and the hostname
software, saas_app, alertno cross-source rule in this version; one asset per source

The feature that sets this apart is that every source records which rule merged it, stored next to the raw vendor evidence. In the assets view you can open one device and see that it was merged from CrowdStrike by serial, from Qualys by MAC, and from Intune by hostname, with the original record behind each source. This is the deliberate answer to correlation engines that behave like a black box.

The merge is conservative. Correlation keys only ever fill an empty field; they never overwrite one, so a key cannot flap between values. Summary fields take the most recent value. A source's assignment to an asset is sticky once made, and the source count on an asset is recomputed from the evidence rows rather than guessed.

Schedulers and serverless behaviour

Two things need to happen on a timer: generators emit tool events, and adapter cycles run heartbeats and fetches. On a long-lived Node server these run in the process itself, on short ticks, started once when the server boots. This keeps the simulation moving without anyone opening the dashboard.

On serverless hosting there is no always-on process, so the in-process timers are skipped and an external cron calls the tick endpoint instead. Whichever path runs, the work is claimed atomically: a due item is marked as taken in the same database update that checks it is still due, so two overlapping ticks cannot run the same probe or fetch twice.

Graceful under a database outage

If the database is unreachable, the scheduler simply finds nothing to claim and does nothing, the catalog still renders from the code registry, and database-backed panels show empty until the connection returns. A short circuit breaker stops the app from hammering a paused database.

The data model

All runtime state lives in 16 tables inside the emulator schema.

TableWhat it holds
adapter_connectionsOne row per configured connection: status, schedule, counters, and the server-side secret.
connection_sessionsMinted and reused sessions, each with a time to live.
connection_eventsThe lifecycle trail: created, heartbeat, status change, fetch started and finished.
fetch_runsDiscovery history, one row per run, with per-step detail.
assetsThe correlated, unified inventory of devices, users, and vulnerabilities.
asset_sourcesPer-source evidence, plus the correlation rule that merged each source.
api_keysInbound authentication, including the per-connection provisioned credentials.
request_logsThe full request trace for direct and gateway calls.
subscriptions, event_deliveriesWebhook subscriptions and their signed, logged deliveries.
generators, scenarios, resourcesScheduled event emitters, fault-injection settings, and durable tool state.
usersDashboard accounts and roles.
tools, endpointsA mirror of the catalog for reference; the code registry remains authoritative.

Is the data real?

The data is synthetic, but the mechanics are real. The distinction is the whole point.

The honest answer has two halves, and both matter. The inventory content is invented, and the machinery around it is genuine.

What is not real

  • The inventory is fabricated. There is no live vendor behind any tool. Every device, user, and vulnerability comes from the one canonical fleet.
  • Vendor responses are hand-authored mocks, not proxied from a real API. They copy real paths, auth schemes, and field names, but the bytes are emulated.
  • The failures are opt-in. Revoked credentials, outages, and slow responses happen only when you choose to simulate them.

What is real

  • The credential and auth flow. A provisioned key is genuinely validated by the engine. Revoke it and you get a genuine 401.
  • The lifecycle. Statuses come from probes that actually ran, with results that actually returned, all persisted and timestamped.
  • The correlation. Because every tool projects the same fleet, the same machine really does appear in several tools and really does merge on matching identifiers.
  • Session reuse. Real rows, a real time to live, and counters you can watch increase.
  • The persistence. Everything lands in a real Postgres database.
The mental model

Think of a flight simulator rather than a video of a flight. The weather is synthetic and you decide when the engine fails, but the cockpit, the controls, and the way the aircraft responds are faithful. You are exercising the real behaviour of an adapter platform against invented inventory. That combination is what makes hard scenarios, such as a connection degrading or a fetch coming back partial, demonstrable and repeatable in a way a real tenant cannot provide on demand.

External services and integrations

ServiceRoleHow it is used
Supabase PostgresSystem of record for runtime stateReached through a connection pool. On serverless the app upgrades a recognized pooler URL to the transaction pooler so many function instances do not exhaust the connection limit.
AutoX SSOIdentity provider (OIDC)Authenticates users via Authorization Code + PKCE (ES256). The app verifies the ID token, reads the per-app role from the JWT access token, and provisions a local account on first sign-in.
VercelServerless hosting targetThe app detects serverless mode, keeps a tiny per-instance pool, and lets an external cron drive the schedulers.
Outbound webhooksEvent delivery to consumersDomain events are delivered to subscriber URLs and signed with HMAC so the receiver can verify them.
AI agents and integrationsThe primary consumersThey point a tool integration at a mock API directly, or at a connection's gateway URL.

Authentication and the security model

Dashboard access is delegated to AutoX SSO, an OpenID Connect provider. Users sign in through the Authorization Code flow with PKCE; the app verifies the ES256 ID token, reads the user's role for this application from the JWT access token, and provisions or links a local account on first sign-in, keyed on the stable subject claim. There is no local password, invitation, or reset flow — identity and per-app roles are managed centrally in AutoX.

After a successful sign-in the app mints its own signed session cookie. The edge middleware verifies the signature, and the server re-checks against the database on each request that the account is still active, so a disabled account loses access immediately. Secrets are never returned to the browser: connection credentials are stored server-side and are redacted or masked in every API response. Request logs redact sensitive headers and bodies before they are stored.

The public surfaces are intentional and narrow. The mock APIs and the gateway are reachable without a session because they authenticate with a tool key or a connection credential. The cron trigger is guarded by a shared secret. Everything else requires a signed-in user, and a few sensitive areas require the administrator role.

The engineering contract for this platform lives in the repository under docs/adapter-platform. This page summarizes the shipped system in plain language.