Your Data Is Portable
You can leave Foir, whenever you like, and take a working database with you.
Not a JSON dump. Not a CSV you’d have to reverse-engineer. One command gives you an ordinary Postgres database: a typed table per model, your content in real columns, references as real foreign keys — and the access rules that protected it, still protecting it.
foir export postgres --target "postgresql://localhost/myproject"That’s the whole thing. Restore it, point your app at it, and it works.
Why we build it this way
Every platform says you own your data. Most mean you can download it.
The difference matters the moment you actually try to leave. A dump of JSON blobs is your data in the way a pile of bricks is your house. You still have to work out the schema, rebuild the relationships, and — the part that quietly takes the longest — reimplement every access rule you were relying on, correctly, from memory, before you can safely let a single customer log in.
That last part is the real lock-in, and it’s the part nobody advertises.
So we treat the exit door as a feature and keep it working. A backend you can’t leave isn’t a backend you chose; it’s one you’re stuck with. We’d rather you stayed because it’s good.
Your row-level security comes with you
This is the part that has no equivalent elsewhere.
Foir enforces who-can-see-what in the database, not in an API layer above it. Which means it’s expressible as ordinary Postgres row-level security — and so it can travel. The bundle includes a policies.sql that recreates it: a customer sees a row when they own it, when it’s public, when they’ve been granted it, or when it hangs off something they can already see. The same rules, enforced by your database, with no application in front of it.
The whole contract is one session variable:
SET request.jwt.claims = '{"customer_id": "<the customer id>"}';Set nothing and the caller is anonymous, and sees exactly the public rows.
If you restore onto Supabase — or anything else in front of Postgres, like PostgREST — you don’t have to do anything at all. Both already set request.jwt.claims from the bearer token, and the bundle creates the anon, authenticated and service_role roles those stacks expect. Your access model is live on the first day, on a stack you now control.
It’s worth comparing this with the usual “it’s just Postgres” promise. It is just Postgres — but “just Postgres” hands you tables and wishes you luck with the security. The rules are the hard part, and the rules are what we hand over.
What the bundle actually contains
schema.sql | A typed table per model. Real columns, real types, real constraints. |
data/*.csv | Your content, published channel. |
constraints.sql | The foreign keys. If they all validate, no reference was lost. |
policies.sql | Your access model, as row-level security. |
assets/ | Your media — a manifest of download links and a script that fetches them. |
validate.sql | The export checking its own work against your restored database. |
README.md | How to restore it, and what didn’t come with it. |
The full walkthrough is in the Export to Postgres guide.
We tell you what didn’t come across
An export that quietly loses something is worse than one that fails, because you find out months later, when it matters.
So every export ends by checking itself, and the bundle says plainly what it couldn’t carry: a reference pointing at something already deleted, a value that wouldn’t fit its column, a field that came out empty. And it ships validate.sql, which re-checks every row count and every column checksum against the database you restored into — so “it worked” is something you verify on your own machine, not something we assure you of.
The guide has a frank list of what doesn’t transfer. The short version: your rows and your row-level security come across; the GraphQL API, per-field permissions, and API scopes are things Foir did for you and that you’d now do yourself.
The other half: deleting your data
Portability isn’t only about taking data out. It’s also about being able to destroy it — the half that regulators, and your customers, actually ask about.
A customer can delete their own account, from your app, with the deleteMyAccount mutation. It’s self-serve and irreversible. It re-authenticates first (their password, or an explicit confirmation for accounts that sign in with a provider), then permanently removes their account, everything they own — their records, their files and the stored bytes behind them, the access they’d been granted — and signs them out everywhere. Data that was shared to them is left alone; only their own access to it goes.
mutation {
deleteMyAccount(password: "…", confirm: true) {
success
recordsDeleted
filesDeleted
}
}The counts come back so you can show the person what actually happened.
You can delete a customer from your side too, from the dashboard or the API, with the same cascade.
Together with export, that’s a complete offboarding story: your customers can get their data out and have it erased, and so can you. If you’re answering a GDPR question — a subject access request, or a right-to-erasure request — these are the two surfaces you need, and neither of them requires asking us.
See also
- Export to Postgres — the full guide: run it, restore it, verify it
- Customer Authentication — accounts, sessions, and sign-in
- Authorization & Access — the access model that travels with your data
- What Foir Replaces — what you’d otherwise assemble yourself