Privacy Policy — Dubyu (Ulster County Legislative AI)
Operator: John Peter Quigley (contact: feedback@trydubyu.com) 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) 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 retentionoverride, 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, orrevoked), 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 acontent_forgottenflag 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/forgetwith 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:
/memoryshows 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/memoryoutput, 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_readrows 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_auditledger 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.