SnapScore

SnapScore privacy policy (draft from code)

Draft dated 27 October 2026. It describes what the SnapScore application and its Supabase edge functions in this repository actually do, at commit 92b6c9b. It is not a legal opinion. The support contact is augie@autumn8.me. The support page is https://snapscore.autumn8.me/support. This policy's public URL is https://snapscore.autumn8.me/privacy. Line numbers below are positions in the application files at that commit. This change adds only these documents.

The in-app Privacy Policy control opens https://snapscore.autumn8.me/privacy (scoresnap/app/settings.tsx:610, scoresnap/app/auth/sign-in.tsx:613). The owner-designated URL for this policy is https://snapscore.autumn8.me/privacy. Nothing in this repository publishes this file to either address.

Owner decisions / gaps

These are places where the code is missing, contradictory, or not enough to publish a finished policy. They are decisions for the owner, not behaviors this draft invents.

  1. The binary's privacy link differs from the owner-designated policy URL. On 27 October 2026 the owner set the support contact to augie@autumn8.me, the support page to https://snapscore.autumn8.me/support, and this policy's public URL to https://snapscore.autumn8.me/privacy. The profile "Help & Support" row still has an empty press handler (scoresnap/app/(tabs)/profile.tsx:695). The Privacy Policy control still opens https://snapscore.autumn8.me/privacy (scoresnap/app/settings.tsx:610, scoresnap/app/auth/sign-in.tsx:613). This repository does not serve https://snapscore.autumn8.me/privacy.
  2. Account deletion does not delete the server account or server data. Settings copy says the action permanently deletes the account and all rounds, scores, statistics, and settings (scoresnap/app/settings.tsx:226-229). The implementation removes five AsyncStorage keys, then signs out (scoresnap/app/settings.tsx:247-256). A comment says a Supabase deleteUser call is still a TODO (scoresnap/app/settings.tsx:254-255). There is no deletion job for profiles, published_rounds, round_messages, marketing_events, revenue_events, share_links, course-search cache, or auth users.
  3. The local wipe misses keys the app actually writes. It removes snapscore-scan (scoresnap/app/settings.tsx:250). The scan store key is snapscore-scans (scoresnap/src/stores/scan-store.ts:66). It does not remove snapscore-guest (scoresnap/src/stores/guest-store.ts:13), snapscore.chat.session.v1, or snapscore.chat.name.v1 (scoresnap/src/services/live-chat.ts:6-7).
  4. Sign-out rewrites some of the keys the wipe just removed. signOut clears the in-memory auth session and calls resetAll on contests (scoresnap/src/stores/auth-store.ts:220-232). Both stores persist back to AsyncStorage (scoresnap/src/stores/auth-store.ts:239-247, scoresnap/src/stores/contest-store.ts:352-355).
  5. No retention period is implemented for accounts, scores, chat, profiles, marketing events, or revenue events. Share links stop working seven days after a round is completed, or not at all while the round is active (scoresnap/supabase/migrations/20261027000000_live_baseline.sql:507-510, 707-708). Expired and revoked rows are not deleted. Chat removal sets removed_at and leaves the row (scoresnap/supabase/migrations/20261027000000_live_baseline.sql:729-736). Course-search and course-detail caches are treated as fresh for 7 days and 90 days (scoresnap/supabase/functions/course-data/index.ts:45-67) and this repository has no purge of stale cache rows.
  6. "Auto Claude Vision" does not control the upload. The setting is stored (scoresnap/src/stores/settings-store.ts:33, 65; scoresnap/app/settings.tsx:552-557). scanScorecard never reads it (scoresnap/src/services/ocr-service.ts:36-131). Cloud OCR still runs when ML Kit is unavailable, when ML Kit throws, when ML Kit returns fewer than two players (including one player at any confidence, and a zero-player result), or when two or more players are returned with average confidence below 0.7 (scoresnap/src/services/ocr-service.ts:44-115). The only skip is two or more players at confidence 0.7 or higher (scoresnap/src/services/ocr-service.ts:54-67).
  7. Privacy toggles and notification toggles are stored and not read by any other feature code. shareStatistics, showInLeaderboards, roundReminders, settlementReminders, and confirmBeforeImport appear in the settings store and settings screen only (scoresnap/src/stores/settings-store.ts:33-42, scoresnap/app/settings.tsx:560-603). There is no notification SDK in scoresnap/package.json.
  8. Anonymous Supabase auth is not what the guest button does. Local config disables anonymous sign-ins (scoresnap/supabase/config.toml:51). The app has no signInAnonymously call. anonymousSignInOptions is defined and never called (scoresnap/src/services/captcha.ts:73-76). docs/CAPTCHA.md describes a future anonymous client (scoresnap/docs/CAPTCHA.md:69-76). "Skip" creates a local profile (scoresnap/src/stores/auth-store.ts:206-217). Server policies do reject JWTs whose is_anonymous claim is true for host writes (scoresnap/src/services/live-share.ts:11, scoresnap/supabase/migrations/20261027130000_guest_write_guard.sql:2-20).
  9. Tournament sync writes tables and columns this baseline does not define. When real auth is on, the client writes contest_rounds, contest_groups, contest_players, hole_scores, and contests (scoresnap/src/services/tournament-sync.ts:24, 57, 68, 99, 111, 146, 173). Active migrations create contest_players and contests (scoresnap/supabase/migrations/20261027000000_live_baseline.sql:38-68) and do not create contest_rounds, contest_groups, or hole_scores. The next migration says it adds no round or group schema (scoresnap/supabase/migrations/20261027120000_sync_foundation.sql:1). Copies of those tables exist only under scoresnap/supabase/archive/pre-baseline/, which is not an active migration. Tables that do exist still lack columns the client sends: contest_players has no sort_order, status, or course_handicaps (scoresnap/supabase/migrations/20261027000000_live_baseline.sql:38-48 versus scoresnap/src/services/tournament-sync.ts:112-121), and contests has no is_tournament or current_round (scoresnap/supabase/migrations/20261027000000_live_baseline.sql:51-67 versus scoresnap/src/services/tournament-sync.ts:174-176). The client writes hole_scores and does not write score_events, which the baseline does create (scoresnap/supabase/migrations/20261027000000_live_baseline.sql:255-263). Whether those writes succeed against the hosted database is not settled by the client code. The data the client attempts to send is still disclosed below.
  10. The Stripe alert mailbox is hardcoded. The signup function declares the same constant and does not send to it. on-user-signup sets ALERT_EMAIL and never reads it (scoresnap/supabase/functions/on-user-signup/index.ts:11). Signup mail goes to the new user and, at three referrals, to the referrer (scoresnap/supabase/functions/on-user-signup/index.ts:38, 168). The Stripe webhook sets to: ALERT_EMAIL, which defaults to jones.augie1@gmail.com when the env var is unset (scoresnap/supabase/functions/stripe-webhook/index.ts:11, 30). Decide whether a published policy should name that mailbox. This draft does not use it as the support contact.
  11. Sentry sends default PII, and session replay uses the SDK's default masks. sendDefaultPii: true is set (scoresnap/app/_layout.tsx:37). mobileReplayIntegration() is called with no options (scoresnap/app/_layout.tsx:51). The pinned SDK is @sentry/react-native 8.4.0 (scoresnap/package.json:19, scoresnap/package-lock.json:7020). That version's MobileReplayOptions defaults maskAllText, maskAllImages, and maskAllVectors to true, and this repository does not override them. enableLogs: true is also set (scoresnap/app/_layout.tsx:38). Application source does not call Sentry.logger. The exact fields sendDefaultPii attaches are still not listed in this repo.
  12. Android RECORD_AUDIO is declared twice, and no application code records audio (scoresnap/app.json:33-38). Confirm the permission is still required by the camera plugin before the store listing claims the app uses the microphone.
  13. Precise location is requested in the course-search UI if expo-location can be loaded, and the search request the app builds does not send coordinates (scoresnap/src/ui/CourseSearch.tsx:28-51, scoresnap/src/services/course-api.ts:22-25). expo-location is not a dependency in scoresnap/package.json, and app.json has no location usage string. The edge function can accept a near query (scoresnap/supabase/functions/course-data/index.ts:54-57). No app caller passes near.

What I could not determine from code

Who this covers

SnapScore is the Expo app named SnapScore, bundle id and Android package com.scoresnap.app, version 1.0.0 (scoresnap/app.json:3-17, 24-32). People use it to photograph or type a golf scorecard, keep a local contest, and, when the matching build flags are on, share a live scoreboard or look up a course.

Data on the device

Contests, including player names, handicaps, hole scores, optional putts, teams, bet unit, and game choices, are persisted on the device under AsyncStorage key snapscore-contests (scoresnap/src/engine/types.ts:17-26, scoresnap/src/stores/contest-store.ts:355). Auth profile fields the app keeps (id, email, name, provider, anonymous flag) are persisted under snapscore-auth (scoresnap/src/stores/auth-store.ts:9-16, 239-247). Settings, including handicap and bet-unit preferences, are under snapscore-settings (scoresnap/src/stores/settings-store.ts:10-42, 83). A scan counter and Pro flag are under snapscore-scans (scoresnap/src/stores/scan-store.ts:19-30, 66-71). Onboarding completion is under snapscore-onboarding (scoresnap/src/stores/onboarding-store.ts:5-25). Guest bindings are under snapscore-guest (scoresnap/src/stores/guest-store.ts:10-13).

On iOS and Android the Supabase auth session is stored with expo-secure-store. On web it is stored in localStorage (scoresnap/src/db/supabase.ts:10-52). This repository does not show application-level encryption of contest data. ITSAppUsesNonExemptEncryption is set to false (scoresnap/app.json:21). That is an export-compliance flag in the app config, not a description of how contest data is stored.

A SQLite schema for contests and scores exists (scoresnap/src/db/schema.ts:3-86). initDatabase is not called from any other application file, so this draft does not treat SQLite as an active store.

The camera saves a picture to a local URI with base64 turned off (scoresnap/app/scan/index.tsx:76-82). The cloud OCR path then reads that file as base64 (scoresnap/src/services/claude-ocr.ts:22-25). This repository does not show a retention period for the photo file.

Accounts and sign-in

Real auth runs only when offline mode is off and both Supabase env vars are set to something other than the placeholder (scoresnap/src/config/auth.ts:1-2, scoresnap/src/config/offline.ts:1-3). Offline mode is opt-in with EXPO_PUBLIC_OFFLINE_MODE=true (scoresnap/src/config/offline.ts:1-3). In offline mode, after onboarding, the app calls skipSignIn and does not require the sign-in screen (scoresnap/app/_layout.tsx:81-84).

When real auth is on:

When real auth is off, Apple and Google can still be invoked, but the store then keeps a local profile and does not call Supabase. The Apple fallback profile uses the literal email golfer@apple.com and the name Golfer. The Google fallback uses golfer@gmail.com (scoresnap/src/stores/auth-store.ts:87-99, 117-129). Email sign-up in that mode stores the typed email and name locally and does not store the password in the persisted profile (scoresnap/src/stores/auth-store.ts:155-168).

Skip sign-in stores a local id of the form anon-<timestamp>, the name Golfer, and provider: "anonymous" (scoresnap/src/stores/auth-store.ts:206-217). That path does not call Supabase and does not request a Turnstile token.

A database trigger inserts profiles.id, profiles.email, and signup_source from auth.users (scoresnap/supabase/migrations/20261027000000_live_baseline.sql:551-561, 1024). The profiles table also has display name, avatar URL, premium flags, referral fields, and scan and round counters (scoresnap/supabase/migrations/20261027000000_live_baseline.sql:192-211). The client does not write those extra columns. The on-user-signup function, if invoked with a user id and email, upserts email, a generated referral code, referred_by from user metadata, and signup source, and it inserts marketing_events rows that include the email (scoresnap/supabase/functions/on-user-signup/index.ts:86-195).

Cloudflare Turnstile

Turnstile is off unless real auth is on, offline mode is off, and EXPO_PUBLIC_CAPTCHA_MODE is optional or required (scoresnap/src/config/captcha.ts:4-10). The default mode is off. When it is on, the root layout mounts a Turnstile host (scoresnap/app/_layout.tsx:164). Email sign-up and email sign-in request a token immediately before the Supabase call (scoresnap/src/stores/auth-store.ts:145, 179). Apple and Google sign-in do not request a token (scoresnap/docs/CAPTCHA.md:14-15).

The widget loads https://challenges.cloudflare.com/turnstile/v0/api.js (scoresnap/src/services/turnstile-html.ts:3). On native that script runs in a WebView whose base URL defaults to https://snapscore-web.augie-os.workers.dev (scoresnap/src/config/captcha.ts:6, scoresnap/src/components/TurnstileHost.tsx:64-80). On web the script is injected into the page (scoresnap/src/components/TurnstileHost.tsx:13-28). The site key defaults to 0x4AAAAAAFnthkbq7bh9N5bW (scoresnap/src/config/captcha.ts:5). The token is passed to Supabase as captchaToken and the captcha module says it does not log bridge data (scoresnap/src/services/captcha.ts:23, 36-70). What Cloudflare stores from the challenge is not in this repository.

The guest / skip path does not run this widget. The unused helper anonymousSignInOptions would request a token with action anonymous (scoresnap/src/services/captcha.ts:3, 73-75). Nothing calls it.

Scorecard photos and OCR

On native, confirming a photo calls scanScorecard (scoresnap/app/scan/index.tsx:94-103). On web, and in offline mode, the scan screen is a text box. Pasted text is parsed locally and is not sent to the OCR function (scoresnap/app/scan/index.tsx:152-177).

scanScorecard tries ML Kit first (scoresnap/src/services/ocr-service.ts:43-51, scoresnap/src/services/ml-kit-ocr.ts:30-34). The photo is sent to Claude in every case except one. Claude runs when ML Kit is unavailable (scoresnap/src/services/ocr-service.ts:46, 112-115), when ML Kit throws (scoresnap/src/services/ocr-service.ts:106-115), when ML Kit returns zero players (scoresnap/src/services/ocr-service.ts:70, 112-115), when it returns exactly one player at any confidence (scoresnap/src/services/ocr-service.ts:55-57, 70-73), and when it returns two or more players with average confidence below 0.7 (scoresnap/src/services/ocr-service.ts:18, 54-73). The skip is only two or more players at average confidence of 0.7 or higher (scoresnap/src/services/ocr-service.ts:54-67).

The Claude path base64-encodes the image and calls the Supabase edge function ocr-scorecard with imageData and mediaType (scoresnap/src/services/claude-ocr.ts:19-38). The function requires a bearer token that supabase.auth.getUser accepts (scoresnap/supabase/functions/ocr-scorecard/index.ts:49-75). It then POSTs the image to https://api.anthropic.com/v1/messages as model claude-sonnet-4-5-20250514, and asks the model to extract player names, 18 hole scores, confidences, and par (scoresnap/supabase/functions/ocr-scorecard/index.ts:8-9, 114-167). The function returns the parsed names and scores to the app (scoresnap/supabase/functions/ocr-scorecard/index.ts:196-210). It does not insert the image into a table or storage bucket. A failed Anthropic response can be copied into the JSON error body returned to the client (scoresnap/supabase/functions/ocr-scorecard/index.ts:169-177). The maximum accepted body is 10 MB (scoresnap/supabase/functions/ocr-scorecard/index.ts:13-14, 82-87).

Names and scores on a photographed card can be other people's names and scores. They are sent to Anthropic whenever this fallback runs.

Contests, live scoreboards, and chat

Creating and scoring a contest updates the local store (scoresnap/src/stores/contest-store.ts). Two network paths exist when real auth is configured.

Live share. If EXPO_PUBLIC_FEATURE_CROSS_DEVICE_SHARING is not true, the share UI renders nothing (scoresnap/src/config/features.ts:6-8, scoresnap/src/ui/LiveShare.tsx:9-13). When the UI is available and the user shares, the app publishes a snapshot and creates a link (scoresnap/src/services/live-share.ts:18-23). The snapshot keeps a completed flag, display names trimmed to 80 characters, hole scores, stroke total, score to par, game ids, and per-player unit standings (scoresnap/src/services/live-share-snapshot.ts:51-71). The server keeps a sanitized copy of those fields (scoresnap/supabase/migrations/20261027000000_live_baseline.sql:809-834). Anonymous JWTs are rejected (scoresnap/src/services/live-share.ts:11, scoresnap/supabase/migrations/20261027000000_live_baseline.sql:689). While sharing is enabled, the client republishes about every 10 seconds and also on contest changes (scoresnap/src/ui/LiveShare.tsx:16-22, scoresnap/src/stores/contest-store.ts:487-494).

The share token is a 64-character hex string returned to the owner. The database stores SHA-256 of that token, not the token (scoresnap/supabase/migrations/20261027000000_live_baseline.sql:497-510). On web the app writes the URL to the clipboard. Otherwise it passes the URL to the system share sheet (scoresnap/src/ui/LiveShare.tsx:39-40). A separate join-code control, when cross-device sharing is enabled, copies or shares a local code and a snapscore://join/ or same-origin /join/ link (scoresnap/src/ui/ContestJoinCode.tsx:15-19). Those are ways the token or code leaves the device. They are not a new server processor. Anyone with the live-share token can call get_shared_round without a session (scoresnap/src/services/live-share.ts:30-40). The public page shows names, scores, and unit standings, and sets a no-referrer robots meta tag (scoresnap/app/r/[token].tsx:29-48). Active links use an expiry of infinity until the round is completed; completion sets expiry to seven days after the end (scoresnap/supabase/migrations/20261027000000_live_baseline.sql:507-510, 707-708). Revoke sets revoked_at and does not delete the snapshot (scoresnap/supabase/migrations/20261027000000_live_baseline.sql:765-776). Sentry is initialized with enabled: !isPublicShare, and isPublicShare is true for web paths under /r/ (scoresnap/app/_layout.tsx:27, scoresnap/src/config/public-share.ts:4-8).

Chat. A viewer picks a display name of 1–30 characters and a body of 1–500 characters (scoresnap/src/services/live-chat-model.ts:4-9). The app stores a random 32-byte hex session and the name in AsyncStorage, and sends the raw session, name, and body to post_message (scoresnap/src/services/live-chat.ts:6-36). The server stores SHA-256 of the session, the display name, and the body (scoresnap/supabase/migrations/20261027000000_live_baseline.sql:651-678). No account is required to read or post. The host can list messages and mark one removed (scoresnap/src/services/live-chat.ts:38-46).

Tournament sync. Scoring a hole with a round id calls syncScore, which writes contest id, round id, player id, hole, and strokes to hole_scores (scoresnap/src/stores/contest-store.ts:192-197, scoresnap/src/services/tournament-sync.ts:133-160). Adding a round or changing groups writes round name, date, course name, course JSON, group name, tee time, player name, handicap, team, and status (scoresnap/src/services/tournament-sync.ts:22-127). These calls return immediately when real auth is off, and they throw or return when the session is missing or anonymous (scoresnap/src/services/tournament-sync.ts:14-20).

Same-device guest join. A six-character join code and a guest display name are stored locally. The claim-history function returns a message that rounds stay on the device (scoresnap/src/services/guest-join.ts:15-36). That path does not call Supabase.

Course search runs only when EXPO_PUBLIC_FEATURE_COURSE_SEARCH is true (scoresnap/src/config/features.ts:1-2, scoresnap/src/services/course-api.ts:22-24). The app sends the typed query to the course-data edge function (scoresnap/src/services/course-api.ts:9-25). The function requires a JWT role, rate-limits using jwt.sub or the first x-forwarded-for address in memory, and stores the upstream payload in course_search_cache (scoresnap/supabase/functions/course-data/index.ts:21-32, 67-74, 115). For a text search the cache key is search.toString(), which is the normalized query plus limit and any coverage filter, not the user id (scoresnap/supabase/functions/course-data/index.ts:67). The request that produces that row still carries the caller's JWT. There is no step that strips that identifier before the query is sent. A course-detail screen can also open a https://www.golfcore.org/ URL in the system browser when the user taps the credit (scoresnap/src/ui/CourseSearch.tsx:533). That tap is separate from the course-data call. GolfCore then sees the browser request. Detail lookups are cached in course_catalog for 90 days of freshness, search results for 7 days (scoresnap/supabase/functions/course-data/index.ts:42-67). On a cache miss the function calls https://api.golfcore.org/v1 with a server-side API key and a User-Agent that includes https://github.com/augsmog/QuickScore (scoresnap/supabase/functions/course-data/index.ts:88). Usage rows store endpoint, HTTP status, and rate-limit headers, not the query text (scoresnap/supabase/functions/course-data/index.ts:93-94).

If the course-search modal opens and expo-location loads, the UI requests foreground location and keeps latitude and longitude in component state to sort results (scoresnap/src/ui/CourseSearch.tsx:40-51, 118-126, 156-160). searchCourses does not add those coordinates to the request (scoresnap/src/services/course-api.ts:22-25).

Purchases

When the RevenueCat native module is present and an Apple or Google public SDK key is set, the app configures RevenueCat and fetches customer info and offerings (scoresnap/src/services/purchases.ts:53-87, 106-110). Purchases and restores go through that SDK (scoresnap/src/services/purchases.ts:142-174). The paywall can present RevenueCat's paywall UI for the entitlement SnapScore Pro (scoresnap/app/paywall.tsx:45-54). Profile can present RevenueCat's customer center (scoresnap/app/(tabs)/profile.tsx:129-132). The app does not pass a SnapScore user id into Purchases.configure, and nothing calls identifyUser or logOutPurchases (scoresnap/src/services/purchases.ts:76-79, 214-234). Card numbers are not read by this code. The comment in the purchases file says web billing is connected to Stripe in the RevenueCat dashboard (scoresnap/src/services/purchases.ts:1-4). That dashboard is not in the repo.

The stripe-webhook function, if Stripe posts an event, stores the event id, type, customer email, amount, currency, subscription id, and metadata, and it can email that information to ALERT_EMAIL through Resend (scoresnap/supabase/functions/stripe-webhook/index.ts:11, 16-35, 68-80, 108-114). If RESEND_API_KEY is missing, the webhook writes the alert subject and HTML body to its log. That body includes the customer email (scoresnap/supabase/functions/stripe-webhook/index.ts:17-19, 108-113). The destination is the function log, not another processor. STRIPE_WEBHOOK_SECRET is read and never used, and the stripe-signature header is stored in sig and never used (scoresnap/supabase/functions/stripe-webhook/index.ts:10, 54-63).

Email sent by edge functions

on-user-signup can POST the user's email and referral code to https://api.resend.com/emails, with the from header ScoreSnap <hello@scoresnap.app> (scoresnap/supabase/functions/on-user-signup/index.ts:24-39). The HTML offers a free month of Premium after three referrals (scoresnap/supabase/functions/on-user-signup/index.ts:60-65). A third referral can email the referrer at the address stored on their profile, from the same header (scoresnap/supabase/functions/on-user-signup/index.ts:158-176). If RESEND_API_KEY is missing, the signup function writes the recipient address to its log (scoresnap/supabase/functions/on-user-signup/index.ts:24-27). It does not send that mail to ALERT_EMAIL. Stripe alerts use Resend with from header ScoreSnap <alerts@scoresnap.app> and to: ALERT_EMAIL (scoresnap/supabase/functions/stripe-webhook/index.ts:22-34). This repository does not show that those from-addresses are inboxes anyone can write to. They are not the support contact.

Signup and Stripe handlers also insert automation_log rows. The signup comment says a Notion sync picks those rows up (scoresnap/supabase/functions/on-user-signup/index.ts:197-203, scoresnap/supabase/functions/stripe-webhook/index.ts:37-44). No Notion client call is in the function. The log title for a signup includes the email (scoresnap/supabase/functions/on-user-signup/index.ts:198-201).

Diagnostics

Sentry is initialized for the app except on public web share pages (scoresnap/app/_layout.tsx:26-28, scoresnap/src/config/public-share.ts:4-8). The DSN host is ingest.us.sentry.io. The Expo plugin names organization august-industries and project scoresnap (scoresnap/app.json:56-62). The init sets:

The root component is wrapped with Sentry.wrap (scoresnap/app/_layout.tsx:103). The error boundary sends the exception and the React component stack (scoresnap/src/ui/ErrorBoundary.tsx:26-29). Purchase failures are sent with tags service=purchases and an action name (scoresnap/src/services/purchases.ts:113, 159, 180). Auth and purchase init failures are sent the same way (scoresnap/app/_layout.tsx:126-127). A dev-only button sends new Error("First error") (scoresnap/app/(tabs)/profile.tsx:718-727). Application code does not call Sentry.setUser.

Other packages that do not, by themselves, show a user-data upload

These are in scoresnap/package.json or app.json and were checked so they are not silent gaps:

Why the data is used

The code uses each flow for the behavior next to it:

This repository does not contain an advertising SDK, an App Tracking Transparency prompt, or code that sends data to an ad network. That is a statement about this source tree, not about a vendor's account settings.

Where it goes and who processes it

DataFirst hop from the appNext hop in this repoProcessor named by the code
Email, password, name, Apple or Google tokenSupabase Auth URL from EXPO_PUBLIC_SUPABASE_URLprofiles trigger; optional on-user-signupSupabase (scoresnap/src/db/supabase.ts:6-7, 46)
Captcha tokenSupabase Auth, after the widget has already talked to Cloudflarenone in this repoCloudflare and Supabase (scoresnap/src/services/turnstile-html.ts:3, scoresnap/src/stores/auth-store.ts:145)
Scorecard imageocr-scorecard on that Supabase projecthttps://api.anthropic.com/v1/messagesSupabase and Anthropic (scoresnap/src/services/claude-ocr.ts:33-37, scoresnap/supabase/functions/ocr-scorecard/index.ts:8, 116)
Live snapshot, chat, tournament rowsSupabase RPC or tablesstored in Postgres as citedSupabase
Course querycourse-datahttps://api.golfcore.org/v1 on a cache missSupabase and GolfCore (scoresnap/supabase/functions/course-data/index.ts:88)
Purchase and entitlementRevenueCat SDK; store payment sheetnot shownRevenueCat, and Apple StoreKit or Google Play as described in the purchases file (scoresnap/src/services/purchases.ts:1-4, 150)
Stripe event, if the webhook is calledthe webhook receives itrevenue_events, profiles, ResendStripe (sender), Supabase, Resend (scoresnap/supabase/functions/stripe-webhook/index.ts:22, 69)
Welcome and referral emailResendthe recipient mailboxResend (scoresnap/supabase/functions/on-user-signup/index.ts:30)
Crashes, traces, replay, logsSentry DSN in app/_layout.tsxnot shownSentry, org august-industries, project scoresnap (scoresnap/app.json:56-62, scoresnap/app/_layout.tsx:37-51)
Google sign-inhttps://accounts.google.com and https://oauth2.googleapis.com/tokenSupabase, when real auth is onGoogle and Supabase (scoresnap/app/auth/sign-in.tsx:29-31)
Apple sign-inApple's sign-in APISupabase, when real auth is onApple and Supabase (scoresnap/app/auth/sign-in.tsx:69)

Anyone who has a live-share token can read the published names, scores, and chat (scoresnap/src/services/live-share.ts:30-33). That is sharing by link, implemented in the app, not a separate analytics export.

Retention

Implemented in this repository:

Not implemented: a retention period or deletion schedule for accounts, profiles, contest rows, score rows, chat rows, share snapshots, marketing events, revenue events, automation log, feedback, scans, or local photos. The scans and feedback tables exist (scoresnap/supabase/migrations/20261027000000_live_baseline.sql:100-116, 244-253). Application code does not insert into them.

Deletion and how to ask for it

The settings screen offers Delete Account. After two confirms it removes snapscore-contests, snapscore-auth, snapscore-settings, snapscore-scan, and snapscore-onboarding, then calls signOut and returns to onboarding (scoresnap/app/settings.tsx:226-257). Read the gaps at the top before relying on that button. It does not call Supabase to delete the user. Sign-out calls supabase.auth.signOut only when real auth is configured (scoresnap/src/stores/auth-store.ts:220-221), which clears the session through the SecureStore or localStorage adapter (scoresnap/src/db/supabase.ts:33-39).

A host can revoke live links and mark a chat message removed. Both leave the underlying rows in place (scoresnap/supabase/migrations/20261027000000_live_baseline.sql:736, 776).

There is no in-app form that submits a deletion request to a server. To ask for deletion of data this button does not remove, write to augie@autumn8.me or use https://snapscore.autumn8.me/support.

Children

The app does not ask for a date of birth, and it does not contain an age check or a parental-consent flow. Searches of the application and SQL for an age gate did not find one. Player handicaps and bet units are part of the contest (scoresnap/src/engine/types.ts:20, scoresnap/src/stores/settings-store.ts:13). Whether the app is directed to children, and which store age rating to set, is not decided in this repository. The owner has to make that decision before publication. This draft does not claim an age floor the code does not enforce.

Contact

Support contact: augie@autumn8.me

Support URL: https://snapscore.autumn8.me/support

Privacy policy URL: https://snapscore.autumn8.me/privacy

The app's Help control does not open a page (scoresnap/app/(tabs)/profile.tsx:695). The Privacy Policy and Terms controls open https://snapscore.autumn8.me/privacy and https://snapscore.autumn8.me/terms (scoresnap/app/settings.tsx:610, 630, scoresnap/app/auth/sign-in.tsx:610-614). Those in-app links are separate from the support and privacy URLs above.

Off-device data-flow inventory

Every path below can send data off the device, or is a server function that sends data onward. Local-only code is listed after the table so it is not mistaken for a disclosure gap.

#FlowLeaves the devicePayload this code sendsGated by
1Supabase Auth sessionYessession refresh, email, password, Apple identity token, Google tokens, optional captcha token, name metadataREAL_AUTH_CONFIGURED (scoresnap/src/config/auth.ts:2, scoresnap/src/stores/auth-store.ts:76-180)
2Apple sign-in APIYes, when the user uses the Apple button on iOSApple-requested name and email scopes; noncethe button (scoresnap/app/auth/sign-in.tsx:69-75)
3Google OAuthYes, when a Google client id is set and the user continuesOAuth code exchange for openid, profile, emailclient id (scoresnap/app/auth/sign-in.tsx:102-118)
4Cloudflare TurnstileYes, when captcha mode is optional or required and the user is signing up or in with emailwidget execution; token returned to the app and then to SupabaseCAPTCHA_MODE (scoresnap/src/config/captcha.ts:8-10)
5ocr-scorecard then AnthropicYes, unless ML Kit returns at least two players at confidence 0.7 or higherbase64 JPEG or PNG and media type, with the user's Supabase JWTfewer than two players, confidence below 0.7, zero players, ML Kit unavailable, or ML Kit throwing (scoresnap/src/services/ocr-service.ts:46-115)
6Live share RPCsYes, when real auth is on and a non-anonymous user sharessnapshot of names, scores, totals, game unit standings; share token handling. The raw URL then goes to the clipboard or the system share sheet (scoresnap/src/ui/LiveShare.tsx:39-40)feature flag for the UI; liveShareEnabled for republish (scoresnap/src/ui/LiveShare.tsx:11, scoresnap/src/services/live-share.ts:7-23)
7Public scoreboard readYes, for anyone opening /r/<token>the tokennone (scoresnap/src/services/live-share.ts:30-33)
8Chat RPCsYestoken or local id, display name, body, raw 32-byte session on posta valid share token (scoresnap/src/services/live-chat.ts:25-46)
9Tournament table writesYes, when real auth is on and the session is not anonymousround, course, group, player name, handicap, scoresscore sync, add round, and group sync (scoresnap/src/stores/contest-store.ts:193-196, 294, 319)
10course-data then GolfCoreYes, when the feature flag is on and the query is at least 2 characterssearch text or slug; server adds the GolfCore call on cache miss. A separate tap can open https://www.golfcore.org/ in the browser (scoresnap/src/ui/CourseSearch.tsx:533)COURSE_SEARCH_ENABLED (scoresnap/src/services/course-api.ts:22-24)
11RevenueCat and store billing UIYes, when the native module and API key existSDK configure, offerings, customer info, purchase, restore, paywall, customer centerkeys and native module (scoresnap/src/services/purchases.ts:66-79, scoresnap/app/paywall.tsx:52)
12SentryYes, outside dev, and outside /r/ web pagesexceptions, traces, navigation, replay (text and images masked by the 8.4.0 defaults), feedback integration, enableLogs: true, sendDefaultPii: true, release and environmentSentry.init (scoresnap/app/_layout.tsx:26-61, scoresnap/package-lock.json:7020)
13Expo Updates endpointConfigured, call not present in app sourcenot visible in app sourceapp.json updates URL (scoresnap/app.json:82-87)
14on-user-signupServer-side, if the function is invokedemail and referral content to Resend; rows in profiles, marketing_events, automation_loginvocation is not shown in the app (scoresnap/supabase/functions/on-user-signup/index.ts:30, 102, 186)
15stripe-webhookServer-side, if Stripe calls itemail, amount, subscription id to Supabase and Resend (to: ALERT_EMAIL). If Resend is unset, the same alert body is written to the function log (scoresnap/supabase/functions/stripe-webhook/index.ts:17-19, 108-113)inbound POST (scoresnap/supabase/functions/stripe-webhook/index.ts:22, 30, 69)

Checked and not an off-device user-data flow in application source: ML Kit recognize call, pasted scorecard text, local guest join, haptics, bundled fonts, the unused SQLite initializer, the settings toggles that nothing else reads, and the empty Help button. Precise location is read into memory only when expo-location loads, and it is not added to the course-search request.

Sources

Citations are path:line in the sections above, against commit 92b6c9b. The files are: