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)
| Field | Purpose | Retention |
|---|---|---|
| SHA-256 of the normalized message | repeat detection, verdict cache key | Retention:VerdictLogDays (default 90 days) |
| Verdict probabilities, severity, action, target confidence | dashboard log, tuning | same |
| Opaque author id (as supplied by the game; never a real name) | per-player erasure, feedback review | same, or until erased |
| Degraded/cached flags, model version, latency, token count, channel type, key kind | operations | same |
| Timestamp, project id, request id | correlation, idempotency | same |
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:
| Field | Content when enabled |
|---|---|
message_text | the raw message text exactly as sent (not normalized, not hashed) |
request_json | the entire request body as received: message, thread lines, author id and metadata, channel, request_id |
response_json | the entire response as returned: id, action, severity, verdicts, target, quota, model, latency |
trace_json | raw 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 |
| columns | timestamp, 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
DELETE /v1/evidence/{projectId}/{authorOpaqueId}(API key) and the dashboard's "Erase a player" action hard-delete all evidence rows for that author and remove the author id from verdict rows. The verdict statistics remain, anonymized.- Deleting a project cascades to its keys, rules, verdicts, evidence and usage.
- Deleting an organization cascades to its projects and members.
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
| Provider | Role | Location | Data |
|---|---|---|---|
| TypeSafe AI (Jev) | model inference | api.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.sh | payments, merchant of record | Polar (EU/US) | organization owner email, organization name, billing details entered at checkout |
| Fly.io | hosting | Amsterdam (ams) | everything above in transit and in process memory |
| Neon | Postgres | EU (Frankfurt recommended) | the tables above |
| Redis provider (Upstash EU or Fly Redis) | cache, counters, queue | EU | caches 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).