biome-plugin-payload
v0.1.4
Published
Biome lint plugin (GritQL) that enforces Payload CMS best practices on Local API calls
Maintainers
Readme
biome-plugin-payload
Biome lint plugin (GritQL) that enforces Payload CMS best practices on Local API calls — so code written by hand or generated by AI agents is caught by biome check instead of manual review.
Rules
| Rule | What it catches | Severity |
| --- | --- | --- |
| no-find-without-select | payload.find / payload.findByID / payload.findGlobal / payload.findVersions / payload.findVersionByID calls that do not pass a select object, causing more fields than needed to be fetched from the database | error |
| no-find-without-depth | The same read operations that do not set depth explicitly — omitting it uses the application default (1), silently populating one level of relationships | error |
| no-missing-override-access | Any Local API call that does not declare overrideAccess explicitly — the Local API bypasses access control by default (overrideAccess: true) | error |
| prefer-explicit-populate | Read operations with depth > 0 (numeric literal) that do not pass a populate object, so populated related documents return more fields than intended | error |
| require-user-with-override-false | Any Local API call with overrideAccess: false that does not pass user, req, or user: null — without a user object, access control functions receive undefined and behave unpredictably | error |
All rules match any receiver that is the identifier payload or a member expression ending in .payload (e.g. req.payload, this.payload).
prefer-explicit-populate is a heuristic: it only fires when depth is a positive numeric literal (depth: 2). Shorthand (depth) and computed values (depth: opts?.depth ?? 0) cannot be verified statically and are skipped. If the target collection defines defaultPopulate in its config, Payload already limits which fields are returned during population — suppress the diagnostic in that case with a biome-ignore comment.
Install
npm install --save-dev biome-plugin-payload
# or
bun add -d biome-plugin-payload
# or
pnpm add -D biome-plugin-payloadEnable
Add each rule to the plugins array in your biome.json or biome.jsonc. Paths must be the literal path to the .grit file — Biome does not resolve plugin entries by package name.
// biome.jsonc
{
"plugins": [
"./node_modules/biome-plugin-payload/rules/no-find-without-select.grit",
"./node_modules/biome-plugin-payload/rules/no-find-without-depth.grit",
"./node_modules/biome-plugin-payload/rules/no-missing-override-access.grit",
"./node_modules/biome-plugin-payload/rules/prefer-explicit-populate.grit",
"./node_modules/biome-plugin-payload/rules/require-user-with-override-false.grit"
]
}Each file becomes one rule named after the file, in the plugin group (e.g. lint/plugin/no-find-without-select).
Intentional Anonymous Access (overrideAccess: false)
When making an intentional anonymous query in public Server Components (where no user or req session exists), pass user: null explicitly to communicate unauthenticated access without needing biome-ignore:
// Explicit unauthenticated query — access control functions run with user = null
const pages = await payload.find({
collection: "pages",
overrideAccess: false,
user: null,
});Suppress an intentional violation
// biome-ignore lint/plugin/no-find-without-select: full document intentionally needed here
const post = await payload.find({ collection: "posts" });
// biome-ignore lint/plugin/prefer-explicit-populate: collection defines defaultPopulate
const post = await payload.find({ collection: "posts", depth: 1, select: { title: true } });Known limitations
- Only calls whose argument is an object literal without spread are analyzed.
payload.find(args)andpayload.find({ ...base })are skipped because the keys cannot be verified statically. payload.db.*direct-database calls are intentionally not covered.prefer-explicit-populateonly fires whendepthis a positive numeric literal — variable or computed depth values are skipped.- The Biome CLI does not support
--onlyfor individual plugin rules. Use--only=pluginto run all plugin rules, or a temporary config with a single plugin entry to isolate one rule.
Development
git clone https://github.com/erikenz/biome-plugin-payload
cd biome-plugin-payload
bun install
bun testRules are plain GritQL files in rules/. Tests in test/plugin.test.ts run biome lint against fixtures in test/fixtures/ and assert the emitted diagnostics. Fixtures violating a rule produce expected diagnostics; negative fixtures must produce zero diagnostics.
Roadmap
no-top-level-get-payload—getPayload()must not be called at module top levelno-hook-query-without-req— nested Local API calls inside hooks must passreqno-query-without-limit-on-slug— unique-field lookups (where: { slug: { equals } }) should uselimit: 1+pagination: false
