Contributing

Add a database adapter, security rule, or language SDK. Each takes one file and one interface.

Setup

git clone https://github.com/brakit-ai/brakit.git
cd brakit
npm install
npm run build
npm test

During development, npm run dev runs tsup in watch mode. Tests use vitest (npm test or npm run test:watch).

Project Structure

src/
  analysis/          # Flow grouping, insights, and security rules
    rules/           # SecurityRule implementations (one file per rule)
  cli/               # CLI command definitions (citty)
    commands/        # install, uninstall, telemetry commands
  constants/         # Shared thresholds, route paths, limits
  dashboard/         # HTML dashboard, REST API handlers, SSE
    api/             # API route handlers
    client/          # Client-side template-literal JavaScript
  detect/            # Framework auto-detection
  instrument/        # In-process instrumentation
    adapters/        # BrakitAdapter implementations (one file per library)
    hooks/           # Core runtime hooks (fetch, console, errors)
  output/            # Terminal output formatting
  runtime/           # Core runtime - activation, HTTP capture, setup
  store/             # In-memory bounded stores with pub/sub
  telemetry/         # Anonymous usage telemetry
  types/             # TypeScript type definitions

Adding a Database Adapter

This is the most common contribution. Each adapter is one file in src/instrument/adapters/ implementing the BrakitAdapter interface.

1. Create the adapter file

src/instrument/adapters/drizzle.ts
import type { BrakitAdapter } from "../adapter.js";
import type { TelemetryEvent } from "../../types/index.js";
import { tryRequire, captureRequestId } from "./shared.js";
import { normalizeSQL } from "./normalize.js";

export const drizzleAdapter: BrakitAdapter = {
  name: "drizzle",

  detect() {
    return tryRequire("drizzle-orm") !== null;
  },

  patch(emit) {
    const drizzle = tryRequire("drizzle-orm");
    if (!drizzle) return;

    // Find the prototype method to patch.
    // Each library is different - read the library's source
    // to find the right interception point.

    const origMethod = /* ... */;

    /* replace with wrapper */ = function (...args) {
      const start = performance.now();
      const requestId = captureRequestId();

      const result = origMethod.apply(this, args);

      emit({
        type: "query",
        data: {
          driver: "drizzle",
          source: "drizzle",
          sql: /* raw SQL if available */,
          normalizedOp: /* use normalizeSQL() */,
          table: /* table name */,
          durationMs: Math.round(performance.now() - start),
          rowCount: /* if available */,
          parentRequestId: requestId,
          timestamp: Date.now(),
        },
      });

      return result;
    };
  },
};

2. Register it

src/instrument/adapters/index.ts
import { drizzleAdapter } from "./drizzle.js";

export function createDefaultRegistry(): AdapterRegistry {
  const registry = new AdapterRegistry();
  registry.register(pgAdapter);
  registry.register(mysql2Adapter);
  registry.register(prismaAdapter);
  registry.register(drizzleAdapter);  // add here
  return registry;
}

3. Mark as external

Add the library to the external array in tsup.config.ts so it's resolved from the user's project at runtime:

external: ["pg", "mysql2", "@prisma/client", "drizzle-orm"]
Always call captureRequestId() before any async operation. Database drivers use connection pools and native bindings that break AsyncLocalStorage propagation.

Adding a Security Rule

Each security rule is one file in src/analysis/rules/ implementing the SecurityRule interface.

src/analysis/rules/my-rule.ts
import type { SecurityRule } from "./rule.js";
import type { SecurityFinding } from "../../types/index.js";

export const myRule: SecurityRule = {
  id: "my-rule-id",
  severity: "warning",
  name: "Human Readable Name",
  hint: "Actionable suggestion for how to fix this.",

  check(ctx) {
    const findings: SecurityFinding[] = [];

    for (const r of ctx.requests) {
      if (/* your detection logic */) {
        findings.push({
          severity: "warning",
          rule: this.id,
          title: this.name,
          desc: `${r.method} ${r.path}: what was found`,
          hint: this.hint,
          endpoint: `${r.method} ${r.path}`,
          count: 1,
        });
      }
    }

    return findings;
  },
};

Register it in src/analysis/rules/scanner.ts and export from src/analysis/rules/index.ts. The scanner automatically picks it up.

Building a Language SDK

Any language can send events to brakit by POSTing to the ingest endpoint. See the SDK Protocol page for the full specification, payload format, and event types.

A minimal SDK needs three things: (1) HTTP middleware to generate a unique request ID, (2) library patches to capture queries, and (3) a batched transport to POST events.

Code Style

  • TypeScript strict mode. ESM only.
  • Comments only where the logic isn't self-evident.
  • One adapter per file, one rule per file, one concern per module.
  • Commit messages in imperative mood: Add drizzle adapter, Fix N+1 false positive
Questions? Open an issue at github.com/brakit-ai/brakit/issues.