Notification Security & Compliance Guide
How to send notifications without getting fined or breached
Notifications contain sensitive data: email addresses, phone numbers, order details, health information. A breach or compliance violation can cost millions. IBM's Cost of a Data Breach Report shows the average breach costs $4.45 million, with healthcare breaches averaging $10.93 million.
This guide covers the security and compliance requirements for notification systems, with practical implementation guidance.
What you will learn:
- GDPR requirements for notifications
- HIPAA compliance for health-related notifications
- Encryption and data protection strategies
- Audit logging and access controls
- Security best practices for notification APIs
Regulatory Requirements
GDPR (European Union)
The General Data Protection Regulation applies if you have EU users:
| Requirement | Notification Impact |
|---|---|
| Lawful basis | Transactional = legitimate interest; Marketing = consent |
| Consent records | Store when, how, and what was consented to |
| Right to access | Provide notification history on request |
| Right to deletion | Delete all notification data within 30 days |
| Data minimization | Only include necessary information |
| Breach notification | Report breaches within 72 hours |
Implementation:
interface ConsentRecord {
userId: string
consentType: 'marketing_email' | 'marketing_sms' | 'marketing_push'
consentedAt: Date
consentMethod: 'signup_form' | 'preference_center' | 'api'
ipAddress: string
consentText: string // The exact text user agreed to
}
async function recordConsent(consent: ConsentRecord) {
await db.consents.create({
data: {
...consent,
// Store consent proof immutably
hash: hashConsent(consent),
}
})
}
async function canSendMarketing(userId: string, channel: string): Promise<boolean> {
const consent = await db.consents.findFirst({
where: {
userId,
consentType: `marketing_${channel}`,
revokedAt: null,
}
})
return consent !== null
}HIPAA (Healthcare - United States)
The Health Insurance Portability and Accountability Act applies to Protected Health Information (PHI):
| Requirement | Implementation |
|---|---|
| Encryption in transit | TLS 1.2+ for all API calls |
| Encryption at rest | AES-256 for stored data |
| Access controls | Role-based access, audit all access |
| Audit logs | Log all PHI access for 6 years |
| Business Associate Agreement | Required with notification provider |
| Minimum necessary | Only include essential PHI |
Safe notification patterns for healthcare:
// BAD: PHI in notification
await sendSMS({
to: patient.phone,
message: `Your HIV test results are ready. Status: Positive.
Please call Dr. Smith at 555-1234.`
})
// GOOD: Minimal information, no PHI
await sendSMS({
to: patient.phone,
message: `You have a new message from your healthcare provider.
Log in to the patient portal to view: https://portal.example.com`
})CAN-SPAM (Email - United States)
| Requirement | Implementation |
|---|---|
| Accurate headers | From address must be valid |
| Clear identification | Identify as advertisement if marketing |
| Physical address | Include sender's postal address |
| Opt-out mechanism | One-click unsubscribe, honor within 10 days |
| No purchased lists | Only email users who opted in |
TCPA (SMS - United States)
| Requirement | Implementation |
|---|---|
| Prior express consent | Written consent for marketing SMS |
| Identification | Identify sender in message |
| Opt-out | Honor STOP immediately |
| Time restrictions | No messages before 8am or after 9pm local time |
| 10DLC registration | Register with carriers for business messaging |
Data Encryption
Encryption in Transit
All notification API calls must use TLS 1.2 or higher:
import { NotiGrid } from '@notigrid/node'
import https from 'https'
// NotiGrid enforces TLS 1.2+ by default
const notigrid = new NotiGrid({
apiKey: process.env.NOTIGRID_API_KEY,
// Optional: custom HTTPS agent for additional control
httpAgent: new https.Agent({
minVersion: 'TLSv1.2',
// Pin certificates for extra security
ca: fs.readFileSync('./notigrid-ca.pem'),
})
})Encryption at Rest
Encrypt sensitive notification data in your database:
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto'
const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY // 32 bytes
const ALGORITHM = 'aes-256-gcm'
function encrypt(text: string): EncryptedData {
const iv = randomBytes(16)
const cipher = createCipheriv(ALGORITHM, Buffer.from(ENCRYPTION_KEY, 'hex'), iv)
let encrypted = cipher.update(text, 'utf8', 'hex')
encrypted += cipher.final('hex')
return {
iv: iv.toString('hex'),
content: encrypted,
tag: cipher.getAuthTag().toString('hex'),
}
}
function decrypt(data: EncryptedData): string {
const decipher = createDecipheriv(
ALGORITHM,
Buffer.from(ENCRYPTION_KEY, 'hex'),
Buffer.from(data.iv, 'hex')
)
decipher.setAuthTag(Buffer.from(data.tag, 'hex'))
let decrypted = decipher.update(data.content, 'hex', 'utf8')
decrypted += decipher.final('utf8')
return decrypted
}
// Store encrypted notification content
await db.notificationLogs.create({
data: {
userId: user.id,
channel: 'email',
encryptedContent: encrypt(JSON.stringify(notificationContent)),
sentAt: new Date(),
}
})Key Management
Never hardcode encryption keys:
// BAD: Hardcoded key
const key = 'abc123...'
// GOOD: Environment variable
const key = process.env.ENCRYPTION_KEY
// BETTER: Key management service
import { KMSClient, DecryptCommand } from '@aws-sdk/client-kms'
const kms = new KMSClient({ region: 'us-east-1' })
async function getEncryptionKey(): Promise<Buffer> {
const command = new DecryptCommand({
CiphertextBlob: Buffer.from(process.env.ENCRYPTED_KEY, 'base64'),
KeyId: process.env.KMS_KEY_ID,
})
const response = await kms.send(command)
return Buffer.from(response.Plaintext!)
}Audit Logging
What to Log
Log all notification-related actions:
interface AuditLog {
timestamp: Date
action: 'notification_sent' | 'notification_opened' | 'preference_updated' | 'data_exported' | 'data_deleted'
userId: string
actorId: string // Who performed the action (user or admin)
actorType: 'user' | 'admin' | 'system'
resource: string // notification ID, preference ID, etc.
metadata: Record<string, any>
ipAddress: string
userAgent: string
}
async function auditLog(log: AuditLog) {
// Write to append-only audit log
await db.auditLogs.create({
data: {
...log,
// Hash for tamper detection
hash: hashLog(log),
previousHash: await getLastLogHash(),
}
})
}
// Log notification send
await auditLog({
timestamp: new Date(),
action: 'notification_sent',
userId: user.id,
actorId: 'system',
actorType: 'system',
resource: notification.id,
metadata: {
channel: 'email',
templateId: 'order-confirmation',
// Don't log PII in audit logs
},
ipAddress: 'internal',
userAgent: 'notigrid-worker',
})Log Retention
// Define retention policies
const RETENTION_POLICIES = {
audit_logs: 6 * 365, // 6 years for HIPAA
notification_logs: 90, // 90 days for operational
consent_records: 7 * 365, // 7 years for compliance proof
}
// Automated cleanup job
async function cleanupOldLogs() {
for (const [logType, retentionDays] of Object.entries(RETENTION_POLICIES)) {
const cutoffDate = subDays(new Date(), retentionDays)
await db[logType].deleteMany({
where: { createdAt: { lt: cutoffDate } }
})
console.log(`Cleaned up ${logType} older than ${cutoffDate}`)
}
}Access Controls
Role-Based Access
Limit who can access notification data:
const ROLES = {
admin: ['read:notifications', 'send:notifications', 'read:logs', 'manage:templates'],
developer: ['read:notifications', 'send:notifications', 'read:logs'],
support: ['read:notifications', 'read:logs'],
marketing: ['send:marketing', 'read:analytics'],
}
function checkPermission(user: User, permission: string): boolean {
const userPermissions = ROLES[user.role] || []
return userPermissions.includes(permission)
}
// Middleware for API routes
function requirePermission(permission: string) {
return (req: Request, res: Response, next: NextFunction) => {
if (!checkPermission(req.user, permission)) {
auditLog({
action: 'permission_denied',
userId: req.user.id,
resource: permission,
// ...
})
return res.status(403).json({ error: 'Insufficient permissions' })
}
next()
}
}
// Usage
app.get('/api/notifications/:id',
requirePermission('read:notifications'),
getNotification
)API Key Security
// Generate secure API keys
import { randomBytes } from 'crypto'
function generateApiKey(): string {
const prefix = 'ng_live_' // or 'ng_test_' for test keys
const key = randomBytes(32).toString('base64url')
return `${prefix}${key}`
}
// Store hashed keys
async function createApiKey(userId: string, name: string) {
const key = generateApiKey()
const hashedKey = await bcrypt.hash(key, 12)
await db.apiKeys.create({
data: {
userId,
name,
keyHash: hashedKey,
keyPrefix: key.slice(0, 12), // For identification
createdAt: new Date(),
}
})
// Return unhashed key only once
return key
}
// Validate API key
async function validateApiKey(key: string): Promise<User | null> {
const prefix = key.slice(0, 12)
const apiKey = await db.apiKeys.findFirst({
where: { keyPrefix: prefix, revokedAt: null },
include: { user: true },
})
if (!apiKey) return null
const valid = await bcrypt.compare(key, apiKey.keyHash)
if (!valid) return null
// Log API key usage
await db.apiKeys.update({
where: { id: apiKey.id },
data: { lastUsedAt: new Date() },
})
return apiKey.user
}Secure Notification Patterns
Webhook Security
Verify webhook signatures to prevent spoofing:
import { createHmac, timingSafeEqual } from 'crypto'
function verifyWebhookSignature(
payload: string,
signature: string,
secret: string
): boolean {
const expectedSignature = createHmac('sha256', secret)
.update(payload)
.digest('hex')
// Use timing-safe comparison to prevent timing attacks
return timingSafeEqual(
Buffer.from(signature),
Buffer.from(`sha256=${expectedSignature}`)
)
}
// Express middleware
app.post('/webhooks/notigrid', (req, res) => {
const signature = req.headers['x-notigrid-signature']
const payload = JSON.stringify(req.body)
if (!verifyWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' })
}
// Process webhook...
})Rate Limiting
Prevent abuse of notification APIs:
import rateLimit from 'express-rate-limit'
import RedisStore from 'rate-limit-redis'
const notificationRateLimit = rateLimit({
store: new RedisStore({
client: redisClient,
prefix: 'rl:notifications:',
}),
windowMs: 60 * 1000, // 1 minute
max: 100, // 100 requests per minute
message: { error: 'Too many requests, please try again later' },
keyGenerator: (req) => req.user.id,
})
app.post('/api/notifications', notificationRateLimit, sendNotification)Input Validation
Sanitize all input to prevent injection:
import { z } from 'zod'
import DOMPurify from 'isomorphic-dompurify'
const NotificationSchema = z.object({
channel: z.enum(['email', 'sms', 'push', 'slack']),
recipient: z.object({
userId: z.string().uuid(),
email: z.string().email().optional(),
phone: z.string().regex(/^\+[1-9]\d{1,14}$/).optional(),
}),
variables: z.record(z.string(), z.unknown()).transform((vars) => {
// Sanitize string values to prevent XSS in email
const sanitized: Record<string, unknown> = {}
for (const [key, value] of Object.entries(vars)) {
if (typeof value === 'string') {
sanitized[key] = DOMPurify.sanitize(value)
} else {
sanitized[key] = value
}
}
return sanitized
}),
})
app.post('/api/notifications', async (req, res) => {
const result = NotificationSchema.safeParse(req.body)
if (!result.success) {
return res.status(400).json({ error: result.error })
}
await sendNotification(result.data)
})Compliance Checklist
Before Launch
- TLS 1.2+ enforced for all API calls
- Encryption at rest for sensitive data
- Audit logging implemented
- Role-based access controls configured
- API key rotation policy defined
- Data retention policy documented
- Consent management for marketing
- Unsubscribe mechanism working
- Privacy policy updated
- DPA signed with notification provider
Ongoing
- Quarterly access reviews
- Annual security audit
- Regular key rotation
- Log monitoring and alerting
- Incident response plan tested
- Staff security training
NotiGrid Security Features
NotiGrid provides enterprise-grade security:
- SOC 2 Type II certified
- HIPAA-compliant with BAA available
- Encryption: TLS 1.3 in transit, AES-256 at rest
- Audit logs: 90-day retention (extended on request)
- Access controls: Team roles and permissions
- Webhook signatures: HMAC-SHA256 verification
- API key management: Rotation, scoping, IP allowlists
Start building secure notifications with NotiGrid.
Ready to send your first notification?
Get started with NotiGrid today and send notifications across email, SMS, Slack, and more.