Security Layers in Simmetric Chat: API Keys, Signed Webhooks, and DLP
How Simmetric Chat authenticates every request, hashes API keys with HMAC-SHA256, signs webhooks and Web Push, and redacts PII with DLP filters.
Security is a stack, not a switch
A chat platform that holds your company's documents has to earn trust in layers: how requests authenticate, how programmatic access is issued and stored, how background integrations prove their legitimacy, and what happens to sensitive content as it flows through prompts and answers. Simmetric Chat addresses each of those with a named mechanism, and v0.22 closed several of the subtler gaps - timing-safe secret comparisons, enumeration-resistant login, and a DLP engine that now covers structured European identifiers. This article walks the stack from the front door inward.
Three ways requests authenticate
Every endpoint under /api falls into one of three credential regimes:
- JWT Bearer (primary). Human users authenticate through the login or registration endpoint and receive a signed JWT. Each token carries a
jticlaim checked against a revocation list (optionally Redis-backed), so logging out or administrative revocation takes effect immediately, not at expiry. Invalid or expired tokens get a clean401; the middleware resolves the user together with roles and permissions on every request. - API keys (`X-Api-Key`). Programmatic access - the widget service account surface, and user-created keys through the API-key endpoints - uses prefixed keys verified against an HMAC digest, described next.
- Collector shared secret. Service-to-service calls from the ingestion collector carry
X-Collector-Secret, compared in constant time with a length guard. This is the last place a plaintext secret comparison lived in the server, and v0.22 closed it: archive-import callbacks now use the samecrypto.timingSafeEqualdiscipline as every other secret gate.
On top of authentication sit the RBAC guards covered in a separate article - permission checks, admin gates, and IDOR prevention on workspace and project access. Authentication answers "who are you"; RBAC answers "what may you touch."
API keys: HMAC-SHA256, hashed at rest
An API key is a bearer credential, so how the server stores it matters as much as how it verifies it. Simmetric Chat issues keys with a recognizable sk- prefix - familiar from the tooling ecosystems most developers already use - and never stores the key itself. At rest, each key exists only as an HMAC-SHA256 digest computed under a dedicated server-side secret, API_KEY_HMAC_SECRET:
sk-abc123… (issued once, shown once to the caller)
│ HMAC-SHA256(API_KEY_HMAC_SECRET, key)
▼
key_hash stored in api_keys - the key itself is unrecoverableThe design has two deliberate properties:
- Decoupled rotation.
API_KEY_HMAC_SECRETis independent ofJWT_SECRETand ofENCRYPTION_KEY. Rotating the session signing key never invalidates API keys, and rotating the at-rest encryption key never breaks key verification. Each secret has one job. - Constant-time verification. Verification is a keyed-HMAC digest lookup - O(1) with no
take-cap on a candidate scan. That is what makes API-key auth cheap enough to sit on every request in a multi-instance deployment.
Users manage their own keys through the API-key endpoints: create when needed, list on demand, revoke on demand. Revocation deletes the ability to reconstruct a valid credential; the digest no longer matches anything.
Signed webhooks and Web Push
Outbound integrations must be as verifiable as inbound ones.
Webhooks are signed with HMAC-SHA256. The server computes a signature over the payload using the webhook's secret, and your receiving service recomputes the digest before trusting the event body. A receiving endpoint never has to trust a caller claiming to be Simmetric Chat - it can prove it, byte for byte, with its own copy of the secret. A test-event endpoint lets you exercise a receiver without generating real platform activity.
Web Push uses the VAPID protocol for browser notifications. The server holds a VAPID keypair; the public key is exposed to authenticated clients through a system endpoint, subscriptions are managed through explicit subscribe/unsubscribe endpoints, and administrators can send a test push to verify the pipeline end-to-end. Both channels are always-on in the Community tier - signing and push are baseline platform behavior, not enterprise features.
Rate limiting at the edge
Rate limiting is layered by surface, with standard RateLimit-* headers emitted on limited routes:
- Global API limiter - 200 requests per minute per IP across every route in production
- Auth limiter - 10 requests per minute on authentication write endpoints, the tightest gate because credential endpoints are the ones abuse targets first
- Widget lead limiter - 3 requests per hour on lead capture, because a public form is where automation abuse concentrates
- Probe limiter - 10 requests per minute on connectivity probe endpoints, so setup-wizard testing cannot be weaponized
When REDIS_URL is configured, the counters live in a shared Redis store so the limits mean the same thing across every instance; otherwise each instance keeps its own store. Requests carrying a widget header skip the global limiter - widget traffic is throttled upstream by the widget service's own per-key limiter - and there is no dedicated chat rate limiter, because the agent enforces per-user concurrency, token budgets, and wallclock timeouts internally.
DLP: redaction before exfiltration
Data-loss prevention in Simmetric Chat is a filter plugin that scans both the inbound prompt and the outbound answer - including token-by-token streaming flushes - and redacts matches before content leaves your network. The scanner recognizes 10 built-in PII pattern classes:
- Email addresses
- Credit card numbers
- API keys and private keys
- Plus structured European identifiers: Italian Partita IVA, codice fiscale, and IBAN, added in v0.22 and seeded idempotently
The pattern configuration is database-backed and admin-managed: enable or disable any pattern by row, add up to 50 custom patterns with live compile validation and a test preview, and inspect a DLP match history panel filtered by source (chat or widget). Built-in regexes are frozen - you can disable a built-in but not rewrite it - while custom patterns carry your own definitions. Scans that can read the configured patterns use them through a short-TTL cache and fall back to the built-in set if the database is unreachable, so a database outage never disables redaction.
Two governance details round out the picture. A DLP_BYPASS_ROLES setting lets admins exempt specific roles from scanning, and every bypass fires an audit event recording who bypassed and on which surface - never the scanned content. Administrators can also reveal or re-hide the matched texts across a conversation through a single toggle, session-scoped and defaulting to hidden, so redaction stays reversible for authorized review but not silently.
The subtler hardening in v0.22
Small changes matter because they close the channels attackers actually probe:
- Timing-safe comparisons everywhere. Every remaining plaintext secret comparison - the collector-secret gates, the archive-import callback, the widget cache-bust key check - now uses
crypto.timingSafeEqualwith a length guard and fails closed when the expected secret is unset. - Enumeration-resistant login. When a username does not exist, login still runs a dummy hash comparison before answering, so response timing no longer distinguishes existing from non-existing users. Invalid bodies return a structured
400rather than a401leaking Zod internals. - Fail-loud misconfiguration. A server-side HMAC misconfiguration on API-key verification returns
500, by design - a misconfigured secret should never masquerade as an authentication failure. - Structured validation. Request bodies are validated with shared Zod schemas using
safeParse; every error is a uniform envelope with field-level details, so clients fail predictably.
The full API surface - endpoint tables, SSE event framing, and per-route auth requirements - is documented in the API reference, and a live Swagger UI ships with the server.
