brick.ts

Lint

Check a Brick app against the project structure and its bug and security rules with brick lint, ESLint or oxlint.

llms.txt

Brick's lint rules make every Brick app follow the same project structure and avoid the common bugs. They ship in @brickkit/cli: the brick lint command, plus an ESLint flat-config plugin (@brickkit/cli/lint/eslint) and the same plugin for oxlint's JS plugin support (@brickkit/cli/lint/oxlint).

bun add -d oxlint
brick lint

Rules come in two kinds:

  • File rules need one file. They run in the editor through ESLint or oxlint, and in brick lint.
  • App rules need the whole app: which service owns which table, whether every emitted event is declared, whether a test exists. Only brick lint runs them. It loads the app the same way brick gen client does and checks the real service graph, instead of guessing from imports.

brick lint

brick lint [entry] [--config <path>]

brick lint reads presets, rules and ignores from brick.config.ts, runs the file rules through oxlint, then loads the app and runs the app rules. It prints one line per problem and exits with 1 when any is an error:

src/services/billing/actions/charge.action.ts:3:1  error  Service "billing" imports orders.table.ts from orders/; call that service's actions or listen to its events instead.  brick/no-cross-service-tables
src/services/billing/actions/charge.action.ts:6:9  error  Set access on this action: authorize, or access: "public" when anyone may call it.  brick/explicit-access

✖ 2 problems (2 errors, 0 warnings)

It needs oxlint in the project, and Node.js 22.18 or later on the PATH (oxlint runs JS plugins in Node).

Presets

PresetHolds
recommendedThe bug and security rules. On by default.
layoutThe file-structure rules.
strictrecommended, plus style, docs and test rules.
pluginRules for plugin packages: the manifest, explicit access and descriptions.
brick.config.ts
export default defineConfig({
  lint: {
    presets: ["recommended", "layout"],
    rules: { "brick/test-colocated": "warn", "brick/naming": "off" },
    ignore: ["src/legacy/**"],
  },
});

lint.rules sets a rule to "error", "warn" or "off", whether or not a preset turns it on. brick/no-cross-service-tables is always an error: setting it to anything else fails.

Rules

Layout

RuleEnforcesPresetKind
one-action-per-fileA *.action.ts file defines exactly one action, and actions are defined nowhere else.layoutfile
file-suffixesEach kind of definition goes in a file with its suffix: .service, .action, .command, .tool, .socket, .listener, .resource, .table.layoutfile
kind-foldersFiles sit in their kind's folder: actions/, tools/, sockets/, listeners/, workflows/, tables/ at service level; the resource and its table, actions/ and commands/ inside resources/<name>/.layoutfile
resource-folderEach .resource.ts is alone in resources/<name>/, named after the resource. Resource actions and commands attach to their folder's resource; commands exist only in a resource folder.layoutfile
file-name-matchesThe file name is the kebab-case of the definition's name, without its owner: get-summary.action.ts defines getSummary.layoutfile
service-folderOne defineService per *.service.ts, inside services/<name>/, named after the service.layoutfile
colocate-in-serviceEverything under services/<name>/ attaches to that service or one of its resources.layoutfile
single-app-entryExactly one brick() call, in the configured entry file.layoutapp
workflow-directive*.workflow.ts files use "use workflow", step files "use step", and the directives appear nowhere else. Fix: inserts the directive.recommendedfile

Boundaries

RuleEnforcesPresetKind
no-cross-service-tablesNo service imports another service's tables, even when services share one database. Always an error.recommendedapp
unique-namesAction, socket and resource names don't share a URL segment within a service; service names are unique.recommendedapp
no-server-in-clientBrowser code (web/) imports only @brickkit/core/client and the generated contract.recommendedfile
no-global-contextNo module-level mutable singletons set from a function (the setOrdersContext pattern); use deps.strictfile
no-env-outside-secretsprocess.env, Bun.env and import.meta.env are read only in secrets.ts and config files.strictfile

Correctness

RuleEnforcesPresetKind
action-schemasEvery action, command and tool declares input and output.recommendedfile
declared-errorsEvery error.X() names an entry in that action's errors or a built-in error.recommendedfile
return-errorError helpers are returned (return error.NOT_FOUND()). Fix: adds return.recommendedfile
path-params-in-inputEach :param in path is a field of input.recommendedfile
get-is-read-onlyGET actions don't insert, update, delete or emit.recommendedfile
emit-in-transactionemitEvent or live.emit inside a transaction callback passes { tx } or { after: tx }. Fix: adds it.recommendedfile
declared-eventsEmitted event names exist in the service's event catalog.recommendedapp
deterministic-workflowNo Date.now, new Date(), Math.random, fetch or database access in a workflow function outside steps.recommendedfile
idempotent-listenerListener writes use deliveryId or onConflictDoNothing, since delivery is at least once. Warning.recommendedfile
socket-schemasSocket schemas have no format or transforms, and event names don't start with $.recommendedfile
no-unregistered-formatA TypeBox format is used only when registered with FormatRegistry in the file, or listed in the rule's formats option.recommendedfile
no-floating-callsPromises from service calls (useService) and emitEvent are awaited. Fix: adds await.recommendedfile

Security

RuleEnforcesPresetKind
explicit-accessEvery action sets authorize or access, every record action and command sets access, and every crud() sets access.recommendedfile
socket-authorizeSockets declare authorize and join.recommendedfile
no-wildcard-originsNo "*" in allowedOrigins or trustedOrigins.recommendedfile
strict-inputInput objects set additionalProperties: false. Fix: adds it.strictfile

Docs and style

RuleEnforcesPresetKind
description-requiredActions, commands, tools, sockets and catalog events have a description.strictfile
namingActions are camelCase verbs (getUser), events are resource.pastTense (order.shipped), rooms are prefix:value.strictfile
no-consoleHandlers log through ctx.logger, so lines carry the request and trace ids.strictfile
test-colocatedEvery *.action.ts and *.command.ts has a sibling *.test.ts.strictapp

Plugins

RuleEnforcesPresetKind
plugin-manifestdefinePlugin has a scoped name, a brick version range and declared options with a description on each; its resource, service, app and tables parts are synchronous and open no connections outside on("start").pluginfile

Editor setup

The editor configs read brick.config.ts through fromProject(), so presets, rules, ignores and paths stay in one place.

ESLint

bun add -d eslint typescript-eslint

ESLint needs a TypeScript parser, such as typescript-eslint:

eslint.config.js
import tseslint from "typescript-eslint";
import brick from "@brickkit/cli/lint/eslint";

export default [
  ...tseslint.configs.recommended,
  ...(await brick.configs.fromProject()),
];

Without a config file, use a preset directly: brick.configs.recommended, brick.configs.layout, brick.configs.strict or brick.configs.plugin.

oxlint

oxlint loads the same plugin through jsPlugins (alpha in oxlint; tested with oxlint 1.88). fromProject() from @brickkit/cli/lint/oxlint returns the jsPlugins, settings, rules and ignorePatterns fields; the plugin specifier is @brickkit/cli/lint/oxlint. The TypeScript config file runs in Node 22.18 or later:

oxlint.config.ts
import { defineConfig } from "oxlint";
import { fromProject } from "@brickkit/cli/lint/oxlint";

export default defineConfig(await fromProject());

oxlint applies lint.ignore to all its rules, not only Brick's.