Connect SDK

Notifications and timers

Declare private criteria at the collection authority, deliver opaque wake-up signals, and schedule generation-fenced one-shot timers.

Notification evaluation runs at the local connector or hosted provider. Connect receives an opaque signal, grant, criterion, and cursor. Static presentation comes from the manifest. Matching record paths, frontmatter, bodies, and runtime event payloads remain at the collection authority.

Declare criteria in the application manifest

Manifest fragment
{
  "notifications": {
    "criteria": [{
      "id": "task.changed",
      "event": {
        "id": "mdbase.record.modified",
        "version": "1.0.0"
      },
      "if": {
        "$expr": "\"task\" in event.data.types"
      },
      "debounce": "5s",
      "minimum_interval": "1m",
      "presentation": {
        "title": "Tasks changed",
        "body": "Open Worklog to see the latest changes.",
        "tag": "task-changes"
      }
    }]
  }
}

Criteria use canonical Runtime 0.2 event contracts and CEL over CloudEvent data. Presentation is static manifest data, so private record content cannot be interpolated into a push. The approved grant stores an exact snapshot of each selected criterion.

Register browser push from a user action

enable-notifications.ts
const worker = await navigator.serviceWorker.register(
  "/service-worker.js"
);

await connection.registerNotifications({
  serviceWorker: worker,
  criteria: ["task.changed"]
});
service-worker.ts
import { showMdbasePushNotification } from "@mdbase/connect";

self.addEventListener("push", (event) => {
  event.waitUntil(
    showMdbasePushNotification(
      self.registration,
      event.data?.json()
    )
  );
});

Treat the signal as a prompt to refresh current authorized state. Re-registering atomically replaces the selected criteria for the installation. Call unregisterNotifications(worker) when the user opts out.

Reconcile one-shot timers at the authority

reminders.ts
await connection.reconcileTimers({
  namespace: "task-reminders",
  criterion_id: "task.reminder",
  timers: [{
    id: "task-123:reminder-1",
    fire_at: "2026-07-25T10:00:00Z",
    data: { kind: "task_reminder" }
  }]
});

Reconciliation is atomic. Unchanged timers keep their generation and fired state, changed timers receive a new generation, new timers are scheduled, and omitted active timers are cancelled. IDs and optional private data remain at the collection authority. Signals carry the timer criterion and cursor metadata.

Use signed webhooks for developer-managed delivery

Distributed applications should normally keep Apple and Firebase sender credentials in their own infrastructure. Declare one HTTPS webhook and verify the signature over the exact raw body before parsing it.

notification-webhook.ts
import { verifyNotificationWebhook } from "@mdbase/connect-webhooks";

const event = verifyNotificationWebhook({
  body: rawRequestBody,
  headers: request.headers,
  keys: cachedConnectSigningKeys
});

if (await deliveries.claim(event.delivery_id)) {
  await sendPush(event.connection_id, event.notification);
}

Cache keys from /v1/notifications/webhook-signing-keys, enforce the built-in five-minute replay window, and store delivery_id before doing work because Connect retries until the endpoint succeeds.

Delivery choices change the trust boundary

ModeBest fitAdditional trust
Web PushBrowser and installed PWABrowser push service and Connect delivery storage
Managed FCMPersonal or single-owner native applicationConnect sender identity can address the application's Firebase project
Signed webhookBroadly distributed applicationDeveloper backend holds its own platform sender credentials