Privacy Policy
Service: Rillo
Policy version: 2026-10-05
Last updated: 2026-10-05
Audience: members of the Rillo social discovery network and partner businesses listing events on our marketplace.
This document explains what personal data we collect, how we use it, how long we keep it, and the rights you can exercise over it under the EU General Data Protection Regulation (GDPR), the UK GDPR, and the Australian Privacy Act 1988. It is the canonical reference for both our members and our engineering team — every behaviour described below is implemented in the codebase, not aspirational.
If you read one section, read §7 — Your rights. It explains how to exercise each right and which endpoint backs each one.
The version string at the top of this file is the same string the application uses internally (
Application/Abstractions/PrivacyPolicyVersions.cs → Current). When we change this policy in a way that affects what data we collect or how we use it, we bump that constant in the same pull request that updates this file. Members are then asked to re-acknowledge the policy on their next sign-in to a flow that requires consent.
1. Who we are
Rillo (the "service", "we", "us", "our") is a social discovery platform that helps adults make new friends nearby and discover events hosted by local partner businesses. The data controller for personal data described in this policy is the legal entity operating the service in your region — contact details are in §13.
This policy applies to:
- the mobile app (iOS and Android, built on .NET MAUI)
- the public REST API that the mobile app talks to (
api/v1/...) - the partner portal at the website host (used by businesses to list events)
- the admin dashboard at the website host (used only by Rillo staff)
- the identity service that handles sign-up, sign-in, two-factor authentication, and social login
It does not apply to third-party services we integrate with (Stripe, Apple, Google, OpenStreetMap geocoding) — each has their own privacy policy and we name them in §9.
2. Data we collect
We collect the minimum data necessary to operate the service. Here's the complete list, grouped by where it comes from.
2.1 Account data — created when you sign up
- Email address (for sign-in and account recovery)
- Hashed password (we never see your plaintext password)
- External identity claim if you sign in with Google or Microsoft (the provider's
subvalue, not your provider password) - Two-factor authentication secret if you enable TOTP
- Recovery codes if you generate them
- Account creation timestamp
2.2 Profile data — provided during onboarding
- Display name (2–60 characters)
- Optional bio (up to 500 characters; URLs are not allowed)
- Date of birth: day, month and year (we store it; other members only ever see your age, never the date). You must be 18 or older, and once you have finished onboarding you can correct it only by contacting support
- Birth year, social-energy score, friendship intent, group-size preference
- Gender and optional custom gender label
- Travel time / radius preferences and whether you allow city-wide meetups
- Availability preferences (which day-parts work for you)
- Interests you select from our catalogue
- Location: suburb, state, country, latitude/longitude, search radius
- Up to a small number of profile photos
- Discovery preferences: preferred genders, minimum/maximum age
- Privacy consent timestamp and the version of this policy you accepted
We do not collect: phone number, postal address, government identifiers, employment, education, religious belief, ethnic origin, political opinion, trade-union membership, health information, sexual life, sexual orientation, biometric data, or genetic data. If you put any of these in your bio voluntarily we cannot stop you, but the service does not ask for them and treats anything in the bio as free-form text.
2.3 Activity data — generated as you use the service
- Swipe decisions (like / pass / super-like, including ones you later undo)
- Matches (which other members you have mutually liked)
- Messages you send through matches and circles
- Profile views: which other profiles you have viewed and which members have viewed yours (used for the "who viewed me" feature on Premium tiers)
- Compatibility scores between you and other members (computed on-the-fly, not stored)
- Last-active timestamp (refreshed at most every five minutes per member)
- Earned badges (achievements such as completing onboarding, sending your first message, attending an event)
2.4 Event and marketplace data
- Events you register for, attend, or are waitlisted on
- Event-attendance lifecycle timestamps (registered, withdrawn, promoted from waitlist, attended)
- If you are a partner, the businesses you operate, the events you publish, and the verification documents you upload to verify your business
2.5 Subscription and billing data
- Your current subscription tier (Free, Plus, Premium) and entitlement status
- Billing platform (Stripe, Apple, Google) and the corresponding external customer ID
- Period start/end dates, trial status, and renewal flag
- Webhook event IDs we have processed (for deduplication)
We do not store full credit card numbers, CVV codes, or bank-account numbers. Card data is held by our payment processors (Stripe / Apple / Google) under their own terms. We only see the last four digits and the card brand if the payment processor supplies them.
2.6 Operational data — generated by infrastructure, not by you
- Request logs (correlation ID, member ID, endpoint, status code, latency)
- Authentication audit events (sign-in success/failure, token issue, role change)
- Moderation and admin audit events (who approved which subscription grant, who decided which erasure request)
- Health-check probe results
- Outbox messages — a transactional queue of domain events, used to fan out notifications and analytics reliably
Logs and traces are retained for 30 days unless tied to a security incident; see §4 — Retention.
2.7 Photo verification selfies — optional
If you want the verified badge shown next to your name, you can submit a selfie through Profile → Verification. You need at least one visible profile photo already on your profile before you can request it.
- Optional and member-initiated. Nothing else in the product requires it, and it isn't part of onboarding.
- Reviewed by a human, not a machine. The selfie exists for one purpose: a member of our staff manually compares it against your existing profile photos to confirm you look like the person in your profile. We do not run automated face-matching or biometric comparison, and we do not send the image to any third party for processing — the comparison happens entirely on our own infrastructure, by our own reviewers.
- Never shown to other members. Only the outcome of the review — a small verified checkmark — is visible to anyone else. The selfie itself is stored under a private storage path (prefixed
verification-selfies/) that our public, member-facing photo endpoint is structurally unable to serve; it is reachable only through a separate, authenticated staff-only review screen. - What happens to your request: a reviewer approves or rejects it, and you get a notification either way. Approval adds the verified badge to your profile; you can try again after a rejection. If you change your profile photos after being verified, your verification is automatically queued for another look — you keep the badge while that re-check is pending, and it is only removed if the re-check is rejected. Every review decision is recorded in our moderation audit log — the same mechanism used for photo-flagging decisions — capturing which staff account made the call and when, in addition to the standard audit stamp applied to every editable record in the system.
- Retention: once a reviewer makes a decision, we keep the selfie image for a further 90 days (in case the decision needs to be revisited) and then delete it automatically — see the retention table in §4. If you request erasure of your account before the 90 days is up, the selfie is deleted immediately along with your other photos rather than waiting for the timer.
3. How we use your data, and the legal basis
| Purpose | Legal basis (GDPR Art. 6) |
|---|---|
| Operating your account, authenticating you, recovering access | Contract (Art. 6(1)(b)) |
| Showing you nearby members and events you might want to meet | Contract (Art. 6(1)(b)) |
| Enforcing minimum age (18+) and our community guidelines | Legal obligation (Art. 6(1)(c)) and legitimate interest (Art. 6(1)(f)) |
| Tailoring the wording of the sign-up questions to your age group (18–24, 25–35, 36–50, 51+), worked out from your date of birth; the questions and the data we collect are the same for everyone | Legitimate interest (Art. 6(1)(f)) |
| Sending in-product notifications (matches, messages, event reminders) | Contract (Art. 6(1)(b)) |
| Sending marketing email about new features | Consent (Art. 6(1)(a)) — opt-in only, withdrawable at any time |
| Processing payments and entitlement changes | Contract (Art. 6(1)(b)) |
| Detecting fraud, abuse, and policy violations | Legitimate interest (Art. 6(1)(f)) |
| Answering support requests, exercising legal claims | Legitimate interest (Art. 6(1)(f)) |
| Statistical aggregates and product analytics (member-level identifiers stripped) | Legitimate interest (Art. 6(1)(f)) |
We do not sell your data, share it with data brokers, or use it for cross-context behavioural advertising. We do not perform automated decision-making that has legal or similarly significant effects on you (the matching algorithm ranks candidates, but it does not make any decision about you in the GDPR Art. 22 sense — you can swipe whoever you like).
4. How long we keep your data
We keep data for the shortest period that's compatible with the purpose we collected it for.
| Data | Retention | What happens at the end |
|---|---|---|
| Active account profile | Until you delete it (see §7.2) | Anonymised in place, then soft-deleted |
| Photos | Same as profile | Deleted from blob storage during the erasure job, before the row is soft-deleted |
| Photo verification selfies | 90 days after a reviewer's decision (or immediately, if you request erasure first) | Deleted from blob storage by the daily VerificationSelfieCleanupJob; deleted immediately by the erasure job instead, if erasure happens first |
| Swipes, matches, messages | Same as profile | The counterpart's view of past matches is preserved (legitimate interest — they have a right to know who they spoke with) but your identifying fields are wiped |
| Subscription history | 7 years from last billing event (tax and consumer-law audit) | Aggregated to a tax row, identifiers stripped |
| Stripe webhook events (dedup table) | 90 days from receipt | Cleaned up by a daily background job |
| Apple/Google notification events (dedup table) | 90 days from receipt | Cleaned up by a daily background job |
| Raw store and payment webhook payloads (processing queue) | 7 days after successful processing; 30 days if processing permanently failed | Cleaned up by a daily background job |
| Idempotency cache | 24 hours | Cleaned up by an hourly background job |
| Outbox messages (transactional event queue) | 7 days after successful dispatch; 30 days for messages that permanently failed | Cleaned up by a daily background job |
| Application logs | 30 days | Rotated out of the log store |
| Authentication audit log | 1 year | Rolled into anonymised counts |
| Admin moderation audit log (subscription grants, erasure approvals, etc.) | 7 years | Retained for compliance audit |
| Inactive accounts | After 90 days without activity your profile is marked dormant and hidden from other members. Nothing is deleted, and it becomes visible again the next time you use the app. Members with an active paid subscription are never marked dormant. | Marked dormant by StaleProfileCleanupJob |
| Failed notification jobs (contain your email address and display name) | 14 days after the final retry fails | Expire automatically from the background-job store |
Soft-delete vs. hard-delete
When you exercise your right to erasure (§7.2), we anonymise the row in place rather than physically dropping it from the database. This is intentional: matches and messages you exchanged with other people are part of their personal data too, and their right to know who they spoke with does not vanish because you left. After anonymisation:
- your display name becomes
[deleted user] - your bio, photos, location, date of birth, gender, interests, availability, and discovery preferences are cleared
- your photo files are deleted from blob storage
- your link to the identity provider's
subclaim is scrambled toerased:{guid}so the row can no longer be reached through your sign-in identity - the row is then soft-deleted (hidden from every API query) and your identity-provider account is signed out and cannot be used to sign back in
The end result for any other member viewing a past match with you is "matched with [deleted user]" — they see the conversation history they participated in, with no way to identify you. This is the same approach used by every comparable service we are aware of.
5. Where your data lives
- Primary database: PostgreSQL, hosted in the same region as the host that serves you. Our production region is Australia (Sydney) for AU members and the EU (Frankfurt) for EU/UK members.
- Photo blobs: server-side file storage in the same region as the database.
- Backups: encrypted PostgreSQL backups, retained for 14 days, in the same region.
- Logs and traces: aggregated to an OpenTelemetry-compatible observability backend in the same region.
- Payment data: held by Stripe (US/EU/AU), Apple (US/EU/AU), and Google (US/EU/AU) under their own terms.
We do not transfer member personal data outside its origin region except (a) when the third-party payment processors named above legitimately need to process a transaction, or (b) when we have an explicit Standard Contractual Clauses (SCC) agreement in place for a service provider. If a transfer requires SCCs we list it in §9.
6. How we keep your data safe
- All API traffic is over HTTPS with TLS 1.2 or higher.
- Passwords are hashed with the ASP.NET Core Identity
PasswordHasher(PBKDF2 with HMAC-SHA256, currently 100,000 iterations) — we never store, log, or transmit plaintext passwords. - Two-factor authentication (TOTP) is available and recommended.
- API tokens are short-lived JWTs issued by our identity server. The mobile app stores them in the platform's secure storage (Keychain on iOS, Keystore on Android).
- File uploads are validated for content type, size, and key shape; the storage path is canonicalised so a malicious key cannot escape the storage root.
- Admin actions on member data (subscription grants, erasure approvals, tier assignments) are written to an immutable audit log.
- Webhooks from Stripe, Apple, and Google are signature-verified and deduplicated before they touch any state.
- Production secrets are externalised — they are never committed to source control.
- We run dependency-vulnerability scans and code scans on every commit.
If we ever discover a personal-data breach affecting you, we will notify you and the relevant supervisory authority within the timelines required by law (under GDPR, that is 72 hours from awareness for the supervisory authority, and "without undue delay" for affected individuals if the risk to your rights and freedoms is high).
7. Your rights
Under the GDPR / UK GDPR / Australian Privacy Act, you have the rights listed below. Every right is exercisable through the mobile app (under Profile → Privacy) or by contacting our DPO at §13. Each one is also backed by a documented API endpoint so a determined developer can verify the implementation.
7.1 Access — right to know what we hold (Art. 15) and portability (Art. 20)
You can download a complete machine-readable copy of every piece of personal data we hold about you, at any time, free of charge.
- Endpoint:
GET /api/v1/members/me/privacy/export - Auth: signed in as the member whose data is being exported (mobile API token).
- Response: a ZIP archive named
rillo-data-export-yyyyMMdd.zipcontaining JSON files for your profile, photos metadata, interests, availability, gender preferences, swipes, matches, messages you authored, active subscription, and earned badges. AREADME.txtinside the archive documents the file format and explains why some fields are excluded (in short: anything that would expose other members' personal data). - What's included: every field listed in §2 — Data we collect, with the exceptions below.
- What's excluded and why:
- Messages other members sent to you. They have a right to control their own personal data, and a portability export of "everything you can see in the app" would override that. Contact our DPO if you need a transcript for a legal dispute.
- Other members' profile data inside your matches list. We export the match (timestamps, status, the counterpart's profile ID) but not their name, bio, or photos.
- Operational logs (request logs, traces). These are not part of an Article 20 portability dataset; they're operational telemetry. You can request access to them under Article 15 by contacting the DPO.
- Aggregated statistics derived from your data after identifiers have been removed.
The export reflects your data at the moment you request it. There is no rate limit beyond standard API rate limiting; you can request it as often as you want.
7.2 Erasure — right to be forgotten (Art. 17)
You can ask us to delete your account and all personal data associated with it.
- Endpoint to request:
POST /api/v1/members/me/privacy/erasewith an optional reason (free text, up to 500 characters). - Endpoint to check status:
GET /api/v1/members/me/privacy/erase - Auth: signed in as the member whose account is being deleted.
What happens after you submit the request:
- Your request is recorded with a timestamp and your optional reason. Your status becomes Pending review. You are signed out straight away and can't sign back in while the request is pending; if the request is rejected, sign-in is restored.
- A member of our staff reviews the request. This is a deliberate human-in-the-loop step to prevent compromised or coerced accounts being maliciously deleted. Reviews are typically same-day; the GDPR allows up to one calendar month.
- The reviewer either approves or rejects the request:
- Approved: a destructive background job runs. It deletes your profile photos and verification selfie from blob storage, anonymises your profile row in place (see §4 — Soft-delete vs. hard-delete), ends your active matches (closing their conversations), removes you from any circles, deletes your device registrations and in-app notifications, and clears notes and feedback you left on events. You then receive a confirmation email, after which your sign-in account is anonymised: your email address, username, phone number, password and linked social sign-ins are removed, so the account can never be used again. Your status becomes Erased.
- Rejected: the reviewer must record a reason. You receive an email explaining the rejection and how to escalate (typically: contact the DPO with verifying information). Your status becomes Rejected. You can submit a new request after addressing the reason.
- The decision is written to our admin audit log and is retained for seven years for compliance audit purposes.
We will reject an erasure request only if we have a legal basis to do so — for example, if there is an open fraud investigation, an unresolved billing dispute, or a legal hold. We will tell you why.
If you simply want to take a break and come back later, set Profile Visibility to hidden in the app's privacy settings. That hides you from discovery without deleting your data, and you can change it back any time.
7.3 Rectification (Art. 16)
You can correct any incorrect personal data through the in-app profile editor. Display name, bio, location, photos, interests, availability, and discovery preferences are all editable. If you find a field that you cannot change in the app (for example, a typo in your sign-up email), contact support and we will correct it.
7.4 Restriction of processing (Art. 18) and objection (Art. 21)
You can ask us to stop processing your data for specific purposes (for example, stop computing compatibility scores against your profile). Most "stop processing X" requests are best handled by toggling the relevant feature in the app — for example, opting out of Circles, hiding yourself from discovery, or unsubscribing from marketing email. For requests that the app does not surface a toggle for, contact the DPO.
Marketing email is consent-based and you can withdraw consent any time by clicking Unsubscribe in any marketing email or by toggling the preference in the app.
7.5 Withdraw consent (Art. 7)
Where we rely on consent (currently: marketing email and acceptance of this privacy policy), you can withdraw it at any time. Withdrawing consent does not affect the lawfulness of processing carried out before the withdrawal.
If you withdraw consent for the privacy policy itself, you will be required to either re-accept the current version on your next sign-in or proceed to erasure. We cannot continue to process your data without a legal basis, and once you have an account with us, this policy is the document that defines that basis.
7.6 Lodge a complaint with a supervisory authority
You have the right to lodge a complaint with a data-protection supervisory authority — typically the one in the EU/UK Member State of your habitual residence, place of work, or place of the alleged infringement. In Australia, the equivalent body is the Office of the Australian Information Commissioner (OAIC). We would prefer you contact us first (§13) so we can try to resolve any issue, but we will not stand in the way of your right to complain.
8. Cookies and local storage
The mobile app does not use cookies. It stores its session token in the platform's secure storage, plus a small cache of feature-flag values and entitlement state.
The website (partner portal and admin dashboard) uses the following cookies:
.FoF.Website— authentication cookie set by OpenIdConnect after sign-in. Required for the site to function.- A correlation-ID cookie used to link a browser request to its server-side log entry, for support and debugging.
- Antiforgery (CSRF) tokens used by every form post.
We do not use third-party advertising cookies or behavioural-tracking cookies on any of our properties.
9. Third parties we share data with
We share the minimum personal data necessary for these third parties to provide the service we use them for, and only on the legal basis named in §3.
| Third party | Purpose | Data shared |
|---|---|---|
| Stripe | Web subscription billing | Email, customer ID, subscription line items. Card data is held by Stripe directly; we never see it. |
| Apple App Store | iOS in-app purchase billing | Apple's anonymised transaction identifier. Apple does not share your Apple ID with us. |
| Google Play | Android in-app purchase billing | Google's anonymised purchase token. Google does not share your Google account with us. |
| OpenStreetMap / Nominatim | Geocoding suburb names to coordinates | The free-text suburb you typed. Not linked to your member identifier on the upstream side. |
| Email delivery provider | Sending transactional and notification email | Your email address, the email subject and body. Transactional email only (matches, messages, security alerts, deletion confirmation, etc.). |
| Push notification provider (Firebase Cloud Messaging) | Sending push notifications | The device token registered by the mobile app. Notification body is the same as the in-app notification text. |
| OAuth providers (Google, Microsoft, Apple) | Optional social sign-in | Only when you choose to sign in with that provider; the provider tells us their sub value, your name, and your email. If you use Apple's "Hide My Email" feature, Apple provides a private relay address (@privaterelay.appleid.com) instead of your real email. We store this relay address as your account email and use it for transactional notifications. If you revoke consent via Apple, we disable email delivery to that address immediately. |
We never sell personal data. We never share it with data brokers, ad networks, or analytics providers that would link it to your identity outside our service.
10. Children
The service is for adults aged 18 or older. We enforce this two ways: (1) the date-of-birth onboarding step rejects anyone under 18 with a clear validation error, and (2) the MemberProfile.Create factory in our domain layer raises a hard error if the supplied DateOfBirth is under 18. We have no facility for processing data about children, and we do not knowingly collect it. If you believe a child has signed up for the service please contact us immediately and we will erase the account.
11. Automated decision-making and profiling
We use a matching algorithm to rank candidates in your discovery feed and to compute a compatibility percentage between you and another member when both of you have a Premium subscription. This is not "automated decision-making producing legal effects or similarly significantly affecting you" in the GDPR Art. 22 sense, because:
- the algorithm only ranks candidates — it does not approve, reject, allow, or block anyone;
- you are always free to swipe whoever you like, in either direction, regardless of how they are ranked;
- the algorithm has no effect on your ability to use the service, your billing, your account standing, or your civil or commercial rights;
- the algorithm uses only data you have voluntarily provided about yourself and your preferences (interests, availability, intent, group-size preference, age range, location).
We also use your age group to choose the wording of the sign-up questions. This changes how the questions are phrased, never which questions are asked, what you can answer, or what we do with your answers. The age group is worked out when needed and is not stored on your profile.
We do not engage in any other profiling or automated decision-making.
12. Changes to this policy
When we materially change this policy we will:
- Bump the
Currentconstant inApplication/Abstractions/PrivacyPolicyVersions.csand the version string at the top of this file in the same pull request. - Notify members on their next sign-in to a flow that requires consent (currently the onboarding completion gate; we will extend this to existing members for the next material change).
- Members must re-acknowledge before they can complete onboarding or take other consent-gated actions.
Trivial editorial changes (typos, broken links, clarifications that do not change meaning) do not bump the version.
The history of policy versions is recorded in source control on this file — you can see exactly what changed and when. We commit to keeping that history intact.
13. How to contact us
Data Protection Officer (DPO): Email the address shown on our Contact page (https://facesoffriendship.com/contact). To delete your account, follow the steps at https://facesoffriendship.com/delete-account. We will respond within 30 days, which is the GDPR maximum response window for rights requests; in practice we respond much sooner.
Postal address: [LEGAL ENTITY NAME], [REGISTERED ADDRESS].
Supervisory authority complaints:
- EU members: the data-protection authority in your country of habitual residence, place of work, or where the alleged infringement occurred.
- UK members: the Information Commissioner's Office (ICO).
- Australian members: the Office of the Australian Information Commissioner (OAIC).
Appendix: technical references for engineers
This appendix is not part of the published policy. It exists so an engineer can verify each claim above against the codebase.
| Claim | Reference |
|---|---|
| Privacy policy version constant | src/FacesOfFriendship.Application/Abstractions/PrivacyPolicyVersions.cs |
| Consent capture endpoint | PATCH /api/v1/onboarding/privacy-consent (OnboardingController) |
| Consent enforcement at onboarding completion | MemberProfile.CompleteOnboarding early-return on !HasPrivacyConsent |
| Data export endpoint | GET /api/v1/members/me/privacy/export (MemberPrivacyController) |
| Data export implementation | src/FacesOfFriendship.Application/Profiles/Services/DataExportService.cs |
| Erasure request endpoint | POST /api/v1/members/me/privacy/erase (MemberPrivacyController) |
| Erasure status endpoint | GET /api/v1/members/me/privacy/erase (MemberPrivacyController) |
| Admin pending queue | GET /api/v1/admin/erasures/pending (AdminErasureController) |
| Admin approval / rejection | POST /api/v1/admin/erasures/{id}/approve, POST /api/v1/admin/erasures/{id}/reject |
| Erasure state machine | MemberProfile.RequestErasure, ApproveErasure, RejectErasure, EraseCompletely |
| Destructive job | ExecuteErasureJob (Hangfire) |
| Audit log writes | MemberErasureApplicationService.WriteAuditAsync (and the BA-A2 admin-audit pattern in SubscriptionApplicationService / MemberTierApplicationService) |
| Email confirmation handler | MemberProfileErasedHandler (reads originalExternalUserId and originalDisplayName from the domain event payload, before the aggregate is wiped) |
| Notification templates seeded | erasure_rejected, erasure_confirmed (NotificationTemplateSeeder) |
| Inactive-account (dormancy) job | StaleProfileCleanupJob — marks 90-day-idle members dormant; MemberProfile.TouchLastActive reactivates them |
| Idempotency cleanup job | IdempotencyCleanupJob |
| Outbox cleanup job | OutboxCleanupJob |
| Webhook dedup cleanup jobs | StripeWebhookCleanupJob, Apple/Google notification cleanup jobs |
| 18+ enforcement | DateOfBirth value object — Create rejects ages < 18 |
| File-storage key validation | LocalFileStorageService.ResolveSafePath |
| Webhook signature verification | StripeWebhookHandler, AppleNotificationVerifier, Google Pub/Sub verification |
| Audit log retention | ModerationAuditLog table — no automated purge; reviewed manually under DPO supervision |
| Identity provider sub-claim scramble | MemberProfile.EraseCompletely final lines (ExternalUserId = $"erased:{Id}") |
| Photo verification request/status endpoints | POST/GET /api/v1/members/me/verification (MemberVerificationController) |
| Photo verification admin queue/decision endpoints | GET /api/v1/admin/verifications, POST .../{id}/approve, POST .../{id}/reject (AdminVerificationController) |
| Photo verification state machine | MemberProfile.RequestPhotoVerification, ApprovePhotoVerification, RejectPhotoVerification |
| Selfie storage path, never publicly reachable | Stored under verification-selfies/{profileId}/...; PhotosController.GetAsync rejects any storage key containing /, so this path can only ever be served through the authenticated admin passthrough (GET /api/v1/admin/verifications/{id}/selfie) |
| Selfie retention cleanup job | VerificationSelfieCleanupJob — daily, purges selfies 90 days after VerificationDecidedUtc |
| Selfie deletion on erasure | MemberErasureApplicationService.ExecuteErasureAsync deletes the verification selfie blob alongside profile photos; MemberProfile.EraseCompletely clears the verification status and timestamps |
| Verification decision attribution | Recorded in the ModerationAuditLog table (action types VerificationApproved / VerificationRejected) by PhotoVerificationApplicationService.ApproveAsync/RejectAsync, the same mechanism AdminModerationApplicationService.ApprovePhotoAsync uses for photo-flagging moderation. IAuditable shadow properties (modified_by/modified_utc) are also set by AuditableInterceptor on every write to the profile row, as with every editable record platform-wide. |
| Notification templates seeded | verification_approved, verification_rejected (NotificationTemplateSeeder) |