Simplify your stack and build anything. Or everything.
Build tomorrow’s web with a modern solution you truly own.
Code-based nature means you can build on top of it to power anything.
It’s time to take back your content infrastructure.

How to implement a per-field access control hook with multiple fields right, using Payload best practices?

default discord avatar
MurzNN2 months ago
1 2

Please give me some advice from your experience on how it is better to restrict users of the GraphQL API from modifying all fields of a document except for several that are explicitly allowed, and allow them to modify only documents related to the current user?

Let's take, as an example, the Notifications collection that has 10+ fields, and I need to allow users to update only two fields: isRead and archived, and only for the notifications related to them (doc.owner == user.id).

How to implement this in the best way? I see these options:

  1. Add an access control function for each field of 10+ ones.

This option is recommended in the docs, but it looks to me not very good for performance, because the same check will be executed 10+ times (one time for each field).

  1. Use a single access control function for the collection, returning false if the operation should be restricted.

Looks good to me, but is it okay to analyze the input data here and produce a conditional result (true/false) depending on the input data?

  1. Use a beforeUpdate hook and add all checks there, using throw new Error() to stop processing.

This looks good too, but I don't like that the access check is performed not in the access control hook, but in the operation execution hook.

  1. Implement a custom GraphQL mutation that accepts only allowed field values and update the doc via server-side code.

This looks too complicated for such a common task.

--

So, which approach is better to use for such tasks? Or suggest another option that is better than described.

And would be awesome to update the documentation with more examples for such cases.

  • Selected Answer
    discord user avatar
    zubricks
    2 months ago

    Hey @MurzNN this is a great question! I've run into this myself a few times.

    My suggested approach would be something like this:

    // Collection-level access returns a where constraint for ownership:
    access: {
      update: ({ req: { user } }) => {
        if (!user) return false
        if (user.roles?.includes('admin')) return true
        // Returns a query constraint — only matches docs where owner === current user
        return {
          owner: { equals: user.id },
        }
      },
    },
    

    Then you can apply that access to each of your fields:

    { name: 'owner',     type: 'relationship', access: { update: adminOnly } },
    { name: 'createdBy', type: 'relationship', access: { update: adminOnly } },
    // ... repeat for all restricted fields
    

    Since adminOnly is a single function reference, there's no real boilerplate — you're just listing which fields are restricted. The performance concern about running it 10+ times is negligible: these are synchronous in-memory checks, not DB queries.

    Only the ownership constraint at the collection level hits the database, and that only runs once per request.

    For read access, you can use the same pattern — one shared adminOnly function on the fields you want to hide from regular users.

    The beforeChange hook and custom mutation approaches work but put access control logic in the wrong layer. The where-constraint pattern is the "Payload way" to handle this.

    1 reply
  • default discord avatar
    MurzNNlast month

    Thank you a lot for the example! My worry is not only about boilerplate, but also about performance: if we have 10 fields with the 'adminOnly' access hook, this function will be executed 10 times just to load a single document. Okay, adminOnly is a pretty lightweight, but if this check needs DB queries, it will be toooo slow.

    For example, when reading the Product document and deciding whether we should return hidden fields, we need to check if the user is a member of the Organization that manages this product, so do a query like this:

    const isProductOwner = payload.find(
      collection: 'organizations'
      where {
        id: { equals: product.organization )
        members: { equals: user.id)
      }
    )

    So, it's better to execute this query once on the collection level access hook than 10 times in each field access hook, right? ;)

  • default discord avatar
    MurzNN2 months ago

    And, actually, the same question is for the access control function for the "read" operation: I need to allow reading only a limited list of fields: id, createdAt, title, body, isRead, and archived, reading other fields should be restricted.

    Creating access control functions for each field would be a lot of boilerplate, and not good for performance, right? It's much better to handle a "allow list" of fields in a single place.

Star on GitHub

Star

Chat on Discord

Discord

online

Can't find what you're looking for?

Get dedicated engineering support directly from the Payload team.