Effective date: August 29, 2026 Last updated: 2026-10-10
WorkoutSmith is a training app built with privacy in mind. It is designed so that your private information (your workouts, steps, heart rate, sleep and the rest of your health data) stays on your device, or in Apple Health if you turn that on. Some features you use with other people, such as competing, sharing and racing, need part of that data to leave your device, and each one is described below. That includes location: routes, race check-ins and race photos involve where you are, and the “Location” section explains exactly how.
The rest of this document is the detail behind those sentences.
This policy covers the WorkoutSmith iPhone app (bundle id com.mig1278.aiworkout), its Apple Watch app, and its home-screen widget.
WorkoutSmith is published by Michael Lee, an individual developer based in Massachusetts, USA. For privacy questions or requests, use Send feedback in the app (Settings > Feedback), or email workoutsmithgeneralcontact@gmail.com if you no longer have the app.
Your data can be in three places, and the differences between them matter.
1. On your devices. This is where almost everything is kept: your profile, programs, every workout and set you have logged, your chat with the coach, your settings, XP and achievements, your groups, your saved home coordinate, and the app’s diagnostic logs. If you use the Apple Watch app, the watch keeps the workout in progress and a short diagnostic log until it can hand them to your iPhone. The app does not sync any of this to iCloud and has no CloudKit configuration. If you restore a new phone from a backup, your data comes back through Apple’s backup, not through us.
2. On our server. This is a database and private file storage in Amazon Web Services (US East, Northern Virginia). It holds what the features you use with other people need, and “What we store on our server” lists it. It does not hold your workout history, programs, coach chat, sleep, heart rate, weight or home location, except for specific parts you choose to share (a program you send to a friend, or workouts you share with a coach).
3. With other companies, under your own accounts or through Apple. If you use the AI coach, your requests go to Google (Gemini) or Anthropic (Claude) on your own API key. If you connect Google Sheets, your device talks to Google directly under your own Google account. Apple handles Sign in with Apple, App Store purchases, Apple Health, notifications and the map services. Each of those companies’ own privacy policies govern what they do with what they receive.
The following are stored on your device and are not sent to our server, to the AI coach, or anywhere else, except where an item says so:
Apple Health is optional. The app works without it. You grant access from Settings one category at a time, and you can revoke it whenever you like in the iOS Health app.
What the app reads. With your permission it reads your workouts (including swimming distance recorded on a workout), step count, walking and running distance, cycling distance, heart rate, body weight, and resting heart rate, which sets your heart-rate zones. Your sleep, heart rate variability, walking heart rate, VO2 max, blood oxygen, breathing rate and sleeping wrist temperature are used only if you turn on Heart rate and recovery (Settings > Health & Fitness; it is off until you do), to show your overnight and cardio trends on your iPhone. Each set’s heart rate (see “What stays on your device”) is also worked out only while Heart rate and recovery is on. It reads workouts from every source, so workouts recorded by an Apple Watch or another app are included. How far back a read reaches is limited by what you allow Apple Health to share; on newer versions of iOS, Apple itself asks you to choose between sharing only your recent history or all of it, and the app works within whatever you choose. Two of these reads treat that history differently:
What the app writes.
Background access. Unless you turn it on, the app reads Health only while you have it open. If you turn on Update my boards in the background (Settings > Health & Fitness; off until you turn it on), Apple Health can wake the app when you have new steps, distance or runs. The app then sends the same daily totals it would send if you opened it, and only for metrics you have opted in as described under (b) below. It uses only the Health access you have already granted and asks for nothing new. Turning it off, or disconnecting Apple Health, stops it.
What leaves your device. Health data leaves your device only when you use one of these features:
a) The AI coach. If you have turned on AI data sharing, the coach’s context includes today’s step count, an average workout heart rate and your Health-recorded body weight. They travel the way every coach request does (see “How a request travels”), and none of them is sent if AI data sharing is off. If you wear an Apple Watch running the WorkoutSmith watch app during a workout, it also includes your heart rate at that moment and which heart-rate zone you are in. That live reading is held in memory for the session only. It is not saved to the app’s database or sent to our server.
b) Competing or sharing an activity metric. The app uploads daily totals only for metrics you have opted in, and there are three ways to opt one in:
If you have done none of these for a metric, the app does not even read it. Setting it back to Private, leaving the league, or the challenge ending stops the uploads. The metrics that come from Health are steps, walking and running distance, running distance and running time. Uploads cover a short rolling window of recent days, so our server keeps a daily record of those totals for as long as you have an account.
c) Live status. If you turn Live on, your friends can see which of a small set of effort bands you are training in. The band is worked out from your heart rate on your device. Only the band is sent, never the heart rate itself.
No current setting sends your sleep, resting heart rate, heart rate variability, overnight readings (breathing rate, wrist temperature and blood oxygen), VO2 max, walking heart rate, each set’s heart rate, cycling distance or lifetime journey mileage anywhere.
The coach is optional. Logging, the program builder, progression, Adventure and Social all work with no AI configured.
Your key, your account. You supply your own API key from Google AI Studio (Gemini) or Anthropic (Claude). It is stored in your device’s Keychain, set so that it does not sync to iCloud Keychain and is not included in backups. It is never written to a log or stored on our server. It is used only as the credential on your own coach requests. We do not run a coach on our own account, and the relay refuses any request that arrives without your key.
How a request travels. A request makes two hops. First, your message and its context, including any Health data listed below, go over HTTPS from your device to a small relay we run on AWS. The relay holds them, and your key, only in memory for that one request, and it keeps no conversation history. Second, the relay adds the coaching instructions and forwards the request to Google or Anthropic using your key. What Google or Anthropic keeps, and for how long, is set by their terms for your own API account.
What the relay logs. It never logs your messages or the coach’s replies, not even an excerpt. When something goes wrong, it logs the shape of the failure: how long a reply was, which check it failed, and a random id for that request. If it turns a request away for going over a usage limit while you are signed in, the log line includes your account id. The relay’s logs are deleted after 14 days. For rate limiting, it keeps counters keyed to your account id if you are signed in, or to your IP address if you are not. The counters expire automatically once their window closes, whether that window is a minute or a day.
What the coach is sent. Each message carries a context block drawn from these categories:
| Your profile | Your goals, fitness level, experience and injury notes, as you wrote them, and which limb you are resting, if you turned on Training one side |
| Your preferences | Coach tone, reply length and training style |
| Your program | Program name, length, days per week, and session names and muscle groups |
| A program request | When you ask the coach to write a program: your equipment, where you train, your schedule and what should happen to a training day you miss, focus areas, whether you wear a heart-rate monitor, working weights from your logged sets and any lifts you have entered, and which limb you are resting, if you turned on Training one side |
| Today and recent training | Today’s session with its targets; a handful of recent workouts with the reps and weights you logged, the distances and times of cardio, reps in reserve and each side’s result; weekly volume by muscle group and similar summaries worked out from your logs |
| A workout in progress | The current exercise, set and what you have logged so far. If you are wearing the watch app, also your heart rate at that moment and your heart-rate zone |
| Health | Today’s step count, an average workout heart rate, and your Health-recorded body weight |
| Fitness test results | Only if you build a plan from the fitness test: your test scores (such as plank, push-up, pulling and walk results), estimated aerobic fitness, heart-rate recovery and your heart rate at the end of the walk |
| Fatigue and milestones | A plain-text fatigue summary and any deload recommendation, level-ups, and the outcome of anything the coach logged for you |
| Where a program came from | A marker when a plan came from a friend or a spreadsheet, so the coach treats that text as a label and never as an instruction. It never carries the other person’s name or any detail about the file |
| What the app has learned about you | A few sentences summarising patterns in your logged workouts |
Your conversation so far in the current chat is sent with each turn.
What the coach is never sent. None of the following leaves your device for the AI coach: your name, email, user id or device id; your age, height or biological sex; your home address or location; your friends, standings or trophies; your raw sleep, resting heart rate, HRV or other overnight readings, VO2 max, walking heart rate or each set’s heart rate; your hand-entered bodyweight.
A caution. The profile fields, especially injuries, are free text you write, and whatever you type there goes to your AI provider. If you record a medical detail there, it is part of your coach requests.
Voice. Speech is transcribed by Apple’s on-device speech recognition where your device supports it. Only the text is sent to the coach. Audio never reaches our servers.
Your controls. Nothing goes to the coach until you have accepted the disclaimer and turned on AI data sharing. Turning that off stops all coach requests. The coach speaks first during a workout, for example as a rest period ends, only if you also turn on Let the coach speak first in Settings, which is off until you do. Removing your key turns the coach off completely.
This section describes how the app uses location today. If a future version uses location differently, this policy will say so before that version is released (see “Features we may add, and changes to this policy”).
What is true everywhere in the app today. The app does not track your location continuously or in the background. It asks iOS only for “while using the app” permission, not “always”, and it reads your location only when you tap something that needs it. Each read is a single position fix, not an ongoing session. Saying no costs you nothing outside the features that need location. You can set a home by typing an address, move the map by typing, and place route stops by address or by tapping the map.
Some of these reads are kept nowhere. Others become part of something you share with other people, and those do reach our server. Here is each one.
Setting a home is optional. You type an address or a town; the app does not detect it. Apple’s system geocoder converts the text to a coordinate, which means the text you type is sent to Apple, just as it is when you search in Maps. Only the latitude and longitude are saved, never the address text, and they stay on your device. The coordinate is used only to measure your progress along a journey. It is not included in coach requests, the widget, the data export or anything sent to our server.
The Adventure map has a button that centres the map on where you are. It reads your location once per tap and keeps the result only while the map is on screen. It is not saved, sent to our server, given to the coach, or included in an export. The first tap shows the standard iOS permission prompt.
When you build a route, each stop is a coordinate. You can place a stop by typing an address, by picking a point on the map, or by tapping use where I am, which takes a single position fix at that moment. A route’s stops, with their coordinates, names and notes, are saved on your device, and they are sent to our server when you share the route or use it for a race. That is what makes it possible for other people to load it.
So a stop placed with “use where I am” records where you were when you placed it. The people you share a route or race with can see its stops on a map. A route’s first stop is often where its author started, so if you begin a route at your front door, your front door is on the route. Place that stop somewhere else if you would rather it was not.
Reaching a stop in a race is a check-in. Your phone takes a single position fix and compares it with the stop on your device. It then sends which stop, when you reached it, and whether your phone’s fix agreed. It does not send your coordinates, the fix’s accuracy, or your distance from the stop, and our server refuses a check-in that includes them.
What other people in the race can work out. They can see the route, and they can see that you reached stop four at 9:52. Together, that tells them which points on the course you reached and at what time. That is how a race board works. It is not a background track of your day, but it is location information about you, and you should know that before you race.
Race photos are not switched on yet. When they are, this is how their location is handled. A race photo’s embedded data usually includes the time it was taken and often where. Your phone reads that on the device, compares the location with the course, and sends only the result (on the course, off the course, or no location) together with the time the photo was taken. The embedded data is then removed before the photo leaves your phone. Our server never receives the photo’s coordinates. See “Photographs taken during a race”.
All of these go to Apple as system services, as they do in Maps. None of them goes to our server, and none carries your account or name.
If you never sign in, our server has no account record for you. The only things it can hold from you are feedback you send and short-lived rate-limit counters.
Signing in. Sign in with Apple gives us a stable identifier that Apple creates for this app, and that identifier is your account. Your display name is the name friends see. It starts as the name Apple shares when you first sign in, if any, and the name in your profile in the app is how you change it: when you are signed in, saving it there saves it on our server.
Your email address. Apple also sends an email address when you sign in. We do not store it. We store a keyed fingerprint of it instead: the address run through a one-way function with a secret key that is kept outside the database. The fingerprint cannot be turned back into your address, and we have no way to email you. It exists so that a friend who already knows your address can send you a friend request.
Friend requests by email. When you send one, the address you typed is fingerprinted the same way and compared with our records.
Your account record contains an internal id, the Apple identifier, the email fingerprint, your display name, your public handle if you set one, and your avatar id. Once you sign in, it also holds a token Apple provides so that deleting your account can end the app’s Sign in with Apple access. It is used for nothing else, and it is deleted with your account. Avatars are not photos: they are one of a limited set of preset icon-and-colour combinations.
What else is stored, and why.
| Sessions | Your sign-in sessions: a hash of the refresh token (not the token itself), plus the IP address and device user-agent of the sign-in. Kept until the session expires, at most 90 days |
| Sharing settings | Which metrics you share and with whom, and your Live setting |
| Live status | Only while you are training with Live on: that you are training, the kind of activity, and one of a small set of effort bands. It does not include your heart rate or your location. It expires a short time after your last logged set and is deleted as soon as you turn Live off |
| Friends | Your friends, unused invite codes, pending friend requests, and email fingerprints for requests to addresses with no account yet (kept 30 days) |
| Challenges and leagues | The challenges you are in (format, metric, goal and your standing), and your league division, weekly score, rank and outcome |
| Cheers | In a challenge: which of our fixed set of cheers you sent, and to whom. Between friends: the emote is not stored. The recipient’s activity feed records that you cheered them, and a marker records that today’s bonus has been used. There is no free-text messaging between users anywhere in this app |
| Metric values | The activity numbers for the metrics you compete on or share, as individual entries and daily totals |
| Daily goals | The daily step goal you set. It is used to work out your own percentage on the friends board and, if you join the Active days league, which of your days count. The goal itself is shown to nobody else |
| Trophies | Placement, medal, metric, score, and your display name at the time |
| Shared groups | Only groups made with an earlier version of the app, when groups were shared: the group name, its members and owner, and whether members could invite others. Nobody can join one any more, and the owner can still rename or delete it. Groups you make now are private lists on your iPhone and are not stored here |
| Programs and workouts you share | A program you send to a friend; if you turn on share-back for a program you adopted, a summary of how your sessions went; and, if you share your workouts with a coach, a summary of each workout you finish (see “Who can see what”) |
| Routes and races | Routes you build (name, stops with their coordinates, names, short notes and links, and distances) and the races run on them (name, activity, schedule, settings, who is racing and who invited whom). Each check-in records which stop you reached, when, and whether your phone’s fix agreed. Check-ins carry no coordinate: the only coordinates on our server are the route’s stops |
| Route photographs | Pictures you attach to a route stop. Kept in private cloud storage, with embedded location and camera data removed on your device and refused by the server if any is left. Screened automatically, as described under “Photographs taken during a race”. Shown through short-lived links to the people you share the route or race with, and deleted after you delete the route |
| Race photographs | Not switched on yet. When available: pictures added during a race, kept in the same private storage with embedded data removed. With each one we keep which stop it belongs to, the time it was taken, whether your device judged it on the course, and who you chose to show it to. Deleted six months after the race ends |
| Passport stamps | Which landmarks your walking and running distance has reached, and when. Landmarks are milestones from a fixed catalogue built into the app. No coordinate of yours is stored |
| Notification devices | Only if you turned on notifications about other people: a device token from Apple for each phone you use. It addresses notifications to that phone and identifies nothing else. Deleted when you turn notifications off, sign out, or delete your account, and 90 days after you last opened the app |
| Notification settings | Which kinds of notification you want, and your device’s UTC offset, so that nothing arrives in the middle of your night |
| Reports, blocks and moderation | Reports about photographs and other content, your block list, an organizer’s own ban list, records of race removals, and records of account suspensions. Each is described under “Races” and “If an account is suspended” |
| Apple purchase link | If you are signed in on iOS 18.4 or later, the identifier Apple assigns to your copy of the app (the app transaction id), linked to your account, so that a subscription can be matched to your account. See “Your subscription” |
| Feedback | Anything you send through the feedback form, including a diagnostic log if you choose to attach one (see “Feedback you send us”) |
| Security records | A log of security-relevant actions on your account, such as sign-ins and friend changes, with your account id, IP address and user-agent. Kept 90 days. A few records (an account deletion, a moderation decision, a preserved photo) are kept longer without the IP address or user-agent |
| Rate-limit counters | Short-lived counters keyed to your account id or IP address, used to stop abuse. They expire within two days |
What is not on our server. No setting uploads your coach conversations, sleep, resting heart rate, HRV, each set’s heart rate, body weight, injuries, training goals, birth year, groups or home coordinate. Your workouts and programs are uploaded only when you choose to send a program to a friend or share your workouts with a coach. Nothing on this list leaves your device by default.
Workout reminders are local. You choose the days and time in Settings, your phone schedules them, and nothing about them is sent anywhere.
Notifications about other people are opt-in and come from our server. These tell you someone invited you to a challenge or race, asked to be friends, shared a plan with you, cheered you on, or that a challenge has finished. They are off until you turn them on, and the app asks only after something has happened that one of them would have been about.
You can give one specific friend permission to see the workouts you finish, which is what a personal trainer and client want.
A race is a route, a schedule, a roster, and the check-ins people make along the way. Check-ins are described under “Location”.
Race photographs are not yet switched on for everyone. When they are available, this is how they work.
Who can add one, and when. Only someone on the race’s roster, while the race is running or for a short time after it ends. Anyone who can see a photo can download it.
Your phone checks the photo before it sends anything. It reads the photo’s embedded time and location on your device, compares the location with the course, and sends only the verdict: on the course, off the course, or no location. It then removes all embedded data. Our server never receives the photo’s coordinates, and it rejects an upload that tries to include them. The time the photo was taken is sent, and the server checks that it falls within the race. A photo with no location is not treated as suspicious. It goes to the organizer to approve, and if the organizer does not review photos, it is refused.
Every photo is screened automatically, including the organizer’s, by an automated classifier from Amazon Web Services that checks for nudity, violence and similar content. Route stop photos are screened the same way. A photo that fails screening is never stored. It is checked in memory, nobody (including us) ever sees it, and there is no appeal queue. We keep only a note of the broad category, whose upload it was, when, and how many times that account has had a photo blocked. The organizer is told that it happened, but never sees the image.
You choose who sees your photo: your friends in that race (the default) or every attendee. If the organizer turned on review, your photo is visible only to you and the organizer until they approve it, and it is deleted after two weeks if nobody does. If an organizer removes you from a race, the photos you added to it are deleted.
Reporting. Anyone who can see a photo can report it by choosing one of a small set of broad reasons. There is nowhere to type. A reported photo is hidden from everyone immediately until the organizer decides whether to restore it or take it down. The organizer is never told who reported it. While a report is open, the photo’s file is kept, hidden from everyone, even if a deletion schedule would otherwise remove it. That is because the law can require content connected to a report to be preserved. This is the one exception to the photo-deletion promises on this page.
Blocking. You can block someone from their photo in a race, and unblock them in Settings > Blocked people. Blocking works in both directions and everywhere: each of you stops seeing the other’s photos, cheers, shared programs, shared workouts, routes and friend requests, any workout sharing between you ends and what was shared is deleted, and a friendship ends (unblocking does not restore it). On leaderboards and rosters, the blocked person appears as “Racer X”, with no name, no avatar of their own and no account id, but with scores intact, so rankings stay true. Nobody is told they have been blocked.
Retention. Photos are deleted six months after the race ends. If you want to keep a picture, download it within that time. Race results (who ran, who reached which stop, finishing order and standings) are kept as long as your account, so your history and trophies stay accurate.
What screening does not do. The automated screener does not detect child sexual abuse material, and nothing here claims that it does. If you ever see anything of that kind in this app, report it to the National Center for Missing and Exploited Children at report.cybertip.org, and do not download, screenshot or forward it.
An organizer can remove anyone from their race without giving a reason. Removing someone also bars them from rejoining that race. The organizer has a list of the people they have barred and the races each one is kept out of. That list is visible only to them, and it is deleted when either account is deleted. Bars set before October 2026 may still cover every race that organizer runs.
We keep a minimal record of each removal: the Apple identifier of the removed account, the race, the organizer and the time. It has no reason and no content. Removals by several different organizers can get an account banned from WorkoutSmith. Repeated removals by the same organizer count once. This rule is also in the community rules you accept in the app.
This record outlives account deletion, because a record that disappears when you delete your account and sign up again would protect nobody.
We can suspend an account from WorkoutSmith, for example when a report of objectionable content or abuse turns out to be right. A suspended account cannot sign in, and any signed-in device stops working immediately. Its race photos are taken down, except that the organizer of the race can still see them and delete them. A suspension does not remove the account’s name from past rosters or leaderboards, because those record a competition other people were in.
We keep a record of the suspension: the Apple identifier, the account id and handle at the time, a short internal note on why, who decided, and when. It holds nothing about what was posted. It outlives account deletion, so that deleting and signing up again does not undo it.
A suspension can be lifted. If you think yours is a mistake, the in-app feedback form still reaches us from a suspended account, or you can email the address at the end of this page.
Anyone can report something that is not a photo, such as a shared plan or route, a workout, a friend request, or the name on a challenge, group, race or person, by choosing one of the same small set of broad reasons. We store what kind of thing it was, which one, who reported it, whose it was, the reason, when, and what was decided. We also keep a short copy of the reported text, so the report still makes sense if the content is later changed or deleted. When reports need attention, the developer receives a shorter excerpt by email alert. Nobody is ever shown who made a report, including the person reported and whoever reviews it. These reports outlive both accounts, for the same reason as removal records.
The in-app feedback form sends a category, your message, and a screenshot if you choose to attach one. It also sends technical details that the form lists before you send: app version and build, device model, iOS version, the screen you were on and the last few screens you visited, your locale and timezone, free disk space, uptime, and whether your AI key passed its checks (never the key itself).
Exercise requests. If the exercise you want is not in the app, you can request it from the exercise picker. A request is sent through the same form, as feedback of its own type, with the exercise’s name and any details you choose to add, such as a YouTube link, the muscles it works, the equipment it needs and how it is measured. It is stored and kept like any other feedback.
If you are offline. A report that cannot be sent straight away, with any screenshot or log you attached, is saved on your device, left out of device backups, and sent automatically when you are back online. It is sent once, however many tries it takes, and “Delete all data” removes anything still waiting.
You can send feedback anonymously. Our server makes that decision, not the app: no account id or name is written to an anonymous report.
Where it goes. Feedback, including any screenshot, is stored in our database and is never shown to other users. For triage, it is also copied to a private spreadsheet that only the developer can access, and screenshots are copied to a private Google Drive folder linked from it. Neither is shared by link or public. When you delete your account, your name and any screenshot are removed from our database straight away, and from the spreadsheet and the folder within 30 days.
How long we keep it. Feedback, including any screenshot or diagnostic log attached to it, is kept for 12 months after you send it and then deleted, from our database and from the triage copies.
The app keeps technical logs on your device to help find and fix problems. They are designed to leave out personal information, they stay on your device unless you send them, and sending them is always optional.
What they contain. Records of what the app did rather than what you entered, for example:
What they are designed to exclude. Names, handles, email addresses, passwords and sign-in tokens, health readings such as heart rate and body weight, coordinates, and anything you typed, including coach messages. Logs are filtered automatically to strip these.
How much and how long.
When a log leaves your device. Only if you choose it:
An attached log is stored with your feedback and copied to the triage spreadsheet. It stays with the feedback after you delete your account, and is deleted with the feedback 12 months after you sent it. If you send feedback anonymously and attach the detailed log, the log’s scrambled account and transaction ids could let us link the report to your account, so leave the log off if that matters to you.
WorkoutSmith offers two monthly subscriptions in the app, WorkoutSmith Premium and WorkoutSmith Coach. Both renew automatically, and neither can be shared through Family Sharing. The only difference is how many people you can coach: a small number with Premium, with no limit on Coach. A subscription unlocks starting a workout, building a program, races and leagues. Your history and progress, the AI coach and voice trainer, Adventure, your steps, streak, journey, map and your own routes, your friends and groups, challenges (joining one, starting one and inviting friends to one), and everything in Settings (including data export and account deletion) stay available whether or not you subscribe.
Apple takes the payment; we never see a card. Purchases go entirely through Apple’s in-app purchase system. No payment processor is built into the app, and no card number, billing address or payment credential is entered into, held by, or sent by WorkoutSmith. You manage or cancel the subscription in your Apple account settings. Apple’s privacy policy covers what Apple does with the transaction.
What we receive today.
The free period is Apple’s to grant. Apple decides whether you are eligible for an introductory offer, based on your Apple ID. The app is told only whether your current subscription came from an introductory offer, so that it can show how many days are left.
What is planned. We are preparing to have our server keep your subscription status, received directly from Apple, so that a subscription you paid for is recognised even when your device cannot confirm it. This is not switched on yet. Before it is, this section will be updated to describe exactly what is stored and for how long (see “Features we may add, and changes to this policy”).
This is an optional feature, off until you set it up. It works with files in your own Google Drive, under your own Google account, and it does three things:
You sign in to Google, not to us. You give consent on Google’s own page, in a private browser session, and we never see your Google password. The spreadsheet is created in your Drive, uses your storage, and is your file.
Narrow permissions. The app asks Google for access only to files it creates (Google’s drive.file permission), plus your Google account’s email address so it can show which account you connected. It cannot see or open anything else in your Drive, and it can import only spreadsheets it created itself.
What goes out. A history sheet has one row per set you logged: date, program, week and session, whether the session was completed, exercise and its prescription, set number, whether it was a warm-up, target and actual reps, weight, duration, distance, reps in reserve, and whether the set was completed. A program sheet, whether it started as a blank template or was exported from a program you already had, contains the plan only. Neither sheet includes your body weight, height, birth year, home location, anything from Apple Health, measured heart rate, notes you typed, or coach conversations. The heart-rate zone on a program sheet is a planned target, not a reading.
What comes back. You can import a program, whether it started as a blank template or was exported from one you already had. You cannot import results: a history sheet is refused. Imported text is cleaned, exercise names must match the app’s catalogue, numbers are range-checked, and a program that came from a file is marked as untrusted before the AI coach sees it. The app does not record a link, file name or Google account with it.
Our server is not involved. Sign-in and every request to Google happen directly between your device and Google. Our server never receives the spreadsheet, its contents, its id, or your Google token. The app keeps your Google email address on your device, with your Google tokens, only to show which account is connected. It is not sent to our server, and disconnecting removes it. We hold no Google email address, name or account id.
Disconnecting. Your Google token is stored in the Keychain, set not to sync to iCloud and not included in backups. Disconnecting erases it and asks Google to revoke it; if the revoke fails, you can remove the app in your Google account. Disconnecting does not delete spreadsheets you already exported. They are your files.
This is how WorkoutSmith works today. If any of it changes, we will update this policy before the version that changes it is released.
Settings has an “Export workout history” option that writes your profile, programs and full workout history (every set, rep and weight, how long each set took and the rest after it, and, if Heart rate and recovery is on, each set’s heart rate) to a JSON file you can save or share. It does not include your chat history, body weight, birth year or home location. The file is written to a temporary folder on your device, which iOS clears periodically.
Your programs and history can also go to a Google spreadsheet (see above), and you can export your diagnostic log from Settings > Diagnostic mode.
To get a copy of the data our server holds about you, email workoutsmithgeneralcontact@gmail.com from the address associated with your account, and we will provide it within 30 days.
There are two separate controls, because they do different things.
“Delete all data” (Settings) returns your device to the state of a fresh install. It removes:
It also deletes the workouts this app wrote to Apple Health, leaving other apps’ workouts alone.
It does not clear the diagnostic logs, which age out on their own within 7 days. You can clear them immediately in Settings > Diagnostic mode.
It does not delete your account on our server. It signs you out on this device, but your account survives, and signing in again brings it back.
“Delete account” (Settings) deletes your account on our server. Your on-device workouts are not affected. It removes your profile, handle, Apple identifier and email fingerprint records, sessions, sharing settings, friendships, invite codes, friend requests, metric values and daily totals, league and challenge participation, activity feed, the routes you shared, your places on race rosters with your check-ins and race photos, your shared programs and workouts, and your block list. It also asks Apple to end this app’s Sign in with Apple access, so the app no longer appears under Sign in with Apple in your Apple ID settings. If Apple cannot be reached, your account is deleted anyway. Challenges you created are removed with their rosters and cheers.
What survives account deletion, and why.
Records that survive do so because a record you could clear by deleting your account and signing up again would protect nobody. Security records otherwise expire after 90 days.
Deleting the app removes everything on the device, including the Keychain items. It does not delete your server account, so use “Delete account” first if you want both.
All traffic between the app and our servers uses HTTPS. Sign-in tokens, your AI key, and your Google tokens are stored in the iOS Keychain, set not to sync to iCloud and not to be included in backups. Our database stores a hash of your refresh token, not the token itself. Photos are stored in private, encrypted storage and served through short-lived links. Handles are cleaned of invisible and direction-reversing characters so nobody can impersonate someone on a leaderboard.
No system is perfectly secure, and this is a small independent app rather than a company with a security team. The design compensates by keeping your most sensitive data, your health data and training history, on your own device, where a breach of our server cannot reach it.
The server database keeps encrypted backups for up to 35 days for disaster recovery. Data you delete leaves those backups as they age out.
WorkoutSmith is not directed to children and is not intended for anyone under 13. We do not knowingly collect personal information from children under 13.
This policy describes what WorkoutSmith does today. We keep improving the app, and some features we are considering would use data in new ways. For example:
None of these is active today. If we add one of them, or any other feature that collects a new kind of data or uses data for a new purpose, we commit to the following:
When this policy changes in a meaningful way, the “Last updated” date changes and the new version is posted at the same address. We do not hold your email address, so we cannot email you about changes; please check this page.
Privacy and deletion requests: use Send feedback in the app (Settings > Feedback), or email workoutsmithgeneralcontact@gmail.com if you no longer have the app.
This policy is governed by the laws of the Commonwealth of Massachusetts, USA.
Every path is relative to the root of the app’s source repository. A bare file name or short path means the app’s file of that name. Line numbers were re-checked against the code on 2026-09-26. Entries added or changed for version 1.3 were checked against the 1.3 release candidate on 2026-10-09 and cite files and symbols rather than line numbers.
ios/AIWorkout/Core/Health/HealthStore.swift (readTypes(for:)). Heart rate and recovery adds VO2 max, walking heart rate average, respiratory rate, sleeping wrist temperature and oxygen saturation, requested only by its own Turn on button and read only while it is on: HealthStore.swift (recoveryReadTypes, vitalType), ios/AIWorkout/Features/Recovery/RecoveryDataSource.swift (RecoveryGate, HealthRecoverySource); kept in recovery.json, excluded from backup: ios/AIWorkout/Features/Recovery/RecoveryStore.swiftios/AIWorkout/Core/Health/HealthStore.swift:305HealthStore.swift (shareTypes); the phone mirrors a workout with activeEnergyKcal: nil: ios/AIWorkout/Core/Health/HealthManager.swift (mirrorWorkout); effort score linked to the workout when RIR was reported: HealthStore.swift (effort-score write)HKLiveWorkoutBuilder with an HKLiveWorkoutDataSource, with share types workouts and active energy: ios/AIWorkoutWatch/WatchWorkoutSession.swift (shareTypes, the saving branch); STANDALONE_WATCH_ENABLED = YES in ios/Config.xcconfig. The watch does no GPS: no HKWorkoutRouteBuilder and no CoreLocation in ios/AIWorkoutWatch/HealthStore.swift:878, called through HealthManager.deleteMirroredWorkout from ios/AIWorkout/Features/Workout/WorkoutView.swift:369 and ios/AIWorkout/Features/Settings/SettingsView.swift:1023TrackingWindow.Bounds.earningStart, set in Settings): ios/AIWorkout/Core/Health/HealthManager.swift (trackingBounds, the journey read)HealthStore.swift:494, HealthManager.swift:102, 108ios/AIWorkout/Core/Health/BackgroundBoardUpdates.swift (categories, BackgroundBoardUpdater), HKObserverQuery and enableBackgroundDelivery in HealthStore.swift (startBackgroundDelivery, stopBackgroundDelivery), called through HealthManager.swift, the com.apple.developer.healthkit.background-delivery entitlement; a wake runs the ordinary consent-gated sync (MetricSyncService through MetricUploadConsent) and requests no new permission. No UIBackgroundModes in the iPhone app: ios/AIWorkout/Info.plist. The one anchored query in the project is the watch app’s live heart-rate read during a workout the user started (ios/AIWorkoutWatch/WatchWorkoutSession.swift:431), under the watch’s workout-processing background mode (ios/AIWorkoutWatch/Info.plist:34-37); it ends with the workoutNSHealthShareUsageDescription and NSHealthUpdateUsageDescription in ios/AIWorkout/Info.plist, and the watch’s in ios/AIWorkoutWatch/Info.plist, held to the requested type sets in both directions, and refusing the word readiness, by ios/AIWorkoutTests/HealthPurposeStringTests.swift and WatchHealthPurposeStringTests.swift (bugs #571)restingHeartRateForZones, read through ReadinessSignalsProvider into HeartRateZonesView.swift and WorkoutView.swift); sleep, HRV and the five further types are used only behind RecoveryGate (mayReadSleep, mayReadHRV, mayReadWorkoutHeartRate) in ios/AIWorkout/Features/Recovery/RecoveryDataSource.swift, ios/AIWorkout/Features/WatchIntel/OvernightBlock.swift and CardioSnapshot.swift. The older signal read (HealthManager.refreshReadinessSignals, run from the Workout tab) still fetches sleep and HRV when those Health categories are on; nothing reads those two values from it, and only restingHeartRateForZones is usedios/ProgressionEngine/Sources/ProgressionEngine/WatchIntel/SetHeartRate.swift (SetHeartRateSummary), written by ios/AIWorkout/Features/WatchIntel/SetHeartRateWrite.swift only through RecoveryGate.mayReadWorkoutHeartRate, kept in SetHeartRateStore (excluded from backup) and joined to a set by setUID (ios/AIWorkout/Models/WorkoutLogModels.swift). It appears in the JSON export (WorkoutDataExport.swift, SetHeartRateExport) and in no coach context, workout-share payload, Sheets export or server payload. Late readings are picked up by an observer query that runs only while the app is open (HealthStore.swift, startHeartRateObserver), with no background deliverymirrorWorkout path with activeEnergyKcal: nil: ios/AIWorkout/AIWorkoutApp.swift (bugs #486), ios/AIWorkout/Core/WatchSessionReceiver.swiftios/AIWorkout/Core/TrainerContextBuilder.swift:109-127, 396-415, ios/AIWorkout/Features/Chat/ChatView.swift:873-891ios/AIWorkoutWatch/WatchWorkoutSession.swift, throttled by ios/ProgressionEngine/Sources/WatchLink/HeartRateThrottle.swift, held in memory only by ios/AIWorkout/Core/CurrentWorkoutState.swift (recordHeartRate, dropped past 90 seconds and on clear()), and reaching the coach at TrainerContextBuilder.swift buildActiveSession. It appears in no SwiftData model, no export, no widget snapshot, and no server payloadios/AIWorkout/Core/Competition/MetricSyncService.swift (activeMetrics(), which fetches) calling ios/ProgressionEngine/Sources/CompetitionKit/MetricUploadConsent.swift (which decides, and is the single place the three opt-in routes are resolved). The rule is unit-tested case by case in MetricUploadConsentTests.swift, including that a private scope opts nothing in and that a metric keeps uploading until the last route to it closes.MetricSyncService.swift (lookbackDays, from TrackingWindow.externalHistoryDays)kSecAttrAccessibleWhenUnlockedThisDeviceOnly, no iCloud sync: ios/AIWorkout/Core/AICoachConfig.swift:122-171ios/AIWorkout/Core/TrainerClient.swift:554-566backend/src/app.ts:72-128, backend/src/providers/gemini.ts:41-47TrainerContextBuilder.swift passes readiness: nil (backlog #774), and the widget snapshot’s readiness fields are always empty (ios/AIWorkout/Core/WidgetSnapshotWriter.swift)ios/AIWorkout/Core/ProgramClient.swift (ProfileRequest.missedDayPolicy), echoed by backend/src/program.ts (missedDayPolicyFrom, bugs #487). The pulling and dumbbell press results in the fitness test payload: ios/ProgressionEngine/Sources/BaselineTest/BaselinePlanInput.swift, backend/src/baseline.ts (V2_FIELDS, backlog #798)ProactiveCoachDirector.swift (settings?.coachMaySpeakFirst ?? false, bugs #500), toggle and footer in ios/AIWorkout/Features/Settings/SettingsView.swiftios/AIWorkout/Core/TrainerContextBuilder.swift (whole file); rendered to prose at backend/src/context.ts:713-777 (renderClientContext)TrainerContextBuilder.swift buildProfile, against the model at ios/AIWorkout/Models/ProgramModels.swift:11ChatView.swift:721-726, 919, 1001-1002backend/src/app.ts (middleware list), all console.* calls in backend/srcconsole.* in backend/src carries a provider SDK error, a rate-limit decision, a token-verification reason, or a length/position, and none takes a message, a prompt, or a reply. The program route’s failure diagnostics: backend/src/app.ts:383-385, 424-436; the content-free parse-failure text and why V8’s own message cannot be used: backend/src/program.ts:750-773; validation errors are field paths and type expectations that quote no value: backend/src/program.ts:436-748; tests asserting nothing identifiable survives into the diagnostics: backend/test/program-diagnostics.test.tsinfra/template.yaml:1754-1760 (TrainerProxyLogGroup)u:<userId> when signed in and a:<ip> otherwise, each window (minute and day) expiring shortly after it closes: backend/src/limits.ts (subjectKey, enforceLimits), backend/src/ratestore.ts; the user id appears in the refusal log line: backend/src/guard.tsios/AIWorkout/Core/Speech/SpeechRecognizer.swift:69-85, 104-109resolveRequestProvider in backend/src/app.ts refuses a keyless request with key_required before any upstream call, and buildProvider cannot construct a provider without being handed a key, so there is nothing left to flag off; pinned by backend/test/byo-key.test.tsios/AIWorkout/Core/AICoachConfig.swift (AICoachChoiceStore, read as a retirement marker), ios/AIWorkout/Features/Settings/AICoachSettingsView.swift (the retirement notice)backend/src/providers/anthropic.ts, ios/AIWorkout/Core/AICoachConfig.swift:12-68ios/AIWorkout/Features/Adventure/HomeLocationEditor.swiftHomeLocationEditor.swift lookUp(); in-app disclosure in privacySection; displayed rounded in coordinateTextios/AIWorkout/Models/AppSettings.swift:201-215, ios/AIWorkout/Features/Adventure/JourneyBridge.swift:38-51JourneyBridge.swift:28-29 (plus onChange observations at ios/AIWorkout/Features/Adventure/AdventureView.swift:1264-1265 and ios/AIWorkout/Features/Walk/WalkView.swift:500-501, fed at AdventureView.swift:197-198 and WalkView.swift:155-156, that do not forward the value)TrainerContextBuilder.swift), widget (ios/AIWorkout/Core/WidgetSnapshotWriter.swift), export (ios/AIWorkout/Features/Settings/WorkoutDataExport.swift), and all server payloads (no matches in backend/, server/, ios/AIWorkout/Core/Competition/)AdventureView.swift:614-615, 640-646CLLocationManager in the repository, and the only file that owns one: ios/AIWorkout/Core/LocationMoment.swiftrequestLocation(), never startUpdatingLocation(), so the hardware stops itself and no session can be left runningNSLocationWhenInUseUsageDescription in ios/AIWorkout/Info.plist; there is no NSLocationAlwaysAndWhenInUseUsageDescription, no background location mode in UIBackgroundModes, and no location entitlement in ios/AIWorkout/AIWorkout.entitlementsLocationMoment.request() returns early on .denied, covered by testARefusalIsNeverAskedAgaintestItAsksNothingUntilItIsAsked, and the UI gate testThePickerOffersLocationRatherThanDemandingItThere are three call sites for that one fix, and they end differently. The distinction is the whole of the location section above.
LocationMoment.state and in the map screen’s @State, both dropped in AdventureMapScreen.onDisappear. No key is added to LocalDataWipe because there is nothing to wipe: ios/AIWorkoutTests/MapLocationTests.swift testTheWipeHasNothingNewToClear. No call site passes this fix to the coach context, the widget snapshot, or the exportios/AIWorkout/Features/Adventure/CustomRouteBuilderScreen.swift takes the fix and AddRouteStopSheet.swift turns it into a stop; it is persisted as latitude/longitude on the stop in CustomRouteModels.swift, put on the wire by RoutePayloadMapping.swift, and required server-side by routeStopSchema in server/src/validation.ts. This is a route’s course geometry rather than a record of the author, but the coordinate is real and it does reach the serverstopIndex, waypointId, at, verified, onBehalfOf): ios/ProgressionEngine/Sources/CompetitionKit/RaceDTOs.swift, pinned by ios/AIWorkoutTests/RaceWireCensusTests.swift. Race photo uploads send stopId, takenAt, a one-word gps verdict and audience: ios/ProgressionEngine/Sources/CompetitionKit/RaceSelfieDTOs.swift, with the read-compare-strip order in ios/AIWorkout/Features/Competition/RaceSelfie.swift. raceCheckinSchema in server/src/validation.ts is .strict(), so a body carrying a latitude, longitude, accuracy or distance is rejected with a 400 rather than silently dropped, and no check-in row has a coordinate column. The comparison against the stop happens on the devicegetRaceDetail in server/src/races.ts returns the route payload and the roster’s check-ins in one response, and default race visibility is participants. route.stops[checkin.stopIndex] is therefore reconstructable by anyone in the race, up to the number of stops. Disclosed under “Checking in during a race”ios/AIWorkout/Features/Adventure/MapAddressSheet.swiftios/AIWorkout/Features/Adventure/NearbyPlaces.swiftios/ProgressionEngine/Sources/JourneyEngine/Resources/landmarks.jsonios/AIWorkout/Core/AppSchema.swift:85-125 (AppSchemaV1.models, forwarded by AppModelContainer.models)AppModelContainer.swift:85-92; entitlements filesios/Shared/WidgetSnapshot.swift:12-64, 164-170, 176-220AICoachConfig.swift:122-171 (AICoachConfigStore), ios/AIWorkout/Core/Competition/TokenStore.swift:13-68, GoogleSheetsTokenStore.swift, and ios/AIWorkout/Core/Subscription/EntitlementCache.swift:81-143 (the remembered subscription tier, instantiated by SubscriptionManager.swift:586). The first three are this-device-only (kSecAttrAccessibleWhenUnlockedThisDeviceOnly); the entitlement cache deliberately is not (kSecAttrAccessibleAfterFirstUnlock, by design, so it survives a restore) and is not named in “Security” or cleared by “Delete all data,” neither of which claims to be a complete Keychain inventory. A further this-device-only token store exists in the code for a third-party health-service integration that is hidden and cannot be connected in the shipped build, because its OAuth client id (GOOGLE_HEALTH_CLIENT_ID) is empty in ios/Config.xcconfig: ios/AIWorkout/Features/Settings/IntegrationsSettingsView.swift:36-56ios/AIWorkout/Core/AvatarIdentity.swift:17-37ios/AIWorkout/Features/Settings/WorkoutDataExport.swift, including setSeconds, restAfterSeconds and SetHeartRateExport; the profile snapshot carries no body weight, birth year or home coordinateios/AIWorkout/Core/Competition/PrivateListStore.swift (backlog #623); the shared server groups an account owned become its own lists once, keeping only members who are its friendsios/AIWorkout/Core/Feedback/FeedbackOutbox.swift (backlog #799); the server’s first-write-wins insert: server/src/feedback.ts (feedbackIdForKey)ios/AIWorkout/Features/Notifications/NotificationScheduler.swiftios/AIWorkout/Features/Notifications/PushRegistrar.swift (OptIn, and the invariant stated at the top of the file)PushRegistrar.consider(_:) (PushRegistrar.swift:145), called with .cheerArrived from CompetitionRootView.swift:385 and with .sentInvitation from AddFriendView.swift:556, CreateChallengeView.swift:570, AddPeopleToChallengeSheet.swift:110 and AddPeopleToRaceSheet.swift:164 in ios/AIWorkout/Features/Competition/server/src/push/tokens.ts; the routes are scoped to the caller: server/src/app.ts (/me/push-tokens)server/src/push/tokens.ts (TOKEN_TTL_MS, dropDeadToken)server/src/push/categories.ts (isQuietHour)server/src/push/categories.ts (CATEGORY_DEFAULT_ENABLED, CATEGORY_DAILY_CAP), server/src/push/prefs.tsserver/src/push/copy.ts, server/test/push-copy.test.tsios/project.yml (only the local ProgressionEngine), ios/ProgressionEngine/Package.swift (no remote dependencies)AppTrackingTransparency, ASIdentifierManager, advertisingIdentifier, NSUserTrackingUsageDescription in ios/ios/AIWorkout/Features/Settings/SettingsView.swift:997-1063; dialog copy at :258-273RecoveryStore, SetHeartRateStore, PrivateListStore, BlockReviewStore and FeedbackOutbox: ios/AIWorkout/Core/LocalDataWipe.swiftios/AIWorkout/Models/AppSettings.swift:540-652 (resetToDefaults())ios/AIWorkoutTests/LocalDataWipeTests.swiftSettingsView.swift:274-301, 331-339; token and cache clearing at ios/AIWorkout/Core/Competition/AuthStore.swift:324-333 (forceSignOut())aiworkout-prod; SQLite is local development only: server/src/lambda.ts, server/src/index.ts, infra/template.yaml:359-421, 367server/src/store/types.ts, server/src/store/dynamo.ts:223-896server/src/email-identity.ts, server/src/users.ts (upsertUserByAppleSub, recordEmailHash), server/src/store/types.ts (UserRow.email_hash)server/test/friend-requests.test.ts (“never stores an email address in any readable form”)server/src/friend-requests.ts, server/src/app.ts (POST /friends/requests), tests in server/test/friend-requests.test.ts (“enumeration defence”)server/src/friend-requests.ts (convertPendingInvites), server/src/store/types.ts (PendingInviteRow), tests “an invitation to somebody who has not joined yet”server/src/friend-requests.ts (declineFriendRequest), test “decline is invisible to the requester”server/src/leagues.ts:790-820, 895-951; test asserting no name or Apple id leaks: server/test/handle-anonymity.test.tsserver/src/app.ts:1437-1446, 3637-3646server/src/shares.ts:21-37, 63-66, server/src/app.ts:1385-1414, server/src/metrics.ts:52-57server/src/app.ts:1456-1458 (syncEnrollmentToScope)server/src/metrics.ts:152-162, server/src/store/dynamo.ts:3315-3352server/src/validation.ts:389-392 (cheerSchema)challengeName (optional on createChallengeSchema and updateChallengeSchema), the route and race label fields (routeLabelText) and stop notes (routeNoteText) in server/src/validation.ts, group names in server/src/groups.ts (shared groups made before they became private lists; nobody can join one, since b359d21f, bugs #518), and display_name and the handle on the user record. A name refused by the word filter at sign-in leaves the account with no name rather than failing (offeredDisplayName in server/src/app.ts), and names in a shared workout or share-back report are replaced rather than refused (renamedForLanguage). The app’s profile name is the server’s display name: ios/AIWorkout/Core/Competition/DisplayNameSync.swift (bugs #547). There is no message field anywhere: no route, race, challenge, group or cheer carries text addressed to a personserver/src/challenges.ts:1222-1239server/src/avatars.tsserver/src/handle.ts:23-41, rationale in server/SECURITY.md:22-43diagnostics attribute, line-capped: server/src/feedback.ts, server/src/store/dynamo.ts (feedback put), server/src/validation.ts (feedback schema, the diagnostics array’s .max()). No TTL on feedback items yet: server/src/retention.ts (the retention-gap note). The 12-month feedback deletion stated in the policy is not yet automated and is tracked for a later build. The triage copy is written by server/src/feedback-sheet-sync.ts; the same daily run removes a deleted account’s name from the triage spreadsheet and deletes its screenshot from the folder (reconcileDeletedAccounts, bugs #352), and a screenshot it cannot delete fails the run so the alarm says so. Exercise requests are a feedback category whose checked fields are folded into the stored message: server/src/exercise-request.ts (formatExerciseRequest, isYouTubeLink), server/src/validation.ts (feedbackSchema, exerciseRequestSchema)server/src/app.ts:1313-1333, server/src/store/dynamo.ts:2769-2931; trophy anonymization server/src/trophies.ts:16; screenshot deletion and its rationale server/src/store/dynamo.ts:2828-2834diagnostics; only user_id, user_label and the screenshot go: server/src/store/dynamo.ts (“Anonymize feedback”)server/src/db.ts, REFERENCES users(id) ON DELETE CASCADE). races.owner_id carries no cascade, so a race an organizer created is not shown here as deleted with the organizer; not asserted in the policyserver/src/route-payload.ts (STOP_NOTE_MAX)server/src/text-filter.ts (bugs #287)server/src/moderation-alerts.ts (ALERT_SNAPSHOT_MAX), delivered through the ModerationAlertTopic SNS topic in infra/template.yamlserver/src/user-blocks.ts (CONCEALED_DISPLAY_NAME, CONCEALED_AVATAR, concealedUserId, bugs #517), ios/ProgressionEngine/Sources/CompetitionKit/ConcealedIdentity.swiftserver/src/consistency-league.ts, server/src/board-scoring.ts (active_days)server/src/app.ts (/friends/requests/by-user), FriendRequestByUserRequest in ios/ProgressionEngine/Sources/CompetitionKit/DTOs.swift (backlog #620)server/src/validation.ts, challenge cheer schema) and between friends with the emote not stored and a cheer change-log row for the recipient: server/src/friend-cheers.tsserver/src/friends-summary.ts, server/src/board-scoring.ts, contract test server/test/store-contract.ts (“omits a day with no counting total rather than reporting it as zero”)server/src/friends-summary.ts (goal is present only on the caller’s own row), test server/test/friends-summary.test.ts (“never shows another person daily goal, only the caller own”)CompetitionKit/LiveRail.swift (PresenceGate.mayWrite, which treats an unknown scope as private), ios/AIWorkout/Core/Competition/PresenceService.swift (which is the only caller), tests LiveRailClientTests.testAnUnknownScopeIsTreatedAsPrivate and testPresenceIsWrittenOnlyWhenTheLiveScopeIsFriendsserver/src/presence.ts (HR_ZONE_BANDS), CompetitionKit/FriendsHub.swift (HrZoneBand), tests LiveRailTests.testPresenceCarriesNoHeartRate and LiveRailUITests.testTheRailNeverPutsAHeartRateOnScreenserver/src/app.ts (PUT /me/shares/:metric, the clearPresence branch)PRESENCE_TTL_MS: server/src/presence.ts, enforced from the stamp in both backendsserver/src/presence.ts (LIVE_SHARE_SCOPES), CompetitionKit/LiveRail.swift (PresenceGate.scopes), test LiveRailClientTests.testThePickerOffersPrivateAndFriendsAndNotGlobalLevelEngine/XPEngine.swift (workoutXP is recomputed with the boosts left out, and workoutXP is the only total MetricSyncService uploads), tests CheerBoostXPTests.testACheerIsWorthNothingInAnyRankedTotal and testABoostDoesNotDisturbTheRankedTotalThroughTheDailyCapEitherRACE_SELFIES_ENABLED, EnableRaceSelfies in infra/template.yaml), recorded as false in prod in CLAUDE.md. Photo screening applies to route photos too: server/src/route-photos.ts (screenOrRefuse), server/src/photo-screening.ts (Rekognition DetectModerationLabels)DURABLE_EVENTS kinds expire after 90 days: server/src/store/dynamo.ts:2029-2040 (auditItem), server/src/retention.ts (AUDIT_RETENTION_MS, DURABLE_EVENTS)server/src/account-suspension.ts, server/src/db.ts (account_suspensions), server/src/store/dynamo.ts (accountSuspensionKey); enforced at all three doors in server/src/middleware.ts (requireAuth), server/src/sessions.ts (rotateSession) and server/src/users.ts (upsertUserByAppleSub); tests in server/test/account-suspension.test.ts (“survives the account being deleted and made again with the same Apple ID”) and server/test/store-contract.ts (“SURVIVES ACCOUNT DELETION, which is the entire point of it”)POST /ops/moderation behind a secret that ships in no client and lives in SSM, guarded by one prefix-mounted middleware that fails closed when the secret is unset, compares it in constant time, rate-limits per address before comparing, and audits every action durably: server/src/middleware.ts (requireOps), server/src/moderation.ts, server/src/app.ts (/ops/moderation), tests in server/test/account-suspension.test.ts (“POST /ops/moderation”). The IAM-gated break-glass path for a leaked secret is server/src/moderation-lambda.ts and infra/template.yaml (ModerationFunction, which has no Events)server/src/middleware.ts:24-57, user-agent read at server/src/app.ts:858, 947, 997@apple/app-store-server-library, hono, jose and zod: server/package.jsonserver/src/app.tsinfra/template.yaml (PointInTimeRecoverySpecification, the LogGroup resources)EnableDevAuth=false (NODE_ENV=production), under which the route at server/src/app.ts:939-990 is never mounted; verified 2026-07-30 (/auth/dev returns 404, /health reports devMode:false)infra/template.yaml:2804 and the Lambda Function URL outputNSAllowsLocalNetworking, with no arbitrary-loads exception: ios/AIWorkout/Info.plistios/AIWorkout/Core/Integrations/GoogleSheets/GoogleSheetsOAuth.swift, ios/AIWorkout/Features/Account/AccountSignInView.swift:193-197, ios/ProgressionEngine/Sources/ProgramBuilder/SheetProvenance.swift:239-243server/src/crypto.tsios/AIWorkout/Features/Settings/ProfileEditorView.swift:70-78, ios/AIWorkout/Models/ProgramModels.swift:37, used at ios/AIWorkout/Features/Tracks/TracksBridge.swift:66ios/AIWorkout/Core/Subscription/SubscriptionManager.swift (SubscriptionTier, SubscriptionProduct.allIDs: com.mig1278.aiworkout.user.monthly, com.mig1278.aiworkout.coach.monthly), ios/AIWorkout/Core/Subscription/CoachingAllowance.swift (baseTierLimit)ios/AIWorkout/Core/Subscription/EntitlementStatus.swift (GatedSurface, GatedSurface.openWhenLocked), applied by ios/AIWorkout/Features/Paywall/PaywallGate.swift. MONETIZATION_ENABLED = YES in ios/Config.xcconfig. Friends and groups are open (ruled 2026-10-01) and so are challenges (ruled 2026-10-05); races and leagues stay behind .compete (CompetitionSection.gate)SubscriptionManager.swift (PurchaseAction via ios/AIWorkout/Features/Paywall/SubscriptionManager+PurchaseAction.swift, AppStore.sync(), Transaction.currentEntitlements, Transaction.updates)appAccountToken that is the account id reformatted as a UUID: SubscriptionManager.swift (purchase options), ios/AIWorkout/Core/Subscription/SubscriptionAccountToken.swiftSubscriptionManager.swift (reportAppTransaction), ios/AIWorkout/Core/Competition/CompetitionAPIClient.swift (me/apple-transaction). The server stores an APPTXN#<id> link row to the user id whether or not AppleSubscriptionsEnabled is on: server/src/app.ts (POST /me/apple-transaction), server/src/subscriptions.ts (linkAppTransaction). Link rows are deliberately outside the user partition and survive account deletion: server/src/store/dynamo.ts (appleLinkKey)AppleSubscriptionsEnabled defaults to 'false' in infra/template.yaml, and docs/held-builds.tsv records the live stack at false on 2026-09-19. What it would store: server/src/store/types.ts (subscription row). GET /me/subscription is advisory and can only widen what the device concluded: server/src/app.tsEntitlementStatus is a plain struct, absent from ios/AIWorkout/Features/Settings/WorkoutDataExport.swift, ios/AIWorkoutWidgets/ and ios/AIWorkout/Core/TrainerContextBuilder.swiftSubscriptionManager.swift (IntroductoryOffer, refreshIntroductoryOffer(), isEligibleForIntroOffer)unknown/entitled/trial/locked: ios/ProgressionEngine/Sources/DiagnosticLog/DiagEvent.swift (DiagEntitlementState), DiagScalar.swift, enforced by DiagVocabularyTests.swift. Memory only on iPhone, 24 hours: DiagRecorder.swift, DiagRetention.swift. Also mirrored to the unified system log: DiagRecorder.swiftDiagRetention.watch, sessionFloor), sent to the phone over WatchConnectivity and deleted once delivered: DiagRetention.swift, DiagRing.swift, DiagStore.swift, WatchLinkClient.swift, ios/AIWorkout/Core/WatchLinkBridge.swiftDeepLog.swift, DeepLogStore.swift. Store context and hashed ids: ios/AIWorkout/Core/Subscription/StoreDiagnostics.swift, SubscriptionManager.swift, ios/AIWorkout/Core/Competition/AuthStore.swiftDeepLogStore.swift; GPS lines carry permission, precise flag, accuracy band and fix age only: ios/AIWorkout/Core/LocationMoment.swiftios/AIWorkout/Core/Diagnostics/DiagnosticModeLaunch.swift, DeepLog.swift; Settings row and toggles: ios/AIWorkout/Features/Settings/SettingsView.swift, ios/AIWorkout/Features/Diagnostics/DiagnosticModeView.swift (export and clear)DeepLogRedactor.swift, tests in DeepLogTests.swift. Ids are hashed with unsalted 32-bit FNV-1a for correlation, not secrecy: DeepLogRedactor.swiftios/AIWorkout/Features/Feedback/FeedbackFormView.swift, DeepLogAttachment.swift, server/src/validation.ts. Copied to the triage sheet’s Diagnostics column: server/src/feedback-sheet-sync.tsios/AIWorkout/Core/LocalDataWipe.swift has no DeepLog call; the only clear is DiagnosticModeView.swiftdocs/tracker-ids.jsonSheetExportService.export, which differs only in the WorkoutSheet handed in: ios/AIWorkout/Core/Integrations/GoogleSheets/SheetExportService.swift (header comment), called from ios/AIWorkout/Features/Program/SheetImportView.swift (exportBlankTemplate), ios/AIWorkout/Features/Program/ProgramView.swift, and ios/AIWorkout/Features/Settings/SettingsView.swift (exportHistoryToSheets). A blank template and an exported program are both a SheetKind.program/.template pair read by the same importer: ios/ProgressionEngine/Sources/ProgramBuilder/SheetProvenance.swifthttps://www.googleapis.com/auth/drive.file, openid and email, all non-sensitive: ios/AIWorkout/Core/Integrations/GoogleSheets/GoogleSheetsConfig.swift (scopes); the spreadsheets constant remains only to read older grants. Narrowed 2026-09-08 (8e9edd38); import locked to sheets the app created and the paste box removed 2026-09-09 (09511c90): SheetImportService.swift, ExportedSheetStore.swiftGoogleSheetsConfig.swift, ios/AIWorkout/Core/AppConfig.swiftGoogleSheetsOAuth.swift (PKCEPair, exchange, prefersEphemeralWebBrowserSession)GoogleSheetsAPI.swift (create)ios/ProgressionEngine/Sources/ProgramBuilder/HistorySheetExport.swift (HistorySheet.columnHeaders), ProgramSheetExport.swift (ProgramSheet.columnHeaders)ios/AIWorkout/Core/Integrations/GoogleSheets/SheetImportService.swift is the only import path and creates a program only; a history sheet is refused at the first cell by ios/ProgressionEngine/Sources/ProgramBuilder/SheetProvenance.swiftios/ProgressionEngine/Sources/ProgramBuilder/WorkoutSheet.swift (SheetText.sanitize), ios/AIWorkout/Core/TrustFence.swift (importedSheet), ios/AIWorkout/Core/TrainerContextBuilder.swift (fenceSources); drift between the two sanitizers is pinned by ios/AIWorkoutTests/SheetSurfacesTests.swiftserver/ touches a lifter’s sheet, sheet id or Google token. The developer’s own triage sync is a different account and a different file: server/src/google-clients.ts, server/src/feedback-sync-lambda.tsGoogleSheetsTokenStore.swift (kSecAttrAccessibleWhenUnlockedThisDeviceOnly), GoogleSheetsManager.swift (disconnect()), GoogleSheetsOAuth.swift (revoke(token:)), ios/AIWorkout/Core/LocalDataWipe.swiftemail claim of the ID token at connect and kept with the tokens on the device to show “Connected as”: GoogleSheetsOAuth.swift (email(fromIDToken:)), GoogleSheetsTokenStore.swift (accountEmail). No profile or userinfo scope, and no route in server/ receives it