Managed installs: preserving read access when downgrading existing write grants

We’re migrating an established automation app to managed installs. Merchants add and remove tasks, so the permissions they need change over time. Today, legacy OAuth lets them reduce product access from write to read when their remaining tasks only need read. We need to preserve that behavior for existing merchants.

The unresolved case is existing write grants. In our dev migration tests, removing write_products also removes read_products, even after explicitly requesting read first. For new grants, we’ve confirmed that requesting read and write together allows write to be removed later while keeping read. But requesting that pair after write is already held doesn’t fix existing grants.

Here’s a minimal reproduction of the failing grant history, with both product scopes optional (scopes = "", use_legacy_install_flow = false), starting from no grants:

await shopify.scopes.request(['write_products']);
// Approve consent. scopes.query() reports write + read.

await shopify.scopes.request(['read_products']);
// Returns granted-all, with no additional prompt.

await shopify.scopes.revoke(['write_products']);
// scopes.query() now reports no grants. Product reads fail.

We also reproduced this with a legacy OAuth write grant moved into a managed dev preview, and with direct appRevokeAccessScopes calls. The migration itself preserved access; the later write removal lost read. The reported scope list contains both names whether read will survive removal or not.

What’s the supported way to downgrade these existing grants to read-only? Can we establish an independent read grant before removing write, or replace write with read in one operation? Revoking first and requesting read afterward could leave working tasks without access if the merchant closes the tab.

Tested September 11 on a dev store with App Bridge and online Admin API 2026-07; released-app and offline-token behavior aren’t verified yet. Happy to share more test details.