Froid is an AI-powered personal journaling backend. It captures your thoughts via a Telegram bot, enriches them with structured analysis and semantic embeddings, and delivers daily and weekly reflections back to you.
Send a message to your Telegram bot. Froid stores it immediately and returns a confirmation. In the background, workers process each entry:
- Extraction — an LLM reads the entry and produces a structured document: emotions (with intensity and confidence), behaviors (with valence), psychological needs (with status), and possible patterns. All inference is explicit about uncertainty and never overstates what a single note can support.
- Embedding — the entry is vectorised for semantic similarity search, so you can query your journal by meaning rather than keywords.
At the end of the day, a review worker synthesises all of that day's raw notes and their structured extractions into a concise reflection delivered via Telegram.
Once a week (Monday by default), a weekly review worker synthesises the previous ISO week's daily reviews and their structured signals into a single reflection covering Monday through Sunday, and delivers it via Telegram. Run /week_review in the chat to request the most recent completed weekly review on demand.
Review delivery is on by default for every user, and daily and weekly delivery can be toggled independently. Run /reviews to see both settings, /reviews daily on|off or /reviews weekly on|off to change one, or /reviews on|off to change both at once — on-demand /day_review and /week_review keep working either way. These per-user settings sit underneath the FROID_DAILY_REVIEW_DELIVERY_ENABLED / FROID_WEEK_REVIEW_WORKER_ENABLED operator switches, which remain the master on/off for the whole instance.
A pre-built image is published to the GitHub Container Registry on every push to main.
Create an .env file (see Configuration for the full variable reference):
TELEGRAM_BOT_TOKEN=your-token-here
TELEGRAM_ALLOWED_USER_IDS=123456789,987654321 # optional: comma-separated list of allowed Telegram user IDs
OPENAI_API_KEY=your-key-here
FROID_EMBEDDING_WORKER_ENABLED=true
FROID_DAILY_REVIEW_EMBEDDING_WORKER_ENABLED=true
FROID_EXTRACTION_WORKER_ENABLED=true
FROID_DAILY_REVIEW_DELIVERY_ENABLED=true
FROID_WEEK_REVIEW_WORKER_ENABLED=trueThen run:
docker run --env-file .env -v ./data:/app/data ghcr.io/asiniscalchi/froid:latest serveSet FROID_MCP_ENABLED=true to expose the analyzer's read-only tools over the MCP Streamable HTTP transport at http://127.0.0.1:8080/mcp. The MCP server runs alongside the Telegram bot in the same process. Every request must carry an Authorization: Bearer <token> header with a token minted via the Telegram /token command (see Authentication).
FROID_MCP_ENABLED=true cargo run -- serveAvailable tools: journal_get, journal_get_recent, journal_search_text, journal_search_semantic, daily_review_get, daily_review_get_range, weekly_review_get, weekly_review_get_range, signals_search.
Authentication is always on: every request to /mcp must carry a bearer token minted through the bot:
Authorization: Bearer <your-token>
Users mint their own tokens by sending /token to the Telegram bot. Telegram is already the authenticated channel (the chat id arrives with the message), so the bot can safely issue HTTP credentials for exactly that identity: it generates a 256-bit random token, stores only its SHA-256 hash in the default database, and replies once with the plaintext. Each request is then served from the token owner's isolated journal database, so every user of a shared instance gets their own MCP endpoint. Sending /token again rotates the token; /token revoke disables access. Onboarding a user requires no operator action and no restart.
Requests without a valid token receive 401 Unauthorized; only the /health probe is served without authentication.
Because Telegram access is what mints HTTP credentials, TELEGRAM_ALLOWED_USER_IDS is the gate that decides who can become a user — set it on any instance whose bot a stranger could message.
Whenever the MCP listener is running, GET /health answers 200 OK with the service name and version. It is intentionally exempt from the bearer-token check so supervisors, load balancers, and container healthchecks can probe it without credentials:
curl http://127.0.0.1:8080/health
# {"name":"froid","status":"ok","version":"..."}All persistent state lives in DATA_DIR (default data/): the legacy database file plus one isolated SQLite database per user under journals/user_<chat_id>.sqlite3. To back up:
- Cold backup — stop the service and copy the whole data directory. SQLite databases are plain files; this is always safe.
- Hot backup — while the service is running, use SQLite's online backup instead of copying files directly (WAL side-files make raw copies of a live database unreliable):
sqlite3 data/journals/user_<chat_id>.sqlite3 ".backup 'backup.sqlite3'"for each database.
Restoring is the reverse: stop the service and put the files back in place.
For per-user data portability, every user can pull their own journal without operator involvement: send /export to the bot to receive a JSON file of all raw messages, and send that file back as a document with /import as the caption to load it (all-or-nothing; a collision with an existing message aborts the import). Telegram bots can download files up to ~20 MB, so an enormous import would need the file-level restore path above instead.
Each user's journal lives in its own SQLite database under DATA_DIR/journals/user_<chat_id>.sqlite3. The users subcommands operate directly on those files, so run them while the server is stopped:
froid users list # chat id, size, last modified, path
froid users delete <chat_id> --yesusers delete permanently removes the user's entire journal (database plus WAL side-files) and refuses to run without --yes. Combined with the /export command this covers data-portability and right-to-erasure requests.
All options can be set via environment variables or the equivalent --flag CLI argument. Copy .env.example as a starting point.
| Variable | Default | Description |
|---|---|---|
TELEGRAM_BOT_TOKEN |
— | Required. Telegram bot credentials |
TELEGRAM_ALLOWED_USER_IDS |
(all private chats) | Comma-separated list of allowed Telegram user/chat IDs |
OPENAI_API_KEY |
— | Required when any worker or the MCP semantic search tool is enabled |
DATA_DIR |
data |
Directory for persistent data |
DATABASE_FILE |
froid.sqlite3 |
SQLite database filename (resolved relative to DATA_DIR) |
RUST_LOG |
info |
Log level filter (e.g. debug, froid=trace) |
All workers are disabled by default and require OPENAI_API_KEY.
| Variable | Default | Description |
|---|---|---|
FROID_EMBEDDING_WORKER_ENABLED |
false |
Enable journal entry embedding worker |
FROID_EMBEDDING_WORKER_BATCH_SIZE |
20 |
Entries processed per cycle |
FROID_EMBEDDING_WORKER_INTERVAL_SECONDS |
300 |
Polling interval (seconds) |
FROID_DAILY_REVIEW_EMBEDDING_WORKER_ENABLED |
false |
Enable daily review embedding worker |
FROID_DAILY_REVIEW_EMBEDDING_WORKER_BATCH_SIZE |
20 |
Reviews processed per cycle |
FROID_DAILY_REVIEW_EMBEDDING_WORKER_INTERVAL_SECONDS |
300 |
Polling interval (seconds) |
FROID_EXTRACTION_WORKER_ENABLED |
false |
Enable structured extraction worker |
FROID_EXTRACTION_WORKER_BATCH_SIZE |
20 |
Entries processed per cycle |
FROID_EXTRACTION_WORKER_INTERVAL_SECONDS |
300 |
Polling interval (seconds) |
FROID_DAILY_REVIEW_DELIVERY_ENABLED |
false |
Enable daily review generation and delivery |
FROID_DAILY_REVIEW_DELIVERY_INTERVAL_SECONDS |
300 |
Polling interval (seconds) |
FROID_SIGNAL_WORKER_ENABLED |
false |
Enable daily review signal extraction worker |
FROID_SIGNAL_WORKER_BATCH_SIZE |
20 |
Reviews processed per cycle |
FROID_SIGNAL_WORKER_INTERVAL_SECONDS |
300 |
Polling interval (seconds) |
FROID_WEEK_REVIEW_WORKER_ENABLED |
false |
Enable weekly review generation and delivery |
FROID_WEEK_REVIEW_WORKER_INTERVAL_SECONDS |
300 |
Polling interval (seconds) |
FROID_WEEK_REVIEW_KICKOFF_DAY |
Monday |
Weekday on which weekly reviews are generated |
FROID_WEEK_REVIEW_MIN_DAILY_REVIEWS |
1 |
Minimum completed daily reviews required before generating a weekly review |
| Variable | Default | Description |
|---|---|---|
FROID_MCP_ENABLED |
false |
Enable the MCP Streamable HTTP server |
FROID_MCP_BIND |
127.0.0.1:8080 |
Bind address (e.g. 0.0.0.0:8080 for Docker Compose) |
Override the OpenAI model used by each pipeline stage. Accepts any model name recognised by the OpenAI API.
| Variable | Default | Description |
|---|---|---|
FROID_EMBEDDING_MODEL |
text-embedding-3-small |
Embedding model for journal entries and daily reviews |
FROID_ENTRY_EXTRACTION_MODEL |
gpt-5-mini |
Model used for structured entry extraction |
FROID_REVIEW_MODEL |
gpt-5-mini |
Model used for daily review generation |
FROID_SIGNAL_EXTRACTION_MODEL |
gpt-5-mini |
Model used for daily review signal extraction |
FROID_WEEK_REVIEW_MODEL |
gpt-5-mini |
Model used for weekly review generation |
OPENAI_BASE_URL |
https://api.openai.com/v1 |
Base URL for all LLM and embedding requests. Point it at any OpenAI-compatible endpoint (Ollama, OpenRouter, a self-hosted gateway) to keep journal data off openai.com; pair it with the model variables above |
Override the prompt file used by each pipeline stage. The version tag recorded in the database is derived automatically from the filename stem (e.g. entry_extraction_v2 from entry_extraction_v2.md).
| Variable | Default |
|---|---|
FROID_ENTRY_EXTRACTION_PROMPT_PATH |
prompts/entry_extraction_v1.md |
FROID_REVIEW_PROMPT_PATH |
prompts/daily_review_v3.md |
FROID_SIGNAL_EXTRACTION_PROMPT_PATH |
prompts/daily_review_signal_extraction_v1.md |
FROID_WEEK_REVIEW_PROMPT_PATH |
prompts/weekly_review_v1.md |
See CONTRIBUTING.md for development setup, build instructions, and project conventions.
This project is licensed under the GNU Affero General Public License v3.0 or later.
SPDX-License-Identifier: AGPL-3.0-or-later — see LICENSE.