Data Transparency
This page provides a plain-language technical explanation of exactly what data Calendar Wizard stores, how it is structured, where it lives, and how encryption works. There are no abstractions here — this is the actual implementation.
This page is for technically curious users who want to understand exactly what is stored about them. For the legal policy, see the Privacy Policy. Both pages are authoritative; where they differ, the Privacy Policy governs.
Data Flow Overview
When you send a message to the Calendar Wizard bot, this is what happens:
You (Telegram app)
|
| Encrypted message via Telegram's servers
v
Telegram Bot API (Telegram's infrastructure)
|
| Webhook POST to our server (HTTPS, TLS 1.2+)
v
Calendar Wizard server (westoverxyz, private VPN)
|
|-- Parse command (held in memory only, never written to disk)
|
|-- Look up your account in PostgreSQL
| (telegram_id → credentials → google_oauth_token)
|
|-- Decrypt OAuth token in memory using pgcrypto
|
|-- Make API call to Google Calendar (HTTPS, TLS 1.2+)
| --> Google's servers
|
|-- Write result to PostgreSQL (event cache)
|
|-- Send reply back to you via Telegram Bot API
v
You see the response in Telegram
At no point does your message text leave memory and get written to a database or log file. The only things that get written to the database are: your account row (on first connect), your encrypted OAuth token (on connect), your timezone (when you set it), and calendar event data (when events are created or fetched).
Database Schema
The Calendar Wizard service uses a PostgreSQL database. Below are the relevant tables and what each column contains. Column names are shown as they appear in the actual schema.
Table: calendar_users
One row per connected user account.
| Column | Type | Content |
|---|---|---|
id |
BIGINT PRIMARY KEY | Internal numeric ID (auto-increment) |
telegram_id |
BIGINT UNIQUE NOT NULL | Your Telegram user ID (a number assigned by Telegram, e.g. 123456789). Not your username or phone number. |
timezone |
TEXT | IANA timezone string you set, e.g. America/New_York. Defaults to UTC. |
created_at |
TIMESTAMPTZ | When your account was created |
last_seen_at |
TIMESTAMPTZ | Timestamp of your last interaction with the bot |
Table: calendar_credentials
One row per connected Google account. Linked to calendar_users.
| Column | Type | Content |
|---|---|---|
id |
BIGINT PRIMARY KEY | Internal numeric ID |
user_id |
BIGINT NOT NULL | Foreign key to calendar_users.id |
google_email |
TEXT | Your Google account email address. Used only to identify which calendar was connected; not used for marketing or contact. |
access_token_enc |
BYTEA | Google OAuth access token, encrypted with pgcrypto symmetric encryption. The plaintext token is never stored. |
refresh_token_enc |
BYTEA | Google OAuth refresh token, encrypted with pgcrypto. Used to obtain new access tokens when the access token expires (every 1 hour). |
token_expiry |
TIMESTAMPTZ | When the current access token expires. Stored in plaintext because it contains no sensitive information. |
connected_at |
TIMESTAMPTZ | When you first connected Google Calendar |
last_refreshed_at |
TIMESTAMPTZ | When we last successfully refreshed the token |
Table: calendar_events
Cached event records. This is a local copy of events from your Google Calendar. It is used to provide faster responses (without calling the Google API on every request) and to show reminders. It is not the primary store of your events. The authoritative source is always your Google Calendar.
| Column | Type | Content |
|---|---|---|
id |
BIGINT PRIMARY KEY | Internal numeric ID |
user_id |
BIGINT NOT NULL | Foreign key to calendar_users.id |
google_event_id |
TEXT | Google's unique ID for the event (e.g. abc123xyz_20260301T140000Z) |
summary |
TEXT | Event title |
start_time |
TIMESTAMPTZ | Event start in UTC |
end_time |
TIMESTAMPTZ | Event end in UTC |
description |
TEXT | Event description, if present |
location |
TEXT | Event location, if present |
synced_at |
TIMESTAMPTZ | When this cache row was last updated from Google |
Event cache rows older than 90 days since synced_at are automatically
deleted by a nightly cleanup job.
Encryption Details
OAuth tokens are encrypted using PostgreSQL's built-in pgcrypto extension
with symmetric (password-based) encryption. Here is the mechanism:
-- How tokens are encrypted before INSERT:
UPDATE calendar_credentials
SET access_token_enc = pgp_sym_encrypt(
plaintext_token,
current_setting('app.encryption_key')
)
WHERE user_id = $1;
-- How tokens are decrypted for use:
SELECT pgp_sym_decrypt(
access_token_enc,
current_setting('app.encryption_key')
)::TEXT AS access_token
FROM calendar_credentials
WHERE user_id = $1;
The encryption key (app.encryption_key) is a randomly generated 256-bit
key that is:
- Never stored in the database.
- Never committed to source control.
- Injected at service start time via an environment variable stored in a secrets file
that is readable only by the
familiarservice account on the host. - Backed up separately from the database backup.
This means that even if an attacker obtained a full database dump, they could not decrypt the OAuth tokens without also obtaining the encryption key from a separate location.
What Telegram Sees vs. What We See
| Data | Telegram sees | We see | We store |
|---|---|---|---|
| Your Telegram username | Yes | Yes (in webhook payload) | No (not persisted) |
| Your Telegram user ID | Yes | Yes | Yes (primary key) |
| Your phone number | Yes | No | No |
| Message text | Yes | Yes (processed in memory) | No |
| Message timestamps | Yes | Yes | Indirectly via last_seen_at |
| Your IP address | Yes (from Telegram app) | No | No |
What Google Sees vs. What We See
| Data | Google sees | We see | We store |
|---|---|---|---|
| Your Google account email | Yes | Yes (from OAuth response) | Yes (plaintext, in calendar_credentials) |
| Your calendar events | Yes (it's their service) | Yes (via API) | Yes (cached, 90-day expiry) |
| OAuth scopes granted | Yes | Yes | No (tracked by Google) |
| Your contacts | Yes | No (we do not request this scope) | No |
| Your email content | Yes (Gmail) | No (we do not request this scope) | No |
Infrastructure
Understanding where the service runs helps you assess the risk profile:
-
Host machine:
westoverxyz— a dedicated server running Ubuntu, located in a private data center. Not a shared cloud VM. - Database: PostgreSQL 15, running on the same machine as the application. No network exposure; connects via Unix socket.
- Network access: The server is not publicly accessible. It is reachable only via Tailscale VPN. The Telegram webhook endpoint is the only publicly-routed surface, and it runs behind Cloudflare's reverse proxy with TLS termination.
- Backups: Database backups are encrypted and stored off-machine. The encryption key is stored separately from the backup.
- Operating system updates: Unattended-upgrades is configured for automatic security patches.
Requesting Data Deletion
You have two ways to delete your data:
Option 1: Bot command
Send /delete to @nds_klaus_bot.
The bot will ask for confirmation, then delete your row from calendar_users,
calendar_credentials, and all rows in calendar_events
associated with your account.
Option 2: Email request
Email james@westover.dev with the subject "Delete my data". Include your Telegram username or user ID if known so we can locate your record. We will confirm deletion within 48 hours and provide a confirmation number.
What happens when deletion runs
-- Deletion cascade (CASCADE is configured on foreign keys)
DELETE FROM calendar_users WHERE telegram_id = $1;
-- This automatically deletes:
-- calendar_credentials WHERE user_id = deleted_id
-- calendar_events WHERE user_id = deleted_id
Application logs that contain your Telegram user ID (used for debugging) are rotated and deleted after 30 days regardless of account status. There is no way to accelerate log deletion beyond that window, as logs are write-once for security auditability.
Deleting your account from our system does not delete events from your Google Calendar. We only manage events you explicitly ask us to create or modify. Your Google Calendar is your data; deleting our account removes our ability to access it, but does not affect the calendar content itself.