VibeWeek
Home/Grow/In-App Status Banners & System Notifications: Chat Prompts

In-App Status Banners & System Notifications: Chat Prompts

⬅️ Back to 6. Grow

There's a difference between your public status page (where people check during outages — "Is X down?") and in-app status banners (in-product surfaces telling current users right now: a payment is failing, a feature is degraded, planned maintenance is tomorrow, you're approaching a usage limit, your trial expires in 3 days). Both matter. They're different surfaces, different UX, different audiences.

Most apps build a hardcoded "Subscription expired" banner first, then add a "We're currently experiencing issues with email delivery" banner during an outage, then realize three months later they've shipped six banners with inconsistent designs, no read tracking, no priority order, and no admin UI to publish a new one without a deploy.

This is the chat-prompt playbook for a unified in-app banner / system-notification surface that handles all of these consistently — degradation alerts, planned maintenance, billing nudges, usage warnings, regulatory disclosures, regional outage messaging — with a clean priority model, dismissal tracking, and an admin UI so non-engineers can publish.

When You Need a Banner (and Not a Toast / Modal / Email)

Use banners for:

  • Persistent, account-relevant state that affects what the user can / will do (your card failed; trial expires in 3 days; feature X is degraded)
  • Time-sensitive but not blocking (planned maintenance Friday)
  • Cross-session: even if user signs out + back in, they should see it
  • Status the user can ignore today, but probably shouldn't ignore for a week

Use toasts for: ephemeral feedback after an action (saved, error, copied)

Use modals for: truly blocking — must acknowledge to continue (e.g., new ToS requires acceptance)

Use email for: out-of-product reach (account issues that affect users not currently in the app)

Use the public status page for: non-customers, customers checking from outside the app, post-incident transparency

A well-designed in-app system shows the right banner, in the right priority order, dismissible if appropriate, persistent if not. It's not noise; it's deliberate communication.

Banner Categories

Different categories need different policies:

Category Example Priority Dismissible? Auto-resolve?
Critical (account-action required) Payment failed; will close in 3 days 1 No (until resolved) Yes (on payment)
Service degradation Email delivery is delayed 2 Yes Yes (when fixed)
Service outage (subset) Search is currently unavailable 1 No Yes
Planned maintenance Sunday 2-4am UTC scheduled downtime 3 Yes Yes (after time)
Billing nudge Trial ends in 3 days 4 Yes (re-shown periodically) Yes (on upgrade)
Usage warning At 80% of plan limit 4 Yes (re-shown at higher thresholds) Yes (on upgrade or new period)
Feature announcement New: AI Insights live 5 Yes (one-time) n/a
Regulatory / legal Updated Privacy Policy; review by [date] 2 After acknowledging Yes (on acknowledge)
Region-specific EU users see GDPR disclosure 3 Yes n/a
Customer-specific (account-level) Your custom domain has SSL expired 1 No Yes (on renewal)

Lower priority number = higher priority. Show one or two banners at most; queue the rest.

Data Model

I'm building a unified system-notification banner system for my B2B SaaS.

Schema (Drizzle):
- `system_banners` table:
  - id
  - title (string, max 100 chars)
  - body (markdown, max 500 chars)
  - category (enum: critical, degradation, outage, maintenance, billing_nudge, usage_warning, announcement, regulatory, region, account)
  - priority (1-5; 1 = most important)
  - cta_label (string, optional)
  - cta_url (string, optional)
  - dismissible (boolean)
  - segments (JSON array: which users see this — by plan, role, region, account_id)
  - starts_at (timestamp)
  - ends_at (timestamp, nullable — null = until manually ended)
  - status (enum: draft, scheduled, active, ended)
  - created_by_admin_id (FK)
  - acknowledgment_required (boolean)

- `banner_dismissals` join table:
  - banner_id, user_id, dismissed_at
  - One row per user per banner they've dismissed

- `banner_acknowledgments` join table:
  - banner_id, user_id, acknowledged_at
  - For regulatory / legal banners that need explicit ack

Implement:
1. The schema
2. A function `getActiveBannersForUser(userId)`:
   - Filter active (status='active', starts_at <= now <= ends_at)
   - Filter by user segment match
   - Exclude banners user has dismissed (if dismissible)
   - Exclude banners user has acknowledged (if acknowledgment-only)
   - Sort by priority ascending
   - Return top 1-2 banners
3. Server action: `dismissBanner(bannerId, userId)` and `acknowledgeBanner(bannerId, userId)`

Stack: Next.js App Router + Drizzle + Postgres.

Banner Component

Build the `<SystemBanner />` component:

Layout:
- Full-width bar at the top of the app (above the sidebar/main area; below any global header)
- Icon left (matches category)
- Title bold + body text
- CTA button right (if cta_url provided)
- Dismiss × button right (if dismissible)
- Color: derived from category (critical=red, degradation=yellow, maintenance=blue, billing=orange, announcement=neutral)

Behavior:
- On dismiss: optimistically remove + call server action `dismissBanner`
- On CTA click: navigate / open / open external
- If acknowledgment_required: dismiss button replaced with "I understand" button that triggers `acknowledgeBanner` and may open a modal with full text

Accessibility:
- role="status" or role="alert" depending on priority (alert for critical/outage; status for everything else)
- ARIA-live "polite" or "assertive" matching role
- Dismiss button has accessible label
- Color is not the only signal (icons + text)

Multi-banner queue:
- If multiple active banners, render top 1 (or 2 max)
- A "More notifications (3)" link at the right opens a panel with all active banners

Stack: Next.js + Tailwind + shadcn/ui Alert as base + Radix.

Where to Render

Where in my Next.js App Router layout should the system banner render?

Considerations:
- Should appear on every authenticated page
- Should NOT appear on auth pages (sign in / sign up / password reset)
- Should NOT appear in the email-sent / post-checkout / post-action confirmation pages where it would distract
- Sticky at the top, above the main app content but below the global nav
- Mobile: same logic; banner is full-width

Implement:
- Render in `app/(app)/layout.tsx` (the authenticated app layout)
- Pass user-specific banners as props from a server component that calls `getActiveBannersForUser`
- Keep it Server Component for fast initial render; the dismiss interaction is via Server Action

Stack: Next.js 16 App Router + Server Components + Server Actions.

Admin UI for Publishing Banners

Engineers shouldn't deploy code to publish "we have an email delivery issue."

Build the admin UI for managing system banners:

1. /admin/banners — list view
   - Columns: title, category, priority, segments, status, starts_at, ends_at, audience size, dismiss rate
   - Filter by status (active, draft, scheduled, ended)
   - Quick "End now" button for any active banner
   - "New banner" CTA

2. /admin/banners/new — create / edit
   - Fields: title, body (markdown editor), category (auto-suggests priority), CTA label + url, dismissible (default per category), segments (multi-select: plan tier, role, region, specific accounts), starts_at, ends_at, acknowledgment_required (default per category)
   - Preview pane showing how it'll render in light + dark mode
   - "Send to test segment" — show only to admins for verification before going live
   - Validate: title ≤ 100; body ≤ 500; future starts_at > now; ends_at > starts_at

3. Authorization:
   - Only admins with `system_admin` role
   - Audit log entry on every banner create / update / publish / end

4. Banner analytics per banner:
   - Total reach (users in segment matched while banner active)
   - View rate (users who landed on a page where it rendered)
   - Dismiss rate (% of viewers who dismissed)
   - CTA click-through rate
   - Acknowledgment rate (if required)

Stack: Next.js + Drizzle + shadcn/ui forms + a markdown editor (TipTap or simpler textarea+react-markdown preview).

Targeting / Segments

Add segment targeting to banners:

Segment types:
- "all" (everyone authenticated)
- "plan:[tier]" (users on a specific plan: free / pro / enterprise)
- "role:[role]" (admin / member / billing-owner)
- "region:[region]" (EU / US / etc — derived from user.country or user.timezone)
- "account_id:[id]" (account-specific banner — useful for "your custom domain SSL expired" cases)
- "feature_flag:[flag]" (only users in a specific GrowthBook / LaunchDarkly cohort)
- "trial_status:[expiring|expired|active]" (computed from user record)
- "billing_status:[hold|active|overdue]" (computed from billing system)

Implement segment-matching as a function:
```ts
function userMatchesSegment(user: User, segment: string): boolean

Each segment evaluates against the user object. Banner shows if user matches at least one segment in the banner.segments array.

Stack: Next.js + your existing auth + billing system.


## Polling / Live Updates

A new banner published mid-session should show up to current users without requiring a refresh.

Make banner state live for active sessions:

Options:

  1. Poll /api/banners every 60 seconds — simple, works everywhere
  2. Server-Sent Events (SSE) channel — push to clients on banner change
  3. WebSocket — overkill for this use case

Pick option 1 (polling) for simplicity. Implementation:

  • TanStack Query with refetchInterval: 60_000 and refetchOnWindowFocus: true
  • The query key includes user ID so different users get different banners
  • On user dismissal, optimistically update the cache; the server action confirms

Edge: don't poll if window is hidden (browsers throttle anyway, but TanStack respects refetchIntervalInBackground: false).

Stack: Next.js + TanStack Query.


## Acknowledgments (Regulatory / ToS Updates)

Some banners need an explicit "I understand" — not just a dismiss.

Build the acknowledgment flow:

Use case: ToS updated; users must read + acknowledge before continuing.

Behavior:

  1. Banner appears at top with a "Review and Acknowledge" CTA
  2. Click opens a full-screen modal with the full text (markdown rendered)
  3. User scrolls to bottom (track scroll progress)
  4. "I have read and agree" button enabled only after scrolling to ≥90%
  5. Click button: server action records acknowledgment with timestamp
  6. Modal closes; banner disappears

Stronger variant: until acknowledged, the rest of the app is read-only. The banner is non-dismissible. Use for legally-required acknowledgments only — annoying for soft updates.

Implement:

  • The modal component with scroll tracking
  • The acknowledgment server action
  • The "lockdown" mode for required acknowledgments

Stack: Next.js + shadcn/ui Dialog + Tailwind.


## Outage / Degradation Auto-Banner

When your status page detects an incident, automatically show the in-app banner.

Auto-publish a banner when our public status page (Statuspage / Better Stack / Instatus) detects an incident:

Pattern:

  1. Status page provider has webhooks (Statuspage's incident.create event)
  2. Endpoint receives webhook → creates a system_banner row with:
    • category: degradation or outage (based on severity)
    • title: incident title from status page
    • body: incident description
    • cta: link to status page
    • segments: 'all' (or filter by affected component if mapping exists)
    • starts_at: now
    • ends_at: null (will be ended when incident resolves)
  3. On incident.resolve webhook: end the corresponding banner

Implement:

  • The webhook handler in app/api/webhooks/statuspage/route.ts
  • HMAC signature verification for the webhook
  • Mapping of status page components → user segments (e.g., "EU region" component → segment "region:EU")
  • Idempotent: if the same incident is fired twice, don't create two banners

Stack: Next.js + Drizzle + Statuspage / Better Stack webhooks.


## Common Pitfalls

**No priority queue.** When 3 banners are active, the user sees one of them randomly (or all of them stacked). Show the top one or two by priority.

**Banner spam.** Marketing + Engineering + Customer Success all publish banners; the user sees 5 in a row. Centralize publishing approval; require category + priority discipline.

**Dismissed banners that never come back.** A user dismisses "Trial ending in 5 days." Trial is now 1 day from ending. Banner should re-emerge at higher urgency thresholds.

**No read state.** Banner shows every page load forever for the same user. Track dismissal per user.

**Banner survives the issue.** Email delivery was fixed an hour ago; banner still shows "email is degraded." Always set `ends_at` or end the banner programmatically when the underlying issue resolves.

**Banner content not localized.** EN-only banner shown to JP / DE users. If you support multiple languages, banner content must too — or fall back gracefully.

**No mobile testing.** Banner overlaps the mobile nav or pushes content off-screen. Test mobile layouts.

**Auto-published outage banner with no human filter.** A 30-second status-page blip becomes a 30-second banner. Set a minimum duration before auto-publishing (5+ minutes).

**Regulatory acknowledgment with no enforcement.** "Please review the new privacy policy" — but users can dismiss without reading. If legal needs proof of acknowledgment, force the read+ack flow.

**No analytics.** You have no data on whether banners are seen / clicked / dismissed. Track events.

**No segment targeting.** Free-tier users see "Pro feature is degraded" banners. Filter by segment.

**Account-specific banners stored in user table.** Account-level info (your custom domain expired) ends up duplicated for every team member. Store at account scope; render to all members.

**Dismissals lost on logout.** User dismissed banner; logged out + back in; banner returns. Persist dismissals server-side, not in localStorage.

**No way to test before going live.** Marketing publishes a banner with a typo to all 50K users. Always have a "preview / test segment" mechanism.

**Banner blocks critical UX.** Sticky banner over the navigation makes the cancel button unreachable. Don't break the app to deliver the message.

**Dual surface mismatch.** Public status page says "everything is fine" while in-app banner says "outage." Resolve from one source of truth.

**Banner copy in HTML.** Embedded HTML allows raw markup that breaks layout. Use markdown with safe rendering.

**No "ended" / archive view.** Admin can't see what banners were live last month. Maintain a status-history view; useful for incident postmortems and compliance.

## See Also

- [Status Page](./status-page-chat.md) — public-facing status page
- [In-App Notifications](./in-app-notifications-chat.md)
- [Toast Notifications UI](./toast-notifications-ui-chat.md)
- [Notification Preferences & Unsubscribe](./notification-preferences-unsubscribe-chat.md)
- [In-Product Release Notes / What's New](./in-product-release-notes-whats-new-chat.md)
- [Account Suspension & Fraud Holds](./account-suspension-fraud-holds-chat.md)
- [Trial Countdown / Conversion UI](./trial-countdown-conversion-ui-chat.md)
- [Trial Extension / Save Offer UX](./trial-extension-save-offer-ux-chat.md)
- [Trial to Paid](./trial-to-paid-chat.md)
- [Quotas, Limits & Plan Enforcement](./quotas-limits-plan-enforcement-chat.md)
- [Dunning & Failed Payments](./dunning-failed-payments-chat.md)
- [Cookie Consent](./cookie-consent-chat.md)
- [Roles & Permissions](./roles-permissions-chat.md)
- [Internal Admin Tools](./internal-admin-tools-chat.md)
- [Service Level Agreements](./service-level-agreements-chat.md)
- [Incident Response](./incident-response-chat.md)
- [Inbound Webhooks](./inbound-webhooks-chat.md)
- [Outbound Webhooks](./outbound-webhooks-chat.md)
- [Empty States, Loading & Error States](./empty-states-loading-error-states-chat.md)
- [Sidebar Navigation Implementation](./sidebar-navigation-implementation-chat.md)
- [Settings & Account Pages](./settings-account-pages-chat.md)
- [Customer Support](./customer-support-chat.md)
- [Background Jobs & Queue Management](./background-jobs-queue-management-chat.md)