NDPA compliance beyond policy documents
The Nigeria Data Protection Act (NDPA) 2023 establishes clear legal boundaries around Bank Verification Numbers (BVN), National Identification Numbers (NIN), debit card details, account numbers, and phone numbers. The Nigeria Data Protection Commission (NDPC) actively enforces these boundaries. In recent enforcement cycles, financial institutions faced regulatory fines of up to ₦10 million or 2% of their annual gross revenue. These penalties were not issued for missing paperwork or outdated privacy policies. The commission targeted active data leaks, unencrypted storage systems, unauthorized record access, and third-party data sharing.
When a fintech application processes user verification or handles a peer-to-peer transfer, every step of the data path must conform to the NDPA. Legally, customer consent must be specific, and data collection must be minimized. Practically, if your system transmits or stores Personally Identifiable Information (PII) without encryption or sanitization, your organization is in active violation of the law. You cannot resolve these violations with a PDF policy on your website. They must be resolved inside your codebase.
Unsanitized logging during API failures
When an integration with a payment gateway like Flutterwave, Paystack, or Monnify fails, developers need to diagnose the error. A common approach is logging the entire request payload and response error. If the incoming payload contains raw customer credentials—like CVV numbers or debit card PANs—and the application logs these objects directly, sensitive data is written to disk in plain text.
// High-risk logging pattern in Express routes
app.post("/api/v1/payments/charge", async (req, res) => {
try {
const paymentResponse = await paystack.charge(req.body);
return res.status(200).json(paymentResponse);
} catch (error) {
console.error("Payment failure payload:", req.body);
console.error("Error details:", error);
return res.status(500).json({ error: "Payment failed" });
}
}); If the console output is piped to an Elasticsearch index or a plain text log file on an unencrypted server, the PII is exposed. If developer credentials are leaked, or if a log dashboard has permissive access controls, unauthorized users can access the complete history of customer credentials. Under the NDPA 2023, storing protected customer variables without access controls and encryption is a direct path to regulatory penalties.
API data over-exposure in frontend clients
Many backend APIs query the database and serialize the resulting ORM model directly to the client. For instance, a mobile banking application might query a user profile to show the user's first name on the dashboard. The backend endpoint GET /api/v1/users/profile returns the complete database row, which includes the user's BVN, NIN, residential address, and phone number.
Developers often assume this is safe because the mobile application UI only renders the first name and hides the other fields. However, any user can intercept their device's traffic using tools like Charles Proxy or mitmproxy. The raw response payload contains all the raw data.
{
"status": "success",
"data": {
"id": 84920,
"first_name": "Chidi",
"last_name": "Okeke",
"email": "chidi@example.com",
"phone": "+2348031234567",
"bvn": "22233344455",
"nin": "11122233344",
"dob": "1992-04-12",
"account_number": "0123456789",
"bank_code": "044"
}
} A client application only needs the first name, but the backend serves the entire user record. If an attacker gains access to a user account, they retrieve all this sensitive personal identity data. Exposing unused PII violates the data minimization principles mandated by the NDPA 2023.
The deletion conflict: NDPA vs. CBN guidelines
NDPA Section 34 grants customers the right to erasure, allowing them to request the deletion of their personal data. However, the Central Bank of Nigeria (CBN) Anti-Money Laundering (AML) regulations require fintech companies to keep transaction records for at least five years. This requirement creates a direct architectural conflict if your system is built on standard relational databases.
If a customer requests data deletion and your database relies on strict foreign key constraints, you cannot delete the customer row without also deleting the transactions linked to that customer. If you configure the relation with ON DELETE CASCADE, you wipe the transaction history, which directly violates the CBN retention mandates. If you block the deletion, you fail to comply with the customer's legal right to erasure.
Many fintech databases are not designed to decouple transaction histories from personal identities. When a customer sends a deletion request, developers often delete the records manually using SQL scripts, or they ignore the request entirely, risking regulatory enforcement.
Implementing database and code level data protection
To satisfy NDPA requirements, engineering teams must implement technical guards in their codebases. You cannot rely on administrative policies; the protection must be enforced at the database and application levels. Here is the implementation pattern for each major gap.
Log sanitization middleware
To prevent PII leaks in crash logs, write a middleware that sanitizes sensitive keys before outputting them to log files or cloud monitoring platforms. Here is a TypeScript utility function that recursively removes sensitive keys from any object:
function sanitizePayload(data: any, sensitiveKeys: string[] = ['bvn', 'nin', 'cvv', 'password', 'pin', 'card_number']): any {
if (data === null || data === undefined) return data;
if (Array.isArray(data)) {
return data.map(item => sanitizePayload(item, sensitiveKeys));
}
if (typeof data === 'object') {
const clean: any = {};
for (const key in data) {
if (sensitiveKeys.includes(key.toLowerCase())) {
clean[key] = '[REDACTED]';
} else {
clean[key] = sanitizePayload(data[key], sensitiveKeys);
}
}
return clean;
}
return data;
} Run this sanitization step on every request and response payload before sending them to your logging system.
Response serialization models
Do not return database ORM entities directly to the client. Use Data Transfer Objects (DTOs) or custom serializers to filter out fields. For example, in a TypeScript API, use a class to define the explicit fields allowed to leave the server:
class UserProfileDTO {
id: number;
firstName: string;
lastName: string;
constructor(user: any) {
this.id = user.id;
this.firstName = user.first_name;
this.lastName = user.last_name;
}
}
// Controller usage
const rawUser = await db.user.findUnique({ where: { id: 123 } });
const responseData = new UserProfileDTO(rawUser);
res.json(responseData); This pattern makes it impossible for developers to accidentally leak newly added database columns to the frontend.
Column-level database encryption
Store sensitive database values in encrypted formats. When storing a BVN or NIN in PostgreSQL, enable the pgcrypto extension to encrypt data at rest. Here is the SQL schema and query structure:
-- Enable pgcrypto extension
CREATE EXTENSION IF NOT EXISTS pgcrypto;
-- Create users table with bytea type for PII
CREATE TABLE users (
id SERIAL PRIMARY KEY,
first_name VARCHAR(100),
bvn_encrypted BYTEA
);
-- Insert encrypted BVN using a secure KMS key
INSERT INTO users (first_name, bvn_encrypted)
VALUES ('Chidi', pgp_sym_encrypt('22233344455', 'your_secure_kms_key'));
-- Query and decrypt BVN
SELECT first_name, pgp_sym_decrypt(bvn_encrypted, 'your_secure_kms_key') AS bvn
FROM users
WHERE id = 1; For applications using ORMs, use field-level encryption wrappers that decrypt the value only when the database record is loaded into memory, using keys fetched from AWS KMS or HashiCorp Vault.
Soft-deletion and anonymization
To resolve the conflict between CBN retention rules and the NDPA right to erasure, do not delete the database row. Instead, anonymize the customer's personal details while preserving the record's primary keys and transaction links. When a user requests erasure, run an anonymization script:
UPDATE users
SET
first_name = 'ANONYMOUS',
last_name = 'USER',
email = 'deleted-user-' || id || '@simpalabs.com',
phone = NULL,
bvn_encrypted = NULL,
nin_encrypted = NULL,
is_deleted = TRUE
WHERE id = 84920; This updates the database fields to remove all identifying records while leaving the transaction history valid for CBN audits. The ledger remains consistent, and you comply with the NDPA.
Automated vulnerability scanners can check your SSL configuration and search for outdated packages, but they cannot trace where your BVNs leak into debug logs or detect missing DTO structures in your API responses—that requires a manual review of your data flows.
KYC Provider Logs Exposed to the Public
During a security review of a consumer fintech platform, we discovered that debug logs contained raw JSON payloads from their identity verification provider. This exposed 42,000 customer selfie photos, full names, and BVN records on an unauthenticated Elasticsearch server. The fix priority was classified as critical.
Want a manual review of your data flows to find leaks before the NDPC issues a compliance audit?
Get a security reviewFrequently asked questions
Does the NDPA conflict with CBN's 5-year transaction record retention requirement?
No. The NDPA permits data retention to satisfy statutory requirements. The solution is data anonymization: erase or mask the user's personal identity indicators while keeping the transaction record intact.
What log fields are considered PII under Nigerian law?
Under the NDPA 2023, protected PII includes email addresses, phone numbers, Bank Verification Numbers (BVN), National Identification Numbers (NIN), residential addresses, and bank account numbers.
How do we implement column-level encryption in PostgreSQL?
You can use the pgcrypto extension to encrypt columns directly in raw SQL queries, or handle encryption at the ORM layer using libraries like prisma-field-encryption or TypeORM transformers backed by KMS-managed keys.
Related reading
Blog: Are .env Files Secure? · KYC & BVN Data Security · Webhook Security
Services: API security testing · Penetration testing · Secure architecture review