Septor
Septor is the platform's immutable audit trail service. It captures every important action as a hash-chained event, making tampering detectable and providing compliance-ready proof of who did what, when, and why.
Overview
Septor sits behind the Janus gateway at /v1/events. Every event is linked to the previous event for the same entity via SHA-256 hash, forming an append-only chain. If any event is modified, the chain breaks and the verify endpoint reports exactly where.
Architecture
Your Service
|
v
Septor --> Validate (Zod) --> Hash chain --> MongoDB
|
v
Seven Elements captured automatically:
WHO - actor (user email, service name)
WHAT - event type
WHEN - server timestamp
WHERE - IP address, user agent
HOW - HTTP method and endpoint
WHY - reason (optional)
HASH - SHA-256 linking to previous eventURLs
| Environment | URL |
|---|---|
| Production | https://septor.insureco.io |
| Production (platform) | https://septor.tawa.insureco.io |
| Sandbox | https://septor.sandbox.tawa.insureco.io |
| In-Cluster (prod) | http://septor.septor-prod.svc.cluster.local:8473 |
| In-Cluster (sandbox) | http://septor.septor-sandbox.svc.cluster.local:8473 |
Quick Start
Add Septor to your service in three steps:
1. Declare the dependency
Add Septor to your service's catalog-info.yaml so the builder injects the connection URL automatically:
# catalog-info.yaml
spec:
internalDependencies:
- service: septor
port: 8473SEPTOR_URL as an environment variable pointing to the in-cluster service address.2. Install the SDK
npm install septor3. Emit your first event
import { Septor } from 'septor'
const septor = new Septor({
apiUrl: process.env.SEPTOR_URL || 'http://localhost:8473',
namespace: 'your-service-name',
})
await septor.emit('entity.created', {
entityId: 'abc-123',
data: { key: 'value' },
metadata: {
who: '[email protected]',
why: 'New entity created by user',
},
})POST /v1/events/emit
Emit a new event to the audit trail. The event is automatically hash-chained to the previous event for the same entity.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
namespace | string | Yes | Your service name (e.g. flood-rater, bind-desk) |
eventType | string | Yes | Semantic event name (e.g. rating.created) |
entityId | string | Yes | The entity being audited (quoteId, policyId, etc.) |
data | object | Yes | Event payload (any JSON) |
metadata.who | string | Yes | Actor — user email, service name, or API key |
metadata.why | string | No | Reason for the action (recommended for compliance) |
Example
curl -X POST https://septor.insureco.io/v1/events/emit \
-H "Content-Type: application/json" \
-d '{
"namespace": "flood-rater",
"eventType": "rating.created",
"entityId": "quote_123",
"data": {
"premium": 850,
"buildingValue": 300000,
"zone": "AE"
},
"metadata": {
"who": "[email protected]",
"why": "Initial flood quote generated"
}
}'Response (201)
{
"success": true,
"data": {
"eventId": "evt_5d5fb86239fde3f5e53e7fbb",
"eventHash": "0e43d16c8185046b4e5c1fe...",
"chainIndex": 0,
"previousHash": null,
"createdAt": "2026-02-16T17:06:00.080Z"
}
}previousHash links to the prior event's hash, forming the chain.GET /v1/events/query
Query events by namespace, entity, type, and date range.
Query parameters
| Param | Type | Required | Description |
|---|---|---|---|
namespace | string | Yes | Filter by namespace |
entityId | string | No | Filter by entity ID |
eventType | string | No | Filter by type (supports * wildcard) |
from | string | No | Start date (ISO 8601) |
to | string | No | End date (ISO 8601) |
limit | string | No | Max results (default: 50) |
Example
GET /v1/events/query?namespace=flood-rater&entityId=quote_123&eventType=rating.*Response (200)
{
"success": true,
"data": {
"events": [
{
"eventId": "evt_5d5fb862...",
"namespace": "flood-rater",
"eventType": "rating.created",
"entityId": "quote_123",
"data": { "premium": 850 },
"eventHash": "0e43d16c...",
"previousHash": null,
"chainIndex": 0,
"metadata": {
"who": "[email protected]",
"what": "rating.created",
"when": { "server": "2026-02-16T17:06:00Z" },
"where": { "ipAddress": "10.244.2.167" },
"how": { "method": "POST", "endpoint": "/v1/events/emit" },
"why": "Initial flood quote generated"
},
"createdAt": "2026-02-16T17:06:00.080Z"
}
],
"total": 1,
"returned": 1
}
}GET /v1/events/verify/:entityId
Verify the hash chain integrity for an entity. Recalculates all hashes and confirms no events have been tampered with.
Example
GET /v1/events/verify/quote_123?namespace=flood-raterResponse (200)
{
"success": true,
"data": {
"entityId": "quote_123",
"valid": true,
"totalEvents": 3,
"firstEvent": "2026-02-16T17:06:00.080Z",
"lastEvent": "2026-02-16T18:30:00.000Z"
}
}valid is false, the response includes a brokenAt field indicating the chain index where tampering was detected. This should trigger an immediate investigation.GET /v1/events/chain/:entityId
Get the full event chain for an entity, including hashes for visualization and debugging.
Response (200)
{
"success": true,
"data": {
"entityId": "quote_123",
"chain": [
{
"chainIndex": 0,
"eventType": "rating.created",
"eventHash": "0e43d16c...",
"previousHash": null,
"createdAt": "2026-02-16T17:06:00Z"
},
{
"chainIndex": 1,
"eventType": "rating.updated",
"eventHash": "a7b3f921...",
"previousHash": "0e43d16c...",
"createdAt": "2026-02-16T18:30:00Z"
}
],
"valid": true,
"totalEvents": 2
}
}Seven Elements
Every event automatically captures seven metadata elements for compliance:
| Element | Field | Source |
|---|---|---|
| Who | metadata.who | Provided by caller |
| What | metadata.what | Derived from eventType |
| When | metadata.when.server | Server timestamp (automatic) |
| Where | metadata.where.ipAddress | Request IP (automatic) |
| How | metadata.how.method | HTTP method + endpoint (automatic) |
| Why | metadata.why | Provided by caller (optional) |
| Hash | eventHash | SHA-256 of event contents (automatic) |
Hash Chain
Each event's hash includes the previous event's hash, creating an immutable chain. Modifying any event breaks the chain for all subsequent events.
hash = SHA-256(JSON.stringify({
eventType,
entityId,
data,
previousHash || '',
timestamp,
who,
}))Event 0 Event 1 Event 2
+-----------+ +-----------+ +-----------+
| hash: abc |<-------| prev: abc |<-------| prev: def |
| prev: null| | hash: def | | hash: 789 |
+-----------+ +-----------+ +-----------+
If Event 1 is tampered with, its hash changes,
breaking the link from Event 2 onward.Event Naming Conventions
Use consistent <entity>.<action> patterns across services:
| Service | Event Types |
|---|---|
| flood-rater | rating.created, rating.updated, quote.submitted |
| bind-desk | policy.bound, policy.cancelled, endorsement.created |
| policypay | payment.processed, payment.refunded, invoice.generated |
| iec-extract | document.uploaded, document.extracted, field.corrected |
Common actions: created, updated, deleted, approved, rejected, submitted, bound, cancelled, exported, viewed
SDK
The septor npm package provides a typed client for all endpoints.
Install
npm install septorSetup
import { Septor } from 'septor'
const septor = new Septor({
apiUrl: process.env.SEPTOR_URL || 'http://localhost:8473',
namespace: 'flood-rater',
})Emit events
const result = await septor.emit('rating.created', {
entityId: 'quote_123',
data: { premium: 850, zone: 'AE' },
metadata: {
who: '[email protected]',
why: 'New flood insurance quote',
},
})
// result.data.eventId, result.data.eventHash, result.data.chainIndexQuery events
const history = await septor.query({
entityId: 'quote_123',
eventType: 'rating.*', // wildcard support
limit: '100',
})
// history.data.events[], history.data.totalVerify chain
const result = await septor.verify('quote_123')
if (result.data.valid) {
// Chain intact - safe for compliance
} else {
// Chain broken at result.data.brokenAt
}Get full chain
const chain = await septor.getChain('quote_123')
// chain.data.chain[], chain.data.valid, chain.data.totalEventsCLI
The septor CLI is useful for debugging and manual operations:
# Install globally
npm install -g septor
# Emit a test event
septor emit rating.created \
-n flood-rater \
-e quote_123 \
-d '{"premium": 850}' \
-w [email protected] \
--url https://septor.insureco.io
# Query events
septor query -n flood-rater -e quote_123
# Verify chain integrity
septor verify quote_123 -n flood-raterError Handling
All errors follow a consistent format:
{
"success": false,
"error": "Validation error",
"details": [
{
"path": ["namespace"],
"message": "Invalid input: expected string, received undefined"
}
]
}| Status | Meaning |
|---|---|
| 201 | Event emitted successfully |
| 200 | Query / verify / chain succeeded |
| 400 | Validation error (missing or invalid fields) |
| 404 | Route not found |
| 500 | Internal server error |
// Fire-and-forget pattern
septor.emit('entity.created', payload).catch((err) =>
console.error('Audit event failed:', err.message)
)Related
- catalog-info.yaml Reference — declaring dependencies
- Databases — MongoDB provisioning
- Getting Started — deploying your first service