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.
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.