Token storage
Where Pipedash keeps provider tokens, how the vault locks and opens, and how the stores encrypt tokens.
Pipedash keeps provider tokens in a token store, separate from config.toml. Four store types exist, and the desktop app picks one at startup from the backend and the vault password.
The four stores
| Store | Used when | Where tokens live |
|---|---|---|
| Keyring | Desktop, SQLite backend, no vault password and no encrypted tokens | One JSON map in the system keyring, service pipedash, account tokens |
| Encrypted SQLite | SQLite backend with a vault password | Columns providers.encrypted_token and providers.token_nonce |
| Encrypted PostgreSQL | PostgreSQL backend | Table encrypted_tokens with provider_id, nonce, ciphertext, created_at and updated_at |
| Environment | Token references in config.toml | Environment variables that ${VAR} or env:VAR reference |
PIPEDASH_TOKEN_<provider_id> has no effect today, and token references cover that need. See Configuration.
Desktop selection
At startup the desktop app checks 3 facts: the backend, whether PIPEDASH_VAULT_PASSWORD has a value, and whether pipedash.db already holds encrypted tokens. It applies the first matching row.
| Order | Condition | Result |
|---|---|---|
| 1 | SQLite, no password, no encrypted tokens | Keyring store |
| 2 | SQLite with encrypted tokens and no password, or PostgreSQL with no password | Locked vault |
| 3 | SQLite with a password | Encrypted SQLite store with the keyring as fallback |
| 4 | Anything else, which means PostgreSQL with a password | Encrypted PostgreSQL store |
The server never uses the keyring. For SQLite it requires the password, and for PostgreSQL it uses the encrypted store.
Keyring fallback migration
In row 3 the encrypted SQLite store is primary and the keyring is the fallback. Tokens move from the keyring to the database as Pipedash reads them.
- Read: the store asks the encrypted store first. On a miss it reads the keyring, copies the token to the encrypted store and then deletes the keyring entry.
- Write: the store writes to the encrypted store and removes any keyring copy.
- Delete: the store deletes from both.
If the copy fails, Pipedash returns the token anyway and retries on the next read.
Vault locking and opening
A vault stays locked while Pipedash needs a password and has none. Locked behavior differs by platform.
- Desktop: the app starts with a locked vault and no providers. It waits to load tokens and start the background tasks, including the refresh loop, until the vault opens.
- Web server: setup completes in locked mode when the password is missing, and token loading waits for the vault to open.
The /unlock page is public. When the vault needs a password and is locked, the UI redirects every other page to /unlock. A browser session also needs the bearer token that a successful open stores. Settings, then Storage, shows a Lock Vault button while a password-protected vault is open.
| Action | Web route |
|---|---|
| State | GET /api/v1/vault/status |
| Open | POST /api/v1/vault/unlock with { "password": "..." } |
| Lock | POST /api/v1/vault/lock |
| Variable status | GET /api/v1/storage/vault-password-status |
Opening the vault on desktop sets PIPEDASH_VAULT_PASSWORD for the process and checks the password by decrypting every token. A wrong password returns Invalid password. On success the app loads the providers, starts the background tasks, and the UI refetches providers and pipelines. With a SQLite backend that holds no encrypted tokens, it only stores the password for a later migration.
Locking removes the variable and answers Restart required to fully clear token cache. Tokens already in memory stay until the next restart.
The password has two jobs
PIPEDASH_VAULT_PASSWORD derives the encryption key, and the web server also uses it as the API bearer token.
- Encryption: the encrypted stores derive their AES key from it.
storage.vault_passwordinconfig.tomltakes priority over the variable. - API access: when the variable has a value, protected
/api/v1/routes need the headerAuthorization: Bearer <password>. The server compares it in constant time and answers401on a mismatch.
The check skips /api/v1/health, /api/v1/setup, /api/v1/vault, /api/v1/ws and /api/v1/plugins. The WebSocket expects { "type": "auth", "token": "..." } as its first message when a password exists.
The server reads the variable on every request. POST /api/v1/vault/lock removes it, so protected routes accept requests without a bearer token afterward. POST /api/v1/vault/unlock sets it to the password in the request.
Encryption
Both encrypted stores use AES-256-GCM with a 32-byte key and a random 12-byte nonce per token. Each store derives its key with Argon2id version 0x13.
| Use | Memory | Passes | Parallelism |
|---|---|---|---|
| SQLite token key | 65536 KiB | 3 | 1 |
| PostgreSQL token key | 4096 KiB | 1 | 1 |
| SQLite export key | 65536 KiB | 3 | 1 |
| Keyring export key | 4096 KiB | 1 | 1 |
The export keys protect the token blob that a storage migration moves between stores. See Setup wizard.
The PostgreSQL store with no password generates a random 64 character password and does not save it. Tokens it writes cannot be read after a restart. The SQLite store refuses to start without a password.