# Versions

What a saved version of a profile is: its ids and times, how every save adds a numbered version, how two devices that pick the same number are sorted out, what a delete does, how an old version is shown and restored, and which page describes which part of a profile.

## Generated table

**Record:** profile · **Covers:** `id`, `created_at`, `updated_at`, `deleted`, `owner_id`, `key`, `profile_id`, `version`, `name`, `notes`

| Field | Type | Required | Limits in the schema | Solver reads | In a shared log |
|---|---|---|---|---|---|
| `created_at` | string | yes | - | no | as stored |
| `deleted` | boolean | yes | - | no | as stored |
| `id` | string | yes | - | no | set by the server for each share |
| `key` | string | yes | - | no | set by the server for each share |
| `name` | string | yes | - | no | as stored |
| `notes` | string | yes | - | no | emptied when notes are hidden |
| `owner_id` | string or null | yes | - | no | set by the server for each share |
| `profile_id` | string | yes | - | no | set by the server for each share |
| `updated_at` | string | yes | - | no | as stored |
| `version` | number | yes | - | no | as stored |

## What it holds

A profile is a chain of saved versions that share one `profile_id`. Each version is one record holding the whole profile as it was at that save; these ten fields name the version, say when it was made and whether it is deleted, and carry the profile's name and notes. No save changes a version that is stored. The profile is its newest version; if that one is deleted, the app treats the profile as deleted.

## What to know first

**Every save is a new version.** A save, whoever makes it, never edits a stored version. It writes a new one numbered one above the highest the profile has, with a new `id`. Groups logged earlier keep the version they were shot with.

**Two devices can pick the same number.** The server keeps one and moves the other to the next free number, with its groups. A device that pulls first does the same itself. A number can change only this way, and never once the server has stored the version.

**A restore is a new version.** "Restore this version" saves an old version, calibration included, as the next one; the connector cannot. Restoring a deleted profile saves its last content and keeps its older versions deleted; fits in the app and by the connector still use their groups.

**The connector can only ask for a delete.** `delete_profile` puts a question on the open devices. The profile is deleted only if the person taps "Delete it" there; the question leaves the screen two minutes after it was sent. It needs its own `delete` token scope.

**`name` reaches every reader of a share; `notes` do not when notes are hidden.** In a spoken line a stored name is quoted and cut to 40 characters unless it is a short code such as `ridge-308-v2`.

## The fields

**`id`** Text that names one saved version, made by whatever saved it (a time-ordered random id). Every save gets a new one. It tells two saves apart when they chose the same `version`, and it lets the Calculator see that the version under it changed. Other records point at a profile by `profile_id` and `version`, not by this. Example: 0199c1a2-7b3e-7f10-8a55-3d2e91c4b6a7.

**`profile_id`** Names the whole chain: the same on every version of one profile, chosen once when the profile is made and never changed. A new profile gets a random time-ordered id; the two sample profiles of a fresh install have readable ones. Example: 0199c1a2-0000-7000-8000-00000000abcd.

**`version`** A whole number from 1. A new profile is version 1. The app's save writes the highest number the profile has, deleted versions counted, plus 1. A number is never reused. The server refuses 0, a fraction or text.

**`key`** `<profile_id>@<version>`, the name a version is stored under on the device and on the server. Every writer in the app and the connector builds it from the other two. Example: 0199c1a2-0000-7000-8000-00000000abcd@3. When the server moves a version to another number it rewrites the key.

**`name`** Free text: the profile's label on the Profiles list, the Calculator and in the connector. A new profile starts as "My rifle" ("My second rifle" from the invite banner; "New profile" on profiles made before 2026-10-18); a sample profile has the name it had in the app it was read from. Two profiles may have the same name. A rename is an ordinary save: it keeps the calibration and never asks keep or reset. Example: "Spring load, 350 gr".

**`notes`** Free text, empty on a profile made in the app (the sample profiles carry one line). No app screen shows or edits it. `edit_profile` and `create_profile` can set it. A copy of a shared profile starts with a line saying where it came from, then the sharer's own notes. A save carries it forward unchanged.

**`owner_id`** The account that owns the version, or null. A version made while signed out is null until the first sign-in on that device claims it. The server sets it to the pushing account on every push, whatever was sent.

**`created_at`** and **`updated_at`** ISO times in UTC, read from the clock of the device (or, for the connector, of the connector service) that saved the version. A new version has both set to one instant. `updated_at` changes only when a version is marked deleted: the delete stamps the moment. Neither orders versions; `version` does. Example: 2026-10-14T15:20:31.402Z.

**`deleted`** False on a live version, true on a deleted one. A deleted version is a tombstone: the record stays, with the same id, key and content, marked deleted. A version that is deleted never comes back.

**Limits on `name` and `notes`.** The profile screen refuses a blank name ("What you call this rifle and load is needed") and, since 2026-10-24, a new name over 80 characters and a rename to a name another of the device's live profiles has (letter case and outer spaces ignored; a profile may change the case of its own name, and a restore that would bring back such a name is refused too). A name saved before and left unchanged is not checked. The connector takes a name of 1 to 80 characters and notes of 0 to 2,000, and refuses a name of only spaces (since 2026-10-15); such a name does not pass on the profile screen either, which trims. The server refuses a version that is too large to sync. `create_profile` refuses a name that another of your profiles has (case and outer spaces ignored); `copy_shared_profile` refuses a given `new_name` the same way; `edit_profile` refuses a rename to a taken name the same way (since 2026-10-24).

**Which page owns which part of a profile.**

- [rifle](rifle.md): the rifle, its twist and sight height. [zero](zero.md): the zero range, air, slope and group offset. [scope](scope.md): turret units, tracking factors, turret wording.
- [ammo](ammo.md): the ammo's name, lot and load recipe. [bullet](bullet.md) and [bullet shape](bullet-shape.md): the bullet. [velocity](velocity.md): muzzle velocity and chronograph strings.
- [drag](drag.md): the ballistic coefficient and the fitted drag correction. [spin](spin.md): the Spin model's coefficient set. [model](model.md): which model solves.
- [calibration](calibration.md) and [calibration goal](calibration-goal.md): what a fit saved and what guided calibration aims at. [setup](setup.md): which sections of the profile screen were looked at. [origin](origin.md): where a profile came from and the copy of a shared one.

## Who writes it

**The app.** Each "Save this section" and "Looks right" on the profile screen, each fit or direct calibration on the Calibrate screen, and setting or clearing the calibration goal are saves through one function, `saveProfileEdit`. It reads the highest number the profile has in the device's store and adds the next one in one transaction, with an add that cannot overwrite, so a version pulled meanwhile is never lost. It copies the previous version and applies the change; it drops the mark that the numbers were read from another app and keeps the mark of a copy (see [origin](origin.md)). "New profile", the sample profiles, an invite and the first-run profiles write version 1 directly, without the save check. The Calibrate screen's "Use this one again" is also a save: it copies an old version's calibrated values onto the current rifle as a new version. It does not bring the old version back. Restoring a backup file replaces the device's versions with the file's and sends them all; the server keeps what it has for a number already taken, and a profile deleted on the server stays deleted.

**The Versions section.** "Restore this version" on an old version (see "Changing it") is a save through `saveProfileEdit`, like any other, numbered above the highest version.

**The fitter.** A fit builds the new calibration and drag values and the app or connector saves them as a version; the fit's record keeps the number of the version it started from. Which fields a fit sets is on [calibration](calibration.md).

**The connector.** `edit_profile` saves the next version of the profile it names (highest live version it lists, plus 1); `create_profile` makes version 1 of a new profile; `calibrate_profile` saves a fit and `tall_target` a tracking factor, each as the next version; `copy_shared_profile` makes version 1 of a copy. All five need the `profiles` scope. `delete_profile` is not a writer: it asks (see "Changing it").

**The server.** It never makes a version. On every push it sets `owner_id`, checks that `version` is a whole number from 1, and, when the number is taken by a different save, renumbers (see "Changing it").

**Copies and restores.** The copy of a shared profile and the restore of a deleted one write a profile without the profile screen's save check. The restore is described under "Changing it", the copy on [origin](origin.md).

## Who reads it

**The newest version is the profile.** The app and the connector pick it differently, and can disagree. The Profiles list, the Calculator, the profile screen, the Session screen and the Calibrate screen take the highest version a profile has; the profile counts as deleted if that version is. The connector's `list_profiles`, its choice of a profile (`pickProfile`) and the version list `calibrate_profile` reads take the highest version that is not deleted. `get_profile` lists every live version, oldest first, and answers "no profile" if none is live. The two agree unless a deleted version stands above a live one; the app then shows the profile as deleted while the connector lists its live version.

**The Versions section shows every version.** Newest first, each with its date and up to four lines of what changed from the version before (for example "Muzzle velocity" from 3010 to 3048), from `versionChanges`, which covers every value a section edits. A coefficient set is named only as "set" or "none", never by its id. A version opens read-only at `#/profiles/<id>/<section>?v=<n>`, with every step section shown as it was; a number with no live version says so.

**The Calculator warns when the newest version changes under it.** If a sync brings a newer version of the profile on screen, it says "Profile updated from another device: now v3 (MV 3048). Check before you dial." until the person dismisses it.

**The connector finds a profile by id or by name.** An exact id, the stored name (any case, outer spaces ignored), or for a copied profile the name its replies show. If more than one profile fits, nothing is chosen: "More than one profile has that name. Use the id". With no profile named it uses the profile in the most recent calculator report from any of the account's devices, however old the report, and the only profile when no report names one; if the reported profile is no longer listed it refuses with "Which profile?". A write (`edit_profile`, `log_group`) on an account with several profiles uses the report only when it is under 30 minutes old, and otherwise, or when its age is unknown, asks "Which profile?" with the list (since 2026-10-15). `get_profile` needs the profile named, and `delete_profile` has its own rule: an id or a name, an exact id before a name, and no default (the overview's "Which profile"). A stored name or note in a spoken line is quoted and cut to 40 characters unless it is a short code (the overview's "How a reply is shaped"); what a tool does with a name given in the call is on its page. The details below the line are not changed.

**The fitter.** A fit is of the newest live version. For each group it looks up the version the group was shot with among the live versions and, in the app since 2026-10-24, the deleted versions a live group names; one that is missing is left out with "profile version N is not on this device" (see [observation](../log/observation.md)).

**The server's admin export.** The admins' pooled export lists each group's `profile_id`, `profile_version` and `profile_name`.

## Why it is stored

A logged group is only worth a fit if it can be solved again exactly as it was shot. So a version never changes, and a group names the profile and version it was shot with. A save that needed to change an old version would silently change the answer for every group that names it.

The `id` is apart from the number because two devices that save offline both pick "next". The number alone cannot tell their two saves apart; the id can, on the device.

A delete is a marked copy, not a removal, so it reaches the account's other devices by sync, and so a device that did not see it cannot bring the profile back by sending an old version.

## Changing it

**A save.** Any writer saves the profile as a new version one above the highest. Name and notes belong to no edit category, so changing only them keeps the calibration without asking. For every other field the category and its keep-or-reset default are on the field's own page. Whatever the category, old groups stay in the log; whether a fit still uses them is each page's "Older groups" part.

**Two devices, one number.** Two devices that both save "version 4" offline each have a different `id`. The server compares the content, not the id: two saves with the same content count as one. The first to reach the server keeps 4. The server stores the other as the highest number plus 1 and tells the device. A repeat of the same push gets the same new number, never another one. The device then moves its own version and the groups it sent, or still has to send, to the new number; the Calculator may show the "Profile updated" notice. A device that pulls the other version before it has pushed gives the pulled one the number and moves its own unsent version up itself, with its unsent groups. The same save sent twice is accepted and changes nothing.

**A delete.** The Delete profile button (and the approval below) first stops the profile's running shares on the server, if signed in; if the server cannot be reached the person is told so, and this step stops nothing. Then it marks every live version this device holds deleted at once, or none. A version another device saved meanwhile is not covered: when it syncs, the profile is live again. The only live profile cannot be deleted. Logged groups are untouched. The server stops a profile's shares, and cancels its open suggestions, when a push leaves it no live version. A fresh install's sample profiles are not added again after a delete.

**A delete sticks.** If a device that has not seen the delete sends the live version again, the server answers that it is stale and the device takes the deleted copy. The server does not compare times for this.

**Restore.** Profiles, Show deleted, Restore saves the last content as a new live version one above the highest number, without the profile screen's save check. The older versions stay deleted. Shares that were stopped do not restart. The restore keeps the fitted values. The old groups still name the deleted versions. The app's fits read those versions (since 2026-10-24), as a shared view and the admins' pooled fit do, and so do the connector's `calibrate_profile` and `next_target` (since 2026-10-25: they are given the deleted versions a live group names).

**Restoring an older version.** Opened from the Versions section, an old version offers "Restore this version". The app first lists what will change against the newest version, and says how many groups were logged since that version ("They keep the version they were logged with"). It refuses a version that would not pass the save check, and names why. On confirm it saves the old version's name, notes, rifle, scope, ammo, model, calibration and calibration goal, its confirmed come-ups and its "don't know yet" list, as the next version; a key the old version lacks is absent from the new one. The looked-at sections are the union of both, so a restore never makes a section "not looked at" again; where the profile came from (`copied_from`, `source`) stays the newest version's. If another version arrived since the list was shown, it shows the list again against that version and asks again. A restore that would change nothing (the old content is already the newest's) saves no version and says so. A restored calibration is the old record exactly, fitted against the old version's groups. The connector's `restore_version` is for themes and layouts only; it cannot restore a profile version.

**Asking for a delete from the connector.** `delete_profile` takes an id or a name (any letter case), and refuses an unclear name, and refuses the only profile. It sends a question to every open device, and refuses if none is open or none shows the question. The question lapses after two minutes. Each device builds the question from its own records; the profile is deleted there only if the person taps "Delete it". A question already showing is not replaced by another. The tool never sends a deletion, so no text in a profile can cause one.

## Sharing and privacy

A shared log shows every version of the shared profile that is not deleted, and any deleted version that a shown group was shot with. Its Profile tab's Versions section lists the live ones with what changed, as text: a reader cannot open one by number or restore it. Each audience sees `name` as the owner has it: hiding notes does not hide the name. `notes` is emptied when the share hides notes, which a public share does by default. `id`, `profile_id` and `key` are replaced by ids that are the same on every load of one share and different on every other share; `owner_id` is null. `version`, `created_at`, `updated_at` and `deleted` are as stored.

What a reader can work out: how many times the shooter saved the profile and when each save was (the `created_at` of each version), which saves carried a fitted calibration, and when a version was deleted. A name that holds a real name, a place or a date is shown to everyone the share reaches. It is also the profile's name on the sharer's own share list, on each reader's Shared with me list, on the list of suggestions a helper has sent (as it was when sent), and in the admins' pool and export. The Community page does not show it.

---

[All reference pages](../index.md)
