Boundaries enforce import direction between architectural zones. A zone groups files by glob pattern; a rule declares which zones may import from which. Violations appear asDocumentation Index
Fetch the complete documentation index at: https://docs.fallow.tools/llms.txt
Use this file to discover all available pages before exploring further.
boundary-violation issues in fallow dead-code output.
For preset descriptions, output formats, and suppression, see Architecture boundaries.
Presets
Fallow ships four built-in presets for common architecture patterns:| Preset | Zones | Description |
|---|---|---|
layered | 4 | Classic N-tier: presentation, application, domain, infrastructure |
hexagonal | 3 | Ports and adapters: adapters, ports, domain |
feature-sliced | 6 | Feature-Sliced Design: strict downward-only imports |
bulletproof | 4 | Bulletproof React: app, features, shared, server |
See Architecture boundaries - Presets for the full zone and rule definitions of each preset.
tsconfig rootDir detection
When a preset is active, fallow readscompilerOptions.rootDir from tsconfig.json to determine the source root for zone patterns. Preset zones become {rootDir}/{zone}/**.
The detection logic:
- Reads
tsconfig.jsonfrom the project root (supports JSONC with comments and trailing commas) - Extracts
compilerOptions.rootDir - Strips a leading
./prefix and any trailing/ - Rejects
.,.., and absolute paths for security - Falls back to
srcif the field is missing, the file doesn’t exist, or the value is rejected
tsconfig.json
hexagonal preset, this produces zones:
rootDir detection only applies to presets. Custom zones use patterns exactly as written.
Custom zones
Define zones manually when presets don’t match your architecture. Each zone has:| Field | Type | Description |
|---|---|---|
name | string | Zone identifier used in rules |
patterns | string[] | Glob patterns relative to the project root, or relative to root when set |
autoDiscover | string[] | Directories whose immediate child directories become separate zones named parent/child |
root | string? | Optional subtree scope; patterns and autoDiscover paths are resolved relative to this root |
- A file belongs to the first zone whose pattern matches (first-match wins)
- Files that don’t match any zone are unrestricted — they can import from and be imported by any zone
- Zone order matters: place more specific patterns before broader ones
Auto-discovered zones
UseautoDiscover for feature-module boundaries where every immediate child directory should become its own zone. Rules can still reference the logical parent; fallow expands them to the discovered child zones.
src/features/auth and src/features/billing exist, fallow list --boundaries shows features/auth and features/billing, and sibling feature imports are checked as cross-zone imports. Explicit child rules, such as from: "features/auth", override generated parent rules regardless of rule order.
When the zone also has patterns, discovered child zones are matched first and top-level files inside the auto-discover directory fall back to the parent zone. The parent fallback rule automatically allows its discovered children, so a src/features/index.ts barrel can re-export feature modules without surfacing features → features/<child> violations, while non-barrel top-level files such as src/features/types.ts still obey the parent features rule. Generated child rules keep the original allow list exactly, so sibling-feature isolation is preserved. Omit patterns from the zone when you want only discovered child directories classified and top-level files left unrestricted.
Custom rules
Rules define which zones may import from which. Each rule has:| Field | Type | Description |
|---|---|---|
from | string | The importing zone |
allow | string[] | Zones that from may import from |
allowTypeOnly | string[] | Zones that from may type-only-import from even when not in allow (optional) |
- Self-imports are always allowed — files within the same zone can freely import each other
- No rule entry means the zone is unrestricted — it can import from any zone
- Empty
allowlist means the zone is isolated — no cross-zone imports permitted - Only zones with an explicit rule entry are constrained
Type-only imports across boundaries
TypeScript projects sometimes need type-level contracts between modules that must not have runtime dependencies on each other. A plugin system, for example, may declare a contribution interface in one feature that consumers in other features depend on at the type level only:allow list to the target zone, add allowTypeOnly:
allowTypeOnly admits:
- Whole-declaration type imports:
import type { Foo } from '...' - Namespace type imports:
import type * as ns from '...' - Inline-type imports where every named specifier carries the
typequalifier:import { type Foo } from '...' - Type-only re-exports:
export type { Foo } from '...'
- Mixed-specifier imports where at least one specifier is value-typed (
import { type Foo, Bar }) — the runtime dependency onBarstill fires a violation - Plain value imports (
import { Foo } from '...') - Side-effect imports (
import '...') — these run the target at runtime
allowTypeOnlyis independent ofallow. A type-only edge is admitted if its target zone is in EITHER list.- The default value is an empty list, so omitting the field preserves pre-feature behavior exactly.
- No preset (Layered, Hexagonal, FeatureSliced, Bulletproof) defaults the field on; type-only crossings are opt-in per rule.
Merging presets with custom config
When bothpreset and zones/rules are specified, fallow merges them:
- Zones with the same name as a preset zone replace the preset zone entirely
- Rules with the same
fromas a preset rule replace the preset rule - New zones and rules are added on top of the preset
Example: override a preset zone
Use the hexagonal preset but put your domain code insrc/core instead of src/domain:
adapters and ports from the hexagonal preset but replaces the domain zone pattern with src/core/**. All three rules remain unchanged.
Example: add a zone to a preset
Add atest zone to the bulletproof preset so test utilities are isolated:
test zone and its rule are added on top.
Inspecting boundaries
Usefallow list --boundaries to see the expanded configuration after preset expansion and merging. This is the fastest way to verify your config is correct.
$ fallow list --boundaries
Common patterns
Monorepo with per-package boundaries
Monorepo with per-package boundaries
Preset zone patterns are flat (
src/<zone>/**), which doesn’t work when packages have separate source directories. Define zones explicitly for each package:tRPC apps with server-to-features imports
tRPC apps with server-to-features imports
In tRPC apps, the server router typically imports from feature modules to compose sub-routers. With the bulletproof preset, this causes Alternatively, add
server -> features violations.The cleanest approach is to suppress the router composition file:src/server/_app.ts
features to the server rule:Adding a test zone alongside a preset
Adding a test zone alongside a preset
Test files often need to import from any zone. Add a Since zones use first-match classification, place more specific patterns (like test file globs) before broader zone patterns. When using a preset with custom zones, custom zones are appended after preset zones. If your test files live inside zone directories (e.g.,
test zone that sits above the preset layers:src/features/auth/auth.test.ts), the preset’s features zone pattern will match first. Move the test patterns into an explicit zone list that comes before the preset zones, or use ignorePatterns to exclude test files from boundary analysis entirely.See also
Architecture boundaries
Presets, output formats, and inline suppression for boundary violations.
Rules & Severity
Set
boundary-violation to error, warn, or off.Inline Suppression
Suppress individual findings in source code.