# 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.

Source: https://docs.fortly.com/data/backups

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.

> **Warning:**
>
> 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

| What | Where | Contents |
| --- | --- | --- |
| The whole database | **Data** › **Backups** › **Exports**, or `GET /projects/{name}/export` | Every table's definition and rows, plus indexes and views. Triggers and routines depend on the format, see below |
| One table | **Data** › **Explorer** › **Export** | The table as CSV, up to 50,000 rows. For more, export the whole database |
| A query result | **Data** › **Query**, then download CSV | The 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:

```bash
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](https://docs.fortly.com/api#project-names-in-paths).

- `format` (string, optional) — `postgres` (the default), `sql`, `mysql` or `json`. `postgresql` and `pg` also mean `postgres`, and `mariadb` means `mysql`.
- `gzip` (string, optional) — `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:

| Format | For | Triggers | Routines |
| --- | --- | --- | --- |
| `postgres` | Loading into PostgreSQL, here or anywhere else | Written as comments | Written as SQL, as you defined them |
| `sql` | Loading back into Fortly | Written as SQL | Written as comments |
| `mysql` | Loading into MySQL or MariaDB. Types are mapped to MySQL's names, `uuid` becomes `CHAR(36)` and arrays become their JSON text | Written as comments | Written as comments |
| `json` | Tools that don't speak SQL | Their SQL, as text | Their 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.

> **Note:**
>
> 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.

> **Warning:**
>
> 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 `INSERT`s 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](https://docs.fortly.com/account/tokens) kept as a secret there:

```bash title="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](https://docs.fortly.com/data/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:

```bash
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:

```bash
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](https://docs.fortly.com/data/import#when-a-statement-fails).
