fallow dead-code enforces architecture boundaries by checking that imports between directories follow your rules. Define zones and declare which zones may import from which.
fallow dead-code --boundary-violations
Boundary violations are included in fallow dead-code output by default. Use --boundary-violations to show only boundary issues.
The fastest way to add boundaries is with a built-in preset. Fallow ships four presets for common architecture patterns:
{
"boundaries": {
"preset": "bulletproof"
}
}Run fallow list --boundaries to see the expanded zones and rules:
Boundaries: 5 zones, 5 rules
Zones:
app 3 files src/app/**
features/auth 7 files src/features/auth/**
features/billing 5 files src/features/billing/**
shared 8 files src/components/**, src/hooks/**, src/lib/**, ...
server 4 files src/server/**
Rules:
app → features/auth, features/billing, shared, server
features/auth → shared, server
features/billing → shared, server
server → shared
shared (isolated, no imports allowed)
A widely used React/Next.js pattern. Feature modules are isolated from each other; shared utilities and server infrastructure form the base layers.
4 logical zones: app, features, shared, server. The features zone auto-discovers immediate child directories so sibling features are isolated as features/<name> zones. Top-level files inside src/features/ (barrels, shared types) fall back to the parent features zone; the parent rule automatically allows discovered children, so barrels can re-export features without false positives while non-barrel top-level files still obey the features rule.
app → every feature, shared, server
features/<name> → shared, server
server → shared
shared (isolated)
The shared zone covers: components, hooks, lib, utils, utilities, providers, shared, types, styles, i18n.
This preset matches the architecture from Bulletproof React and is the most common pattern in modern React and Next.js projects.
Preset zone patterns use {rootDir}/{zone}/**. Fallow auto-detects the source root from tsconfig.json:
{
"compilerOptions": {
"rootDir": "./lib" // zones become lib/app/**, lib/features/**, etc.
}
}
If no rootDir is found, fallow falls back to src.
For full control, define zones and rules directly:
{
"boundaries": {
"zones": [
{ "name": "ui", "patterns": ["src/components/**", "src/pages/**"] },
{ "name": "data", "patterns": ["src/db/**", "src/api/**"] },
{ "name": "shared", "patterns": ["src/lib/**", "src/utils/**"] }
],
"rules": [
{ "from": "ui", "allow": ["shared"] },
{ "from": "data", "allow": ["shared"] },
{ "from": "shared", "allow": [] }
]
}
}allow list is isolated: it cannot import from other zonesUse autoDiscover when one logical zone should create one concrete zone per child directory. This is useful for feature-module architectures where src/features/auth and src/features/billing should be isolated from each other without writing a zone and rule for every feature.
{
"boundaries": {
"zones": [
{ "name": "app", "patterns": ["src/app/**"] },
{ "name": "features", "patterns": ["src/features/**"], "autoDiscover": ["src/features"] },
{ "name": "shared", "patterns": ["src/shared/**"] }
],
"rules": [
{ "from": "app", "allow": ["features", "shared"] },
{ "from": "features", "allow": ["shared"] }
]
}
}
If src/features/auth and src/features/billing exist, fallow expands this to features/auth and features/billing. Rules that reference the logical features parent apply to every discovered feature. Explicit child rules, such as from: "features/auth", override generated parent rules.
When the zone also has patterns, top-level files inside the auto-discover directory fall back to the parent zone, whose rule automatically allows its discovered children. See Auto-discovered zones for the full semantics.
root)Monorepos with per-package boundaries usually have the same internal directory layout under each package (packages/app/src/, packages/core/src/, ...). Writing flat patterns from the project root forces zone definitions to scale with the cross product of (zones, packages):
// Without `root`: one zone per (layer, package) pair.
{ "name": "ui-app", "patterns": ["packages/app/src/**"] },
{ "name": "domain-core", "patterns": ["packages/core/src/**"] }
Set root on a zone to scope its patterns to a subtree. At classification time, fallow checks that the file's path starts with the root prefix and strips that prefix before matching the patterns against the remainder. Files outside the subtree never match the zone.
{
"boundaries": {
"zones": [
{ "name": "ui", "patterns": ["src/**"], "root": "packages/app/" },
{ "name": "domain", "patterns": ["src/**"], "root": "packages/core/" }
],
"rules": [
{ "from": "ui", "allow": [] }
]
}
}In this example, packages/app/src/login.tsx classifies as ui and packages/core/src/order.ts classifies as domain. Adding packages/billing/ later only requires another zone entry with the same patterns: ["src/**"] and a different root.
Trailing slashes and a leading ./ are normalized for you, so "packages/app", "packages/app/", and "./packages/app/" are equivalent. Backslashes are converted to forward slashes.
Patterns must NOT redundantly include the root prefix. root: "packages/app/" paired with patterns: ["packages/app/src/**"] is rejected at config-resolve time with FALLOW-BOUNDARY-ROOT-REDUNDANT-PREFIX, because patterns are already resolved relative to the root. Drop the root prefix from the pattern (patterns: ["src/**"]).
Start from a preset and customize specific zones or rules. Zones with the same name replace the preset zone; rules with the same from replace the preset rule. See Merging presets with custom config for examples.
Suppress individual findings with inline comments:
// fallow-ignore-next-line boundary-violation
import { db } from '../data/client';
Or suppress all boundary violations in a file:
// fallow-ignore-file boundary-violation
To suppress boundary checking entirely, set the rule severity to off:
{
"rules": {
"boundary-violation": "off"
}
}
The boundary-violation token and rule cover the whole boundary family: import-direction violations, coverage violations, and forbidden-call violations. The rule-id-shaped boundary-call-violation and boundary-call-violations tokens are accepted as aliases; any of the three suppresses every boundary finding on that line or file.
When introducing boundaries to an existing codebase, start with "warn" severity. Fix violations incrementally, then switch to "error" once the codebase is clean.
Boundary violations appear in all output formats:
Boundary violations (2)
src/features/auth/login.ts:3 → src/features/billing/api.ts (features → features)
src/components/Button.tsx:1 → src/server/db/client.ts (shared → server)