Internal Documentation internal
TalkIDE internal documentation

View and update the authenticated user’s notification preferences. Requires a valid JWT access token. Preferences are loaded via GET /api/v1/me/notification-preferences and saved via PUT /api/v1/me/notification-preferences with optimistic auto-save on every change.

  • All 7 fields have defined defaults (see entity model below); a preference row is created lazily on the first GET — no migration is run for existing users.
  • The full replace strategy is used on PUT (not PATCH) — the client always sends all 7 fields.
  • Toggle change triggers an immediate API call (debounced 500 ms to batch rapid multi-toggle changes).
  • Optimistic update: FE applies the change locally before the API response; on error it rolls back and shows a toast notification.
  • Only emailBilling has an active BE gate (HostingDunningService). The remaining 6 fields are persisted for forward-compatibility but have no BE side-effect until the corresponding channels are implemented.
  • Transactional e-mails (password reset, GDPR export/delete) and admin-initiated e-mails (waitlist invite) are always sent regardless of notification preferences — only BILLING_ALERT is gated.
sequenceDiagram
    actor User

    %% --- GET notification preferences ---
    User->>+FE: opens Notifications section in Settings

    FE->>+BE: GET /api/v1/me/notification-preferences <br> Authorization: Bearer {accessToken}

    BE->>BE: validate JWT access token
    alt access token invalid or missing
        BE-->>FE: 401 Unauthorized <br> ErrorResponse
        FE-->>User: redirect to login
    end

    BE->>DB: find UserNotificationPreferences by userId
    alt row does not exist (first visit)
        BE->>DB: insert row with default values
    end

    BE->>-FE: 200 OK <br> NotificationPreferencesResponse

    FE->>-User: render 1 active toggle (emailBilling) <br> + 6 disabled "Coming soon" controls

    %% --- PUT notification preferences ---
    User->>+FE: toggles emailBilling switch

    FE->>FE: optimistic update (apply change in local state immediately)
    FE->>FE: debounce 500 ms

    FE->>+BE: PUT /api/v1/me/notification-preferences <br> Authorization: Bearer {accessToken} <br> UpdateNotificationPreferencesRequest

    BE->>BE: validate JWT access token
    alt access token invalid or missing
        BE-->>FE: 401 Unauthorized <br> ErrorResponse
        FE-->>User: rollback optimistic update <br> show error toast
    end

    BE->>BE: validate request (digest enum, not_null booleans)
    alt request is invalid
        BE-->>FE: 400 Bad Request <br> ErrorResponse
        FE-->>User: rollback optimistic update <br> show error toast
    end

    BE->>DB: upsert UserNotificationPreferences for userId <br> set updatedAt = now()

    BE->>-FE: 200 OK <br> NotificationPreferencesResponse

    FE->>-User: confirmed (no additional UI feedback on success)

Get Notification Preferences

GET /api/v1/me/notification-preferences (no request body)

200 OK NotificationPreferencesResponse:

{
  "emailDeploys": true,
  "emailErrors": true,
  "emailWeekly": true,
  "emailBilling": true,
  "pushAgent": false,
  "pushMentions": true,
  "digest": "weekly",
  "updatedAt": "2026-05-23T10:00:00Z"
}

401 Unauthorized (missing or invalid access token) ErrorResponse:

{
  "status": 401,
  "code": "AUTHENTICATION_FAILED",
  "message": "Access token is missing or invalid"
}

Update Notification Preferences

PUT /api/v1/me/notification-preferences UpdateNotificationPreferencesRequest:

{
  "emailDeploys": true,
  "emailErrors": true,
  "emailWeekly": true,
  "emailBilling": false,
  "pushAgent": false,
  "pushMentions": true,
  "digest": "weekly"
}

200 OK NotificationPreferencesResponse (returns updated state):

{
  "emailDeploys": true,
  "emailErrors": true,
  "emailWeekly": true,
  "emailBilling": false,
  "pushAgent": false,
  "pushMentions": true,
  "digest": "weekly",
  "updatedAt": "2026-05-23T10:05:00Z"
}

400 Bad Request (validation) ErrorResponse:

{
  "status": 400,
  "code": "VALIDATION_ERROR",
  "message": "Validation failed",
  "errors": [
    { "field": "digest", "message": "must be one of: off, daily, weekly" }
  ]
}

401 Unauthorized (missing or invalid access token) ErrorResponse:

{
  "status": 401,
  "code": "AUTHENTICATION_FAILED",
  "message": "Access token is missing or invalid"
}

Entity Model

UserNotificationPreferences

One-to-one with users. Created lazily on first GET.

ColumnTypeNullableDefaultNote
idbigint PKnoauto
user_idbigint FKnoFK → users.id, UNIQUE constraint
email_deploysbooleannotrueFUTURE — channel not implemented
email_errorsbooleannotrueFUTURE — channel not implemented
email_weeklybooleannotrueFUTURE — digest not implemented
email_billingbooleannotrueACTIVE — gates BILLING_ALERT
push_agentbooleannofalseFUTURE — push infra not implemented
push_mentionsbooleannotrueFUTURE — push infra not implemented
digestvarcharno'weekly'Enum-as-string: off, daily, weekly. FUTURE — scheduler not implemented
updated_attimestamptznonow()Set on every upsert

Liquibase changeset: new file 00XX-add-user-notification-preferences.yaml (exact number = next in sequence). PRODUCTION project — forward-only, never edit existing changesets. BE dev creates the file; schema above is the authoritative spec.

Side-Effect: BILLING_ALERT Gate

HostingDunningService is the only BE consumer of emailBilling. Before sending a BILLING_ALERT e-mail (triggered by HostingDunningBatch on failed payment / PAST_DUE / SUSPENDED events):

  1. Load UserNotificationPreferences for the account owner by userId.
  2. If the row does not exist → treat as default (emailBilling = true) → send the e-mail.
  3. If emailBilling = false → write an audit log entry "BILLING_ALERT skipped — user preference emailBilling=false (userId=<id>)"do not send e-mail.
  4. If emailBilling = true → send the e-mail normally.

E-mail types that are NOT gated (always sent regardless of preferences):

EmailTypeReason
FORGOT_PASSWORDTransactional — security-critical
WAITLIST_CONFIRMATIONPre-auth flow
WAITLIST_INVITEAdmin-initiated action
GDPR_EXPORT_READYGDPR Article 15 — mandatory
GDPR_DELETE_REQUESTEDGDPR Article 17 — mandatory
GDPR_DELETE_COMPLETEDGDPR Article 17 — mandatory

UX Guidelines

Page Layout

The Notifications section appears in the Settings sidebar. Content area follows the same padding: 32px 32px 64px layout as other sections.

Toggle Controls

Controls are rendered as ToggleSwitch.vue rows using FieldRow.vue layout: label on the left, control on the right.

FieldLabel (en)DefaultStatus
emailBillingBilling alerts (email)onActive
emailDeploysDeploy notifications (email)onComing soon
emailErrorsError alerts (email)onComing soon
emailWeeklyWeekly digest (email)onComing soon
pushAgentAgent activity (push)offComing soon
pushMentionsMentions (push)onComing soon
digestDigest frequencyweeklyComing soon
  • Active toggle (emailBilling): interactive ToggleSwitch, triggers PUT on change.
  • Coming soon controls: rendered as disabled ToggleSwitch / select with a "Coming soon" badge (small pill, muted color). No interaction, no API call.
  • Digest selector: disabled <select> with options off | daily | weekly, “Coming soon” badge. Not interactive in this release.

Auto-save Behaviour

  • Toggle change → debounced 500 ms before firing PUT /me/notification-preferences.
  • Optimistic update on FE before response arrives.
  • On API error: roll back to previous value in local state + display toast "Could not save. Please try again." (var(--rose) background).
  • No explicit “Save” button.

Loading State

  • On initial load, show skeleton placeholders for all rows while GET is in flight.
  • Disable all interactive controls during an in-flight PUT request.

Frontend

Validations

FieldConstraintsSizePatternNote
emailDeploysnot_nullboolean; field sent even when toggle is disabled in UI
emailErrorsnot_nullboolean
emailWeeklynot_nullboolean
emailBillingnot_nullboolean; the only user-interactive toggle in current release
pushAgentnot_nullboolean
pushMentionsnot_nullboolean
digestnot_null^(off\&#124;daily\&#124;weekly)$string enum; selector enforces valid values client-side

Backend

Validations

FieldConstraintsSizePatternNote
emailDeploysnot_nullboolean
emailErrorsnot_nullboolean
emailWeeklynot_nullboolean
emailBillingnot_nullboolean
pushAgentnot_nullboolean
pushMentionsnot_nullboolean
digestnot_null, enum_valueoff, daily, weeklyReject any value outside these three strings

Test Cases

GIVENWHENTHEN
authenticated user with existing preferences rowGET /me/notification-preferences is called200 OK, all 8 fields returned with persisted values
authenticated user with no existing preferences row (first visit)GET /me/notification-preferences is called200 OK, defaults returned (emailBilling=true, pushAgent=false, digest="weekly", etc.), row created in DB
no Authorization headerGET /me/notification-preferences is called401 AUTHENTICATION_FAILED error response is returned
authenticated user, all fields validPUT /me/notification-preferences is called200 OK, updated row returned, updatedAt refreshed in DB
authenticated user sets emailBilling = falsePUT /me/notification-preferences is called200 OK, emailBilling=false persisted in DB
authenticated user with emailBilling = false, HostingDunningBatch firesBILLING_ALERT send attempted for that usere-mail NOT sent, audit log entry written: “BILLING_ALERT skipped — user preference emailBilling=false”
authenticated user with emailBilling = true (default), dunning batch firesBILLING_ALERT send attempted for that usere-mail sent normally
user has no preferences row, dunning batch firesBILLING_ALERT send attempted for that userrow absence treated as default (true), e-mail sent normally
digest = "monthly" (invalid value)PUT /me/notification-preferences is called400 VALIDATION_ERROR, field digest error returned
emailBilling field missing from request bodyPUT /me/notification-preferences is called400 VALIDATION_ERROR, field emailBilling error returned
no Authorization headerPUT /me/notification-preferences is called401 AUTHENTICATION_FAILED error response is returned
authenticated user saves emailBilling = false, then calls GET againGET /me/notification-preferences is calledreturned emailBilling is false (persisted value reflected)

Out of Scope

The following remain BACKLOG and are explicitly excluded from this UC:

  • Implementation of emailDeploys channel (no deploy e-mail sender exists).
  • Implementation of emailErrors channel (no error alert e-mail sender exists).
  • Implementation of emailWeekly digest e-mail (no scheduler/digest builder exists).
  • Push notification infrastructure (pushAgent, pushMentions) — no push provider integrated.
  • digest scheduler (no cron/digest pipeline exists).
  • Admin override of another user’s notification preferences.
  • Per-project or per-workspace granularity (preferences are global per user).

Was this page helpful?

Thanks for the feedback.