Backup destinations
Configure and verify storage for backups.
A backup destination is a reusable storage target — an S3-compatible bucket, an SFTP server, a local
filesystem path, or another Synq server — that backup policies write their archives
to. FTP/FTPS servers, Dropbox and Google Drive are destinations too; the last two connect through OAuth with
your own app (see Connect Dropbox or Google Drive). This API creates and manages those targets and runs a preflight that proves a destination is
actually reachable (it writes, reads back, and deletes a probe object). Credentials are encrypted at rest;
serialized destinations never return secret values, only hasX flags showing which are configured.
This module is available on both self-hosted and cloud instances. The local destination kind is
self-hosted only — it is rejected in cloud mode and, on self-hosted, must be explicitly enabled (see below).
Create a destination
const result = await ship.backupDestinations.create({
name: "prod-s3",
kind: "s3_compatible",
bucket: "my-backups",
region: "us-east-1",
accessKeyId: "AKIA...",
secretAccessKey: "...",
});
console.log(result);curl -X POST "$SYNQ_URL/api/backup-destinations" \
-H "Authorization: Bearer $SYNQ_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"prod-s3","kind":"s3_compatible","bucket":"my-backups","region":"us-east-1","accessKeyId":"AKIA...","secretAccessKey":"..."}'Prop
Type
Required fields depend on kind:
s3_compatible—bucket,accessKeyId, andsecretAccessKey.sftp—sshHost,sshUser, and one ofsftpPassword/sftpPrivateKey.synq_server—serverId(a server in your org).local—endpoint(an absolute path insideBACKUP_LOCAL_ROOT).ftp:ftpHost,ftpUserandftpPassword.pathPrefixis the folder (absolute, or relative to the login folder).dropbox/google_drive:oauthClientIdandoauthClientSecret.pathPrefixis the Dropbox folder (for example/Synq Backups) or the Google Drive folder name (defaultSynq Backups). The destination can't store backups until it is connected.
Update a destination
Send only the fields you want to change. For credentials, undefined (omitted) leaves the stored value
unchanged, null clears it, and a string replaces it. The kind cannot be changed after creation.
const result = await ship.backupDestinations.update("bkd_123", {
pathPrefix: "nightly/",
isDefault: true,
});
console.log(result);curl -X PATCH "$SYNQ_URL/api/backup-destinations/bkd_123" \
-H "Authorization: Bearer $SYNQ_TOKEN" \
-H "Content-Type: application/json" \
-d '{"pathPrefix":"nightly/","isDefault":true}'Prop
Type
Connect Dropbox or Google Drive
Dropbox and Google Drive destinations use an OAuth app you own: a Dropbox app (scoped access, with
files.metadata.read, files.content.read and files.content.write) or a Google Cloud OAuth client of type
Web application with the https://www.googleapis.com/auth/drive.file scope. After creating the
destination with the app's credentials, connect it in two calls:
- Start: send the redirect URI, which is the panel's own callback page (
<panel origin>/backups/oauth/callback, HTTPS or localhost, registered in the app). The response has the provider URL to open in a browser. - Complete: the provider sends the browser back to that page with
codeandstate. Post both. Synq exchanges the code for a refresh token (stored encrypted), records the approving account, creates or reuses the Google Drive folder, runs a preflight, and returns the destination.
const { authorizeUrl } = await ship.backupDestinations.oauthStart("bkd_123", {
redirectUri: "https://panel.example.com/backups/oauth/callback",
});
console.log(authorizeUrl); // open it, approve access
// Later, with the query parameters the callback page received:
const destination = await ship.backupDestinations.oauthComplete({ code: "...", state: "..." });
console.log(destination.oauthConnected, destination.oauthAccount);curl -X POST "$SYNQ_URL/api/backup-destinations/bkd_123/oauth/start" \
-H "Authorization: Bearer $SYNQ_TOKEN" \
-H "Content-Type: application/json" \
-d '{"redirectUri":"https://panel.example.com/backups/oauth/callback"}'
curl -X POST "$SYNQ_URL/api/backup-destinations/oauth/complete" \
-H "Authorization: Bearer $SYNQ_TOKEN" \
-H "Content-Type: application/json" \
-d '{"code":"...","state":"..."}'The state is sealed (encrypted and authenticated) by Synq and expires after 15 minutes. It only completes
for the user and workspace that started the connection. The destination's read model shows the connection as
oauthConnected, oauthAccount and oauthConnectedAt, and for Google Drive the folder as remoteFolderId;
the refresh token and the app secret are never returned. When the provider stops honoring the refresh token
(access removed, or a Google app left in Testing for 7 days), runs and preflights fail with a message that
the access "was revoked or has expired"; start and complete the connection again to fix it.
Preflight (verify connectivity)
Proves the destination works by writing, reading back, and deleting a small probe object. On success it
stamps lastVerifiedAt; on failure it records the error in lastVerifyError.
const result = await ship.backupDestinations.preflight("bkd_123");
console.log(result);curl -X POST "$SYNQ_URL/api/backup-destinations/bkd_123/preflight" \
-H "Authorization: Bearer $SYNQ_TOKEN"A failed check is still a 200
Preflight returns 200 with {"ok": false, "reason": "..."} when the target is unreachable or
misconfigured — the reason (bad credentials, DNS failure, permission denied) is in the body, not the HTTP
status. A 404 here means the :id doesn't belong to your organization.
Errors
400 — validation failed
The body is missing a field required for its kind (e.g. an S3 destination without a bucket or
credentials), the name is blank / over 80 chars or duplicates an existing destination, or you passed an
unsupported kind. http_upload is defined but not yet supported. On the OAuth routes: a redirect URI that is
not HTTPS (or localhost) or does not end in /backups/oauth/callback, or a state that is invalid, expired,
or was started by another user.
400 — local destinations disabled
A local destination requires BACKUP_ALLOW_LOCAL_DESTINATION=true and a configured BACKUP_LOCAL_ROOT,
and the endpoint must resolve to an absolute path inside that root. Both the root and the endpoint are
symlink-resolved before the check, so a symlink that escapes the root is rejected. BACKUP_LOCAL_ROOT
itself must be a dedicated directory — not /, /var, /var/lib, Synq's own state directory, or
anything inside a system tree such as /etc or /var/lib/docker. Local destinations are always rejected
in cloud mode.
404 on a per-destination route
The :id doesn't belong to your organization (or was deleted). List your destinations with
GET /api/backup-destinations to get valid IDs.
Operations
| Operation | SDK | REST API |
|---|---|---|
| List backup storage destinations. | backupDestinations.list() | GET /api/backup-destinationsbackup_destination:list |
| Create a backup destination. | backupDestinations.create(input) | POST /api/backup-destinationsbackup_destination:write |
| Test destination configuration before saving it. | backupDestinations.preflightDraft(input) | POST /api/backup-destinations/preflightbackup_destination:write |
| List every volume backup schedule with its destination and last run. | backupDestinations.volumeBackups() | GET /api/backup-destinations/volume-backupsbackup_destination:list |
| Read a destination with credentials protected. | backupDestinations.get(id) | GET /api/backup-destinations/:idbackup_destination:read |
| Read destination storage usage. | backupDestinations.usage(id) | GET /api/backup-destinations/:id/usagebackup_destination:read |
| Update destination settings. | backupDestinations.update(id, input) | PATCH /api/backup-destinations/:idbackup_destination:write |
| Remove an eligible destination. | backupDestinations.remove(id) | DELETE /api/backup-destinations/:idbackup_destination:admin |
| Test a saved destination and record its verification state. | backupDestinations.preflight(id) | POST /api/backup-destinations/:id/preflightbackup_destination:write |
| Start connecting a Dropbox or Google Drive destination; returns the provider URL to open. | backupDestinations.oauthStart(id, input) | POST /api/backup-destinations/:id/oauth/startbackup_destination:write |
| Finish connecting with the code and state the provider sent back. | backupDestinations.oauthComplete(input) | POST /api/backup-destinations/oauth/completebackup_destination:write |