The backup format.

.qlbak version 2 (version 1 still described). Page dated 9 November 2026.

A Quietlift backup is a single file you can keep anywhere. Its format is documented here in full, so you, or any programmer, can read your data without us. You are never locked in.

The same file is used for manual backups, automatic backups, restoring, and moving between iPhone and Android. Backups are named quietlift-YYYYMMDD-HHmm.qlbak, for example quietlift-20261106-1830.qlbak.

Overview

  1. A key is derived from your backup passphrase with PBKDF2-HMAC-SHA256.
  2. That key opens an AES-256-GCM envelope.
  3. Inside is an ordinary zip file with uncompressed (stored) entries: manifest.json and export.json.
  4. export.json holds everything you made: exercises, routines, workouts, body weight, conditioning and saved timers.

Per-phone preferences such as unit, theme and accent colour are not in the backup.

Byte layout

All offsets in bytes, counted from 0
OffsetSizeField
06Magic: ASCII QLBAK followed by one zero byte (51 4C 42 41 4B 00)
61Format version, unsigned. 2 for this specification.
74PBKDF2 iteration count, unsigned, big-endian. Quietlift writes 600,000 (00 09 27 C0).
1116PBKDF2 salt, random per passphrase
272Hint length h in bytes, unsigned, big-endian. 00 00 when there is no hint.
29hPassphrase hint, UTF-8, exactly as the person typed it (at most 60 characters). Readable without the passphrase.
29 + h12AES-GCM nonce, random per file
41 + hnAES-256-GCM ciphertext of the zip
41 + h + n16AES-GCM authentication tag

Bytes 0 to 28 + h (magic, version, iterations, salt, hint length and hint) are the header. The header is not encrypted, but it is authenticated: it is passed to AES-GCM as associated data, so the hint can be read without the passphrase but not changed without breaking the file. The nonce is not part of the associated data.

Version 1 (files written before 9 November 2026) has no hint fields: the header is bytes 0 to 26, the nonce starts at byte 27 and the ciphertext at byte 39. Everything else is the same. Quietlift reads both.

Key and encryption

Reading rules

The zip

A plain zip with stored (method 0, uncompressed) entries and UTF-8 names. The whole zip is encrypted, so compression would add nothing. Once decrypted, any unzip tool opens it. Versions 1 and 2 contain the same members (the hint changed only the header; manifest.json still says formatVersion 1):

manifest.json

FieldTypeMeaning
formatVersioninteger1
appVersionstringThe Quietlift version that wrote the file
createdAtstringISO-8601 instant, UTC
devicestringA plain device name, such as “Pixel 9” or “iPhone”
countsobjectworkouts, sets, routines, exercises, bodyWeights, conditioning: integers
membersarrayOne object per zip member: name, sha256 (lowercase hex), size (bytes)
{
  "formatVersion": 1,
  "appVersion": "1.0.0",
  "createdAt": "2026-11-06T18:30:00Z",
  "device": "Pixel 9",
  "counts": { "workouts": 212, "sets": 9431, "routines": 4, "exercises": 61, "bodyWeights": 40, "conditioning": 12 },
  "members": [
    { "name": "export.json", "sha256": "6edf3a4b…", "size": 1843302 }
  ]
}

export.json

The same document as More › Your data › Quietlift JSON, which you can export without encryption at any time.

{
  "format": "quietlift-export",
  "schemaVersion": 1,
  "exportedAt": "2026-11-06T18:30:00Z",
  "signature": "…",
  "data": {
    "exercises":    [ … ],
    "routines":     [ … ],
    "workouts":     [ … ],
    "bodyWeights":  [ … ],
    "conditioning": [ … ],
    "savedTimers":  [ … ]
  }
}

exercises[]

FieldMeaning
idStable id
nameKeyLibrary exercises: a name key such as exercise_bench_press, shown in the app’s language
customNameYour own exercises: the name you gave it
primaryMuscle, secondaryMuscleschest, shoulders, triceps, biceps, forearms, lats, upper_back, lower_back, traps, abs, glutes, quads, hamstrings, calves, adductors, abductors, neck, full_body
equipmentbarbell, dumbbell, machine, cable, bodyweight, kettlebell, ez_bar, smith_machine, band, trap_bar, plate, other
kindweighted, bodyweight, assisted, duration, distance
originlibrary, user (made in the app) or import
archivedBoolean
aliasesOther names, as strings

routines[]

FieldMeaning
id, name, originAs above; origin is user or import
positionOrder in your list
archivedBoolean
exercises[]In order: exerciseId, targetSets, repsMin, repsMax (absent for a fixed rep target), restSeconds, supersetGroup (exercises sharing a number form a superset), note, nextWeightKg

workouts[]

FieldMeaning
id, nameThe workout and its title
routineIdThe routine it was started from, if any
startedAt, finishedAtInstants
exercises[]In order: exerciseId, supersetGroup, restSeconds, targetRepsMin, targetRepsMax, note, sets[]
sets[]In order: type (normal, warmup, drop, failure), weightKg, reps, durationSeconds, distanceMeters, rpe, completedAt

bodyWeights[], conditioning[], savedTimers[]

ListFields
bodyWeightsloggedAt, kg
conditioningFinished interval blocks: id, presetKey (a built-in preset) or customName, durationSeconds, rounds, note, workoutId (if done inside a workout), endedAt
savedTimersYour interval timers: id, name, round (a list of {kind, seconds} with kind one of prep, work, rest), rounds, createdAt

A decoder in Python

About fifty lines, using the standard library and the cryptography package (pip install cryptography). It opens a backup, checks the hashes and returns both JSON documents. Download read_qlbak.py.

# read_qlbak.py: open a Quietlift .qlbak (format v1 or v2) with your passphrase.
# Needs Python 3.8+ and the "cryptography" package (pip install cryptography).
import getpass, hashlib, io, json, struct, sys, zipfile
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

def read_header(data):
    if data[:6] != b"QLBAK\x00":
        raise ValueError("not a .qlbak file")
    version = data[6]
    if version not in (1, 2):
        raise ValueError(f"format version {version}; this reader knows versions 1 and 2")
    (iterations,) = struct.unpack(">I", data[7:11])      # big-endian
    if not 1 <= iterations <= 10_000_000:
        raise ValueError("implausible iteration count")
    salt = data[11:27]                                     # 16 bytes
    end, hint = 27, ""
    if version == 2:
        (size,) = struct.unpack(">H", data[27:29])        # hint length, big-endian
        end = 29 + size
        if end + 12 + 16 > len(data):
            raise ValueError("hint length runs past the file")
        hint = data[29:end].decode("utf-8")
    return iterations, salt, hint, end

def open_qlbak(path, passphrase):
    data = open(path, "rb").read()
    iterations, salt, hint, end = read_header(data)
    header = data[:end]                                    # authenticated, not encrypted
    nonce = data[end:end + 12]                             # 12 bytes
    sealed = data[end + 12:]                               # ciphertext followed by the 16-byte tag
    key = hashlib.pbkdf2_hmac("sha256", passphrase.encode("utf-8"), salt, iterations, dklen=32)
    plain = AESGCM(key).decrypt(nonce, sealed, header)     # raises InvalidTag on a wrong passphrase
    members = zipfile.ZipFile(io.BytesIO(plain))
    manifest = json.loads(members.read("manifest.json"))
    for m in manifest["members"]:
        body = members.read(m["name"])
        if hashlib.sha256(body).hexdigest() != m["sha256"]:
            raise ValueError(f"{m['name']} does not match its hash")
    export = json.loads(members.read("export.json"))
    return manifest, export

if __name__ == "__main__":
    hint = read_header(open(sys.argv[1], "rb").read())[2]
    if hint:
        print("Hint:", hint)
    manifest, export = open_qlbak(sys.argv[1], getpass.getpass("Passphrase: "))
    print(json.dumps(manifest, indent=2))
    print(len(export["data"]["workouts"]), "workouts")

Run it as python3 read_qlbak.py quietlift-20261106-1830.qlbak. With a wrong passphrase, decrypt raises InvalidTag. The key derivation takes a moment; that is the 600,000 iterations doing their job.

We tested this script against a sample .qlbak written by the app before publishing it.

Why it’s published

Quietlift has no account and no server, so your backup is the only copy that isn’t on your phone. A file you can only open with one app isn’t fully yours. With this page, it is.

Questions or a mistake on this page: support@quietlift.app.