Skip to main content
Please be aware that while Forest make every effort to ensure that our platform updates are broadly compatible and offer detailed instructions for upgrading, Forest cannot guarantee that custom code developed by the developers will always be compatible with new versions of our software. This includes any custom modifications or extensions to core functionalities, such as method overrides or custom integrations. It is the responsibility of the developers to review and test their custom code to ensure compatibility with each new version. Our team provides comprehensive upgrade guides to assist in this process, but these cannot encompass the unique customizations that may be present in each customer’s environment. Therefore, Forest strongly recommend establishing a thorough testing protocol for your specific customizations to safeguard against potential issues during the upgrade process.
Please follow the recommended procedure to upgrade your agent version by following this note.
This note applies only to the v1 Rails agent, the forest_liana gem. If your project runs the v2 Ruby agent (forest_admin_rails / forest_admin_agent gems), it does not apply to you.
This upgrade unlocks the following features:
  • Role permissions are enforced on related data: fields, filters, sorts and searches that reach a collection a role cannot read are refused or redacted, the same way they are on Node.js agents.
  • Lists select only the columns the page actually displays, instead of SELECT *.
  • Smart fields can declare the columns and relations they need with a new dependencies: option, removing N+1 queries on lists, related lists and CSV exports.
  • A collection can disable its record count with countable: false, without overriding a controller.
  • belongs_to and has_one relations to another database are preloaded once per page on lists and CSV exports (and on related lists under Rails 6.1), instead of once per row.
  • The agent announces its capabilities to the Forest Admin interface, which enables the projection features above and lets the interface adapt to what your agent supports.
No database migration, generator or configuration file change is required. The minimum Rails version is unchanged (Rails 6.1 to 8.1 are supported).

Upgrading to v9.22

As for any dependency upgrade, it’s very important to test this upgrade in your testing environments. Not doing so could result in your admin panel being unusable. This release changes what some existing requests return: read the Before you upgrade checklist first.
If you are upgrading from an older major version, please make sure you have also read the previous upgrade notes (v9, v8, v7, …). To upgrade, update the version in your Gemfile to at least 9.22.2:
Do not stay on an earlier 9.22 version:
  • On 9.22.0, a smart field declaring a multi-hop dependencies: path (such as address:city:name) answers 500 on the whole list when the list also displays the first relation of the path. Fixed in 9.22.1.
  • On 9.22.0 and 9.22.1, a belongs_to or has_one whose target lives in another database is loaded once per row on lists and CSV exports. A cross-database relation not named after its target model can also be offered as filterable, and filtering on it answers 500. Fixed in 9.22.2.
Then run:
Restart your agent and check its startup logs: this release logs every configuration it detects and every declaration it cannot honor (see Logs to watch).
In case of a regression introduced in Production after the upgrade, a rollback to your previous agent version is the fastest way to restore your admin panel. Nothing in this release writes to your database or to your Forest Admin project settings, so rolling the gem back fully restores the previous behavior.

Deployment order with the Forest Admin interface

The interface reads the capabilities your agent announces and enables features one by one. You can upgrade the agent before or after the interface is updated on our side:
  • Agent upgraded first: everything described in this note applies immediately on the agent side (permission refusals, redaction, column selection, count deactivation). The interface simply does not use the capabilities yet.
  • Interface updated first: your current agent does not serve the capabilities route, so the interface keeps its current behavior until you upgrade.
The permission changes below are enforced by the agent as soon as it is upgraded, independently of the interface. They do not wait for a capability to be announced.

Before you upgrade

Work through this checklist in your development environment. Each item names what changes, how to detect whether you are affected, and what to do.

1. Review the read permissions of your roles

Until now, a role could see the data of a related collection through a relation, a filter, a sort or a search even without the read permission on that collection. The agent now resolves every path to the collection it reaches and checks the role’s permissions on it. Two more checks are now enforced on the Related data section of a record:
  • Listing or counting related records requires the browse permission on the related collection itself, and exporting a related list to CSV requires export on it. A role that could open a parent record and see or export a related list without those permissions will now get a 403 there.
  • Associating or updating related records requires edit; dissociating or replacing records that get destroyed requires delete.
How to detect it: in your development or staging environment, log in with each non-admin role and open the collections, related data sections, filters, sorts and searches your teams use daily, including the export buttons of related lists. A 403 with one of the messages above means the role is missing a permission it was implicitly relying on. What to do: grant the missing read (and browse for related lists) permissions in Project settings > Roles, as described in the User Guide. If you cannot review every role before upgrading, use the temporary opt-out below.
A permission you grant is not always visible right away. The agent caches the environment permissions for FOREST_PERMISSIONS_EXPIRATION_IN_SECONDS (900 seconds — 15 minutes — by default), and only a request that is about to refuse re-reads them before refusing. So a request that would have answered 403 picks the change up immediately, while a field that was silently removed (a related field of a default-expanded record, a column of a CSV export) can stay missing for up to the cache lifetime. Wait it out, lower FOREST_PERMISSIONS_EXPIRATION_IN_SECONDS, or restart the agent to see the change at once.
Only the collections a path reaches are checked. The collection the request is rooted on is not re-checked here — browse (list), read (record) and export (CSV) already gate it — so a role that may browse a collection without reading it keeps seeing its list.Scopes injected by the agent are never checked: only the filter tree sent by the caller is. Smart field search: lambdas are not checked either, by design, since their reach cannot be described.

2. Temporary opt-out: skip_relation_read_permissions

If you need to upgrade before your roles are reviewed, you can keep the previous behavior for relation paths:
When it is true, every exposed collection reached through a relation path is treated as readable: the redaction of related fields and the refusal of filters, sorts and searches described in the previous section are disabled. The agent logs it at startup and announces it to the interface, so that the interface does not rely on server-side pruning.
The option must be the boolean true. A string such as "true", "1" or an unset environment variable keeps the checks enabled. If you drive it from an environment variable, cast it: ForestLiana.skip_relation_read_permissions = ENV['FOREST_SKIP_RELATION_READ_PERMISSIONS'] == 'true'.
It does not disable:
  • Collection-level permissions (browse, read, edit, delete, add, export), including the new browse check on related lists.
  • The refusal of paths reaching a collection that is not exposed to Forest Admin.
  • Scopes, segments, and smart action permissions.
There is one global option: it cannot be set per collection or per field. We recommend treating it as a transition tool and turning it off once your roles are reviewed.

3. Check the read_only: and is_searchable: options of your collections

The options collection :X, read_only: true and collection :X, is_searchable: ... in lib/forest_liana/collections/*.rb were silently ignored on collections backed by an Active Record model. They are now honored. How to detect it:
What to do: for each match on a model-backed collection, confirm you still want it. A collection declared read_only: true will lose its create, edit and delete buttons in the interface as soon as you upgrade. Remove the option if it was left there by mistake.

4. Check model code that reads columns the page does not display

Lists now select only the columns the request asks for, plus the primary key, the foreign keys of the relations in the request, the STI type column, and the columns of your model’s default ordering. Anything the query loads on its own is covered too: a relation joined by a Forest scope is selected whole, and a relation an association scope or a default_scope preloads keeps the key the preload reads off the row. Any Ruby code that runs on these records and reads another column raises ActiveModel::MissingAttributeError, which answers 500.
The rule is per request, not per collection. A collection that declares smart fields is still projected on every request that does not display them — a smart field defined but absent from the list view is the common case. Only a request that displays a smart field declaring no dependencies: (or a smart relation / integration field, which cannot declare its reach) keeps the previous SELECT * behavior, and only for that request. Do not read “this collection has smart fields” as “this collection is not affected”.
Typical places to look at, on your models:
  • after_find and after_initialize callbacks
  • to_s or name overrides used as record labels
  • default_scope blocks and decorators reading attributes
  • Smart actions and route overrides that reuse the records of a list
How to detect it: open the lists of every collection in your development environment and watch for 500s. Enable your database query log to confirm which columns are selected. What to do: read the column through a smart field that declares it in dependencies: (see below), or make the code tolerant to a missing attribute (object.has_attribute?(:column)).
Smart field getters are protected: a getter reading an undeclared column reloads the record once (at the cost of an extra query, logged once per process) and never returns a 500. This protection covers smart field getters only, not the model-level code listed above.

5. Check your search configuration

Two search behaviors change:
  • A search whose term matches no real column now returns no records instead of the whole table. You are affected if a collection’s search_fields names only smart fields without a search: lambda, or only dotted relation entries used on a plain (non-extended) search. Previously the search silently returned every record.
  • A plain search (the default, without “extended search”) no longer joins the related tables to count the results: the count matches what the list actually searches. Collections that keep a smart field search: lambda keep their joins.
How to detect it: run the searches your teams use on each collection and compare the number of results before and after the upgrade. What to do: add real columns to search_fields, or give the smart field a search: lambda (see the reference).

6. Check the associations you will preload

The new dependencies: option (see New options) preloads relations instead of loading them once per row. One shape of association is incompatible with preloading: an association whose own scope narrows its select and drops the column the preload matches records on. That column is in the target table: the foreign key for a has_many or has_one, the target’s primary key (id, or the primary_key: option) for a belongs_to. For example:
It works when loaded lazily and raises ActiveModel::MissingAttributeError when preloaded. Do not name such an association in a dependencies: path, or add that column to its select.

7. Check your route and controller overrides

If you override Forest routes or controllers:
  • Permission refusals raise ForestLiana::Ability::Exceptions::UnauthorizedFieldsError, UnauthorizedQueryFieldError and UnexposedQueryCollectionError, rendered as 403. Overrides that rescue only the previous error classes should be extended.
  • The record view, bulk delete and the related-data actions now render every other ForestLiana::Errors::ExpectedError themselves, with the error’s own status — as the list and the count already did before this release. A rescue_from ForestLiana::Errors::HTTP422Error (or similar) you added in an override no longer fires for an error raised inside those three — rescue it inside your override, or rely on the status the agent already returns. The three permission classes above are the exception: they are re-raised on purpose, so that their name and data reach the response.
  • A filter, sort or projection whose prefix names no relation now answers 422 Relation not found: '<Collection>.<relation>' instead of resolving to the root collection.
  • A malformed filter (a raw value instead of a condition tree) answers 422 Filters cannot be a raw value instead of a 500.
  • Count overrides that call deactivate_count_response keep working. For a whole collection, countable: false is now simpler (see below).

New options you can adopt

None of these are required. They are the features this release adds, and they can be adopted one collection at a time — with one caveat on dependencies:, which changes how the requests displaying the field are queried.

Disable the count of a collection

The count endpoints of the collection, and of its related lists on other records, answer {"meta":{"count":"deactivated"}} without running any query. The interface shows the list without a total. On a related list, the option of the related collection applies.
On a smart collection, the option only takes effect if the collection also declares at least one field, action or segment.

Declare what a smart field needs with dependencies:

A smart field getter runs once per record. Declaring what it reads lets the agent select the right columns and preload the right relations for the whole page:
dependencies: accepts a string, a symbol, or an array of them. What you get:
  • A list, related list or CSV export that displays the field runs one query per relation of the path instead of one per row.
  • The columns named are selected with the list, so the getter reads them without an extra query.
What happens without a declaration:
  • The field keeps working exactly as before. A request that displays a smart field with no dependencies: keeps the previous SELECT * behavior for that request, and a getter reaching a relation keeps its one-query-per-row cost.
  • You can therefore declare your smart fields one at a time, starting with the ones on your largest lists.
Each step is itself a behavior change, not only an optimisation: declaring dependencies: on a smart field is what switches the requests displaying it from SELECT * to column selection. Re-run check 4 on a collection after declaring a field there — model code that was safe immediately after the upgrade can start raising ActiveModel::MissingAttributeError.
Validation happens at startup and never prevents the agent from booting:
  • An entry that does not resolve to a real column or relation, or that crosses a polymorphic relation, is ignored with a warning, and the field behaves as if it declared nothing.
  • On Rails 6.1, a path crossing an association with an instance-dependent scope cannot be preloaded: it is logged once and loaded per record as before.
Preloading is not opt-out for a declared path: if a path you declared should not be preloaded, remove it from dependencies:.
The validation above is lenient on purpose. The v2 agents reject the same declarations at boot — see If you plan to migrate to a v2 agent later.
The interface now sends the agent the exact fields it displays, on lists, records and related data. Records (show) used to load every column and every to-one relation; they now load only what is displayed. Nothing is required on your side: this is enabled by the capabilities your agent announces.

Adapt your code to get the full benefit

This section applies to the v1 Rails agent only: the forest_liana gem, from 9.22. It does not apply to the v2 Ruby agent (forest_admin_rails / forest_admin_agent gems), which declares the same options with its own syntax.
The upgrade works without touching your code. The changes below are optional. They let the agent select fewer columns and load relations once per page instead of once per row. Start with the collections behind your largest lists and CSV exports, and apply them one collection at a time.
Each step names the code it touches:
1

Declare dependencies: on your smart fields — Forest customization

A smart field without dependencies: makes every request that displays it fall back to SELECT *. If its getter reaches a relation, it also runs one query per row. Declare the columns and relation paths the getter reads:
Your models are not modified. However, the requests that display the field now select only the columns they need: run check 4 again on the collection, since model callbacks reading other columns can start to fail. Then look for read the "..." column without declaring it warnings in your logs: each one names a column that is missing from the declaration.
2

Replace lookup smart relationships with real associations — Forest customization and application code

A smart relationship cannot declare what it reads, so a request that displays one falls back to SELECT * for the whole page, and its block runs once per row. When the relation can be described by a foreign key, declare a real belongs_to or has_one on the model instead. Every list that displays it then gets column selection back, and the agent loads the relation for the whole page instead of one query per row.This step touches both kinds of code: the smart relationship is removed from your Forest customization, and the association is added to your application model.
Restart the agent afterwards to regenerate the schema.The association also works when its key is not the target’s primary key, or when the target lives in another database:
After — app/models/customer.rb (application code)
How the agent loads it depends on where the target lives:
The association becomes part of your model, so the rest of your application sees it too:
  • Since Rails 5, a belongs_to is required by default. Without optional: true, saving a record whose foreign key is empty or matches no target fails validation everywhere in your application, not only in Forest.
  • Check that the name you choose does not collide with an existing method of the model.
  • If your block looked the record up on a nullable column, a record with an empty key used to show the first target whose column is NULL (find_by(column: nil)). The association shows no related record instead. This is the correct result, but these rows change.

The association is already declared on the model

If your model already declares an association with the same name as the smart relationship, you have nothing to add: only remove the smart relationship from your Forest customization.Keeping both does not break the upgrade: requests that display the relation behave exactly as they did before 9.22. But the Forest declaration is not merely redundant, it takes precedence over the association:
  • Its block serves the value, still once per row. The association the agent joins for the list is loaded, then ignored.
  • Every request that displays the relation keeps the SELECT * behavior, as for any smart relationship.
  • The relation is listed twice in .forestadmin-schema.json.
  • For a smart has_many, the related list is served by the route you declared for it (/forest/<Collection>/:id/<relation>), not by the agent.
Before removing the smart relationship, check that the association returns the same records as the block. A block that adds a condition, or looks the record up on another column, must be reproduced on the association first, with a scope or the primary_key: and foreign_key: options shown above. For a smart has_many, also remove the route and the controller action that served it.
3

Keep the matching key in association scopes — application code

An association whose scope narrows its select must keep the column the preload matches records on: the foreign key for a has_many or has_one, the target’s primary key (id, or the primary_key: option) for a belongs_to. Otherwise, as soon as a dependencies: path names it, the preload raises ActiveModel::MissingAttributeError and the request answers 500 (see check 6):
This change applies to every use of the association in your application. It only adds a column to the query, but review it like any other change to your models.
4

Disable the count of your largest collections — Forest customization

If you overrode the count action of a whole collection to call deactivate_count_response, replace the override with the collection option:
Remove the controller and its route once the option is in place.

If you plan to migrate to a v2 agent later

The features of this release exist on the v2 agents (@forestadmin/agent for Node.js, forest_admin_agent for Ruby) with the same option names and the same 403 messages, so what you configure here carries over. Two behaviors are more lenient on this v1 release than on v2. If a migration to v2 is on your roadmap, writing your customizations the strict way now saves you a boot failure later.

Invalid or empty dependencies: fail the agent boot on v2

On this release, a dependencies: entry that does not resolve (unknown column, unknown relation, path crossing a polymorphic relation) is ignored with a warning, and dependencies: [] is accepted. On v2, both are rejected when the agent starts:
  • an unknown column or relation, or a path crossing a polymorphic relation, raises a schema error and stops the agent;
  • an empty list raises Computed field '<Collection>.<field>' must have at least one dependency.
What to do: treat the Invalid dependency ... warnings of this release as errors and fix them now, and give every computed field at least one real dependency instead of [].

A smart field search: lambda is not exempted from permission checks on v2

On this release, a smart field search: lambda runs on every search without any permission check, because the fields it reaches cannot be described. On v2, a search handler whose reach is not declared is refused on extended search with a 403 (You cannot run an extended search on the '<Collection>' collection: the fields it reaches cannot be determined, so they cannot be checked against your permissions. on Node.js; on Ruby, unless the handler declares the fields it reaches). Plain search keeps working on both. What to do: nothing on this release. When migrating, expect to declare the fields your search handlers reach, or to restrict them to plain search.

Behavior changes at a glance

Logs to watch after deploying

Search your agent logs for Forest 🌳🌳🌳 after the first requests in each environment:
  • ForestLiana.skip_relation_read_permissions is true: ... — the opt-out is active. Plan its removal.
  • Invalid dependency '...' declared on smart field '...' of the '...' collection: it does not resolve to a real column, or crosses a polymorphic relation. Ignored ... — fix the declaration.
  • Invalid dependencies declared on field "...": expected a String, Symbol, or Array of them. Ignored ... — fix the declaration.
  • Field "..." of the "..." collection read the "..." column without declaring it in dependencies: — reloaded the record to serve it, at the cost of an extra query. Add it to the field's dependencies: to avoid this. — add the column to dependencies:.
  • Cannot retrieve the "..." value of the "..." collection because of an internal error in the getter implementation: ... — the getter could not be served (it raised again after a reload, or read a column of a relation that a reload cannot restore); its value is served as nil.
  • The "..." dependency of the "..." collection cannot be preloaded ("..." has an instance-dependent scope, which Rails 6.1.x cannot preload) — the relation is loaded once per record instead ... — expected on Rails 6.1; upgrade Rails to benefit from the preload.
  • The "..." relation of the "..." collection lives in another database and cannot be preloaded (its "..." key is not in the projected select) — ... — the cross-database relation shows as empty. Report it to Forest support with the collection and relation names.
  • The "..." relation of the "..." collection cannot be preloaded (its "..." key is not in the query's select) — ... — a segment or scope narrows the select of the collection and drops the key the relation reads. Add that key to the select.

Rollout recommendation

  1. Upgrade in development. Restart the agent and read the startup logs.
  2. Go through the Before you upgrade checklist with each role your teams use.
  3. Decide whether you set skip_relation_read_permissions for the transition, and record when you will remove it.
  4. Deploy to staging, then production, following Push your new version to production.
  5. Watch the logs above and the 403/422/500/503 rate of your agent for a few days.
  6. Once the upgrade is stable, adapt your code on your heaviest collections.