Skip to content

Backup and save D1 database

Backup D1 using export API and save it on R2

In this example, we implement a Workflow periodically triggered by a Cron Trigger. That Workflow initiates a backup for a D1 database using the REST API, and then stores the SQL dump in an R2 bucket.

When the Workflow is triggered, it fetches the REST API to initiate an export job for a specific database. Then it fetches the same endpoint to check if the backup job is ready and the SQL dump is available to download.

As shown in this example, Workflows handles both the responses and failures, thereby removing the burden from the developer. Workflows retries the following steps:

  • API calls until it gets a successful response
  • Fetching the backup from the URL provided
  • Saving the file to R2

The Workflow can run until the backup file is ready, handling all of the possible conditions until it is completed.

This example provides simplified steps for backing up a D1 database to help you understand the possibilities of Workflows. In every step, it uses the default sleeping and retrying configuration. In a real-world scenario, more steps and additional logic would likely be needed.

import {
WorkflowEntrypoint,
WorkflowStep,
WorkflowEvent,
} from "cloudflare:workers";
// We are using R2 to store the D1 backup
type Env = {
BACKUP_WORKFLOW: Workflow;
D1_REST_API_TOKEN: string;
BACKUP_BUCKET: R2Bucket;
};
// Workflow parameters: we expect accountId and databaseId
type Params = {
accountId: string;
databaseId: string;
};
// Workflow logic
export class backupWorkflow extends WorkflowEntrypoint<Env, Params> {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
const { accountId, databaseId } = event.payload;
const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/d1/database/${databaseId}/export`;
const method = "POST";
const headers = new Headers();
headers.append("Content-Type", "application/json");
headers.append("Authorization", `Bearer ${this.env.D1_REST_API_TOKEN}`);
const bookmark = step.do(`Starting backup for ${databaseId}`, async () => {
const payload = { output_format: "polling" };
const res = await fetch(url, { method, headers, body: JSON.stringify(payload) });
const { result } = (await res.json()) as any;
// If we don't get `at_bookmark` we throw to retry the step
if (!result?.at_bookmark) throw new Error("Missing `at_bookmark`");
return result.at_bookmark;
});
step.do("Check backup status and store it on R2", async () => {
const payload = { current_bookmark: bookmark };
const res = await fetch(url, { method, headers, body: JSON.stringify(payload) });
const { result } = (await res.json()) as any;
// The endpoint sends `signed_url` when the backup is ready to download.
// If we don't get `signed_url` we throw to retry the step.
if (!result?.signed_url) throw new Error("Missing `signed_url`");
const dumpResponse = await fetch(result.signed_url);
if (!dumpResponse.ok) throw new Error("Failed to fetch dump file");
// Finally, stream the file directly to R2
await this.env.BACKUP_BUCKET.put(result.filename, dumpResponse.body);
});
}
}
export default {
async fetch(req: Request, env: Env): Promise<Response> {
return new Response("Not found", { status: 404 });
},
async scheduled(controller: ScheduledController, env: Env, ctx: ExecutionContext) {
const params: Params = {
accountId: "{accountId}",
databaseId: "{databaseId}",
};
const instance = await env.BACKUP_WORKFLOW.create({ params });
console.log(`Started workflow: ${instance.id}`);
},
};

Here is a minimal package.json:

{
"devDependencies": {
"@cloudflare/workers-types": "^4.20241224.0",
"wrangler": "^3.99.0"
}
}

Here is a wrangler.toml:

name = "backup-d1"
main = "src/index.ts"
compatibility_date = "2024-12-27"
compatibility_flags = [ "nodejs_compat" ]
[[workflows]]
name = "backup-workflow"
binding = "BACKUP_WORKFLOW"
class_name = "backupWorkflow"
[[r2_buckets]]
binding = "BACKUP_BUCKET"
bucket_name = "d1-backups"
[triggers]
crons = [ "0 0 * * *" ]