Technical privacy assessment
This assessment records the current data flows and release checks. It does not certify compliance or replace the controller's assessment under the GDPR. The privacy policy describes the user-facing choices.
Status: The first-party v2 collector was verified with synthetic events on the production server. Trilho’s own Firebase/GA4 project is configured and part of the app since version 1.1.0 (September 2026); it collects only after the consent described below.
GA4 event and user retention is set to 2 months, with reset on new activity off. This setting does not cover standard aggregated reports or prove physical deletion. Google Signals, user-provided data, granular location/device collection and ad personalisation are disabled.
What is processed
Trilho works without an account. Route and nearby-stop requests may send precise or approximate coordinates and stop identifiers to the server. Stop searches go to Trilho as you type. Explicit place searches and shared map links may reach Nominatim or the relevant map-link provider. Geocoding uses a bounded memory cache with a 24-hour freshness period; this is not purely real-time processing.
OpenFreeMap provides light and dark maps and receives network requests and the viewed area. Google ML Kit may send SDK diagnostics separately from Trilho's usage-sharing setting.
V2 optional events include installation UUID, event ID for deduplication, day, limited categories, platform, app version and environment. Measurement covers sessions, planner visits, route attempts, map loads, return milestones, displayed answers and active days; technical delivery counters record discarded events. Local observation starts with consent: a cohort is that consent period, not a person or first installation. Returns on days 7/30 are positive milestones after enrollment is acknowledged; unknown endings are not classified as errors.
Since version 1.5.0, optional events also include how receiving a place shared from Google Maps ended (route, pair of points, choice between candidates, search or failure), which teaching surface preceded it, whether it was the first share, a response-time bucket, the tips shown and tapped, and a cohort milestone (first share on days 0–6). Never the address, name, coordinates, link or searched text. The app keeps on the phone, without sending them, the flags it needs so as not to repeat the tips. Google Maps opens only on an explicit tap, with the text typed in the search or just the map; from then on Google’s policy applies.
On iPhone, since version 1.5.0, if the user turns on “Notification after sharing” (the system permission prompt appears only after that tap, never at launch or during onboarding), the Share extension creates on the phone itself a local notification with only the place’s name, when the source app provides it, and a fixed text — never the address, link or coordinates. The notification is created and delivered by the phone’s own system, with no APNs, no device token and no Firebase Messaging: nothing new leaves the phone and Trilho’s server receives nothing new. What was shared stays in a protected app file, as before: up to 15 minutes to open Trilho manually and up to 24 hours to tap the notification; once it is used, expires or is replaced by another share, the file and the notification are deleted. The place name may appear on the lock screen, depending on the iOS notification-preview setting. With optional sharing on, events record whether the trip was opened from the notification or manually, a time-since-sharing bucket, whether the notification was shown (or why not), whether a saved share expired or was dropped without opening the trip, the answer to the permission prompt, and the offer of this option shown and tapped — never the place, name or shared text. The app keeps on the phone, without sending them, the choice and flags it needs so as not to repeat the offer. No new data categories.
Also since version 1.5.0, optional events include a cohort milestone when the planner shows an answer on days 0–6 of the consent period, in the same optional statistics and with no new data categories.
The server uses persistent SQLite storage. Deduplication records contain the SHA-256 installation hash, event ID and payload fingerprint, with an 8-day event-day retention window. Counts by installation/category/day retain pseudonymous linkage for 45 days. Cleanup runs at startup and hourly; these windows do not guarantee physical deletion at an exact moment or automatically cover backups or providers. This stream contains no addresses, coordinates, free text, NFC contents or journey histories.
Trilho’s internal reports suppress positive groups below 25 self-reported installation IDs; that does not establish distinct people or anonymity. Google Analytics for Firebase separately receives individual semantic events after new consent. The SDK adds its own app-instance identifier, event times, app lifecycle/session data and technical context, and can derive approximate location from IP. It receives no Trilho installation/event/session IDs, GPS coordinates, search text, card contents or journey histories. Advertising identifiers, ad personalisation and screen reporting are disabled; no user ID is set. The 25-ID threshold and day-only timestamps do not apply to Google or individual Sentry reports. See Google’s SDK disclosure.
The configured GA4 retention is separate from the own-store 45-day window; physical deletion has not been verified. No EU-only processing or automatic deletion of uploaded provider data is promised. The SDK has its own delivery queue; invoking it is not a receipt. Exact first-party KPI definitions remain based on accepted records. Cohorts combine app versions and become mature after the observation window plus seven delivery days: D7 on day 15 and D30 on day 38, with enrollment on day 0.
The local queue holds at most 2000 events and discards events older than 7 days. Delivery is attempted during use, at startup, on consent and when connectivity returns; background delivery is not guaranteed. Only received starts can enter denominators, and a missing outcome remains unknown.
Saved journeys, preferences and transport-card history are stored locally. The app has no background-location permission and does not upload the contents of transport cards.
Since version 1.6.0, Home and Work can be an address rather than only a stop, and saved trips keep their ends and the line the Home screen follows (operator, line, boarding and alighting stops, minutes on foot). All of this stays in the local database and the system backup; a counter of repeated searches (coordinate pairs and days, up to 30 pairs and 14 days) never leaves the phone and is cleared with “Clear recent history”. While Home is on screen, the app asks the server for that line’s departures with its boarding and alighting stops, at most 5 requests a minute; the server does not store them (its log holds only status and method). The Home and Work coordinates reach route planning when the user opens the trip, and once to determine the line after they are set. “I’m at home now” and saving from “Your location” send the coordinates to the address lookup only on an explicit action. With optional sharing on, events record closed categories — which place was set or cleared and of what kind, how a trip was saved, the answer to the suggestion to save it, which card was opened and the state of the times shown — never addresses, stops, lines, times or coordinates. No new data categories.
With Trilho’s widgets (on the home screen and, on iPhone, also on the Lock Screen), while a widget is placed the phone asks the server for the next departures of the saved trip or the pinned stop, periodically and without any action by the user: about every 15 minutes on Android, on iPhone when the system decides, usually every 15 to 60 minutes, and also when the app opens or “Refresh” is tapped. The request carries only the operator, stop and line codes — never coordinates, addresses or names — and the server does not log it (its log holds only status and method). Without consent, the server only counts how many widget requests it receives per day, platform, widget type and reason, with no IP address, identifier or stops, and keeps these totals for 400 days. The copy the widget needs (trip titles, stop names, lines and departures) stays on the phone: outside the backup on Android; on iPhone, in a container shared with the widget, protected until the first unlock after the phone starts and excluded from the backup. On the iPhone Lock Screen, place and stop names are hidden if the user turns off access when locked. With optional sharing on, events record closed categories — widgets added, removed or configured, their type and size, the state shown when a widget was tapped, the steps of the suggestion to add a widget and, once a day, which widget types are placed and whether a trip is saved — never stops, lines, times or places. The app still has no background location. No new data categories.
In “My passes”, the user can set the city pass (Navegante Metropolitano, Municipal, +65, Urbano 3ª Idade or youth; Cartão Porto, Andante Metropolitano or Andante youth), that they pay by bank card instead of a pass, and whether they hold CP’s Passe Ferroviário Verde. The answers stay on the phone; the municipality chosen for the Municipal pass never leaves it, and route requests do not include the passes (the server marks each leg the same way for everyone and the app decides “Included” on the phone). With optional sharing on, events and Google user properties record only the chosen category. Some categories reveal more than a pass: +65, Urbano 3ª Idade, youth and Andante youth indicate an age group (65 or over, up to 23), Urbano 3ª Idade also a tax address in Lisbon, and Cartão Porto that the person lives or studies in Porto; the bank card is a means of payment and reveals neither age nor address. First-party reports only show groups of at least 25 installations; Google receives the properties without that threshold.
Since version 1.8.0, in Lisbon and Porto, a trip says whether a bank card or phone can pay for each leg, and the “How to pay for this trip” sheet explains how. It is information only: Trilho does not read, store or ask for bank card details and makes no payments. With optional sharing on, an event records that the sheet was opened from the trip and what the answer was (the card is enough, except on one leg, may not be enough, or a ticket is needed) — never the trip, the operators or the places. No new data types or recipients.
The one exception, since 28 September 2026, is a stop hint the rider chooses to send («Sabes que paragem é?»): the operator, route and stop codes of that tap, entry/exit and the stop picked from Trilho’s catalogue — never the card number, trip times, balance or any device, installation or analytics ID, and no free text. The server stores vote totals only (no row per hint, no time, no IP). The IP is used in memory for a rate limit and a same-day salted fingerprint whose secret rotates daily and is never persisted. Accepted names (at least three agreeing hints and 75 % of the code’s hints, only when the line is known, or a manual check) are served to every user.
Controls and remaining checks
- First-party metrics, Google Analytics/Firebase and Sentry require current opt-in. Migration resets a previous grant to undecided and preserves a denial. It clears the local queue/session/cohort state and replaces the UUID; no consent is inferred for Google. The app remains usable without sharing. Withdrawal disables collection, clears local state and resets Google’s local analytics data/app-instance ID; it does not automatically delete received records.
- Request-log filtering covers coordinates and typed/shared text. The release configuration disables duplicate Caddy access logs; deployment still needs verification.
- SQLite on the persistent volume and rejection of unauthenticated admin access were verified in production. Continue checking 8/45-day cleanup over time, including after restart. Verify backups, Sentry retention, providers, contracts, transfer safeguards and actual provider deletion separately. No fixed 7/90-day retention is assumed for all logs.
- Verify SDK traffic and payloads on fresh installation, upgrade with old consent, restart, grant and offline withdrawal. Earlier Sentry evidence does not validate Firebase. Check disabled advertising identifiers, screen reporting and ad personalisation in the final binaries and property settings.
- External place search requires an explicit action; the server globally limits forward and reverse lookups to one request per second. Verify deployment of these controls and provider terms before public release.
Accountability and rights
The controller is Trilho’s independent developer in Portugal (see the privacy policy). The controller’s own assessment of 3 October 2026, made without legal advice: (a) a data protection officer is not required under Article 37 GDPR, because Trilho is not a public authority, its core activities are not regular and systematic large-scale monitoring of people (location is used in the foreground and only when a feature needs it; usage statistics are opt-in) and it does not process special categories of data on a large scale; (b) a formal Article 35 impact assessment is probably not mandatory for the same reasons, but this assessment and the internal one are kept as if it were; (c) the internal assessment also serves as the record of processing activities (Article 30). These conclusions are reviewed whenever the processing changes. GDPR duties depend on the processing and its risk, not on a user-count threshold. Residual risk has not been signed off as low.
No account does not mean no personal data or no rights. Data-subject requests and complaints can be sent to [email protected]. Deleting the app does not automatically delete server or provider records. Incidents must be assessed under GDPR Articles 33 and 34, including notification deadlines where applicable. You may complain to CNPD.