ClaireDocs

Ask Claire

Search the project, not the web.

Answers are grounded in Claire’s published documentation and include the source pages used.

⌘/Ctrl + J opens Ask Claire anywhere in docs.

Per-conversation notification controls

Product and technical specification for turning Claire notifications on or off for one conversation at a time.

DraftReviewed 2026-08-19View source ↗

Claire should let a person decide which individual conversations may interrupt them. The control belongs in Conversation settings, applies across that person's Claire devices, and suppresses Claire notifications only. It never changes notification settings in WhatsApp, Instagram, Telegram, or another connected service.

Decision and scope#

Version one is a simple two-state preference: Notifications on or Notifications off. It is stored per user and per Claire conversation, so a mute set on iPhone also applies on desktop and future devices. The preference affects only new-message push notifications; messages still arrive, increment the unread count, appear in search, and remain available in the inbox.

In scopeNot in version one
Turn notifications on or off for one Claire conversation.Timed mute durations, schedules, keyword rules, and notification digests.
Reflect the result immediately in Conversation settings and the inbox.Changing the mute setting inside the underlying platform's app.
Suppress outbound Claire pushes on every registered device.Suppressing message sync, unread counts, AI processing, or reminders.
Explain when a device or global setting still prevents delivery.A promise that the operating system or an external provider will always show a notification.

Product experience#

Entry point#

In apps/client/app/chat/settings/[chatId].tsx, add a Notifications section below the conversation identity and above relationship/AI controls. This keeps a delivery preference separate from what Claire remembers about someone.

text
Conversation settings

Notifications
  [bell] Notifications                         [On | Off]
  Receive Claire alerts for new messages in this conversation.

Relationship memory
  …

States and microcopy#

StateRow valueSupporting copy
Default / enabledOnReceive Claire alerts for new messages in this conversation.
Disabled by this conversationOffNew messages will stay in your inbox without sending Claire alerts.
Global notifications disabledOff + disabled controlTurn on notifications in Claire settings to manage alerts for individual conversations.
OS permission denied or no device tokenOnThis conversation can notify you when notifications are enabled for this device.
SavingSaving…Keep the prior value visible; do not optimistically claim success before the write completes.
Save failureUnchangedCouldn't update notifications. Try again. No provider, bridge, or database error text is shown.

The switch must have an explicit accessible label: "Notifications for {conversation name}". Haptics and a brief confirmation are appropriate, but no destructive confirmation dialog is needed: switching it back on is one tap.

User flow#

  1. Open a conversation, then open Conversation settings.
  2. Read the current notification state and its consequence.
  3. Turn the switch off. Claire saves the preference for that user and conversation.
  4. The row changes to Off; the inbox may show a small muted-bell indicator without hiding the conversation.
  5. For a later inbound message, Claire records a suppressed delivery with the metadata-only reason conversation_muted and sends no push.
  6. Turn the switch back on to restore normal eligibility for the next incoming message.

This does not retract a notification already submitted to APNs/Expo, and it does not alter messages that arrive while the app is foregrounded. The existing active-chat suppression remains in effect independently.

Notification eligibility and precedence#

Delivery is allowed only after all applicable controls allow it. A per-conversation setting narrows delivery; it never overrides a global, device, operating-system, or presence suppression.

text
Incoming message
  → Ignore if it is sent by the account owner
  → Account-level notifications enabled?             no → suppress: account_disabled
  → Message notifications preference enabled?         no → suppress: messages_disabled
  → Conversation notifications enabled?               no → suppress: conversation_muted
  → Device enabled and a provider token is valid?     no → no delivery candidate
  → Quiet hours on that device?                       yes → suppress: quiet_hours
  → Device is actively viewing this conversation?     yes → suppress: active_chat
  → Queue provider delivery and track the receipt

Badge counts continue to use the user's unread total, including muted conversations. A muted conversation is intentionally not a hidden or archived conversation.

Data model and API#

New table: conversation_notification_preferences#

Do not overload chats.is_muted. That legacy/imported platform signal can describe an upstream platform's state and may not be user-managed by Claire. Claire's own preference needs an explicit, user-scoped source of truth.

sql
CREATE TABLE public.conversation_notification_preferences (
  user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
  chat_id UUID NOT NULL REFERENCES public.chats(id) ON DELETE CASCADE,
  notifications_enabled BOOLEAN NOT NULL DEFAULT TRUE,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  PRIMARY KEY (user_id, chat_id),
  CONSTRAINT conversation_notification_preferences_chat_owner
    FOREIGN KEY (chat_id, user_id) REFERENCES public.chats(id, user_id)
);

CREATE INDEX conversation_notification_preferences_lookup
  ON public.conversation_notification_preferences (user_id, chat_id)
  WHERE notifications_enabled = FALSE;

ALTER TABLE public.conversation_notification_preferences ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users manage their conversation notification preferences"
  ON public.conversation_notification_preferences
  FOR ALL USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id);

If chats lacks a unique composite key for the foreign key above, addUNIQUE (id, user_id) in the same migration. The migration must include the project's standard updated_at trigger and send the PostgREST schema reload notification after deployment.

Client contract#

Use Supabase with RLS for the settings screen, matching the existing conversation category and profile writes. The client reads and upserts only its own row:

ts
type ConversationNotificationPreference = {
  user_id: string;
  chat_id: string;
  notifications_enabled: boolean;
  updated_at: string;
};

await supabase.from('conversation_notification_preferences').upsert(
  { user_id: user.id, chat_id, notifications_enabled: enabled, updated_at: new Date().toISOString() },
  { onConflict: 'user_id,chat_id' },
);

Add notificationEnabled, isSavingNotificationPreference, andsetNotificationEnabled to useConversationSettingsStore. On an optimistic update failure, refresh from the server and show the safe, generic error copy above.

Delivery service change#

Before calculating badge and device delivery inapps/server/src/services/notification-delivery.ts, fetch the one preference forevent.userId + event.chatId. Missing row means enabled, preserving the current behavior for all existing conversations.

ts
const { data: conversationPreference } = await supabase
  .from('conversation_notification_preferences')
  .select('notifications_enabled')
  .eq('user_id', event.userId)
  .eq('chat_id', event.chatId)
  .maybeSingle();

if (conversationPreference?.notifications_enabled === false) {
  return await recordSuppressedDeliveries({
    event, devices, reason: 'conversation_muted',
  });
}

The implementation must preserve idempotency: a delivery row continues to be unique per message, device, and notification type. A suppressed event is a valid delivery outcome, not an error or retry candidate.

Implementation plan#

AreaChangeCompletion criterion
DatabaseMigration, RLS, composite ownership constraint, index, schema reload.A user cannot read or write another user's preference; deleting a chat or user cascades safely.
Mobile dataExtend conversationSettingsStore with fetch, optimistic update, rollback, and error state.The setting persists after a force close and appears consistently on a second device.
Mobile UIAdd an accessible Notifications section to Conversation settings using useSafeAreaInsets for scroll and bottom action spacing.The row is reachable on small screens, correctly labels its state, and works with VoiceOver.
InboxShow a restrained muted-bell affordance on a muted conversation; do not visually de-prioritize urgent unread messages.A person can tell why one conversation does not alert without opening it.
Server deliveryAdd the conversation check and conversation_muted suppression outcome before provider enqueue.No provider submission is attempted for a muted conversation.
OperationsCount suppressions by platform and reason only; retain no title, body, participant name, token, or raw payload.Operators can see a delivery was intentionally suppressed without learning which conversation it was.

Privacy and safety#

  • The preference is account metadata, not message content.
  • Push payload content is constructed only after the conversation eligibility check passes.
  • Operational logs, metrics, traces, alerting, and the Operations Console record only the suppression reason and aggregate platform counters.
  • Do not store contact names, message previews, device tokens, or raw provider receipts in the preference or related telemetry.
  • Deletion of the conversation or account removes the preference through database cascades; backups follow the existing deletion and retention policy.

Verification and acceptance criteria#

  1. With global notifications on, turn one conversation off and confirm its next inbound message creates no Expo/APNs submission while another conversation still notifies.
  2. Confirm muted messages continue to sync, increment unread count, affect the app badge, and are visible in the inbox.
  3. Turn the same conversation on from one device; confirm the next inbound message is eligible on another registered device.
  4. Confirm global disable, quiet hours, invalid token, and active-chat suppression still win over the conversation setting.
  5. Confirm a missing preference row is treated as on for every pre-existing conversation.
  6. Run server tests for eligibility order, idempotent suppressed delivery rows, and no provider call when conversation_muted.
  7. Run mobile tests for loading, optimistic success, rollback, system-permission copy, accessible switch label, and safe-area layout.
  8. Inspect structured logs and Operations Console payloads to prove no message body, sender name, chat title, token, or raw provider response is emitted.

Explicit follow-ups#

After version one has reliable production evidence, add timed mute choices (one hour, until tomorrow, custom date), group-only mentions/replies, and a notification schedule. Those capabilities require an explicit expiry model and a separate product decision; they must not be silently inferred from the basic on/off setting.