RTBF Custom Script
Advanced automation for Right to Be Forgotten compliance
This guide provides advanced patterns and complete scripts for automating Right to Be Forgotten (RTBF) compliance across your Typeform infrastructure and integrated systems.
Complete RTBF automation script
Here’s a production-ready Node.js script for handling RTBF requests:
const fetch = require('node-fetch');class RTBFManager {constructor(config) {this.apiToken = config.apiToken;this.baseUrl = 'https://api.typeform.com';this.auditLog = config.auditLog || [];}/*** Main RTBF handler*/async processRTBFRequest(params) {const {identifier,identifierType = 'email',formIds = [],reason = 'RTBF request',requesterEmail,} = params;const startTime = new Date();const deletionRecord = {id: this.generateDeletionId(),timestamp: startTime,identifier,identifierType,reason,requesterEmail,status: 'in_progress',deletedResponses: [],errors: [],};try {// Step 1: Find all matching responsesconsole.log(`Searching for responses matching ${identifierType}: ${identifier}`);const matchingResponses = await this.findMatchingResponses(identifier,identifierType,formIds);if (matchingResponses.length === 0) {deletionRecord.status = 'no_data_found';this.auditLog.push(deletionRecord);return {success: true,message: 'No data found for the provided identifier',deletionRecord,};}console.log(`Found ${matchingResponses.length} matching responses`);// Step 2: Group by form for efficient deletionconst responsesByForm = this.groupResponsesByForm(matchingResponses);// Step 3: Delete from Typeformfor (const [formId, responses] of Object.entries(responsesByForm)) {try {await this.deleteResponses(formId,responses.map((r) => r.token));deletionRecord.deletedResponses.push({formId,count: responses.length,tokens: responses.map((r) => r.token),});console.log(`Deleted ${responses.length} responses from form ${formId}`);} catch (error) {deletionRecord.errors.push({formId,error: error.message,});console.error(`Failed to delete from form ${formId}:`, error);}}// Step 4: Delete from integrated systemsawait this.deleteFromIntegratedSystems(identifier, deletionRecord);// Step 5: FinalizedeletionRecord.status =deletionRecord.errors.length > 0 ? 'partial' : 'completed';deletionRecord.completedAt = new Date();deletionRecord.duration = deletionRecord.completedAt - startTime;this.auditLog.push(deletionRecord);// Step 6: Send notificationif (requesterEmail) {await this.sendDeletionConfirmation(requesterEmail, deletionRecord);}return {success: true,deletionRecord,};} catch (error) {deletionRecord.status = 'failed';deletionRecord.error = error.message;deletionRecord.completedAt = new Date();this.auditLog.push(deletionRecord);console.error('RTBF process failed:', error);return {success: false,error: error.message,deletionRecord,};}}/*** Find all responses matching the identifier*/async findMatchingResponses(identifier, identifierType, formIds) {const allMatches = [];// If no specific forms provided, get all formsif (formIds.length === 0) {const forms = await this.listAllForms();formIds = forms.map((f) => f.id);}for (const formId of formIds) {try {const responses = await this.fetchAllResponses(formId);const matches = responses.filter((response) =>this.responseMatchesIdentifier(response, identifier, identifierType));allMatches.push(...matches.map((r) => ({...r,formId,})));} catch (error) {console.error(`Error fetching responses for form ${formId}:`, error);}}return allMatches;}/*** Check if a response matches the identifier*/responseMatchesIdentifier(response, identifier, identifierType) {// Check hidden fieldsif (response.hidden && response.hidden[identifierType] === identifier) {return true;}// Check answersfor (const answer of response.answers || []) {if (identifierType === 'email' && answer.type === 'email') {if (answer.email.toLowerCase() === identifier.toLowerCase()) {return true;}}if (identifierType === 'phone' && answer.type === 'phone_number') {if (this.normalizePhone(answer.phone_number) ===this.normalizePhone(identifier)) {return true;}}// Custom field matchingif (answer.field?.ref === identifierType && answer.text === identifier) {return true;}}return false;}/*** Fetch all responses for a form (handles pagination)*/async fetchAllResponses(formId) {const allResponses = [];let hasMore = true;let lastToken = null;while (hasMore) {const url = new URL(`${this.baseUrl}/forms/${formId}/responses`);url.searchParams.append('page_size', '1000');if (lastToken) {url.searchParams.append('before', lastToken);}const response = await fetch(url.toString(), {headers: {Authorization: `Bearer ${this.apiToken}`,},});if (!response.ok) {throw new Error(`Failed to fetch responses: ${response.statusText}`);}const data = await response.json();allResponses.push(...data.items);hasMore = data.items.length === 1000;if (hasMore) {lastToken = data.items[data.items.length - 1].token;}}return allResponses;}/*** Delete responses from Typeform*/async deleteResponses(formId, tokens) {if (tokens.length === 0) return;// Typeform API has a limit on URL length, so batch if necessaryconst batchSize = 50;for (let i = 0; i < tokens.length; i += batchSize) {const batch = tokens.slice(i, i + batchSize);const tokenString = batch.join(',');const url = `${this.baseUrl}/forms/${formId}/responses?included_tokens=${tokenString}`;const response = await fetch(url, {method: 'DELETE',headers: {Authorization: `Bearer ${this.apiToken}`,},});if (response.status !== 204) {const error = await response.text();throw new Error(`Deletion failed: ${error}`);}// Rate limiting: wait between batchesif (i + batchSize < tokens.length) {await this.sleep(1000);}}}/*** Delete from integrated systems*/async deleteFromIntegratedSystems(identifier, deletionRecord) {const integrations = [];// Add your integrations here// Example: Delete from databasetry {await this.deleteFromDatabase(identifier);integrations.push({ system: 'database', status: 'success' });} catch (error) {integrations.push({system: 'database',status: 'failed',error: error.message,});}// Example: Delete from CRMtry {await this.deleteFromCRM(identifier);integrations.push({ system: 'crm', status: 'success' });} catch (error) {integrations.push({system: 'crm',status: 'failed',error: error.message,});}// Example: Delete from email marketingtry {await this.deleteFromEmailMarketing(identifier);integrations.push({ system: 'email_marketing', status: 'success' });} catch (error) {integrations.push({system: 'email_marketing',status: 'failed',error: error.message,});}deletionRecord.integrations = integrations;}/*** Integration placeholders - implement based on your systems*/async deleteFromDatabase(identifier) {// Implement your database deletion logicconsole.log(`Deleting from database: ${identifier}`);}async deleteFromCRM(identifier) {// Implement your CRM deletion logicconsole.log(`Deleting from CRM: ${identifier}`);}async deleteFromEmailMarketing(identifier) {// Implement your email marketing deletion logicconsole.log(`Deleting from email marketing: ${identifier}`);}/*** Utility functions*/async listAllForms() {const response = await fetch(`${this.baseUrl}/forms`, {headers: {Authorization: `Bearer ${this.apiToken}`,},});if (!response.ok) {throw new Error('Failed to list forms');}const data = await response.json();return data.items || [];}groupResponsesByForm(responses) {return responses.reduce((groups, response) => {const formId = response.formId;if (!groups[formId]) {groups[formId] = [];}groups[formId].push(response);return groups;}, {});}normalizePhone(phone) {return phone.replace(/[^0-9]/g, '');}generateDeletionId() {return `del_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;}async sleep(ms) {return new Promise((resolve) => setTimeout(resolve, ms));}async sendDeletionConfirmation(email, deletionRecord) {// Implement your email notification logicconsole.log(`Sending deletion confirmation to ${email}`);console.log(`Deleted ${deletionRecord.deletedResponses.length} response groups`);}/*** Get audit log*/getAuditLog() {return this.auditLog;}/*** Export audit log*/exportAuditLog(format = 'json') {if (format === 'json') {return JSON.stringify(this.auditLog, null, 2);}if (format === 'csv') {// Implement CSV exportreturn this.auditLogToCSV();}throw new Error('Unsupported export format');}auditLogToCSV() {const headers = ['ID','Timestamp','Identifier','Status','Responses Deleted','Errors',];const rows = this.auditLog.map((record) => [record.id,record.timestamp.toISOString(),record.identifier,record.status,record.deletedResponses.length,record.errors.length,]);return [headers, ...rows].map((row) => row.join(',')).join('\n');}}// Usage exampleasync function main() {const rtbfManager = new RTBFManager({apiToken: process.env.TYPEFORM_API_TOKEN,});// Process a single RTBF requestconst result = await rtbfManager.processRTBFRequest({identifier: 'user@example.com',identifierType: 'email',formIds: [], // Empty array searches all formsreason: 'GDPR deletion request',requesterEmail: 'user@example.com',});console.log('RTBF Result:', result);// Export audit logconst auditLog = rtbfManager.exportAuditLog('json');console.log('Audit Log:', auditLog);}// Export for use in other scriptsmodule.exports = { RTBFManager };// Run if executed directlyif (require.main === module) {main().catch(console.error);}
Environment setup
Create a .env file:
TYPEFORM_API_TOKEN=your_token_hereDATABASE_URL=postgresql://localhost:5432/mydbCRM_API_KEY=your_crm_keyEMAIL_API_KEY=your_email_key
Package dependencies
package.json:
{"name": "typeform-rtbf-automation","version": "1.0.0","dependencies": {"node-fetch": "^2.6.1","dotenv": "^16.0.0"}}
Running the script
# Install dependenciesnpm install# Run the scriptnode rtbf-script.js
API endpoint setup
Create an endpoint in your application:
const express = require('express');const { RTBFManager } = require('./rtbf-script');const app = express();app.use(express.json());const rtbfManager = new RTBFManager({apiToken: process.env.TYPEFORM_API_TOKEN,});app.post('/api/rtbf/request', async (req, res) => {try {const { email } = req.body;// Verify the requesterif (!(await verifyRequester(email))) {return res.status(401).json({ error: 'Unauthorized' });}const result = await rtbfManager.processRTBFRequest({identifier: email,identifierType: 'email',requesterEmail: email,});res.json(result);} catch (error) {res.status(500).json({ error: error.message });}});app.listen(3000, () => {console.log('RTBF API listening on port 3000');});
Scheduled cleanup
Automate data retention with cron:
const cron = require('node-cron');// Run daily at midnightcron.schedule('0 0 * * *', async () => {console.log('Running automated data retention cleanup');const cutoffDate = new Date();cutoffDate.setDate(cutoffDate.getDate() - 365); // 1 year retention// Delete old responsesawait enforceRetentionPolicy(cutoffDate);});
Testing
const { RTBFManager } = require('./rtbf-script');describe('RTBF Manager', () => {let rtbfManager;beforeEach(() => {rtbfManager = new RTBFManager({apiToken: process.env.TEST_API_TOKEN,});});it('should find matching responses', async () => {const result = await rtbfManager.findMatchingResponses('test@example.com','email',['test_form_id']);expect(result).toBeInstanceOf(Array);});it('should delete responses successfully', async () => {const result = await rtbfManager.processRTBFRequest({identifier: 'test@example.com',identifierType: 'email',formIds: ['test_form_id'],});expect(result.success).toBe(true);});});
Best practices
- Batch operations: Process deletions in batches to avoid rate limits
- Comprehensive search: Search across all forms and systems
- Audit everything: Maintain detailed logs of all deletions
- Verify identity: Always authenticate deletion requests
- Handle errors: Implement robust error handling and retry logic
- Test thoroughly: Test with sandbox data before production use
Next steps
- Review Right to Be Forgotten basics
- Learn about webhooks for real-time processing
- See the API Reference for complete documentation