Docs

Backups and exports

Download a full copy of your project's database in PostgreSQL, MySQL or JSON form, keep it on a schedule, and restore from it.

Updated October 12, 2026
On this page
  1. What you can take out
  2. Export the whole database
  3. Choose a format
  4. Keep exports on a schedule
  5. Restore from an export

The copy of your database you can rely on is the one you export and keep. Fortly does not keep automatic backups of your project's database that you can restore from. The Backups section of the Data tab says so: Automatic backup: Not running. This page covers taking a full export, automating it, and restoring from it.

Nothing copies your database unless you export it. If the data matters, export it on a schedule and store the files somewhere of your own.

What you can take out

WhatWhereContents
The whole databaseData › Backups › Exports, or GET /projects/{name}/exportEvery table's definition and rows, plus indexes and views. Triggers and routines depend on the format, see below
One tableData › Explorer › ExportThe table as CSV, up to 50,000 rows. For more, export the whole database
A query resultData › Query, then download CSVThe rows the query returned, up to the 500 the editor shows

The Backups section also shows the database's current size, as measured by the server it runs on.

Export the whole database

In the console, open Data › Backups › Exports, choose a format and select Download. The console downloads it gzipped.

From the API, any member of the project's team can export:

Terminal
curl "https://api.fortly.com/projects/my-app-ada/export?format=postgres&gzip=1" \
  -H "Authorization: Bearer $FORTLY_TOKEN" \
  -o my-app-ada.sql.gz

my-app-ada is the project's full name: the name you chose plus your account's slug. See Project names in paths.

formatstring
postgres (the default), sql, mysql or json. postgresql and pg also mean postgres, and mariadb means mysql.
gzipstring
1, true or yes to gzip the file. The response is then application/gzip, and .gz is added to the file name.

The response is the file itself, with a Content-Disposition file name of the project name and the date, such as my-app-ada-2026-10-12.sql or my-app-ada-2026-10-12.json. An unknown format is refused with no export format called xml — try sql, postgres, mysql or json.

Choose a format

Every format writes column types as they were declared, so numeric(12,2) stays numeric(12,2), and values come out with the digits the server holds. Each table carries its keys, checks, defaults and identity columns, followed by its rows as INSERT statements. The formats differ in what they do with triggers and routines:

FormatForTriggersRoutines
postgresLoading into PostgreSQL, here or anywhere elseWritten as commentsWritten as SQL, as you defined them
sqlLoading back into FortlyWritten as SQLWritten as comments
mysqlLoading into MySQL or MariaDB. Types are mapped to MySQL's names, uuid becomes CHAR(36) and arrays become their JSON textWritten as commentsWritten as comments
jsonTools that don't speak SQLTheir SQL, as textTheir definitions, as data

When something is written as a comment, it's still in the file for you to port by hand. It isn't silently dropped.

A trigger calls a function, so a database with triggers doesn't round-trip completely in a single SQL file. From postgres you get the routines but not the triggers. From sql, the triggers fail to load unless their functions already exist. Keep the routines and triggers in your own migration files, or recreate the commented-out triggers after a restore.

The PostgreSQL formats begin with BEGIN; and SET CONSTRAINTS ALL DEFERRED; and end with COMMIT;. Each table's key counter is set past the highest id in the file, so new rows don't collide with restored ones. The MySQL file switches on ANSI_QUOTES and turns foreign key checks off while it loads.

Tables are written in alphabetical order, each with its foreign keys declared inline and its rows straight after it. If a table references one whose name sorts after it, such as orders pointing at users, loading the file into PostgreSQL stops at that table because the one it references doesn't exist yet. Before you restore such a file, move the referenced table's CREATE TABLE and its INSERTs above the tables that point at it.

Each SELECT pg_catalog.setval(…) line sets a key counter to the table's highest id, or to 0 when the table is empty, and PostgreSQL refuses 0 with value 0 is out of bounds. If the export has an empty table with a generated key, delete that table's setval line before you restore.

The JSON file is a single document. identity is PostgreSQL's own code for an identity column: d for generated by default, a for generated always, and empty for neither:

JSON
{
  "project": "my-app-ada",
  "exported_at": "2026-10-12T09:30:00Z",
  "format": "fortly/1",
  "tables": [
    {
      "name": "orders",
      "columns": [
        { "name": "id", "type": "bigint", "not_null": true, "primary_key": true, "default": null, "identity": "d", "generated": null }
      ],
      "rows": [{ "id": 1, "customer_id": 7, "total": 129.50 }]
    }
  ],
  "views": [],
  "triggers": [],
  "indexes": [],
  "routines": []
}

Keep exports on a schedule

Fortly doesn't run exports for you, but anything that can run curl on a timer can, such as a CI job or a cron entry on a machine you control. Use an API token kept as a secret there:

export-db.sh
#!/usr/bin/env bash
set -euo pipefail

file="my-app-ada-$(date -u +%Y-%m-%dT%H%M).sql.gz"
curl --fail --silent --show-error \
  "https://api.fortly.com/projects/my-app-ada/export?format=postgres&gzip=1" \
  -H "Authorization: Bearer $FORTLY_TOKEN" \
  -o "$file"

# then copy "$file" to storage you control

--fail makes the script exit with an error, instead of saving the error message as your backup, if the export is refused.

Restore from an export

A restore is an import: Fortly runs the SQL script against the database as it is, in one transaction. A postgres export loads back in, subject to the table-order warning above. A sql export does too, unless it contains triggers.

  • Into a new project, upload the file as it is. This is the simplest way to check that an export restores.
  • Into the same project, the tables in the file already exist, so the import stops at the first CREATE TABLE and changes nothing. Drop the tables first, then import.

Take an export of the current state

Even if it's the state you're replacing, it's your way back if the restore isn't what you expected.

Drop the tables you're restoring

Send the drop statements to the SQL route. cascade also removes views and foreign keys that depend on them:

Terminal
curl -X POST https://api.fortly.com/projects/my-app-ada/sql \
  -H "Authorization: Bearer $FORTLY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"sql": "drop table if exists orders cascade; drop table if exists customers cascade"}'

Upload the export

Gzipped files aren't accepted, so decompress first:

Terminal
gunzip my-app-ada-2026-10-12.sql.gz

curl -X POST https://api.fortly.com/projects/my-app-ada/import \
  -H "Authorization: Bearer $FORTLY_TOKEN" \
  --data-binary @my-app-ada-2026-10-12.sql

In the console, the same upload is Data › Backups › Restore › Restore from a file. Only the account owner or an admin can restore.

If any statement fails, nothing is applied and the reply names the statement. See When a statement fails.

Something missing or unclear? Tell us and we will fix the page.© 2026 Fortly