import { Septor } from '@insureco/septor'
const septor = new Septor({
apiUrl: process.env.SEPTOR_URL,
namespace: process.env.SERVICE_NAME || 'my-service',
})
// Fire-and-forget — NEVER await septor on the critical path
septor.emit('payment.created', {
entityId: orgSlug,
data: { amount, referenceId: paymentId },
metadata: { who: userId },
}).catch((err) => logger.error({ err }, 'Septor emit failed — audit event lost'))
SEPTOR_URL is auto-injected by the builder for every deployment, but it is a
direct in-cluster URL and Septor's NetworkPolicy only admits namespaces that
carry the tawa.pro/direct-dep.septor=true label. The builder applies that label
from your declared dependencies, so you MUST declare septor in
internalDependencies:
spec:
internalDependencies:
- service: septor
Without this, the namespace is never labeled, every Septor call is dropped by the
NetworkPolicy and hangs until it times out (connect ETIMEDOUT), and the audit
trail is silently lost (emits are fire-and-forget). The builder may warn
"septor is auto-injected — remove it"; ignore that warning and keep the
declaration — it is what applies the NetworkPolicy label.
Septor verifies a Bio-ID token on every emit/query. The SDK (≥ 1.4.0) handles
this automatically: when no apiKey is passed, it mints a client_credentials
token from BIO_CLIENT_ID / BIO_CLIENT_SECRET / BIO_ID_URL and refreshes it
ahead of expiry. No per-app token code is needed.
This means your service MUST have Bio-ID credentials. Declare spec.auth in
catalog-info.yaml so the builder injects BIO_CLIENT_ID / BIO_CLIENT_SECRET:
spec:
auth:
mode: sso # or: service-only (no user login, creds still injected)
Without spec.auth, those vars are absent and every Septor emit fails with
Authorization header required (the audit trail is silently lost since emits
are fire-and-forget). See https://tawa.insureco.io/reference/convention-septor
Pin
@insureco/septorto^1.4.0. Earlier versions do not authenticate and will be rejected by Septor.
A Septor outage must NEVER break your service. Always fire-and-forget:
// ✅ CORRECT
septor.emit('payment.created', { ... })
.catch((err) => logger.error({ err }, 'Septor emit failed — audit event lost'))
// ❌ WRONG: blocking your user on an audit write
await septor.emit('payment.created', { ... })
| Category | Required Events |
|---|---|
| Payments | payment.created, payment.completed, payment.failed, payment.refunded |
| Policies | policy.bound, policy.endorsed, policy.cancelled, policy.renewed |
| Compliance | ofac.cleared, ofac.flagged, kyc.verified, kyc.failed |
| Authorization | user.login, permission.granted, permission.revoked |
| Data Access | record.accessed, report.exported, data.modified |
Automatically written by the platform (no code needed):
Use {resource}.{action} dot notation, lowercase:
payment.created ✅
policy.bound ✅
ofac.screen.cleared ✅ (nested resource.sub.action is OK)
payment-created ❌ (hyphens — use dots)
createPayment ❌ (camelCase)
PAYMENT_CREATED ❌ (uppercase)
const { data } = await septor.query({
entityId: orgSlug,
eventType: 'payment.created', // optional filter
from: '2024-01-01',
to: '2024-01-31',
limit: '100',
})
for (const event of data.events) {
console.log(event.eventType, event.createdAt, event.metadata.who)
}
// Run in background jobs, not request handlers — this is O(n)
const { data } = await septor.verify(orgSlug)
if (!data.valid) {
logger.error({ brokenAt: data.brokenAt }, 'Audit chain broken!')
}
entityId is the primary index — use a stable identifier (orgSlug or userId).catch() is a compliance gap — failures must be logged// ❌ WRONG: blocking on audit write
await septor.emit(...)
// ❌ WRONG: silent failure
septor.emit(...).catch(() => {}) // swallows the error — compliance gap
// ❌ WRONG: vague event type
septor.emit('event.happened', ...)
// ✅ CORRECT
septor.emit('payment.refunded', {
entityId: orgSlug,
data: { amount, reason, refundId },
metadata: { who: userId, why: 'Customer requested refund' },
}).catch((err) => logger.error({ err }, 'Septor emit failed — audit event lost'))
Last updated: July 15, 2026