Chat Guard

Privacy notes

Chat Guard is operated by Kirill Tolmachev pr Beograd (Belgrade, Serbia), the data processor for moderated messages; the game studio using the service is the controller. This page describes what the service stores and for how long.

Chat Guard is a moderation proxy. Game servers send chat messages to POST /v1/moderate; the service returns verdicts and does not keep the message text unless a project opts in.

What is stored by default (per moderated message)

FieldPurposeRetention
SHA-256 of the normalized messagerepeat detection, verdict cache keyRetention:VerdictLogDays (default 90 days)
Verdict probabilities, severity, action, target confidencedashboard log, tuningsame
Opaque author id (as supplied by the game; never a real name)per-player erasure, feedback reviewsame, or until erased
Degraded/cached flags, model version, latency, token count, channel type, key kindoperationssame
Timestamp, project id, request idcorrelation, idempotencysame

Not stored by default: message text, thread context, IP addresses of players (the API sees only the game server), device identifiers. Publishable client keys (cg_pub_) change nothing about what is stored: the required author.id is the same opaque field, and the per-player rate-limit buckets live in Redis as hashed ids for at most a few minutes. When clients call the API directly, the API does see player IP addresses in transit; they are not logged or stored.

The normalized-text hash is unsalted, as specified; a short common message therefore has a predictable hash. It is treated as pseudonymous data and purged with the verdict row.

Evidence logging (opt-in per project)

When a project owner or admin enables evidence logging (Indie and Studio plans), the raw message text and the thread context sent with it are stored in evidence_log for the configured retention period (1 day to the plan maximum: 90 days on Indie, 365 on Studio; default 30). Rows are hard-deleted by a scheduled job once expired. Studio projects can export evidence as JSON.

Debug capture (temporary, off by default)

For the rollout testing phase only, the API has a private diagnostic table, debug_captures, that is disabled by default (DebugCapture:Enabled=false) and is not used by any product feature. When an operator enables it, every POST /v1/moderate call is written to that table, in addition to the rows above:

FieldContent when enabled
message_textthe raw message text exactly as sent (not normalized, not hashed)
request_jsonthe entire request body as received: message, thread lines, author id and metadata, channel, request_id
response_jsonthe entire response as returned: id, action, severity, verdicts, target, quota, model, latency
trace_jsonraw model verdicts before allow-rule suppression, local-filter diagnostics (matched/suppressed categories, block-rule hit and rule id, applied languages), reasons, tokens, cache/model/language/channel details
columnstimestamp, organization, project, key id and kind, author id, channel type, language, action, severity, degraded flag/reason, cached flag, model, latency

Not captured: idempotent replays (a repeated request_id served from the cache) and requests that are rejected before evaluation (validation errors, rate limits, quota errors).

Rows are hard-deleted after DebugCapture:RetentionHours (default 72 hours) by a purge job that exists only while the feature is enabled; disabling the feature stops new writes immediately and the remaining rows can be deleted with a single DELETE FROM debug_captures. The table has no foreign keys and no API or dashboard surface; it is read only with database access or the debug-capture-tail console command (see docs/ops.md).

This must stay off in GDPR regions. It stores raw player messages and author ids without a project-level opt-in, so it is only acceptable for rollout testing in deployments that do not serve EU/EEA or UK players. The API logs a warning at startup whenever it is enabled. The whole feature is scheduled for removal after the rollout phase.

Erasure

Caches (Redis, transient)

Verdict cache entries (10 minutes) hold probabilities keyed by hash; idempotency entries (10 minutes) hold the full response for a request_id, which contains no message text. Quota counters hold per-organization daily counts of model verdicts (a number, no message data) for 31 days.

Sub-processors

ProviderRoleLocationData
TypeSafe AI (Jev)model inferenceapi.typesafe.ai, AWS us-west-2 (measured 2026-09-22)message text, last 5 thread lines, channel/author metadata, policy lines; not used for training per TypeSafe's terms (zero data retention available for enterprise plans)
Polar.shpayments, merchant of recordPolar (EU/US)organization owner email, organization name, billing details entered at checkout
Fly.iohostingAmsterdam (ams)everything above in transit and in process memory
NeonPostgresEU (Frankfurt recommended)the tables above
Redis provider (Upstash EU or Fly Redis)cache, counters, queueEUcaches and queued log rows (seconds)

Message text leaves the EU when it is sent to Jev for inference. Projects that cannot accept this should not enable Jev (local-filter-only deployments are possible with Jev:Enabled=false) or should ask TypeSafe about an EU endpoint.

Accounts

Dashboard users sign in with Google or GitHub; only the verified email, display name and avatar URL are stored. No passwords. Refresh tokens are stored hashed and expire after 30 days.

Logs and traces

Structured logs never contain message text. OpenTelemetry spans carry ids, timings and actions only. Log retention is set by the hosting platform (Fly.io: a few days).