Privacy Policy
Privacy Policy
Last updated: 2026-05-31
This page describes what personal data Ibis Ballistic collects, how
long it is kept, and the controls users have over it. The application
is a ballistic calculator + shooting logbook; the data we hold is
minimal by design.
Data we collect
| Category | Source | Stored where | Purpose |
|---|---|---|---|
| Registration form / Google OAuth | users.email |
Account identifier, password-reset, account-state notifications | |
| Password hash | Registration form | users.password_hash (bcrypt cost 12) |
Authentication. The plaintext is never stored. |
| OAuth provider | Google OAuth | users.oauth_provider |
Indicate which third-party identity is linked |
| IP address (truncated) | Every authenticated request | audit_log.payload.ip_subnet (/24 for IPv4, /48 for IPv6) |
Rate-limit + forensic trail. Full IPs are never persisted. |
| User-Agent (truncated) | Login flow | active_sessions.device_label (first 200 chars) |
«Active devices» list on the security page |
| Ballistic profiles | User input | complex_profiles, user_calculator_profiles |
Save shooting setups |
| Shooting sessions | User input via the Logbook | sessions, groups |
Track training history |
We do not collect: full IP addresses, browser fingerprints,
trackers, analytics pixels, per-user page-view logs, third-party
ads. (We do keep anonymous, aggregate page-view counts — see
Anonymous usage analytics below — which
carry no identifier and are not personal data.)
Anonymous usage analytics
To decide which features and calculator tools are worth maintaining, the
app keeps aggregate, anonymous page-view counts — and nothing else.
- What is stored: one counter row per
(day, route)— e.g. - What is NOT stored: no user id, no IP address, no session, no
- No cookie, no consent: because the counts are anonymous aggregates
- Retention: counter rows are purged after 180 days
- Operator control: an administrator can disable the counting
«/tools/twist was viewed 42 times on 2026-05-31». The key is the
route template (not the exact URL you visited), so record ids never
enter the data.
cookie, no per-request log line. Counting works by incrementing the
aggregate on each page-view, so individual browsing behaviour is never
recorded and cannot be reconstructed.
they are not personal data under GDPR — they require no consent
and set no cookie, consistent with the «no analytics cookies» promise
under Cookies. Collection is purely server-side.
(scripts/workers/cron_usage_counters_purge.sh).
entirely (usage_telemetry_enabled).
Retention
| Data | Retention | Mechanism |
|---|---|---|
| Audit log | 1 year | scripts/workers/cron_audit_purge.sh (#891) |
| Password-reset tokens | 1 hour | scripts/workers/cron_purge_tokens.sh (#975) |
| Email-verification tokens | 24 hours | same cron |
| Soft-deleted accounts | 30 days then hard-purge | scripts/workers/cron_purge_soft_deleted.sh (#965) |
| Active-session rows | 14 days past last_seen | scripts/workers/cron_purge_sessions.sh (#1012) |
| Anonymous usage counters | 180 days | scripts/workers/cron_usage_counters_purge.sh (#1561) |
| User data after account deletion | 30-day grace; then full cascade-delete | hard_delete_user (#965) |
| Backups | 30 days | BACKUP_DIR retention |
Your rights
- Access — GET
/profile/exportreturns a JSON dump of every - Deletion — POST
/profile/delete-accountstarts a 30-day soft-delete - Correction — Edit any field via the profile / logbook editors.
- Withdrawal of consent — Delete the account; we do not retain
field tied to your account.
grace; after that the row + all cascading tables are dropped and the
audit_log entries are anonymised (PII stripped, security-trail
preserved).
data past the grace window.
GDPR Art. 15-17 / 21 are honoured by the above flows. Requests can also
be sent to the admin email on the contact page.
Cookies
| Cookie | Type | Purpose | Lifetime |
|---|---|---|---|
access_token |
Strictly necessary | JWT for session auth | 1 hour |
refresh_token |
Strictly necessary | Rotate access tokens | 7 days |
_csrf |
Strictly necessary | CSRF protection | Session |
All three are HttpOnly + Secure on HTTPS deploys; SameSite=Lax on the
access cookie, SameSite=Strict on refresh. There are **no analytics or
advertising cookies** — the usage analytics described above is
cookieless and anonymous.
Third parties (sub-processors)
The following third-party services receive limited personal data
when you use the app. None receive your password, ballistic
profiles, or logbook contents.
| Sub-processor | Data shared | Purpose | Operator opt-out |
|---|---|---|---|
| Google OAuth (optional) | Email + verified flag | Identity attestation when you choose «Sign in with Google» | Unset GOOGLE_OAUTH_CLIENT_ID env-var (button disappears, password-login still works) |
| Resend (transactional email) | Recipient email, subject, body, message-ID | Password reset, email verification, admin alerts. Body is constructed per-request; not stored locally. | Unset RESEND_API_KEY in non-prod (prod requires it — serve.py fail-fast) |
| ipwho.is (IP→geo fallback, #1371) | Client IP address at request-time | Fallback geo lookup for /api/weather/live when browser geolocation returns POSITION_UNAVAILABLE / TIMEOUT. Returns latitude + longitude. The full IP is not stored locally — only the /24 (IPv4) or /48 (IPv6) prefix lands in audit_log. |
Set IBIS_IP_GEO_DISABLED=1 (recommended for strict-GDPR deploys per docs/env-vars.md); or swap the resolver via IBIS_IP_GEO_PROVIDER_URL to a self-hosted MaxMind / GeoIP service |
| Railway (hosting) | Application logs (HTTP method / path / status / IP), Postgres rows, uploaded images | Hosting + runtime infra. Retention follows Railway's platform policy. | Self-host — the app is open-source (MIT, #1395) |
Google's, Resend's, ipwho.is's, and Railway's own privacy policies
apply to what they collect from that interaction. We do not share
data with these processors beyond what each row above lists.
Contact
Questions about this policy or your data: write to the operator email
listed in the deployment's ADMIN_ALERT_EMAIL (visible in the page footer).
Privacy · Terms · Back to app