One way to prevent an old version from breaking is to split a migration into multiple steps.
E.g. you want to rename a column in the database. Renaming the column directly would break old versions of the app. This can be split into multiple steps:
- Add a db migration that inserts the new column
- Change the app so that all writes go to the old and new column
- Run a task that copies all values from the old to the new column
- Change the app that it reads from the new column
- Add a migration that remove the old column
This is unfortunately quite a hassle, but prevents having a downtime with a maintenance page up.