Skip to main content

Documentation

In-app notices

Declare in-app messages in your manifest; the platform picks what each person sees by audience, conditions and schedule, remembers dismissals, and one shared renderer draws them.

Notices are declared, not coded

An in-app notice is a short message in a fixed place in your app: "Turn on two-step sign-in", "Your trial ends in 3 days", "Reports now export to CSV". On SynaptaGrid you declare notices in your app manifest under spec.notices, name the places they can appear under spec.notice_slots, and one shared renderer in the frontend SDK draws them. Your frontend writes no notice code: it mounts the renderer once and puts a slot where a notice may appear.

The Control Plane decides, for each person asking, which notices are shown. It applies the audience, the conditions, the schedule and what the person already dismissed, and it picks what fits each slot. A notice the person is not meant to see never reaches the browser, so nothing about it shows in the network tab.

Notices sit next to guided tours. A tour walks a new user through the product once; a notice is a message that stays until it is dismissed or no longer applies. New in-app prompts use this framework instead of one-off banners.

app.manifest.yamlyaml
spec:
  notice_slots:
  - code: global.top
    tier: portal
    presentations: [banner]
    max_visible: 1
  notice_conditions:
  - code: reports.none_scheduled
    tier: [portal]
    description: The organization has no scheduled report.
  notices:
  - code: schedule-a-report
    version: 1
    tier: portal              # portal = your customers; cp = platform operators
    placement: global.top     # a slot declared in this manifest
    presentation: banner
    severity: info
    priority: 50
    title: Send reports on a schedule
    body: Pick a report and a time, and it arrives in your inbox each week.
    action:
      label: Schedule a report
      route: /reports
    when:
    - reports.none_scheduled
    audience:
      requires_permission: reports.update
    dismiss:
      mode: forever
      scope: user_org

Notice fields

FieldMeaning
codeUnique within the manifest.
versionStarts at 1. Raise it to show the notice again to people who dismissed an older version.
tierportal for your customers' users, cp for platform operators.
placementThe slot that draws it. Bare, such as global.top, for a slot in your own manifest; <app>:<slot>, such as control-plane:global.top, for another app's slot.
presentationbanner, card or modal. The slot must offer it.
severityinfo (default), success, warning or critical.
priority0 to 100, default 50. When a slot has more notices than room, critical notices come first, then the higher priority, then declaration order.
title, bodyPlain language for the reader, up to 120 and 600 characters.
actionOptional. One link: label (up to 40 characters) and route, a path in the frontend that renders the slot, starting with /.
whenUp to 10 conditions, all of which must be true. See below.
audienceWho it is for. See below.
dismissHow it goes away. See below.
frequencyuntil_dismissed (default) or once_per_session, which the renderer applies once per browser session.
max_impressionsOptional, 1 to 1000. Stop showing it after this many impressions.
org_can_disableWhether an organization's admins may switch it off for their organization. Unset means yes, except for critical notices, which always means no.
scheduleOptional starts_at and ends_at, timestamps with a timezone such as 2026-11-01T00:00:00Z. Outside the window the notice is never shown.

Audience

Every field is optional. A notice is shown only when all the ones you set match.

FieldMatches
subscriptionany (default), active, trial, none or payment_failed.
plan_tiersA list of plan tier codes. Cannot be combined with the range below.
plan_tier_min, plan_tier_maxA range on the plan tier ladder. Either or both.
feature_on, feature_offA plan feature, always written <app>:<feature>, that must be on or off. Use feature_off for an upsell to a feature the organization does not have.
uses_appThe organization has this app.
requires_permissionA permission code the person must hold. A bare code is checked against your manifest at validation.

Take the permission from what the destination page enforces. A gate looser than the page's shows the notice to people who then cannot open the page.

Conditions

A condition is a yes or no question about the caller that an app answers on the server. You declare it under spec.notice_conditions with a code, the tier list it applies to, and a description, and you list it in a notice's when.

The Control Plane answers these itself, so any app uses them as control-plane:<code>:

ConditionTrue when
identity.mfa_not_enrolledThe person has no authenticator app for two-step sign-in. Portal and operator.
identity.email_unverifiedThe person has not confirmed their email address. Portal.
subscription.none_activeThe organization has no active or trial subscription. Portal.
subscription.payment_failedThe last renewal payment failed. Portal.
subscription.trial_ends_within_7_daysThe trial ends within 7 days. Portal.
org.no_other_membersThe person is the only member. Portal.

A condition you declare for your own app is written bare in when. Never qualify your own codes.

Dismissal

FieldMeaning
modeforever (default), snooze or none.
snooze_days1 to 365. Required with snooze, refused otherwise.
scopeWhose record the dismissal is: user, user_org (default), org or user_instance.
requires_permissionOnly with scope: org. Who may dismiss it for the whole organization. Unset, it follows audience.requires_permission.
  • A dismissal is recorded on the server against the version that was dismissed. Raising version shows the notice again.
  • A snooze hides it for snooze_days, then it returns.
  • mode: none means the notice cannot be dismissed. It is allowed only for severity: critical, and the notice must then carry schedule.ends_at or a when condition so it cannot stay on screen forever.
  • A modal that cannot be dismissed ignores Esc and a click outside it.

Slots

A slot is a place in an app shell where notices may appear.

FieldMeaning
codeUnique per tier within the manifest.
tierportal or cp.
presentationsOne to three of banner, card, modal.
max_visible1 to 10, default 1. How many notices the slot draws at once; critical first, then by priority.
modaltrue exactly when modal is in presentations.

The Control Plane declares three slots for the customer portal and two for the operator console. Use them as control-plane:<code>:

SlotTierOffersShows
global.topportal, cpbanner1
dashboard.cardsportal, cpcard3
after-sign-inportalmodal1

What publishing checks

The manifest validator and the Control Plane refuse a manifest with any of these:

  • a notice whose placement is not declared for its tier, or whose presentation the slot does not offer;
  • a when condition that is not declared, or not declared for the notice's tier;
  • an audience or dismiss permission your manifest does not declare, or a feature of your own app that spec.features does not declare;
  • plan_tiers together with plan_tier_min or plan_tier_max;
  • a snooze with no snooze_days, or snooze_days without a snooze;
  • schedule.ends_at that is not after schedule.starts_at, or a timestamp with no timezone;
  • mode: none on a notice that is not critical, or one with no end;
  • a critical notice with org_can_disable: true;
  • a code declared twice, or one of your own codes written as <your app>:<code>.

A reference to another app's slot, condition, permission or feature is checked by the Control Plane when you publish. It refuses the publish if the other app does not declare it.

Older drafts used requires_permission, requires_feature and state_scope on the notice itself. They are refused with a message naming the replacement: audience.requires_permission, audience.feature_on and dismiss.scope.

Mount the renderer once

tsx
import { NoticeProvider, NoticeSlot, createPortalNoticesClient } from "@egav/app-sdk/notices";

<NoticeProvider client={createPortalNoticesClient(api)} routeKey={location.pathname} navigate={navigate}>
  <NoticeSlot placement="control-plane:global.top" />
  <AppShell />
</NoticeProvider>
  • api is your SDK ApiClient pointed at the Control Plane, with the active app instance set.
  • routeKey changes with the page. The provider then asks once for every slot on the new page.
  • navigate opens a notice's action route. Without it the page does a full navigation.
  • For the operator console use createCpNoticesClient(api) with the same provider.
  • Each <NoticeSlot placement="..."> takes the slot as <owner>:<slot>, as the manifest declares it.

A banner is drawn as an alert, a card as a card, a modal as a dialog. Dismissing is optimistic: the notice hides at once and comes back, with the error reported, if the server refuses. The renderer never writes its own state. If notices cannot be loaded the person sees none and your app keeps working.

Keep a test in your frontend that renders the shell and asserts each slot you mount exists and each action.route resolves to a page.

Organizations can switch notices off

Unless a notice is critical or sets org_can_disable: false, an organization's admins can switch it off for their organization in the customer portal. A notice dismissed for the whole organization can be brought back there too.

Releasing a change

Notices ship with your manifest. Change a notice, raise the manifest version, publish, and the next request serves it. No frontend release is needed unless you add a slot or move an action route.

Announcements: notices written by operators

Platform operators can also write notices without a manifest, as announcements in the Control Plane console. An announcement uses the same notice fields, the same renderer and the same slots; the server chooses its code and version. It can additionally be limited to named organizations. Use a manifest notice for a message that belongs to your app, and leave announcements to the platform's operators.