DOCS · SECURITY MODEL

How TillPulse handles your data.

A developer-facing rundown of the security primitives in the platform. For our customer-facing security commitments, see the public security page.

Authentication

  • Passwords hashed with bcryptjs (12 rounds).
  • Access token: JWT RS256 (jose), 15-minute TTL, signed with JWT_PRIVATE_KEY.
  • Refresh token: opaque 32-byte base64url string, only sha256 stored in auth_tokens, 30-day TTL, single-use, rotated on every refresh.
  • Both tokens delivered as httpOnly cookies (tp_access, tp_refresh) with SameSite=Lax.
  • Logout revokes the refresh row; subsequent refresh attempts return 401.
  • Password reset invalidates all active refresh tokens for that user — sign-out everywhere.

Two-factor authentication

TOTP (RFC 6238). Secrets encrypted at rest with AES-256-GCM using TOTP_ENCRYPTION_KEY. Backup codes are sha256-hashed; only prefixes shown back to the user.

SSO via OIDC

Per-org issuer + client ID + client secret + allowed-email-domains list. Authorisation flow uses PKCE; state + code_verifierare round-tripped through a 10-minute signed cookie. JIT user creation gated by domain match. id_token verified against the issuer's JWKS via createRemoteJWKSet. Client secrets stored AES-256-GCM-encrypted with ENCRYPTION_KEY.

API keys

Personal API keys: tp_sk_<region>_<random>. Shown once on creation; only key_prefix + sha256 of the rest stored. Revoke is an immediate DB delete + the next request hits the cache TTL ≤ 60s.

Multi-tenancy

Every resource is scoped to org_id. requireSession()returns { sub, org, role, email } from the signed JWT; queries filter by org_id derived from session.org. Role hierarchy: owner > admin > developer > viewer. Write operations check ['owner', 'admin'].includes(session.role).

PII scrubbing

Two-pass scrub. Patterns matched in both:

  • Email addresses
  • Phone numbers (NG, KE, ZA, GH, RW formats)
  • Payment card numbers (Luhn-checked)
  • NIN / BVN
  • M-Pesa transaction references
  • Bearer tokens in Authorization headers
  • IPv4 addresses in payloads
  1. SDK: Before transport. Configurable via beforeSend.
  2. Ingest API: Before publishing to the Redis Stream. Server can never trust client.

Rate limiting

Per-DSN sliding-window rate limiting. Per-IP ZSET as a fallback against DSN-spray. Both honoured by the ingest API; 429 returns include Retry-After.

Webhook signature verification

  • GitHub: X-Hub-Signature-256, timing-safe HMAC against per-project secret. Raw body required (re-serialising breaks the HMAC).
  • Slack interactive: X-Slack-Signature + X-Slack-Request-Timestamp; basestring is v0:{ts}:{rawBody}; 5-minute replay window.
  • Linear: Linear-Signature, HMAC-SHA256.
  • Jira: Shared ?token= query parameter (Jira REST webhooks are unsigned).
  • Slack action buttons: JWT (HS256, 24h, SLACK_ACTION_SIGNING_SECRET) embedded in button value; verified before applying any state change.

Encryption at rest

AES-256-GCM for: TOTP secrets, SSO client secrets, tracker integration tokens. Format iv:tag:ct (all hex). Key from ENCRYPTION_KEY(or TOTP_ENCRYPTION_KEY for the TOTP table — they are namespaced for blast-radius reasons).

Audit log

Every privileged action writes a row to audit_log: actor, action, resource, IP, user-agent, created_at. Visible at Settings → Audit log for owners and admins. Retained 365 days.

Threat intel

Cross-customer hashed indicators (sha256 of the indicator value — never the plaintext). An indicator is surfaced once it is confirmed malicious, corroborated across multiple organizations, or seen locally.

Reporting a vulnerability

Email security@tillpulse.io. We respond within one business day. Please do not file public issues for vulnerabilities. Coordinated disclosure preferred. See the public security page for our full statement.