---
name: Storage profiles and profile_terms CRUD
type: architecture-map-page
slice: 05-core-storage-hotkey-build
last_verified: 2026/05/09
---

# Storage profiles and profile_terms CRUD

> **Where you are:** [Architecture map](../README.md) → [Core, Storage, Hotkey, Build](README.md) → Storage profiles CRUD

**Plain English summary.** Profiles are user-defined contexts (Work, Personal, Game). Each profile has its own initial prompt and a list of custom dictionary terms. The default profile (`00000000-0000-0000-0000-000000000001`) is seeded by migration v6 and protected by SQL triggers from rename or delete.

## At a glance

- Source: `crates/storage/src/database.rs:776-1124`.
- Public surface: `list_profiles`, `get_profile`, `create_profile`, `update_profile`, `delete_profile`, `list_profile_terms`, `add_profile_term`, `delete_profile_term`.
- Public types: `ProfileRow`, `ProfileTermRow`.
- Public constant: `lumotia_storage::DEFAULT_PROFILE_ID = "00000000-0000-0000-0000-000000000001"` at `crates/storage/src/lib.rs:7`.
- Consumers: slice 2 profiles command, transcript inserts (every transcript carries a `profile_id`), the LLM prompt builder (slice 4) reads the profile's `initial_prompt` and `profile_terms` to condition cleanup.

## Public types

### `ProfileRow` — `crates/storage/src/database.rs:776`

```rust
pub struct ProfileRow {
    pub id: String,
    pub name: String,
    pub initial_prompt: String,
    pub created_at: String,
}
```

### `ProfileTermRow` — `crates/storage/src/database.rs:784`

```rust
pub struct ProfileTermRow {
    pub id: String,
    pub profile_id: String,
    pub term: String,
    pub note: String,
    pub created_at: String,
}
```

## Functions

### `list_profiles(pool) -> Result<Vec<ProfileRow>>` — `crates/storage/src/database.rs:950`

`SELECT ... FROM profiles ORDER BY created_at ASC`. Default profile is always first by virtue of being the earliest-created.

### `get_profile(pool, id) -> Result<Option<ProfileRow>>` — `crates/storage/src/database.rs:959`

Single-row select.

### `create_profile(pool, id, name, initial_prompt) -> Result<ProfileRow>` — `crates/storage/src/database.rs:968`

UUID is generated by the caller. `name` has a `UNIQUE` constraint at the column level (migration v6); a duplicate name returns `lumotia_storage::Error::Query { operation: "create_profile", source }` carrying the sqlx UNIQUE-constraint error.

### `update_profile(pool, id, name, initial_prompt)` — `crates/storage/src/database.rs:995`

Renames the profile and / or rewrites the initial prompt. **Updating the default profile's name** is short-circuited in Rust before hitting sqlite, raising `lumotia_storage::Error::InvalidReference { entity: Entity::Profile, reason: "Default profile cannot be renamed" }`. The `trg_protect_default_profile_rename` trigger (migration v6) is the structural backstop. Updating only `initial_prompt` is allowed.

### `delete_profile(pool, id) -> Result<()>` — `crates/storage/src/database.rs:1024`

Two guards before the DELETE:

1. **Application-layer guard:** if the profile owns any transcripts, refuse with a friendly error pointing the user at re-assignment. Implemented via `transcript_count_for_profile` (`database.rs:1103`).
2. **Database-layer guard:** `trg_protect_default_profile_delete` raises `RAISE(ABORT, 'cannot delete the default profile')` when the id matches.

The cascade on `profile_terms.profile_id` removes the profile's custom terms automatically.

### `list_profile_terms(pool, profile_id) -> Result<Vec<ProfileTermRow>>` — `crates/storage/src/database.rs:1044`

`SELECT ... FROM profile_terms WHERE profile_id = ? ORDER BY term ASC`. Used by the LLM prompt builder.

### `add_profile_term(pool, profile_id, term, note) -> Result<String>` — `crates/storage/src/database.rs:1062`

Generates a UUID v4 (per `crates/storage/Cargo.toml`'s `features = ["v4"]`), inserts the row, returns the new id. The `Cargo.toml` comment claims "v7 random" but the feature flag is `v4`. Practical effect is identical for our purposes (random unique ids); the comment is the inconsistency to clean up if anyone touches that line.

### `delete_profile_term(pool, id) -> Result<()>` — `crates/storage/src/database.rs:1085`

`DELETE FROM profile_terms WHERE id = ?`.

## Data flow / contract

- Every transcript references a profile via `transcripts.profile_id` (FK installed in migration v9). `insert_transcript` validates the FK at the application layer for a friendlier error.
- The LLM prompt builder loads the active profile's `initial_prompt` and the matching `profile_terms` at cleanup time.
- The default profile's id is a `pub const &str` so frontend code via Tauri commands can also reference it as a known constant (it is stamped into the JSON bridge by the Tauri preferences command).

## Watch-outs

- **Default profile protection is enforced by triggers, not by code.** A future migration that drops or renames the trigger silently removes the protection. There is no head-schema test that asserts the triggers are still present; today only the v6 migration tests assert protection at DML time. Consider adding a head-schema integration test that explicitly attempts to delete the default profile and asserts the failure.
- **Profile name `UNIQUE`-ness is case-sensitive.** "Work" and "work" are two different profiles. Worth a discussion about whether case-insensitive uniqueness is the right behaviour; today it is not enforced.
- **`transcript_count_for_profile` is the only application-layer FK guard.** Tasks reference no profile (the table predates the profile concept). If task-to-profile FKs land later, `delete_profile` needs a parallel check.
- **No `update_profile_term`.** A user fixing a typo in a term's note has to delete and re-add. Worth adding if the volume of profile terms grows.

## Tests

Migration tests cover the v6 trigger protection at `crates/storage/src/migrations.rs:977-1015` (`migration_v6_trigger_rejects_default_profile_delete`, `migration_v6_trigger_rejects_default_profile_rename`). The CRUD functions themselves do not have dedicated unit tests in `database.rs`; coverage is via the slice-2 integration tests.

## See also

- [Schema and migrations (v6, v7, v9)](storage-schema-and-migrations.md)
- [Storage overview](storage-overview.md)
- [Slice 4 LLM prompt builder](../04-llm-formatting-mcp/README.md) — consumer of `list_profile_terms`.
