Documentation Index Fetch the complete documentation index at: https://docs-terra.withunify.org/llms.txt
Use this file to discover all available pages before exploring further.
Audit Logging
Every significant action is logged with who, what, when, and why. Designed for SOC 2 compliance.
What’s Logged
Category Actions Details Captured Forms Create, edit, publish, delete, duplicate Schema diff, field changes, version info Submissions Status changes, bulk operations Before/after status, affected IDs Settings System settings, branding, integrations Changed fields, before/after values User Management Role changes, invites, removals Target user, old/new role Data Access Exports, PDF downloads Format, record count Authentication Login/logout User, IP (anonymized), user agent
Log Structure
interface AuditLog {
id : string ;
action_type : string ; // "create", "update", "delete", "export", etc.
entity_type : string ; // "form", "submission", "user", "settings"
entity_id : string ;
user_id : string ;
user_email : string ;
user_role : string ;
workspace_id ?: string ;
changes ?: {
[ field : string ] : { before : unknown ; after : unknown };
};
metadata ?: Record < string , unknown >;
ip_address ?: string ; // Anonymized (last octet zeroed)
user_agent ?: string ;
request_id ?: string ; // For distributed tracing
success : boolean ;
error_message ?: string ;
created_at : string ;
}
Using the Audit Functions
Basic Audit Log
import { auditLog } from "@/lib/audit" ;
await auditLog ({
actionType: "update" ,
entityType: "form" ,
entityId: formId ,
changes: {
title: { before: "Old Title" , after: "New Title" },
},
metadata: { formTitle: "New Title" },
});
With Request Context (IP/User Agent)
import { auditLog , getRequestContext } from "@/lib/audit" ;
import { headers } from "next/headers" ;
const requestContext = getRequestContext ( await headers ());
await auditLog ({
actionType: "export" ,
entityType: "submission" ,
entityId: "bulk:50" ,
metadata: { format: "csv" , count: 50 },
requestContext ,
});
Convenience Functions
import {
auditFormCreate ,
auditFormPublish ,
auditSettingsChange ,
auditSubmissionStatusChange ,
} from "@/lib/audit" ;
// Form operations
await auditFormCreate ( formId , { title , createdBy });
await auditFormPublish ( formId , { version: 1 });
// Settings changes
await auditSettingsChange ( "settings" , "system" , "system_settings" , {
before: oldSettings ,
after: newSettings ,
});
// Submission status
await auditSubmissionStatusChange (
submissionId ,
{ oldStatus: "pending" , newStatus: "approved" },
{ formId }
);
Querying Logs
Via API
// GET /api/audit?page=1&limit=50&actionType=update&entityType=form
const response = await fetch ( "/api/audit?" + params );
const { logs , total } = await response . json ();
Direct SQL
-- Find all changes to a form
SELECT * FROM audit_logs
WHERE entity_type = 'form'
AND entity_id = $ 1
ORDER BY created_at DESC ;
-- Find all actions by a user
SELECT * FROM audit_logs
WHERE user_id = $ 1
ORDER BY created_at DESC
LIMIT 100 ;
-- Find all exports (for compliance review)
SELECT * FROM audit_logs
WHERE action_type = 'export'
ORDER BY created_at DESC ;
Access Review Report
For SOC 2 access reviews, export a report of all users with system access:
# Download CSV
curl -H "Authorization: Bearer $TOKEN " \
"https://your-domain.com/api/audit/access-report?format=csv" \
-o access-review.csv
# Get JSON
curl -H "Authorization: Bearer $TOKEN " \
"https://your-domain.com/api/audit/access-report"
The report includes:
All users with access
Their roles and permissions
Account status (Active/Pending)
Last activity timestamp
Retention Policy
Audit logs are retained for 7 years (2555 days) by default for SOC 2 compliance.
Configuration
The retention period is stored in system_settings.audit_retention_days.
Automated Cleanup
A daily cron job cleans up expired logs:
// /api/cron/audit-cleanup
// Called daily by Vercel cron
// Runs: SELECT * FROM cleanup_expired_audit_logs();
// Returns: { deleted_count, retention_days }
To set up the cron job, add to vercel.json:
{
"crons" : [
{
"path" : "/api/cron/audit-cleanup" ,
"schedule" : "0 3 * * *"
}
]
}
IP Anonymization
IP addresses are anonymized for privacy while preserving geographic analysis capability:
IPv4 : Last octet zeroed (e.g., 192.168.1.100 → 192.168.1.0)
IPv6 : Last 80 bits zeroed
function anonymizeIP ( ip : string ) : string {
// IPv4: 192.168.1.100 → 192.168.1.0
// IPv6: 2001:db8:85a3::8a2e:370:7334 → 2001:db8:85a3::
}
Immutability
Audit logs are append-only. The database enforces this via RLS:
-- Only service role can insert
CREATE POLICY "Service role can manage audit logs"
ON audit_logs
FOR ALL
USING (true)
WITH CHECK (true);
Application code never updates or deletes audit logs.
UI Dashboard
Access the audit log dashboard at Settings → Audit Logs :
View all actions with filtering by type, entity, date
Search by user email or entity ID
Export access review reports
View detailed change diffs
Database Schema Full table reference
Security Security best practices