Exporting the tables takes minutes. What catches people out is everything that is not in the tables: the rules deciding who may read a row, the accounts people sign in with, and the files that were never in the database at all. Move the tables without those and you get an app that looks migrated and is quietly wide open.
Take a full export first, download it, and open it. Not because the migration will fail, but because the copy you have never looked at is the one that turns out to be empty on the day you need it.
Do not delete anything at the old end until the new end has been used by real people for a week. A migration is not finished when the data arrives; it is finished when nobody has found something missing.
This is the one that actually hurts. In a modern hosted database, the rule that says a person may only read their own rows usually lives in the database's own configuration — as a policy attached to the table, not as a column inside it. A table dump contains rows. It does not contain the policies.
So the tables land in the new database, everything looks right, and every user can now read every other user's records. Nothing errors. Nothing looks broken. You find out when a customer mentions seeing somebody else's name.
Write the rules down before you export, restore them deliberately at the other end, and then test them the only way that counts: sign in as an ordinary user and try to fetch a row that belongs to somebody else. If it comes back, you are not finished.
The same goes for anything else that lives beside the tables rather than in them — the functions, the triggers and the scheduled jobs. They are invisible in a row count.
User accounts generally are not in your own tables. They sit in the authentication system, alongside password hashes you cannot read and often should not move, and your tables reference them by an identifier.
You have two honest options. Move the accounts, keeping every identifier byte-for-byte identical so the references in your tables still point at the right person — get one identifier wrong and rows quietly attach to the wrong user. Or start the accounts fresh and re-invite everybody, mapping old identifiers to new ones as people sign up.
Re-inviting sounds worse and is usually cleaner, particularly if the user list is small. What is never acceptable is migrating the rows and leaving the accounts behind: the data is then attached to identifiers that no longer exist anywhere.
Uploads — profile pictures, PDFs, anything a user attached — are almost never inside the database. The database holds a path or a URL; the file itself is in separate storage.
Export those separately, and check the paths still resolve afterwards. A restored database full of links to files that no longer exist is a common and very quiet way to lose a year of attachments.
A new database means new connection details, and every place holding the old ones has to be updated: the app, any background job, anything you wired up months ago and forgot.
Then revoke the old keys rather than merely replacing them. A key that still works is still a way into the data you just moved, and it is now in a place nobody is watching. If any of those keys were ever pasted into a chat window, a screenshot or a frontend file, treat them as public and rotate them regardless.
Run the whole thing once into a throwaway copy, with the app pointed at it, and use it. Sign in as a normal user. Create something. Try to read somebody else's row. Restore a backup. The rehearsal is where you find the missing policy and the broken file paths, and it costs you an evening instead of a customer.
For the real cutover: put the app in read-only or take a short maintenance window, take the final export at that moment so nothing is written in between, import, point the app at the new database, and only then let writes back in. A migration taken while people are still writing produces two half-current copies and no way to reconcile them.
You keep building in Lovable. Softkiln is where the result runs: it provisions the database and the separate development and production copies, holds your keys outside the code and injects them at runtime, and takes backups on a schedule that it verifies by restoring them.
The rehearsal above is also the normal way of working afterwards — try it in the private copy, publish when it is right, and put the previous version back in one action if it is not.
Free for 15 days, no card. The safest time to do this migration is before real customers are depending on the old copy.
The row-level access rules. They live in the database's configuration rather than in the tables, so a table export leaves them behind, and the app carries on working — with every user able to read every other user's records. Test it by signing in as an ordinary user and trying to fetch somebody else's row.
Not usefully, at this size. A short read-only window is far simpler than reconciling writes that landed in two databases, and for a small app it is minutes. Announce it rather than avoiding it.
No. Keep describing changes there exactly as you do now. What changes is where the result runs, not how you build it.
Count the rows in every table at both ends, open a file that was uploaded before the move, sign in as a real user, and attempt one read you should not be allowed. All four, not just the first.