Skip to content

feat: notifications in PlatformClient - #40

Closed
ABujalance wants to merge 9 commits into
mainfrom
feature/notifications-client
Closed

feat: notifications in PlatformClient#40
ABujalance wants to merge 9 commits into
mainfrom
feature/notifications-client

Conversation

@ABujalance

Copy link
Copy Markdown
Contributor

Apps can read and manage the signed-in user's notifications, subscribe to
automations, and listen for new ones live. Nothing here existed before — the
package had no notification surface at all.

Changes

  • PlatformClient gains getNotifications, getUnreadNotificationCount,
    markNotificationsRead, markAllNotificationsRead,
    getNotificationSubscriptions, subscribeToAutomation,
    updateAutomationSubscription and unsubscribeFromAutomation. All scoped to
    the signed-in user through the bearer token an app already holds.
  • onNotification, a live socket subscription. Unlike onExecutionProgress it
    stays connected for the session rather than closing on a terminal event, and
    returns a function that disconnects.
  • src/types/notifications.ts mirrors the backend's wire DTOs by hand, the
    same as every other type here, including outcome, groupKey and
    groupLabel.

outcome is worth calling out: it is how an automation run ended, and it is
the field to read rather than matching on the copy. title is built from the
user's own automation name, so an automation called "Failover sync" makes every
successful run look failed to anything parsing the text. That one shipped in
our own client before it was caught.

Related

Consumes the notifications API from platform_backend-api. The CI commits here
authenticate the cross-repo submodule fetch with a GitHub App credential rather
than a PAT.

Apps can already call the notification API — the iframe hands them the
signed-in user's JWT — so this is client methods over endpoints that are
already live, not new surface.

Adds getNotifications, getUnreadNotificationCount, markNotificationsRead,
markAllNotificationsRead, getNotificationSubscriptions and
unsubscribeFromAutomation.

The types come from the backend repo through a submodule rather than being
copied into src/types like every other type here. Those copies are why
base.ts has to alias ObjectId to string and hope it stays true; the
backend's src/common/dto describes what the API actually sends and imports
nothing from mongodb, so it compiles in a browser build unchanged.

The submodule is sparse-checked-out to that one directory, so 37 files land
on disk instead of the whole backend repo. Sparse config lives in the
submodule's .git and does not survive a clone, which is why CI runs
types:init after checkout. Moving the pin is `yarn types:update`, a command
rather than a postinstall hook: an install that silently moved it would
break builds with no commit explaining why.

vite-plugin-dts and tsconfig both needed the vendored path adding. Without
it the published .d.ts re-exports from ../../vendor/... and that resolves
to nothing once installed — the build succeeds and consumers get broken
types.

Pinned to the backend's feature/notification-dtos branch until that merges.
backend-api#321 is merged, so the DTOs no longer live on a feature branch.
Tracking dev rather than main because that is where they are: main gets
them with the next release, and repointing is a one-line change here plus
yarn types:update.

Pin includes the review fixes on that PR — the base.dto helpers are gone
and the type union guard is bidirectional.
The backend repo is private, so the default GITHUB_TOKEN cannot clone it
and checkout with submodules: true fails outright with "Repository not
found".

Credential is scoped to the fetch step via url.insteadOf and unset
immediately after, rather than handed to actions/checkout. Passing it to
checkout would make it the identity for everything else in the job,
including the release workflow's version-bump push.

Needs a BACKEND_TYPES_TOKEN secret on this repo with read access to
contents on platform_backend-api.
The failure was a bare "Repository not found" from git, which says nothing
about the cause or the fix. CI now fails with an annotation, a job summary
and step-by-step setup instructions.

Also accepts a deploy key as an alternative to the token. A read-only
deploy key on the backend repo never expires and belongs to the repo
rather than to whoever created it, so the yearly token rotation goes away.
Both work by rewriting the https URL rather than editing .gitmodules, so
cloning over https locally is unaffected.

The credential handling moved out of the workflows and into the script, so
the two workflows cannot drift and the guidance sits next to the failure.
Replaces the standing token with an App-derived one, scoped to
platform_backend-api and valid for an hour, so the only long-lived secret
is an App key that grants nothing by itself. A plain token or a deploy key
still work if those secrets are set instead.

Failure instructions now lead with the App setup.
Three methods the client was missing.

subscribeToAutomation and updateAutomationSubscription cover the project
routes that have been live since task 04. Without them an app could list
and cancel subscriptions but never create one, which is a daft half of the
feature.

onNotification opens the /notifications socket namespace added in
backend-api#325. It is deliberately unlike onExecutionProgress: that one
follows a single execution and closes when it ends, while a bell stays
connected for the session, so this returns a disconnect function instead of
managing its own lifecycle.

Events carry ids rather than notifications, so a burst stays cheap and the
socket is never the source of a stale render. The token is resolved per
connection so a provider-backed client opens with a current one.
This repo is public and the backend is private, so vendoring its DTOs made
a clean clone unbuildable and left fork PRs, which get no secrets, with no
way to run CI. Reaching outside src also moved tsc's root: the published
layout became dist/src/... plus dist/vendor/... with index.d.ts as a stub,
so every deep import broke. Both are Sergio's findings and both are hard
blockers rather than preferences.

The types are declared in src/types/notifications.ts instead, the same as
every other type in this package, with a note on why and what sharing them
properly would take. Sharing needs the backend's wire DTOs published as
their own package so they arrive through node_modules rather than through
the source tree.

The client methods are unaffected and stay. Also removes the App-token CI
step, which had no fallback path anyway: without continue-on-error the job
died before the || could be evaluated.
The API returns `outcome`, `groupKey` and `groupLabel` on every automation
notification and this mirror did not have them, so an app building against the
package was told they do not exist.

`outcome` matters most. Without it the only way to know whether a run failed is
to match on `title`, and `title` is built from the user's own automation name —
an automation called "Failover sync" reports every successful run as a failure
to anything reading the copy. That is not a hypothetical; it shipped in our own
client and had to be fixed there.

`groupKey` and `groupLabel` are what a client needs to collapse a busy
automation's runs into one row instead of fifty.
Three changesets on one unreleased feature meant three changelog entries for
what a reader experiences as a single addition, split by the order the branch
happened to be built in rather than by anything meaningful to a consumer.
@ABujalance

Copy link
Copy Markdown
Contributor Author

Wrong scope on my side, and built on a stale main. Replaced by #41, which is the type mirror only, off current main. The client methods this branch carries are already on main via #39.

@ABujalance ABujalance closed this Aug 17, 2026
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.

1 participant