Comprehensive logging system for tracking data changes in Hasura-based applications. The system provides two types of logging:
- Diffs: Track individual column changes with differential patches using diff-match-patch
- States: Track complete state snapshots for specified columns
Both systems use PostgreSQL triggers and integrate with Hasura Event Triggers for automated processing.
CREATE TABLE logs.diffs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
_schema TEXT NOT NULL, -- Source schema name
_table TEXT NOT NULL, -- Source table name
_column TEXT NOT NULL, -- Source column name
_id TEXT NOT NULL, -- Source record identifier
user_id UUID, -- User who made the change
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ,
_value TEXT, -- New value before diff calculation
diff TEXT, -- Calculated diff from previous state
processed BOOLEAN DEFAULT FALSE -- Whether processed by event trigger
);CREATE TABLE logs.states (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
_schema TEXT NOT NULL, -- Source schema name
_table TEXT NOT NULL, -- Source table name
_column TEXT NOT NULL, -- Source column name
_id TEXT NOT NULL, -- Source record identifier
user_id UUID, -- User who made the change
created_at TIMESTAMPTZ DEFAULT NOW(),
state JSONB -- State snapshot (null for delete)
);{
"logs-diffs": {
"diffs": [
{
"schema": "public",
"table": "users",
"column": "name"
}
]
},
"logs-states": {
"states": [
{
"schema": "public",
"table": "users",
"columns": ["email", "status"]
}
]
}
}Configuration note:
- Логи и триггеры настраиваются через
hasyx.config.jsonи применяются CLI‑командами. Связанные с логами переменные окружения попадают в автогенерируемый.env— не редактируйте его вручную, используйтеnpx hasyx config.
# Apply diffs configuration
npm run logs-diffs
# Apply states configuration
npm run logs-states
# Apply both configurations
npm run logs
# Apply event triggers
npm run events# Run all logs tests
npm test logs.test.ts
# Run specific test with debug output
DEBUG="hasyx*" npm test logs.test.ts -- -t "test name"- Track individual column changes with differential patches
- Uses diff-match-patch library for creating human-readable diffs
- Preserves original values and calculated diffs
- Prevents unauthorized updates to preserve history integrity
- Automatic processing via Event Triggers
- Configuration: Define tables/columns to track in hasyx.config.json
- Trigger Creation: Database triggers created automatically on configured tables
- Data Change: When data changes, trigger inserts record into logs.diffs with _value
- Event Processing: Hasura Event Trigger calls webhook to process diff
- Diff Creation: API route creates diff patch and updates record with processed=true
export async function applyLogsDiffs(hasura: Hasura, config: LogsDiffsConfig)
export async function handleLogsDiffsEventTrigger(payload: HasuraEventPayload)CREATE OR REPLACE FUNCTION hasyx_record_diff()
RETURNS TRIGGER AS $$
DECLARE
user_id_val UUID;
record_id TEXT;
BEGIN
-- Get user_id from Hasura session variable
user_id_val := NULLIF(current_setting('hasura.user.id', true), '')::UUID;
-- Record the diff
INSERT INTO logs.diffs (_schema, _table, _column, _id, user_id, _value)
VALUES (
TG_TABLE_SCHEMA,
TG_TABLE_NAME,
TG_ARGV[0], -- column name
record_id,
user_id_val,
(row_to_json(NEW)->>TG_ARGV[0])::TEXT
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;- Track complete state snapshots for multiple columns
- Record state changes including deletes (null state)
- Support for multiple columns per table
- Separate triggers for insert/update and delete operations
- Configuration: Define tables and columns to track in hasyx.config.json
- Trigger Creation: Database triggers created automatically
- Data Change: Triggers insert state snapshots into logs.states
- State Recording: Complete column states stored as JSONB
export async function applyLogsStates(hasura: Hasura, config: LogsStatesConfig)CREATE OR REPLACE FUNCTION hasyx_record_state_insert_update()
RETURNS TRIGGER AS $$
DECLARE
user_id_val UUID;
record_id TEXT;
state_data JSONB;
col_name TEXT;
BEGIN
-- Process each column specified in trigger arguments
FOR i IN 0..TG_NARGS-1 LOOP
col_name := TG_ARGV[i];
state_data := jsonb_build_object(col_name, row_to_json(NEW)->>col_name);
INSERT INTO logs.states (_schema, _table, _column, _id, user_id, state)
VALUES (TG_TABLE_SCHEMA, TG_TABLE_NAME, col_name, record_id, user_id_val, state_data);
END LOOP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;{
"name": "logs_diffs_created",
"table": {
"schema": "logs",
"name": "diffs"
},
"webhook_path": "/api/events/logs-diffs",
"insert": {
"columns": "*"
},
"retry_conf": {
"num_retries": 3,
"interval_sec": 10,
"timeout_sec": 60
}
}export const POST = hasyxEvent(async (payload: HasuraEventPayload) => {
const result = await handleLogsDiffsEventTrigger(payload);
return NextResponse.json(result);
});The handleLogsDiffsEventTrigger function:
- Validates payload is INSERT to logs.diffs table
- Extracts _value from the new record
- Queries for previous values to create diff
- Uses diff-match-patch to generate diff patch
- Updates record with diff and processed=true
User updates users.name: "John" → "John Doe"
↓
Database trigger creates logs.diffs record with _value="John Doe"
↓
Hasura Event Trigger calls /api/events/logs-diffs
↓
API handler gets previous value "John", creates diff patch
↓
Updates logs.diffs record with diff="@@ -1,4 +1,8 @@\n John\n+ Doe\n" and processed=true
- Diffs: Updates restricted to diff and processed fields only
- States: Read-only after creation
- User Permissions: Role-based access (user/admin)
CREATE OR REPLACE FUNCTION prevent_diffs_update()
RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'UPDATE' THEN
-- Allow updates only to diff and processed fields
IF (OLD._schema IS DISTINCT FROM NEW._schema OR
-- ... other core fields check
OLD._value IS DISTINCT FROM NEW._value) THEN
RAISE EXCEPTION 'Updates to core diffs fields are not allowed to preserve history integrity. Only diff and processed fields can be updated.';
END IF;
END IF;
RETURN COALESCE(NEW, OLD);
END;
$$ LANGUAGE plpgsql;- ✅ Trigger creation and configuration
- ✅ Data change recording
- ✅ Event trigger processing
- ✅ Diff patch generation
- ✅ Processed field updates
- ✅ Trigger creation for multiple columns
- ✅ State recording on insert/update
- ✅ Delete state handling (null)
- ✅ JSONB state structure
- ✅ Simultaneous diffs and states operation
- ✅ Configuration integration
- ✅ Cleanup and restoration
describe('Logs System Tests', () => {
it('should process diffs event trigger and create diff patches', async () => {
// 1. Create test data and diff record
// 2. Simulate event trigger payload
// 3. Call handleLogsDiffsEventTrigger
// 4. Verify diff was created and processed=true
});
});{
"diff-match-patch": "^1.0.5",
"@types/diff-match-patch": "^1.0.5"
}migrations/1746999999999-hasyx-logs/
├── up.ts # Creates logs schema, tables, triggers, permissions
└── down.ts # Cleanup and rollback
npm run migrate logs- Event triggers process asynchronously
- Database triggers have minimal overhead
- Indexes on common query patterns
- Use DEBUG="hasyx*" for detailed logging
- Monitor Event Trigger success/failure rates
- Track logs table growth
- Event triggers protected by HASURA_EVENT_SECRET
- Role-based permissions on logs tables
- Audit trail preservation via trigger protections
- Consider partitioning logs tables for high-volume applications
- Archive old logs data based on retention policies
- Monitor and optimize trigger performance
The logs system integrates seamlessly with existing Hasura applications:
- Zero Code Changes: No application code modifications required
- Configuration-Driven: Everything controlled via hasyx.config.json
- Hasura Native: Uses Hasura Event Triggers and permissions
- Type Safe: Full TypeScript support with proper interfaces
- Testable: Comprehensive test suite included
All commands are integrated into the hasyx CLI:
# Apply specific configurations
npm run logs-diffs
npm run logs-states
npm run logs
# Apply event triggers
npm run events
# Testing
npm test logs.test.tsThe system is production-ready and provides a complete audit trail for your Hasura application data changes.