# Adding Audit Events

> Use when adding or changing an audited Nango endpoint, or deciding one shouldn't be audited - covers the vocabulary table, metadata typing, spec placement, mount ordering, and the webapp filter list

- Skill: `nangohq-nango/adding-audit-events` (Agent Skill)
- Install (CLI): `npx skillmds@latest add nangohq-nango/adding-audit-events`
- Raw SKILL.md: https://api.skillmd.com/api/skills/nangohq-nango/adding-audit-events/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: NangoHQ (https://skillmd.com/u/nangohq-nango)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/nangohq-nango/adding-audit-events

---


# Adding Audit Events

`AuditEventTable` in `packages/types/lib/audit-trail/event.ts` is the source of truth: it maps each
resource to its actions and the metadata each may carry. The event union, the metadata lookup, the
per-resource action lists and the webapp filters all derive from it. Add the action there first and let
the compiler tell you what else to touch.

The trail is **control-plane only** — configuration, state and authentication. Runtime traffic (records,
proxy, sync execution) is data plane and stays out; audit it and you get millions of rows a month.

## Workflow

1. **Vocabulary** — in `AuditEventTable`, under the resource: `<action>: <MetadataShape>;` or
   `<action>: never;` when the action carries no metadata.

2. **Metadata shape**, if any — `packages/types/lib/audit-trail/metadata.ts`. Name it for what it holds,
   never for one action: `integration.created` and `integration.deleted` share
   `IntegrationProviderMetadata` because both record only the provider.

3. **Endpoint type** — every customer-facing endpoint declares one:
   - `Audit: AuditPolicy<'<resource>', '<action>', 'account' | 'environment'>`
   - `Audit: { kind: 'no-audit'; reason: '<why>' }` — use `'data-plane operation'` for runtime traffic.

4. **Spec** — `packages/server/lib/middleware/audit/<resource>.middleware.ts`:

   ```ts
   export const auditThingDone = auditable<PostThing>({
       policy: Audit.auditable({ resource: 'thing', action: 'done', scope: 'environment' }),
       target: (req) => makeTarget('thing', nonEmptyString(req.body.id)),
       metadata: (req) => omitUndefined<ThingDoneMetadata>({ … })
   });
   ```

   Mounted middleware first, in the vocabulary's action order, private spec then public for the same
   action; multi-action emitters last; helpers below all of them.

5. **Barrel** — add the export to `audit/index.ts`. Hand-written on purpose: it declares what has scope
   beyond the folder, so never `export *`.

6. **Mount** — `routes.public.ts` / `routes.private.ts`, **before `withScope`**:
   `.post(apiAuth, auditThingDone, withScope('…'), handler)`. After the scope check, denials are lost.

7. **Test** — `<resource>.middleware.unit.test.ts`, scaffolding from `./testing.js`.

8. **Webapp** — add the action to `actionsByResource` in `packages/webapp/src/pages/Audit/constants.ts`.
   A type check pins it to the vocabulary, so omitting it is a build error. Labels are derived.

9. **New kind of target** — add it to `AuditTargetType`.

## Gotchas

- **Resolvers run before zod.** `req.body` / `params` / `query` are raw at that point, whatever the
  endpoint type says. Use the guards in `input.ts`: `nonEmptyString`, `positiveInt`, `param`, `query`,
  `bodyField`.
- **`target` and `metadata` resolve before `next()`**, so a value the handler generates isn't available
  yet. Use `targetFromResponse` / `metadataFromResponse`.
- **A 403 means no handler ran**, so anything only the handler knows is missing from exactly the rows an
  auditor cares about most. Prefer reading from the request.
- **`scope: 'account'` nulls the event's environment**, whatever `res.locals` holds.
- **On the MCP path only** (`defineManagementMcpTool`), a stray metadata key *alongside* a valid one is
  accepted — the audit type is a union over the vocabulary. A stray key alone, a wrong type, and metadata
  on a `never` action all fail.
- **Grep `input.ts` and `lookups.ts` before adding a helper.** The target and metadata builders usually
  exist, and near-identical `…Target` functions get flagged in review.

## Review Checklist

- [ ] `npm run ts-build` clean — it catches a missing webapp entry, metadata that doesn't fit the action,
      and metadata on an action declared `never`
- [ ] Unit test asserts the common fields (`accountId`, `environment`, `actor`, `outcome`), not only the
      one it is named for
- [ ] Test break-checked: remove the target or a metadata key and confirm it goes red
- [ ] Audit middleware sits before `withScope` on every new mount
- [ ] Denial and failure paths still identify the event — check what a 403 records, not just the 200

