Data portability

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": [ ... ]
}
formatstringrequired
Always quiver-garage. Identifies this as a Quiver export file.
versionnumberrequired
Schema version. Currently 4. Quiver rejects files with a version higher than it understands.
exported_atstring (ISO 8601)required
UTC timestamp of when the export was generated.
bikesarray<Bike>required
All bikes in the garage. Each bike includes its components and service history nested inline.
ridesarray<Ride>required
All rides, stored flat at the top level rather than nested per bike. Each ride references its bike via bike_id.
setupsarray<Setup>
All named setups (Race, Training, Trainer, etc.) across all bikes.
componentAssignmentsarray<ComponentAssignment>
All component assignment stints across all bikes.
entityAttachmentsarray<EntityAttachment>
All attachments (photos, documents) for bikes and components. File content is base64-encoded inline.
serviceLogTasksarray<ServiceLogTask>
Per-task checklist state for service log entries. Each row references its parent 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)required
Exactly one setup per bike has is_default: 1. The default setup cannot be deleted.
display_ordernumberrequired
Sort order for the tab strip on the bike detail screen. Lower values appear first.

Bike 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": [ ... ]
}
idstringrequired
Unique identifier (nanoid). Stable across imports — re-importing the same bike will update it in place rather than creating a duplicate.
typestringrequired
One of: road, gravel, mountain, cyclocross, commuter, tt, track, bmx, other.
starting_kmnumberrequired
Kilometres already on the bike when it was added to Quiver. Total bike km = starting_km + sum of all ride distances.
image_datastring | null
Base64-encoded image data. null if the bike has no photo.
image_mimestring | null
MIME type of image_data. Either image/jpeg or image/png.
active_setup_idstring | null
ID of the setup currently selected for new rides. References a Setup in the top-level setups array.
componentsarray<Component>required
All components ever installed on this bike, including retired ones (those with replaced_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_idstringrequired
Component category identifier: chain, cassette, chainring, tire_front, tire_rear, brake_pads_front, brake_pads_rear, derailleur_rear, derailleur_front, bottom_bracket, and others.
statestringrequired
Lifecycle state: active (on a bike), spare (in spares box), retired (discarded), or replaced (superseded).
installed_at_kmnumberrequired
Total bike km at the time this component was installed.
interval_kmnumber | null
Per-component service interval override. When null, the type default is used.
replaced_atstring | null
ISO date when this component was retired. Null means still installed.
wears_indoors_overridenumber | null
Per-component indoor wear override: 1 = 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 | null
JSON-encoded array of strings — a customised tools list for this component's service checklist. null means use the type-level default.
service_tasks_overridestring | null
JSON-encoded array of { 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 | null
Base64-encoded component photo. null if no photo.
photo_mimestring | null
MIME type of photo_data. Either image/jpeg or image/png.
service_logarray<ServiceLog>required
All service events recorded for this component.

Service 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"
}
actionstringrequired
service, inspect, or note.
action_kindstring | null
Specific action subtype (e.g. chain_hotwax). Null for free-text entries.
recurrence_kmnumber | null
How many km until this action should repeat. Null means one-off.

ServiceLogTask 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_idstringrequired
ID of the parent service_log entry this task belongs to. Cascades on delete.
task_idstringrequired
Stable task identifier (from lib/components/checklist.ts for type-default checklists, or from the override). Logical uniqueness with service_log_id.
task_titlestringrequired
Snapshot of the task title at save time. Preserved historically even if the dictionary title is later edited.
orderingnumberrequired
Display order at save time. Preserves the historical sequence across releases that reorder tasks.
checkednumber (0 or 1)required
1 if the task was ticked at save time; 0 if not.
notestring | null
Optional per-task note added during service.

Ride 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 | null
The setup active when this ride was logged. Used to attribute wear only to components that belong to this setup.
is_indoornumber (0 or 1)required
1 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.
sourcestringrequired
manual for rides entered by hand; strava for rides imported via the Strava integration.
external_idstring | null
Activity ID on the source platform (Strava activity ID). Used to deduplicate on re-import.

ComponentAssignment 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_assignnumberrequired
Total bike km when this component was installed.
distance_at_unassignnumber | null
Total bike km when this component was removed. Null while the assignment is still open (component currently installed).
setup_idstring | null
The setup this assignment belongs to. Only rides matching this setup contribute wear to this component.

EntityAttachment 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_typestringrequired
bike or component.
entity_idstringrequired
ID of the bike or component this attachment belongs to.
mime_typestring | null
MIME type of the file, e.g. image/jpeg or application/pdf.
file_base64stringrequired
Base64-encoded file content. Decode to reconstruct the original file.

Import behaviour

Importing offers two modes:

  • Merge — upserts records. Existing records with the same id are 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.