A read-only grant enforces a boundary a convention cannot

A shared database with a single application user has no boundaries at all, whatever the code layout suggests.

CREATE USER 'fulfilment'@'%' IDENTIFIED BY '...';

GRANT SELECT, INSERT, UPDATE, DELETE
  ON shop.orders TO 'fulfilment'@'%';
GRANT SELECT ON shop.catalogue_products_v1 TO 'fulfilment'@'%';

-- and the migration user, which is separate and is not
-- what the application connects as:
GRANT ALTER, CREATE, DROP, INDEX ON shop.* TO 'migrator'@'%';

Separating the migration user from the application user is the part that is skipped most often and is the one that prevents an application bug from dropping a table. The failure mode of getting a grant wrong is loud — a query fails with a permission error — which makes this much safer to introduce than it sounds.