Skip to content

feat(notifications): scope notification preferences and split email vs in-app#297

Merged
martian56 merged 2 commits into
Devlaner:mainfrom
cavidelizade:feat/scoped-notification-preferences
Jul 11, 2026
Merged

feat(notifications): scope notification preferences and split email vs in-app#297
martian56 merged 2 commits into
Devlaner:mainfrom
cavidelizade:feat/scoped-notification-preferences

Conversation

@cavidelizade

@cavidelizade cavidelizade commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Feature summary

Notification preferences can now be scoped to a workspace or project (falling back project → workspace → account), and each type has independent in-app and email toggles. Previously preferences were account-only with a single toggle per type, and every in-app notification also sent an email.

Linked issues / discussion

Closes #203

User-facing behavior

Settings → Notifications now shows a matrix: each notification type (property changes, state change, completed, comments, mentions) with an In-app and an Email switch. The same panel appears in three places:

  • Account settings — your defaults everywhere.
  • Workspace settings — overrides your account defaults for that workspace.
  • Project settings — overrides workspace/account for that project.

A GET returns the effective (resolved) preferences for the scope; toggling writes an override at that scope. Muting email for a type keeps the in-app notification, and vice versa.

What changed

API (apps/api/)

  • Migration 000008: per-type email_* columns (the existing booleans remain the in-app channel; email defaults to on to preserve today's behavior).
  • UserNotificationPreferenceStore: GetScoped, UpsertScoped, and Resolve (project → workspace → account). Writes go through a map so a disabled (false) toggle is persisted rather than dropped to the column default.
  • service/notification.go: the fan-out resolves each receiver's scoped preference and gates the in-app and email channels independently.
  • handler/auth.go: account GET/PUT now return/accept the email_* fields (shared render/apply helpers).
  • handler/notification_preference.go: new workspace- and project-scoped GET/PUT endpoints with membership/access checks.

UI (apps/web/)

  • NotificationPreferencesPanel: reusable In-app/Email matrix, driven by load/save props so one component serves all three scopes.
  • notificationPreferenceService: the scoped endpoints.
  • SettingsPage + settings nav: a Notifications section under workspace and project settings; the account section now uses the panel.

Database

  • One additive migration (five email_* boolean columns), down migration included.

Why this design

The schema already carried workspace_id/project_id, so scoping is "most specific stored row wins", which is simple and predictable. Splitting the fan-out into independent in-app and email sets (rather than emailing whoever got an in-app notification) is what makes the two channels genuinely separable.

Test plan

  • go vet ./... and full go test ./... green (testcontainers).
  • New tests: TestChannelAllows (per-channel gating), TestNotifPref_ResolvePrecedence (project→workspace→account resolution), TestNotifPref_ScopedRoundTrip + TestNotifPref_NonMemberForbidden (scoped endpoints, inheritance, isolation, access control). Existing account-pref test still passes.
  • npm run typecheck, npm run lint, npm run build green.
  • Manual, against the running stack: opened Project → Notifications, turned Comments → Email off. Verified via API that the project row has email_comment=false with comment (in-app) still true, the account row still has email_comment=true (project override doesn't leak up), and the workspace scope resolves to the account value (inheritance).

Out of scope (follow-ups)

  • None.

Rollout notes

  • Additive migration; no backfill. Existing users keep all-enabled behavior (email columns default to true).

AI assistance

  • AI tools were used — tool(s): Claude Code — and AI-assisted commits include a Co-Authored-By: trailer

Checklist

  • PR title follows Conventional Commits and is <= 100 chars
  • Trailing slashes on new routes match neighboring routes
  • Added apps/api/migrations/000008_*.up.sql AND matching .down.sql
  • No --no-verify bypass on the commit (full suite ran on pre-commit)
  • Acceptance criteria from the linked issue are all met

Summary by CodeRabbit

  • New Features

    • Added notification preference controls for account, workspace, and project settings.
    • Added separate toggles for in-app vs email notifications (including new email-specific options).
    • Workspace and project preferences now use clear inheritance with project overrides for specific scopes.
    • Notification delivery now respects channel-specific preferences per receiver and scope.
  • Bug Fixes

    • Improved preference updates to support partial changes while correctly persisting disabled options.
    • Fixed effective preference resolution so results are consistent when scoped preferences are missing or overridden.

…s in-app

Notification preferences were global-only with a single toggle per type, even
though the schema carries workspace_id/project_id and the notification path
already fanned in-app and email together. This makes the scoping and the two
channels real.

Backend: per-type email_* columns (in-app stays the existing booleans);
UserNotificationPreferenceStore gains GetScoped/UpsertScoped and Resolve
(project -> workspace -> account); the notification fan-out now gates the
in-app and email channels independently against the resolved preference;
account GET/PUT return and accept the email fields; new workspace- and
project-scoped GET/PUT endpoints.

Frontend: a reusable NotificationPreferencesPanel with independent In-app and
Email columns, mounted in account, workspace, and project settings via a new
Notifications section; notificationPreferenceService for the scoped endpoints.

Closes Devlaner#203

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cavidelizade cavidelizade requested a review from a team as a code owner July 11, 2026 06:52
@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e4536df9-80a3-493e-9b0b-0c7524de87a6

📥 Commits

Reviewing files that changed from the base of the PR and between 21c3651 and a982369.

📒 Files selected for processing (2)
  • apps/api/internal/handler/notification_preference.go
  • apps/web/src/components/settings/NotificationPreferencesPanel.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/web/src/components/settings/NotificationPreferencesPanel.tsx
  • apps/api/internal/handler/notification_preference.go

📝 Walkthrough

Walkthrough

Notification preferences now support account, workspace, and project scopes with project-to-workspace-to-account inheritance. Email and in-app channels have independent toggles across API delivery, persistence, and web settings.

Changes

Scoped preference storage and resolution

Layer / File(s) Summary
Preference schema and resolution
apps/api/internal/model/user_notification_preference.go, apps/api/internal/store/..., apps/api/migrations/*
Adds email channel fields, scoped lookup/upsert support, precedence resolution, and migration coverage.
Scoped preference API
apps/api/internal/handler/..., apps/api/internal/router/router.go, apps/api/internal/handler/notification_preference_test.go
Adds authenticated workspace/project GET and PUT endpoints with inheritance, partial updates, access checks, and round-trip tests.

Notification delivery

Layer / File(s) Summary
Channel-specific notification delivery
apps/api/internal/service/notification.go, apps/api/internal/service/notification_test.go
Resolves preferences per receiver and independently filters in-app notifications and email delivery.

Settings UI

Layer / File(s) Summary
Settings preference controls
apps/web/src/components/settings/NotificationPreferencesPanel.tsx, apps/web/src/services/notificationPreferenceService.ts, apps/web/src/api/types.ts, apps/web/src/pages/SettingsPage.tsx
Adds reusable optimistic channel toggles and wires account, workspace, and project preference services and panels.
Settings navigation sections
apps/web/src/components/settings/sections-config.tsx, apps/web/src/components/settings/SettingsNav.tsx
Adds notifications to workspace and project settings navigation.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SettingsPage
  participant NotificationPreferenceService
  participant NotificationPreferenceHandler
  participant UserNotificationPreferenceStore
  participant NotificationService
  participant EmailQueue

  SettingsPage->>NotificationPreferenceService: load or save scoped preferences
  NotificationPreferenceService->>NotificationPreferenceHandler: GET or PUT preference endpoint
  NotificationPreferenceHandler->>UserNotificationPreferenceStore: resolve or upsert scoped preference
  UserNotificationPreferenceStore-->>NotificationPreferenceHandler: effective or saved preference
  NotificationPreferenceHandler-->>NotificationPreferenceService: preference response
  NotificationService->>UserNotificationPreferenceStore: resolve receiver preference
  UserNotificationPreferenceStore-->>NotificationService: channel-specific toggles
  NotificationService->>EmailQueue: enqueue allowed email notifications
Loading

Suggested labels: enhancement, API, UI

Poem

I’m a rabbit toggling bells in the night,
In-app hops left, email hops right.
Workspace burrows, projects too,
Defaults bloom in every hue.
Preferences now dance just right—
Hop, save, and send delight!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise, conventional, and accurately summarizes the main notification preference scope and channel-splitting changes.
Description check ✅ Passed The description covers the feature summary, linked issue, behavior, implementation, testing, rollout notes, and AI disclosure.
Linked Issues check ✅ Passed The changes implement #203 by adding workspace/project preference resolution, separate in-app/email toggles, and scoped settings UI.
Out of Scope Changes check ✅ Passed The diff stays focused on notification preferences, related UI, storage, routing, and tests with no clear unrelated additions.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
apps/web/src/components/settings/NotificationPreferencesPanel.tsx (1)

90-120: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

No feedback distinguishing "loading" from "failed to load".

When load() rejects, prefs is set to null (line 99), which is the same state as before load resolves. All toggles are simply disabled with no message, so a user hitting a network error sees the same UI as a slow load with no way to retry or understand what happened.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/settings/NotificationPreferencesPanel.tsx` around
lines 90 - 120, Distinguish the initial loading state from load failure in the
notification preferences component. Update the state and useEffect error
handling around load() to track an explicit error, render a clear failure
message with a retry action that re-invokes loading, and keep toggles disabled
only while loading or when preferences are unavailable.
apps/api/internal/store/user_notification_preference_test.go (1)

15-59: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add email_ field assertions to verify the core feature.*

The test verifies in-app field resolution precedence (Comment, Mention) but never asserts any email_* fields, even though UpsertScoped writes all ten toggles via the map. Since this PR's primary objective is separate email toggles, the store-level test should verify that EmailComment, EmailMention, etc. are correctly persisted and resolved across scopes.

♻️ Proposed additions
 	// Workspace row with comment on, mention off -> now wins over account.
 	require.NoError(t, s.UpsertScoped(ctx, &model.UserNotificationPreference{
 		UserID: w.User.ID, WorkspaceID: &ws, PropertyChange: true, StateChange: true, Comment: true, Mention: false, IssueCompleted: true,
+		EmailPropertyChange: true, EmailStateChange: true, EmailComment: false, EmailMention: true, EmailIssueCompleted: true,
 	}))
 	got, err = s.Resolve(ctx, w.User.ID, &ws, &proj)
 	require.NoError(t, err)
 	require.True(t, got.Comment)
 	require.False(t, got.Mention, "workspace row wins over account")
+	require.False(t, got.EmailComment, "workspace email_comment off")
+	require.True(t, got.EmailMention, "workspace email_mention on")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/internal/store/user_notification_preference_test.go` around lines 15
- 59, Add assertions for the email preference fields in
TestNotifPref_ResolvePrecedence, including EmailComment and EmailMention (and
the remaining email_* toggles as appropriate), after each account, workspace,
project, and fallback Resolve call. Seed distinct email values in each
UpsertGlobal and UpsertScoped fixture so the assertions verify persistence and
scope precedence rather than only default values.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/api/internal/handler/notification_preference.go`:
- Around line 92-99: Update baseForScope to return the resolved preference and
an error, propagating Resolve failures instead of falling back to
DefaultNotificationPreference. Preserve the ID reset on successful resolution,
then update UpdateWorkspace and UpdateProject to handle the returned error and
respond consistently with respondResolved before proceeding to UpsertScoped.

In `@apps/web/src/components/settings/NotificationPreferencesPanel.tsx`:
- Around line 106-117: In toggle, avoid restoring the entire captured prefs
snapshot when save fails, since concurrent updates may have changed other keys.
In the catch handler, update only the failed key back to its previous value
while preserving the latest state for all unrelated preferences, using the key
and its captured prior value.

---

Nitpick comments:
In `@apps/api/internal/store/user_notification_preference_test.go`:
- Around line 15-59: Add assertions for the email preference fields in
TestNotifPref_ResolvePrecedence, including EmailComment and EmailMention (and
the remaining email_* toggles as appropriate), after each account, workspace,
project, and fallback Resolve call. Seed distinct email values in each
UpsertGlobal and UpsertScoped fixture so the assertions verify persistence and
scope precedence rather than only default values.

In `@apps/web/src/components/settings/NotificationPreferencesPanel.tsx`:
- Around line 90-120: Distinguish the initial loading state from load failure in
the notification preferences component. Update the state and useEffect error
handling around load() to track an explicit error, render a clear failure
message with a retry action that re-invokes loading, and keep toggles disabled
only while loading or when preferences are unavailable.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6139ac88-445b-4c39-ace1-2866417aa35f

📥 Commits

Reviewing files that changed from the base of the PR and between 68a0e64 and 21c3651.

📒 Files selected for processing (17)
  • apps/api/internal/handler/auth.go
  • apps/api/internal/handler/notification_preference.go
  • apps/api/internal/handler/notification_preference_test.go
  • apps/api/internal/model/user_notification_preference.go
  • apps/api/internal/router/router.go
  • apps/api/internal/service/notification.go
  • apps/api/internal/service/notification_test.go
  • apps/api/internal/store/user_notification_preference.go
  • apps/api/internal/store/user_notification_preference_test.go
  • apps/api/migrations/000008_notification_pref_channels.down.sql
  • apps/api/migrations/000008_notification_pref_channels.up.sql
  • apps/web/src/api/types.ts
  • apps/web/src/components/settings/NotificationPreferencesPanel.tsx
  • apps/web/src/components/settings/SettingsNav.tsx
  • apps/web/src/components/settings/sections-config.tsx
  • apps/web/src/pages/SettingsPage.tsx
  • apps/web/src/services/notificationPreferenceService.ts

Comment thread apps/api/internal/handler/notification_preference.go Outdated
Comment thread apps/web/src/components/settings/NotificationPreferencesPanel.tsx
…ed toggle

baseForScope now returns Resolve errors instead of falling back to the
all-true default, so a transient DB error can't reset inherited (disabled)
fields on save. The preferences panel reverts only the failed key on error
(and merges only that key on success) via functional updates, so a concurrent
toggle of another switch is never stomped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@martian56 martian56 merged commit 841cc80 into Devlaner:main Jul 11, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[IMPROVEMENT] Add workspace/project notification preferences and email-vs-in-app toggles

2 participants