# Privacy Policy — Dubyu (Ulster County Legislative AI)

**Operator:** John Peter Quigley (contact: johnpeter@quigley.email)
**Candidate version:** 3.0 — 2026-08-15 (invite-only browser pilot)
**Release state:** the browser clauses below describe the accepted pilot
contract. They do not mean that the browser pilot has been deployed or opened;
that requires matching code (including the required-version gate), migrations,
staging evidence, and an explicitly approved production release.
**Quick summary:** in Telegram, send `/privacy` to the bot. In the browser,
open the Privacy link. The Telegram command answers without a model call and
without storing the request.

This service answers questions about the Ulster County Legislature (resolutions,
votes, legislators, districts, meetings) from a database built on the public
record. Everything below describes what it stores about *you* when you use it,
for how long, who can see it, and how to control or delete it.

## 0. Unofficial resource — fact-check against the real record

Stated up front, and repeated in the Terms of Service
([TERMS.md](/terms)) you must accept before use:

- Dubyu is a **private, unofficial project** — not affiliated with, operated
  by, or endorsed by Ulster County, the Ulster County Legislature, or any
  government body.
- Answers are **AI-generated** over scraped public records and **can be wrong,
  incomplete, or out of date** even when they sound confident and carry a
  citation. **Verify anything that matters against the official record**
  (the Legislature's published resolutions, minutes, and roll calls at
  ulstercountyny.gov) before relying on it or repeating it. Where an answer
  and the official record disagree, the official record wins.
- Before Dubyu answers questions for you, you must **acknowledge in the browser
  or in chat
  that you have read this policy and the Terms of Service**. Your acceptance
  (a timestamp and the accepted version — never any content) is stored on
  your access record; a materially changed version requires a fresh
  acknowledgment.

The promises in this document are kept by minimization, short retention,
controls you hold yourself, and visible logs — not by encryption theater. Where
there is a limit to what we can technically guarantee, this document says so
plainly.

## 1. Who can use Dubyu

Access is **deny-by-default** and decided by a roster in Dubyu's own
database (`app_users`): one row per approved person, with a role —
**founder, admin, party_leadership, legislator, candidate, or guest** — and a
status. A **guest** is a person the operator approves for temporary access to
public-record Q&A only: guest access expires automatically (30 days by
default), carries no legislator-specific personalization, and has no digest.
Approved end-user roles (`party_leadership`, `legislator`, `candidate`, and
`guest`) may ask up to 15 questions in their first hour after activation, then
use the standard 5-question-per-hour rolling limit. Everything in this policy
— what is stored, the memory controls in §3, retention in §5, and who sees it
in §6 — applies to guests exactly as to every other role. An
environment-variable allowlist exists only as a bootstrap/fallback for the
founder; the database row is the authority.

Three ways onto the roster, all ending in an explicit grant:

- a one-time **claim QR/link** handed to you at onboarding;
- **`/request_access`**, which asks the operator to approve you;
- a **manual grant** by the operator.

Browser access does not create a second identity or offer public signup. An
operator may issue a one-time access code only to a named person who is
already active and unexpired on this roster and has an active workspace
membership. The code is consumed once to create a browser session. Suspending
or revoking the person, their membership, or their browser session blocks the
next authenticated request. Access codes and sessions do not grant Telegram
group access.

For people who are not on the roster:

- In a **direct message**, you get a short denial reply. No update row, message
  row, profile, username, question text, or raw Telegram id is stored. The bot
  does keep one content-free security counter under a SHA-256 pseudonym so
  repeated probing can be detected; it is deleted after 90 days.
- An update without an identifiable sender is always denied.
- In the **browser**, an invalid, consumed, expired, or revoked code is denied,
  and an invalid or revoked session is signed out. The service does not call
  the AI model for a denied request. Any retained security event is
  content-free and contains no raw code, cookie, IP address, or raw user agent.

### Group chats

The bot participates only in **explicitly authorized group chats**, organized
into topic lanes (General / Ask Dubyu / The Brief / Watch Alerts / Help). The
lane governs behavior: questions are answered where the bot is addressed,
scheduled briefs and alerts go to their own lanes, and general member
conversation is left alone.

- **Only messages that address the bot** (a reply to it, its name, or a clear
  legislative question where question-assist is enabled) are processed and
  stored. Everything else in the group is **neither answered nor stored** —
  the bot being in the room does not mean the room is recorded.
- Group membership is permissioned-users-only. If an account that is not on
  the roster speaks to the bot in a group, the bot stays **silent** toward it
  and flags the membership error to the operator. It does not answer, and it
  does not store that person's messages.
- DM and group history are walled off from each other: your private
  conversation never surfaces in a group answer, and per-user memory is never
  used in group answers.

## 2. What is stored

### Browser

For the invite-only browser pilot, Dubyu stores:

- **Access-code records** (`browser_access_codes`): a SHA-256 digest of the
  one-time code, the named roster user, expiry, consumption/revocation state,
  and content-free operator audit metadata. The raw code is never stored.
- **Session records** (`browser_sessions`): a SHA-256 digest of the opaque
  session token, the named roster user, expiry/revocation state, and
  content-free session timestamps. The raw session token is never stored.
- **Web conversations and messages** (`conversations`, `messages`): the owner,
  workspace, title, archive state, question text, final answer text, and the
  certified cited source URLs shown with the answer. Exploratory tool URLs are
  not displayed as citations.
- **Answer-run records** (`web_answer_runs`): the client request UUID,
  conversation claim/lease, completion state, and final structured response
  needed to prevent a retry from calling the model or saving the turn twice.
  Only completed question/answer pairs are reused as history.
- **Optional feedback** (`web_answer_feedback`): one bounded helpful/not-
  helpful assessment for an assistant message and, when supplied, one bounded
  category from the supported list.
- **Session-feedback submissions** (`web_session_feedback_submissions`): a
  transcript snapshot plus per-message ratings, filed when you submit a
  conversation for review. Each snapshot row is deleted 90 days after
  submission, or sooner under your shorter `/settings retention` override,
  and is removed on full live-data erasure.

The session token is sent only in an essential host-only cookie marked
`HttpOnly`, `Secure`, `SameSite=Lax`, and `Path=/`. It is never returned in
JSON, placed in a URL, written to browser local/session storage, or logged.
The browser does not use advertising or cross-site tracking cookies.

### Telegram

All storage is minimized at intake. From each Telegram update, **only** this
allowlisted projection is persisted (`telegram_updates.raw_update`):

| Field group | Kept keys |
|---|---|
| Update | `update_id` |
| Message | `message_id`, `date`, `text` |
| Sender (`from`) | `id`, `username`, `first_name` |
| Chat | `id`, `type` |
| Callback queries | `id`, `data`, plus the same sender/chat basics |

Everything else Telegram sends — chat titles, language codes, forwarded-message
provenance, entities, attachments, profile flags, and any future fields — is
**dropped before it reaches the database**.

Across the service, Dubyu also stores:

- **Messages** (`messages`): the text of your questions and Dubyu's answers,
  linked to a conversation.
- **Conversation record** (`conversations`): the channel and routing/ownership
  data. Telegram conversations use a chat id; browser conversations use the
  authenticated roster owner, title, and archive state.
- **User record** (`telegram_users`): your Telegram id, username, and first
  name (the same fields as the projection above).
- **Access roster** (`app_users`): your existing identity/binding, display name, role,
  status, who granted access, and your terms acknowledgment (the timestamp
  and version you accepted — see §0) — required to operate deny-by-default
  and the acknowledgment gate.
- **Onboarding and organization records** (`tenant_memberships`,
  `user_onboarding_state`): which organization/workspace you belong to in this
  deployment, your role/status there, and content-free onboarding timestamps
  such as profile confirmed, Privacy/Terms accepted versions, and completion.
- **Claim-link records** (`claim_tokens`, `claim_token_group_entitlements`):
  only the SHA-256 hash of the raw claim token, role/display-name payload,
  expiry, claim timestamp/account, and any group eligibility attached to that
  token. The raw claim token is never stored.
- **Group eligibility records** (`user_group_entitlements`): optional group
  access status (`eligible`, `invited`, `joined`, `left`, or `revoked`), the
  Telegram group id, invite sent/expiry timestamps, join/leave timestamps, and
  a SHA-256 hash of any personal invite link. Raw invite links are never
  stored.
- **Settings** (`user_settings`): typed preference columns only — digest
  on/off and day, verbosity, memory on/off, and an optional shorter retention
  window. There is **no free-text field** here (see §4).
- **Deletion/retry fence** (`telegram_user_privacy_epochs`): your Telegram id,
  a monotonic deletion generation, the last Telegram update id covered by
  `/forget`, and timestamps. It contains no message or answer text. Erased
  update rows retain only an opaque update id plus a `content_forgotten` flag
  until normal raw-update retention removes them. These content-free records
  stop a delayed Telegram retry from recreating data after deletion.
- **Security-event counters** (`security_events`): a fixed event category and
  reason, surface (browser, Telegram DM/group/voice, REST, or MCP), timestamp, and
  domain-separated SHA-256 pseudonyms used for the principal and retry
  deduplication. These rows cover denied or revoked access attempts, prompt
  injection/private-operations refusals, and rate/spend-limit friction. They
  contain no question/answer text, username, raw Telegram/chat/API-key id, IP
  address, user agent, route, or free-form payload. Routine watchdog and
  operations access sees aggregates only, never the pseudonyms.
- **Audit logs** (access-control/self-service metadata, never questions or
  answers):
  - `user_audit_log` — your own self-service actions (a settings change, a
    memory toggle, a `/forget` with its deleted-row counts). Ages out after
    90 days.
  - `permission_audit` — privileged operator actions (grants, revocations,
    key or browser-access mints, session/access revocations, and deletions). It
    can retain the acting and affected identifiers,
    timestamps, key ids/names, access-request display names, role/status or
    legislator-binding changes, and limited operator reason metadata. It never
    stores questions or answers. **Kept indefinitely and exempt from the
    retention purge** — disclosed here on purpose: it is the permanent record
    that access control was exercised correctly, and those existing rows are
    not rewritten during a full live-data erasure.

### Voice messages

Voice is processed by **Groq** (the `whisper-large-v3` transcription model):
your audio is sent to Groq, transcribed, and the transcribed *text* then
follows the normal question path above. **The audio itself is never stored by
this bot**, and only the transcript (as your question text) enters the
database. Groq may temporarily retain API inputs under its own service terms
unless a zero-retention arrangement is in place; this bot does not control
Groq's retention. If voice transcription isn't configured, the bot says it
can't listen yet rather than failing silently.

## 3. What Dubyu remembers — and your controls

By default Dubyu uses recent completed question/answer turns from your own
private thread so follow-ups like "who is he?" work. Browser threads are
durable and owner-scoped; one browser user cannot list, read, change, or delete
another user's thread. Unknown and cross-user conversation identifiers both
return "not found."

Telegram provides three controls, all instant and all yours:

| Control | What it does |
|---|---|
| `/memory off` | **Off = don't use.** Your history stops being used in answers. It is still *stored* under the normal retention window — turning memory off stops *use*, not *storage*. |
| `/settings retention <days>` | **Retention = keep shorter.** Shortens how long your messages are kept (shorten-only — you can never extend past the deployment default). The nightly purge enforces it. |
| `/forget` | **Forget = delete now.** After a confirmation button, permanently deletes your stored DM messages and erases raw-update payloads immediately. Content-free update-id tombstones and a deletion generation remain solely to stop delayed retries from recreating the deleted data. The bot reports exactly how many message rows and update payloads were erased. |

And one inspection command:

- **`/memory`** shows everything the bot holds about you — message counts and
  ages, your settings, and your memory state — drawn from the exact data the
  answering path reads. **If it isn't in `/memory` output, it cannot be used
  in an answer.**

These controls operate on your **DM conversation**. Group history is governed
by the group rules in §1 and operator runbook procedures, not by individual
commands.

In the browser, deleting a conversation deletes that owned thread and its
mutable messages, answer-run records, and feedback. Logging out ends the
current browser session; it does not delete conversation history. Full
live-data erasure is available under §8. A shorter per-user retention setting,
when set through an available settings surface, applies to that user's browser
messages as well as Telegram messages.

## 4. Durable memory: structured settings only

The bot stores **no free-text facts, notes, or profiles about you** — only the
typed settings columns listed in §2, each from a fixed menu of values. The bot
never infers or auto-saves anything about you, and the AI model cannot write
to memory at all (its tools are read-only). Any future "remember this about
me" feature will be opt-in, command-driven, and announced in a new version of
this policy before it exists.

## 5. How long it is kept (retention)

A purge job runs nightly and enforces, driven by deployment configuration:

| Data | Retention | What happens at expiry |
|---|---|---|
| Raw Telegram updates | 30 days (default) | Row deleted |
| Content-free forgotten-update tombstones | Same window as raw Telegram updates | Row deleted; the separate per-user deletion generation/cutoff remains |
| Deletion/retry fence | Retained after `/forget` and full live-data erasure | Content-free safety tombstone remains so delayed work cannot recreate erased data |
| Messages (questions + answers) | 90 days (default), **or your shorter `/settings retention` override** | **Row deleted entirely** |
| Browser access codes | Raw code never stored; consumed, expired, or revoked digest records may be purged after 30 days | Row deleted |
| Browser sessions | Raw cookie token never stored; expired or revoked digest records may be purged after 30 days | Row deleted |
| Browser answer runs + feedback | Answer runs are kept no longer than the related message-retention window; feedback follows its assistant message | Deleted by retention, with the related conversation/message, or on full live-data erasure |
| Session-feedback submissions (`web_session_feedback_submissions`) | 90 days after submission (default), **or your shorter `/settings retention` override** | Row deleted by the same purge, or on full live-data erasure |
| Self-service audit log (`user_audit_log`) | 90 days | Row deleted |
| Per-user usage records (`api_usage`) | Question text: 90 days; token counts, model, outcome, and cost totals: kept indefinitely | Question text is nulled at expiry, and immediately on full live-data erasure; the content-free totals remain for cost accounting |
| Content-free pseudonymous security events (`security_events`) | **90 days, fixed** | Row deleted entirely |
| Privileged-action audit (`permission_audit`) | Kept indefinitely (purge-exempt, see §2) | — |
| Conversation record | Telegram routing rows are kept while the service operates; browser rows are kept until the owner deletes them or full erasure is completed | Browser rows hold owner/title/archive metadata; messages remain subject to the message-retention window |
| User + roster records | Kept while access is active, suspended, or revoked | Removed from the live database on deletion request; revocation alone keeps a status record so access stays denied |
| Settings (`user_settings`) | Kept while you have access | Deleted with your roster row |
| Onboarding, membership, and group eligibility records | Kept while access/group eligibility operates | Deleted with your roster row on deletion request |
| Claim-token records | Raw token never stored; hashed records kept while useful for claim/audit-adjacent history | Claimed user-linked token records are deleted with your roster row on deletion request; unclaimed/expired token records contain no raw token |

**Design decision — delete content, retain only the retry fence:** expired
message and raw-update rows are deleted outright. `/forget` likewise deletes
message rows and all update/outbox content, but keeps an opaque update id as a
temporary completed tombstone plus the content-free generation/cutoff above.
Without that fence, Telegram could redeliver an erased update and cause the
bot to recreate the answer. The parent conversation row is kept because it
contains only the chat id the bot needs to operate.

`/forget` is the immediate path; the nightly purge is the backstop.

**Documented honest loophole:** the hourly rate limit counts your stored
recent messages, so a `/forget` also resets your rate counter. At the current
scale (a handful of known users) we accept and disclose this rather than keep
extra per-user counters; if it is ever abused, a separate content-free usage
counter will replace it (and this policy will be updated).

## 6. Who sees it

- **The operator** (John Peter Quigley) owns and runs the database. Stated plainly: **the operator can read your conversations with
  the bot, and uses them to improve the service** — for example turning real
  questions into evaluation test cases, tuning the bot's prompts, and
  reviewing answer quality. If you would not put it in a message to the
  operator, do not put it in a message to the bot.
  - Precision on the word "training": **Anthropic's commercial API does not
    train Anthropic's foundation models on this data.** When this policy says
    conversations are used to improve the service, that means improving
    *Dubyu Ulster itself*, by its operator — not feeding AI model training.
  - Operator review tooling logs content-review access in the audit trail
    (`operator_message_read` rows with counts/window/purpose, never message
    content). That log documents access for review; it is a hygiene measure,
    not a technical restraint on the database operator.
- **Browser hosting** — when you use the browser, your request goes directly
  to the Railway-hosted service. The essential session cookie is sent only to
  that host. Browser use is not routed through Telegram.
- **Telegram** — if you use the service through Telegram, Telegram receives and
  processes the messages, account identifiers, group membership events, and
  bot interactions that Telegram necessarily handles before the bot receives
  them. Telegram's own privacy terms also apply to your Telegram account and
  Telegram's platform handling.
- **Railway** — the hosting provider; data lives in a Railway-hosted
  PostgreSQL database, encrypted in transit.
- **Anthropic** — the text of your question (and the recent completed turns
  used from your private browser thread or, when memory is on, your DM) is sent
  to Anthropic's Claude API to generate the answer,
  under Anthropic's commercial API terms (API inputs and outputs are not used
  to train Anthropic's models). Anthropic may retain API inputs/outputs for a
  limited period under its commercial/API retention policy unless a different
  contractual retention setting applies.
- **Groq** — voice messages only: audio is sent to Groq for transcription and
  is never stored by this bot (see §2). Groq's own temporary API retention
  rules may still apply.
- No data is sold, shared with advertisers, or disclosed to any other third
  party. Server logs are kept PII-clean (no message bodies).

## 7. Weekly digest, announcements, and fair-use limits

- The **weekly digest** is on by default (Mondays). One command turns it off:
  `/digest off` (or `/settings digest off`); `/settings digest <day>` moves it.
- Scheduled posts to authorized group lanes (briefs, vote recaps, digests) are
  deterministic summaries of public record — they never contain anyone's
  conversation content.
- To keep the service available for everyone, usage is budgeted: roughly **$5
  per person per day** across that person's browser and Telegram use, **$20 per
  day** for a Telegram group, and a
  **$50 per day** global backstop. Approved end users may ask up to 15
  questions in their first hour after activation, then 5 questions per rolling
  hour. Founder/admin operator accounts have higher operational ceilings. When
  a limit is hit Dubyu says so plainly and the question is simply not
  processed — never silently dropped.
- Use is also counted in **tokens**: free accounts have a daily token
  allowance and Pro accounts a higher fair-use ceiling. To keep the service
  honest and affordable, the operator reviews usage **in aggregate** —
  per-user totals, and, for accounts whose use is flagged as unusually heavy
  or automated, a sample of their questions — to confirm the service is
  being used for its civic purpose and to prevent abuse. Sign-up is instant
  and self-serve; there is **no approval process** — limits are enforced
  automatically, after sign-up, only when an allowance is actually exceeded.

## 8. Deletion on request

- **Self-serve:** `/forget` (see §3) deletes your stored DM messages, erases
  raw-update and answer-outbox content immediately, and reports the counts.
  Only the content-free retry fence described in §2 remains.
- **Browser conversation:** use the conversation delete control to delete one
  thread you own. Logging out revokes the current session. Clearing the cookie
  only removes it from that browser; it does not revoke the server-side
  session, so ask the operator to revoke sessions for a lost device. Neither
  action erases saved conversations.
- **Full live-data erasure:** ask the operator (in the bot chat or at the
  contact above). The governed erasure transaction removes your DM and
  attributable group exchanges, raw-update/answer/command outboxes,
  browser codes, sessions, answer runs, feedback, owned browser conversations,
  conversation messages and profile records, alert subscriptions/prepared alerts,
  delivery checkpoints, settings, roster/onboarding/membership/group
  eligibility, claim-token links, callback claims, and in-flight group
  reservations. Person-bound API keys are revoked and cryptographically
  scrubbed; API usage is retained only under random per-row labels, and
  provider-call scope labels are randomized while retaining global cost
  accounting; recomputable person-level rate/spend keys are removed.
- **What still remains after full live-data erasure:** the immutable
  `permission_audit` ledger retains privileged-action evidence, including
  Telegram identifiers and the limited access-control metadata listed in §2,
  indefinitely. The content-free
  deletion/retry fence retains your Telegram id, deletion generation,
  update-id cutoff, and timestamps so an in-flight worker or delayed Telegram
  redelivery cannot recreate erased content. The final proof-of-erasure audit
  row records what was deleted, when, and row counts — never conversation
  content.
- **Backup caveat, stated honestly:** routine encrypted database backups age
  out on their own rotation schedule. Deletion from the live database is
  performed promptly; expired backups are not restored except for disaster
  recovery.

## 9. Privacy surfaces

Sending `/privacy` to the bot returns a short version of this policy with the
**deployed** retention values — and, for approved users, your live memory
state and effective retention — answered before any AI processing: no model
call is made and the `/privacy` request itself is not stored. The command
works even for users who are not on the roster.

The browser Privacy link presents this policy without requiring an active
session. Browser claim, legal, conversation, message, feedback, and logout
responses are non-cacheable; state changes require same-origin JSON requests.

## 10. Changes to this policy

Material changes are announced on the applicable browser/chat surface
**before** they take effect, and
this document is versioned in the project repository. The current version is
always available through the browser Privacy link and via `/privacy` in
Telegram.
