Garage Export Format
Your full garage as a single JSON file: bikes, setups, components, rides, service entries, checklist tasks, and attachments. The format is documented here so you can take your data anywhere.
Version 4 · Format identifier: quiver-garage
Overview
Quiver can export your entire garage — bikes, components, rides, service history, component assignments, and attachments — to a single JSON file. You own your data and can take it with you at any time.
To export: open Settings → Export garage. To import on another device or after reinstalling: open Settings → Import garage.
Photos and documents are embedded as base64-encoded data so the file is fully self-contained — no sidecar folders needed.
Top-level structure
{
"format": "quiver-garage",
"version": 4,
"exported_at": "2026-05-06T14:30:00.000Z",
"bikes": [ ... ],
"rides": [ ... ],
"setups": [ ... ],
"componentAssignments": [ ... ],
"entityAttachments": [ ... ],
"serviceLogTasks": [ ... ]
}formatstringrequiredquiver-garage. Identifies this as a Quiver export file.versionnumberrequired4. Quiver rejects files with a version higher than it understands.exported_atstring (ISO 8601)requiredbikesarray<Bike>requiredridesarray<Ride>requiredbike_id.setupsarray<Setup>componentAssignmentsarray<ComponentAssignment>entityAttachmentsarray<EntityAttachment>serviceLogTasksarray<ServiceLogTask>service_log entry by service_log_id. Empty for entries logged via the free-text form rather than the checklist flow.Setup object
{
"id": "stu901",
"bike_id": "abc123",
"name": "Race",
"is_default": 0,
"display_order": 1,
"created_at": "2026-05-01T10:00:00.000Z",
"modified_at": "2026-05-01T10:00:00.000Z"
}A named configuration for a bike. Each bike has at least one setup (the Default, with is_default: 1). Components are linked to setups via ComponentAssignment.setup_id.
is_defaultnumber (0 or 1)requiredis_default: 1. The default setup cannot be deleted.display_ordernumberrequiredBike object
{
"id": "abc123",
"name": "Sunday Tarmac",
"brand": "Specialized",
"model": "Tarmac SL7",
"year": 2023,
"type": "road",
"catalog_id": 42,
"frame_material": "carbon",
"color": "Gloss Tarmac Black",
"size": "56",
"purchased_at": "2023-04-01",
"starting_km": 0,
"archived": false,
"active_setup_id": "stu901",
"image_data": "<base64>",
"image_mime": "image/jpeg",
"created_at": "2023-04-01T10:00:00.000Z",
"modified_at": "2026-05-01T08:22:10.000Z",
"components": [ ... ]
}idstringrequiredtypestringrequiredroad, gravel, mountain, cyclocross, commuter, tt, track, bmx, other.starting_kmnumberrequiredstarting_km + sum of all ride distances.image_datastring | nullnull if the bike has no photo.image_mimestring | nullimage_data. Either image/jpeg or image/png.active_setup_idstring | nullsetups array.componentsarray<Component>requiredreplaced_at set). Active components have replaced_at: null.Component object
{
"id": "def456",
"type_id": "chain",
"brand": "Shimano",
"model": "CN-LG500",
"notes": null,
"state": "active",
"installed_at": "2025-01-15",
"installed_at_km": 12500,
"interval_km": 3000,
"design_life_km_override": null,
"wears_indoors_override": null,
"service_tools_override": null,
"service_tasks_override": null,
"replaced_at": null,
"replaced_at_km": null,
"photo_data": null,
"photo_mime": null,
"created_at": "2025-01-15T09:00:00.000Z",
"modified_at": "2025-01-15T09:00:00.000Z",
"service_log": [ ... ]
}type_idstringrequiredchain, cassette, chainring, tire_front, tire_rear, brake_pads_front, brake_pads_rear, derailleur_rear, derailleur_front, bottom_bracket, and others.statestringrequiredactive (on a bike), spare (in spares box), retired (discarded), or replaced (superseded).installed_at_kmnumberrequiredinterval_kmnumber | nullreplaced_atstring | nullwears_indoors_overridenumber | null1 = always wears indoors, 0 = never wears indoors, null = use the dictionary default for this component type (tires, brake pads, and rotors default to false; everything else defaults to true).service_tools_overridestring | nullnull means use the type-level default.service_tasks_overridestring | null{ id, title, detail?, torque? } objects — a customised task list. null means use the type-level default. A non-null value on either service_tools_override or service_tasks_override opts the component out of the default entirely.photo_datastring | nullnull if no photo.photo_mimestring | nullphoto_data. Either image/jpeg or image/png.service_logarray<ServiceLog>requiredService log entry
{
"id": "ghi789",
"date": "2025-03-20",
"km_at_service": 15200,
"action": "service",
"action_kind": "chain_hotwax",
"recurrence_km": 500,
"notes": "Waxed in Molten Speed Wax",
"created_at": "2025-03-20T18:00:00.000Z",
"modified_at": "2025-03-20T18:00:00.000Z"
}actionstringrequiredservice, inspect, or note.action_kindstring | nullchain_hotwax). Null for free-text entries.recurrence_kmnumber | nullServiceLogTask object
{
"id": "tsk012",
"service_log_id": "ghi789",
"task_id": "chain-degrease",
"task_title": "Degrease the chain",
"ordering": 1,
"checked": 1,
"note": "needed two passes — chain was filthy",
"modified_at": "2025-03-20T18:00:00.000Z"
}One row per checklist task on a service log entry created via the "Service this" flow. Snapshots the task title at the time of service so historical replay still shows what the user actually saw, even if a dictionary task title is later edited.
service_log_idstringrequiredservice_log entry this task belongs to. Cascades on delete.task_idstringrequiredlib/components/checklist.ts for type-default checklists, or from the override). Logical uniqueness with service_log_id.task_titlestringrequiredorderingnumberrequiredcheckednumber (0 or 1)required1 if the task was ticked at save time; 0 if not.notestring | nullRide object
{
"id": "jkl012",
"bike_id": "abc123",
"date": "2026-04-28",
"distance_km": 87.4,
"duration_min": 195,
"notes": null,
"setup_id": "stu901",
"is_indoor": 0,
"source": "strava",
"external_id": "12345678901",
"created_at": "2026-04-28T14:00:00.000Z",
"modified_at": "2026-04-28T14:00:00.000Z"
}setup_idstring | nullis_indoornumber (0 or 1)required1 if this was a trainer or indoor ride; 0 for outdoor rides. Components with wears_indoors = false (tires, brake pads, rotors) don't accrue distance on indoor rides.sourcestringrequiredmanual for rides entered by hand; strava for rides imported via the Strava integration.external_idstring | nullComponentAssignment object
{
"id": "mno345",
"component_id": "def456",
"bike_id": "abc123",
"distance_at_assign": 12500,
"distance_at_unassign": 15200,
"assigned_at": "2025-01-15",
"unassigned_at": "2025-06-01",
"setup_id": "stu901",
"created_at": "2025-01-15T09:00:00.000Z",
"modified_at": "2025-06-01T12:00:00.000Z"
}One row per stint on a bike. A component that has been on two bikes will have two assignment rows. An open assignment (currently installed) has unassigned_at: null and distance_at_unassign: null. With multiple setups, a component can have one open assignment per setup simultaneously.
distance_at_assignnumberrequireddistance_at_unassignnumber | nullsetup_idstring | nullEntityAttachment object
{
"id": "pqr678",
"entity_type": "bike",
"entity_id": "abc123",
"mime_type": "image/jpeg",
"file_base64": "<base64>",
"created_at": "2026-01-10T08:00:00.000Z",
"modified_at": "2026-01-10T08:00:00.000Z"
}A photo or document attached to a bike or component — for example, a purchase receipt or warranty card. The file content is base64-encoded inline; there is no file_path field (paths are device-specific and meaningless across devices).
entity_typestringrequiredbike or component.entity_idstringrequiredmime_typestring | nullimage/jpeg or application/pdf.file_base64stringrequiredImport behaviour
Importing offers two modes:
- Merge — upserts records. Existing records with the same
idare updated; records not present in the file are left untouched. Safe for syncing between devices without losing new data added on the destination. - Replace all — wipes local data first, then inserts everything from the file. Use when restoring from a backup.
After a successful import, Quiver pushes the imported records to iCloud so they appear on all your devices automatically.