Secret Storage
TeamWeb AI manages several types of sensitive credentials: LLM provider API keys, channel plugin tokens (Slack bot tokens, Telegram bot tokens), third-party service credentials (WordPress, Unsplash, analytics), and user passwords. Each is handled differently depending on where it’s configured and how it’s used.
Environment Variables
Core infrastructure secrets are stored as environment variables, loaded from a .env file or the deployment environment:
| Secret | Purpose |
|---|---|
SECRET_KEY | Flask session signing and CSRF token generation |
DATABASE_URL | PostgreSQL connection string |
REDIS_URL | Redis broker connection |
S3_ACCESS_KEY_ID / S3_SECRET_ACCESS_KEY | S3-compatible media storage (optional) |
These are never stored in the database and are only available to the web and worker processes.
SECRET_KEY in production. The default value in development is not secure. This key signs session cookies and CSRF tokens — if compromised, an attacker could forge sessions.Plugin Configuration Secrets
LLM provider API keys, channel tokens, and third-party service credentials are configured through the plugin system and stored in the database as JSON:
| Plugin Type | Storage Level | Database Table |
|---|---|---|
| LLM / Embedding providers | Global (one active) | core_plugin_configs |
| Channel plugins (Slack, Telegram, etc.) | Per assistant | assistant_plugins |
| Tool plugins (WordPress, Unsplash, etc.) | Per project | project_plugins |
Each plugin defines a configuration schema that can mark fields as secret. Secret fields receive special treatment in the UI:
- When displaying a configuration form, secret fields are shown as empty with a placeholder indicating a value is already stored
- On form submission, if a secret field is left empty and a value already exists, the existing value is preserved — preventing accidental overwrites
- Secret values are never rendered back into form fields or exposed in API responses
Technical Details
Storage format — Plugin configuration is stored in config_json columns as plain JSON. Fields marked secret: true in the plugin’s config_schema are not encrypted at rest — the protection is at the application layer (masking in forms, not returning values in responses). Database-level access to these tables would expose the raw credentials.
Masking implementation — When rendering a plugin configuration form, the system iterates over the schema and replaces any field with secret: true with an empty string in the display data. A separate has_secrets dictionary tracks which secret fields already have stored values, so the UI can show “Value stored” indicators without revealing the actual value. On POST, the save logic checks whether each secret field was left blank — if so, it keeps the existing value from the database rather than overwriting with an empty string.
Implications — Since secrets are stored as plain JSON in PostgreSQL, protecting the database is critical. Anyone with database access (direct SQL, backup files, or a compromised database connection string) can read all configured API keys and tokens. See Database Protection for how TeamWeb AI isolates the database from untrusted code execution.
User Passwords
User passwords are hashed using Werkzeug’s password hashing utilities before storage. Plaintext passwords are never stored or logged. The hashing uses a secure algorithm with salt — even with database access, recovering passwords requires a brute-force attack against the hash.
Session Security
TeamWeb AI uses Flask-Login for session management with server-side session state. Sessions are signed using the SECRET_KEY to prevent tampering. Public chat visitors receive a separate session with a unique identifier that is isolated from authenticated user sessions.
CSRF Protection
All form submissions are protected by CSRF tokens via Flask-WTF. The CSRF middleware is applied globally, with targeted exemptions only for:
- Webhook endpoints — Inbound messages from Slack, Telegram, and Postmark are verified using channel-specific mechanisms (HMAC signatures, secret tokens) rather than CSRF tokens
- Public chat — Anonymous chat uses session-based identification instead of CSRF tokens
Webhook Verification
Each channel plugin implements its own request verification to ensure inbound webhooks are authentic:
| Channel | Verification Method |
|---|---|
| Slack | HMAC-SHA256 signature using the signing secret, verified against the X-Slack-Signature header with timestamp replay protection |
| Telegram | Secret token comparison via X-Telegram-Bot-Api-Secret-Token header |
| Postmark | Relies on external network-level controls (IP allowlisting) |
Unverified requests are rejected before any message processing occurs. After verification, the system also checks that the sender is a registered, active user — unknown senders receive a 403 response.
Best Practices
- Use strong, unique values for
SECRET_KEYandPOSTGRES_PASSWORDin production - Restrict database access to only the application services that need it
- Use a secrets manager or encrypted environment variable store in production deployments
- Rotate API keys periodically, especially if you suspect a credential leak
- Review the Database Protection and Code Sandboxing pages to understand how TeamWeb AI isolates untrusted code from your credentials