---
module: profile.appearance
title: Appearance
tab: Architect
order: 30
audience: dev
---

How the **Appearance** feature is built. It is a thin CRUD over a JSONB column — **no Kafka, no executor→record-store pattern, no cross-service events**. Theme rendering is entirely client-side.

## Services involved
| Concern | Service | Detail |
|--------|---------|--------|
| UI | `hiveops-profile` (SPA) | `AppearanceSettings.svelte`, `profile.bcos.cloud`, dev port 5184 |
| API + persistence | **hiveops-auth** | owns the `users` table and the preferences JSON; port 8082, nginx `/api/auth/` |
| Data store | Postgres `hiveiq_auth` | `users.preferences` JSONB column |

`hiveops-auth` is the **single owner** of user preferences — no other service reads or writes `themeMode`/`tableDensity`. See [platform.service-ownership].

## Data flow (read + write)
```
AppearanceSettings.svelte
  └─ profileAPI.getPreferences()  → GET  /auth/api/users/me/preferences
  └─ profileAPI.updatePreferences({themeMode, tableDensity})
                                  → PUT  /auth/api/users/me/preferences
        │  (axios authApi, baseURL = gatewayUrl; Bearer JWT from localStorage)
        ▼
UserProfileController  (@RequestMapping "/auth/api/users/me")
  └─ @AuthenticationPrincipal → email
        ▼
UserProfileService.getPreferences / updatePreferences
  └─ UserRepository.findByEmail(email)
  └─ merges non-null request fields onto User.preferences
  └─ userRepository.save(user)
        ▼
Postgres hiveiq_auth: users.preferences (JSONB)
```

There is **no message bus in this path**. Nothing is published to Kafka on a preference change. (Contrast with the executor→Kafka→record-store pattern used elsewhere — see [platform.kafka] and [platform.data-architecture]; it does **not** apply here.)

## Persistence detail
- `User.preferences` is a `UserPreferences` POJO persisted as JSON via Hibernate:
  - `@JdbcTypeCode(SqlTypes.JSON)` + `@Column(columnDefinition = "jsonb")`.
- Column created by migration `V9__add_user_preferences.sql`:
  `ALTER TABLE users ADD COLUMN IF NOT EXISTS preferences JSONB NOT NULL DEFAULT '{}'::jsonb;`
- `UserPreferences` bundles display / notifications / **appearance** / security settings in one blob. Appearance fields:
  - `themeMode` — default `"SYSTEM"` (`LIGHT | DARK | SYSTEM`)
  - `tableDensity` — default `"comfortable"` (`comfortable | compact`)
- `updatePreferences` is a **partial merge**: only non-null fields from `UpdatePreferencesRequest` overwrite the current blob, so the frontend safely PUTs just `{themeMode, tableDensity}` without clobbering notification/security prefs.

## Key API endpoints
| Method | Path | Body | Returns |
|--------|------|------|---------|
| GET | `/auth/api/users/me/preferences` | — | full `UserPreferences` |
| PUT | `/auth/api/users/me/preferences` | `UpdatePreferencesRequest` (partial) | updated `UserPreferences` |

Authz: both are `authenticated()` only (no role gate). Identity comes from the JWT principal, so the write target is always the caller's own row.

## Theme application (client-side)
- On save, `applyTheme(themeMode)` toggles `document.documentElement.classList` `dark-mode`:
  - `DARK` → add, `LIGHT` → remove, `SYSTEM` → follow `matchMedia('(prefers-color-scheme: dark)')`.
- Cross-module sync in the Electron Browser: `window.electronAPI?.setTheme?.(themeMode.toLowerCase())` so sibling SPAs repaint immediately. Plain web tabs pick up the theme on their next preferences load.
- CSS lives in per-component `:global(.dark-mode) …` rules (e.g. in `AppearanceSettings.svelte`); there is no central theme service.

## Notes for future work
- `themeMode` and `tableDensity` are plain `String` in `UpdatePreferencesRequest` with **no** `@Pattern`/enum validation, so the API will accept and store arbitrary values. Consider server-side enum validation if this becomes a data-quality concern.
- Whether every SPA honors `tableDensity` is not centrally enforced (each frontend reads the same pref independently) — treat consistent density as a per-app contract, not something auth guarantees.

Cross-links: [platform.data-architecture] · [platform.kafka] · [platform.service-ownership]
