Implementing Consent and Account Deletion in a Fitness App
A user disables analytics tracking in your fitness app. The settings screen shows that tracking is off, but your backend continues sending workout events to an analytics provider.
Another user requests account deletion. Your API removes their profile, but workout history, cached records, and third-party data remain untouched.
Both situations reveal the same engineering problem: privacy controls exist in the interface, but they are not enforced throughout the system.
In this tutorial, we'll implement two connected workflows:
- Consent management: Store user preferences, enforce them, and stop optional processing when consent is withdrawn.
- Account deletion: Authenticate requests, coordinate deletion across services, handle failures, and verify completion.
We'll use TypeScript-style examples and a backend architecture suitable for many fitness applications.
The code is illustrative rather than production-ready. Framework-specific authentication, persistence, infrastructure, and legal requirements must be addressed separately.
1. Define the System Architecture
A fitness app may contain several systems that process user information.
The mobile app handles user interactions. Backend services store workout information and manage user accounts. Analytics services collect events, while external integrations may receive activity data.
A privacy workflow needs to account for these components.
flowchart TD
A[Fitness Mobile App] --> B[Backend API]
B --> C[Authentication Service]
B --> D[Consent Service]
B --> E[Primary Database]
B --> F[Deletion Service]
D --> G[Analytics Controls]
F --> H[Job Queue]
H --> E
H --> G
H --> I[Third-Party Integrations]
H --> J[Cache and Search Index]
E --> K[Backup System]
Component responsibilities
| Component | Responsibility |
|---|---|
| Mobile app | Display privacy preferences and deletion options |
| Authentication | Verify account ownership |
| Consent service | Manage and enforce user choices |
| Backend API | Validate requests and authorise operations |
| Database | Store necessary application data |
| Job queue | Execute asynchronous deletion tasks |
| Analytics provider | Respect optional tracking preferences |
| External integrations | Handle data sharing and deletion |
| Backup system | Manage retention and restoration safeguards |
A smaller app might implement these responsibilities within one backend rather than separate services.
The important requirement is consistent enforcement, not a particular microservice architecture.
2. Implement Consent Management
Consent management should begin with a defined processing purpose.
For example, a fitness application might collect information for:
- Workout history and progress tracking.
- Optional product analytics.
- Marketing communications.
- Wearable data synchronisation.
These purposes should not automatically share one consent setting.
Also, not every processing activity relies on consent as its legal basis. The appropriate lawful basis depends on the purpose, data, organisation, and jurisdiction.
Design a consent data model
A simple preference record might look like this:
type ConsentStatus = "granted" | "withdrawn";
interface ConsentPreference {
userId: string;
purpose: string;
status: ConsentStatus;
noticeVersion: string;
updatedAt: string;
}
Example stored record:
{
"userId": "user_123",
"purpose": "optional_analytics",
"status": "granted",
"noticeVersion": "privacy-v2",
"updatedAt": "2026-10-20T09:00:00Z"
}
For production systems, consider recording historical changes separately from the current preference.
That allows you to retain a limited audit history without depending on an overwritten database row.
Implement a preference update
The following example demonstrates server-side validation, persistence, and downstream synchronisation.
// Illustrative pseudocode
type ConsentPurpose =
| "optional_analytics"
| "marketing_messages";
async function updateConsent(
authenticatedUserId: string,
purpose: ConsentPurpose,
granted: boolean
) {
const supportedPurposes = [
"optional_analytics",
"marketing_messages"
];
if (!supportedPurposes.includes(purpose)) {
throw new Error("Unsupported consent purpose");
}
await database.transaction(async (tx) => {
await tx.consent.upsert({
userId: authenticatedUserId,
purpose,
status: granted ? "granted" : "withdrawn",
noticeVersion: CURRENT_NOTICE_VERSION,
updatedAt: new Date()
});
await tx.outbox.insert({
type: "CONSENT_UPDATED",
userId: authenticatedUserId,
purpose,
granted
});
});
return {
purpose,
status: granted ? "granted" : "withdrawn"
};
}
The authenticated user identifier must come from a verified session, not an untrusted request body.
The database transaction saves the preference and an outbox event together. A background worker can later deliver the event to affected services.
For real implementations, add strict input validation, transaction isolation or version checking, idempotent event processing, and safe error handling.
Handle concurrent updates
Imagine a user enables analytics on one device and disables it on another.
Without ordering controls, an older request could overwrite the latest preference.
Use a server-managed revision number, optimistic concurrency checks, or an equivalent ordering mechanism.
A simple version field can help:
{
"purpose": "optional_analytics",
"status": "withdrawn",
"revision": 8
}
A conflicting update should be rejected or reconciled according to a documented policy.
3. Enforce Consent Beyond the Settings Screen
Saving a preference is not enough.
The application must prevent optional processing when the relevant permission is absent or withdrawn.
Example: Analytics event tracking
// Illustrative server-side enforcement
async function trackWorkoutEvent(
userId: string,
eventName: string
) {
const preference = await consentStore.get(
userId,
"optional_analytics"
);
if (preference?.status !== "granted") {
return {
tracked: false,
reason: "CONSENT_NOT_GRANTED"
};
}
await analytics.track({
userId,
event: eventName
});
return { tracked: true };
}
This example deliberately denies optional analytics when no valid preference is available.
However, a consent check followed by an asynchronous send can still race with a concurrent withdrawal.
For stronger enforcement, use coordinated preference revisions, collection gateways, controlled queues, and mechanisms that suppress stale events.
What happens when consent is withdrawn?
A reliable withdrawal workflow should:
- Save the new preference.
- Stop new optional collection at the source.
- Disable applicable mobile SDK tracking.
- Invalidate stale preference caches.
- Prevent queued optional events from being sent when prohibited.
- Propagate changes to relevant downstream services.
- Handle existing data according to the applicable retention and deletion rules.
Device-level access permissions are a separate control. For example, health data access through Apple HealthKit or Android Health Connect follows platform-specific permission models.
Key lesson: A disabled toggle is not evidence that optional processing has stopped. Verify actual behaviour.
4. Design an Account Deletion API
Account deletion should be treated as a coordinated workflow rather than a single database operation.
For a small application, deleting local records might be straightforward.
For a larger fitness platform, user data may also exist in workout storage, coaching records, analytics systems, object storage, background queues, and external integrations.
Endpoint design
A possible API contract is:
DELETE /v1/account
Authorization: Bearer <access_token>
Idempotency-Key: delete-request-123
The server should:
- Validate the authentication session.
- Confirm account ownership.
- Apply additional identity verification where appropriate.
- Check for an existing deletion request.
- Create a durable deletion job.
- Prevent conflicting account changes.
- Return a request identifier.
Example response
HTTP/1.1 202 Accepted
Content-Type: application/json
{
"requestId": "delete_123",
"status": "requested",
"statusUrl": "/v1/account/deletion-status"
}
A 202 Accepted response means the request has been accepted for asynchronous processing. It does not mean the user's data has already been erased.
Use a consistent idempotency policy so repeated requests do not create unrelated deletion workflows.
Status endpoint
GET /v1/account/deletion-status
Authorization: Bearer <access_token>
Example response:
{
"requestId": "delete_123",
"status": "processing",
"updatedAt": "2026-10-20T09:30:00Z"
}
If user credentials are revoked during deletion, status checking requires a separate secure mechanism, such as a short-lived request-specific token or a verified notification channel.
Never restore account access merely to let someone view deletion progress.
5. Build the Account Deletion Workflow
Deletion should be durable, idempotent, and observable.
A useful state model includes:
stateDiagram-v2
[*] --> requested
requested --> processing: Verified
requested --> requires_review: Identity issue
processing --> completed: Cleanup verified
processing --> failed_retryable: Temporary failure
processing --> requires_review: Retention or provider issue
failed_retryable --> processing: Retry
requires_review --> processing: Resolved
completed --> [*]
Step 1: Persist the deletion request
The database should record the request before dispatching asynchronous cleanup.
A simplified record might include:
interface DeletionRequest {
id: string;
userId: string;
status:
| "requested"
| "processing"
| "completed"
| "failed_retryable"
| "requires_review";
createdAt: string;
updatedAt: string;
}
Keep operational records minimal and avoid including sensitive workout details.
Step 2: Process cleanup tasks
// Illustrative pseudocode
async function processDeletion(requestId: string) {
const request = await deletionStore.get(requestId);
if (!request || request.status === "completed") {
return;
}
await deletionStore.markProcessing(requestId);
try {
await blockAccountWrites(request.userId);
await deleteWorkoutRecords(request.userId);
await deleteEligibleProfileData(request.userId);
await removeCachedData(request.userId);
await requestAnalyticsDeletion(request.userId);
await requestIntegrationDeletion(request.userId);
await verifyRequiredCleanup(request.userId);
await deletionStore.markCompleted(requestId);
} catch (error) {
await deletionStore.markRetryableFailure(requestId);
throw error;
}
}
This demonstrates orchestration, not a complete implementation.
In production, external deletion tasks may be long-running. Provider requests and verification should be tracked as separate durable steps.
A timeout or unresolved provider response must not automatically be treated as successful deletion.
Step 3: Handle partial failures
Suppose workout records are deleted successfully, but an analytics provider becomes unavailable.
The application should not restart every destructive operation from the beginning without checking its previous results.
Instead, maintain independent task states:
| Task | Status |
|---|---|
| Disable account access | Completed |
| Delete workout records | Completed |
| Remove cached data | Completed |
| Delete analytics records | Retry pending |
| Verify external integrations | Pending |
| Final completion | Blocked |
This approach makes retries safer and gives operators visibility into incomplete work.
Step 4: Address backups
Deleting information from the active database does not necessarily remove it from existing backups.
Backups may be subject to documented retention periods and technical restoration constraints.
Your restoration process should prevent previously deleted data from becoming accessible again where deletion obligations remain applicable.
One approach is to maintain minimal deletion suppression records and reapply applicable deletions after restoration, before the restored service becomes accessible.
Avoid promising immediate deletion from every backup when that cannot be technically or legally guaranteed.
6. Handle Security and Failure Scenarios
Privacy workflows have several common failure modes.
| Scenario | Engineering response |
|---|---|
| Consent withdrawn but analytics continues | Enforce current preferences at collection points and monitor outbound events |
| Deletion worker runs twice | Use durable task identifiers and idempotent operations |
| Multiple deletion requests are submitted | Reuse the existing active workflow when appropriate |
| External provider deletion fails | Retry safely and retain an unresolved task status |
| Backup restores deleted records | Reapply deletion suppression before restoring access |
| User attempts to delete another account | Use session-bound identity and server-side authorization |
| Sensitive fitness data appears in logs | Use logging allowlists, restricted access, and incident procedures |
For sensitive operations, apply least-privilege access, appropriate encryption, rate limiting, secret management, and structured logging.
Avoid returning unnecessary personal details in error messages.
7. Test Consent and Deletion Workflows
A privacy feature should be tested based on what the system actually does, not simply whether an API returns a successful response.
Test consent withdrawal
// Illustrative test pseudocode
test("withdrawal prevents new analytics events", async () => {
const user = await createTestUser();
await updateConsent(
user.id,
"optional_analytics",
true
);
await trackWorkoutEvent(user.id, "workout_completed");
expect(await acceptedEventCount(user.id)).toBe(1);
await updateConsent(
user.id,
"optional_analytics",
false
);
await trackWorkoutEvent(user.id, "workout_completed");
expect(await acceptedEventCount(user.id)).toBe(1);
});
This checks the intended local behaviour.
Production integration tests should also verify SDK configuration, caching, queued events, and downstream service behaviour.
Test account deletion
// Illustrative test pseudocode
test("repeated deletion requests use one workflow", async () => {
const user = await createTestUserWithWorkouts();
const first = await requestAccountDeletion(
user.id,
"same-key"
);
const second = await requestAccountDeletion(
user.id,
"same-key"
);
expect(first.requestId).toBe(second.requestId);
await processDeletion(first.requestId);
expect(await hasWorkoutData(user.id)).toBe(false);
expect(
await requiredProviderCleanupConfirmed(user.id)
).toBe(true);
expect(
await deletionStore.getStatus(first.requestId)
).toBe("completed");
});
A passing unit test is not proof that a real production deletion workflow handles all connected services.
Use integration testing and operational monitoring to verify provider responses, background processing, and restoration behaviour.
8. Privacy and Platform Requirements
Engineering design must reflect the product's actual legal and platform obligations.
iOS and Android
Apple requires apps supporting account creation to offer account deletion within the app, subject to its documented rules.
Google Play also requires covered apps that allow account creation to provide both an in-app deletion pathway and an external web resource for deletion requests.
Official guidance:
United States and United Kingdom
HIPAA does not automatically apply to every independently operated consumer fitness app. Applicability depends on the organisation's role, the processing relationship, and the information involved.
Under UK GDPR, consent withdrawal and erasure rights may impose relevant obligations, but erasure is not unconditional. Certain information may need to be retained for valid legal reasons.
Official guidance:
Implementation teams should also review data minimization, purpose limitation, access controls, and retention requirements.
These technical examples do not establish legal compliance.
9. Developer Implementation Checklist
Before releasing consent and deletion functionality, verify the following:
Consent management
- [ ] Each consent-dependent purpose is identified.
- [ ] Users receive clear explanations.
- [ ] Preferences are saved securely.
- [ ] Withdrawal is supported.
- [ ] Backend and SDK collection respect current choices.
- [ ] Concurrent preference changes are handled safely.
Account deletion
- [ ] Requests require appropriate authentication.
- [ ] Account ownership is checked server-side.
- [ ] Deletion requests are durable and idempotent.
- [ ] Relevant data locations are documented.
- [ ] Background jobs support retries.
- [ ] Third-party deletion is verified.
- [ ] Retention exceptions are handled appropriately.
- [ ] Backups have restoration safeguards.
- [ ] Completion is reported accurately.
Testing and monitoring
- [ ] Consent withdrawal tests pass.
- [ ] Cross-account authorization tests pass.
- [ ] Duplicate deletion requests are handled safely.
- [ ] Partial failures are tested.
- [ ] Logs exclude unnecessary sensitive information.
- [ ] Failed privacy jobs are monitored.
- [ ] Relevant user notifications are verified.
10. Practical Implementation Takeaways
Consent management and account deletion should be designed together with authentication, API architecture, storage, third-party integrations, and background processing.
The important engineering principles are straightforward:
Enforce consent where processing occurs. Don't rely exclusively on a mobile toggle.
Treat deletion as a distributed workflow. Removing a profile from the primary database may leave information in other systems.
Design for partial failure. Use durable requests, idempotent tasks, retries, and verification.
Keep personal data out of unnecessary systems. Limiting collection and duplication reduces the number of places deletion must reach.
For teams planning a custom fitness application, MTechub's healthcare and fitness development services provide a relevant resource for exploring application development requirements.
Ultimately, a reliable privacy implementation is one that remains effective after preferences change, services fail, or data must be removed.
Build these workflows into the architecture, test their behaviour across system boundaries, and keep them maintainable throughout the application's lifecycle.

Top comments (0)