An OpenControlPlane Platform Service that keeps users informed about what happens around them on the platform β starting with email.
This service notifies platform users about activity that concerns them:
- A user is added to a
Project,Workspace, orControlPlaneβ they get an email with a deep link to the resource. - A user is seen on the platform for the first time β they get a one-time welcome / enablement email pointing at the docs.
- (planned) A user's
ControlPlaneis running an outdated service or provider version β the admins get one aggregated digest per admin encouraging them to upgrade.
Two guarantees underpin all of this:
- No double-notifications. Every notification is recorded; a user is never told about the same event twice, even across restarts, retries, or repeated reconciles.
- Opt-out is always honored. Project/workspace admins can mute resources; any member can silence their own notifications β all managed via CRDs on the onboarding cluster.
PLATFORM cluster ONBOARDING cluster
βββββββββββββββββββββββββββββββββββββ ββββββββββββββββββββββββββββββββββββββ
β NotificationConfig (singleton) β β Project / Workspace / ControlPlane β
β UserProfile (email + registry) β β ServiceProvider resources β
β NotificationRecord (dedup ledger) β β NotificationOptOut (admin) β
β ServiceProvider (version source) β β UserNotificationOptOut (self-serve) β
βββββββββββββββββββββββββββββββββββββ ββββββββββββββββββββββββββββββββββββββ
β² reconcilers watch ββββββββββββββββββ
β
βββββββββββββ΄ββββββββββββββββββββββββββββββββ
β pipeline: suppress β dedup β render β send β
β Notifier: Email (SMTP, go-mail) [Slack β¦] β
ββββββββββββββββββββββββββββββββββββββββββββββ
- Persistence is Kubernetes-native. Three CRDs (
openmcp.cloud/cluster=platform) hold all state β no external database or queue. Everything goes through a smallStoreinterface, so a different backend could be swapped in later without touching the reconcilers.NotificationConfigβ the platform-owner configuration (singleton, named after the provider).UserProfileβ one per user identity; doubles as the registry, the email resolution, and the opt-out store.NotificationRecordβ the dedup ledger; a deterministic record name derived fromhash(recipient + category + eventKey)makes "create-or-AlreadyExists" the atomic delivery gate. Records are TTL-pruned (recordRetention, default 30 days).
- Delivery is behind a
Notifierinterface. Email is the first implementation (github.com/wneessen/go-mail,multipart/alternativeHTML + plaintext, STARTTLS/auth). Templates are rendered withhtml/template+text/templateand embedded viago:embed. - Effectively-once = at-least-once delivery + the dedup ledger. A
Pending/Failedrecord is re-claimable so a transient SMTP failure retries; onlyDelivered/Suppressedare terminal.
The service is deployed like any other OpenControlPlane Platform Service: the operator's cluster-scoped
PlatformService CR runs the image's init (installs CRDs, requests cluster access) and run
(the controller manager) commands. Everything below is applied to the platform cluster.
Create a Secret with username and password keys in the provider's namespace. Omit this
entirely for an unauthenticated relay (e.g. MailHog in tests).
apiVersion: v1
kind: Secret
metadata:
name: notifications-smtp
namespace: openmcp-system # the provider's namespace (POD_NAMESPACE)
type: Opaque
stringData:
username: apikey
password: "<smtp-password>"The object's name must equal the provider name (notifications). It is cluster-scoped.
apiVersion: notifications.platform.open-control-plane.io/v1alpha1
kind: NotificationConfig
metadata:
name: notifications
spec:
productName: Acme Cloud # shown in subjects/bodies (default "OpenControlPlane")
webappUrl: https://mycompany.eu # base of the platform web UI (hash-router SPA)
docsURL: https://mycompany.eu/help/ # user docs link, surfaced in the welcome email
# usernameIsEmail: true # default: a User subject's Name is its email
# enabledCategories: [] # empty = all categories
# enabledChannels: [Email] # default
email:
host: smtp.example.com
port: 587 # default 587
startTLS: true # default true
senderAddress: no-reply@example.com
senderName: OpenMCP Notifications
replyTo: platform-team@example.com
secretRef:
name: notifications-smtp # omit for no-auth relays
newVersionDigest:
aggregationWindow: 1h # collect affected control planes before sending one digest per admin
recordRetention: 720h # how long delivered records are kept (default 30d)The ConfigReconciler applies this live: changing the config or its Secret hot-reloads the SMTP
settings without a restart, and the object reports a Ready condition once applied.
No
Ready, but email delivery is inert until SMTP is configured. This is the smoke-test path exercised by the e2e suite.
Apply the ProjectWorkspaceConfig described in
docs/optout-rbac.md once per platform deployment. This grants project and
workspace members the ability to create opt-out objects in their namespace.
apiVersion: openmcp.cloud/v1alpha1
kind: PlatformService
metadata:
name: notifications
spec:
image: ghcr.io/openmcp-project/images/platform-service-notifications:<version>
initCommand: ["init"]
runCommand: ["run"]The init job requests read-only (get;list;watch) access on the onboarding cluster to
projects, workspaces, controlplanes, and (currently, broadly) other resources so it can
resolve the dynamically-typed service instances back to their ControlPlanes.
Opt-out is self-service, driven by two CRDs on the onboarding cluster (where end users have
RBAC access). The namespace where the object lives determines its authorization scope; the
spec.target field makes the muted resource explicit.
NotificationOptOut β resource-wide, admin-managed. Mutes notifications for all recipients
on the targeted resource:
# Mute all notifications for everyone in project "poc" (cascades to its workspaces/CPs).
# Created by a project admin in namespace "project-poc".
apiVersion: notifications.platform.open-control-plane.io/v1alpha1
kind: NotificationOptOut
metadata:
name: poc-silent
namespace: project-poc
spec:
target:
kind: Project
name: poc
# categories: [NewServiceVersion] # omit to mute everythingUserNotificationOptOut β per-user self-service. Any member (including viewers) can mute
notifications for themselves on a specific resource:
# Priya mutes version digests on one specific control plane.
# Created by priya (or a workspace admin) in namespace "project-poc--ws-dev".
apiVersion: notifications.platform.open-control-plane.io/v1alpha1
kind: UserNotificationOptOut
metadata:
name: priya-no-digests-prod
namespace: project-poc--ws-dev
spec:
subject:
kind: User
name: priya@example.com
target:
kind: ControlPlane
name: prod-cp
categories: [NewServiceVersion]Cascade rules:
- A
Projecttarget suppresses events for the project, all its workspaces, and all its control planes. - A
Workspacetarget suppresses events for that workspace and its control planes. - A
ControlPlanetarget suppresses only that specific control plane.
The opt-out check runs before dedup, so suppressed notifications are recorded with
phase: Suppressed in the NotificationRecord (still counted as delivered for dedup purposes).
See docs/optout-rbac.md for the ProjectWorkspaceConfig that grants
the required RBAC and a discussion of known limitations.
The e2e harness spins up a full OpenControlPlane landscape in kind (via
openmcp-testing), loads a locally built
image of this service, deploys it as a PlatformService, and asserts behavior.
# 1. Build the provider image locally and run the e2e suite against a fresh kind landscape.
task test-e2etask test-e2e (see Taskfile.yaml) builds
ghcr.io/openmcp-project/images/platform-service-notifications:<version> and hands it to the
harness in test/e2e/main_test.go, which:
- creates the platform cluster and installs the
openmcp-operator, - registers the
kindcluster provider, - deploys this service as the
notificationsPlatformService(withLoadImageToCluster), - runs test/e2e/platformservice_test.go: it creates the
NotificationConfigsingleton and waits for itsReadycondition, then deletes it.
If you run
go test ./test/e2e/...directly you'll see "image ... not present locally" β the image must be built first, which is whytask test-e2eis the entry point.
task test-e2e-mailhog spins up the full kind landscape, deploys a
MailHog SMTP relay into it, pre-wires the
NotificationConfig to send through MailHog, creates a test user, and asserts that the welcome
email arrives β all with zero manual setup.
# Run the delivery demo and tear down when done.
task test-e2e-mailhog
# Keep the landscape alive for hands-on exploration (Ctrl-C tears it down).
KEEP=1 task test-e2e-mailhogWhen KEEP=1 is set the test parks after the delivery assertion and prints the kind cluster
name together with the exact kubectl port-forward command to open the MailHog inbox on
http://localhost:8025.
While the landscape is live you can interact freely:
- Create more
UserProfileobjects β each one triggers a welcome email in MailHog. - Edit
NotificationConfig(e.g. setproductName) β the service hot-reloads within seconds. - Create a
UserNotificationOptOutfor a user β subsequent emails are suppressed; check theNotificationRecordstatus (phase: Suppressed).
This full delivery scenario is the current e2e milestone; the config-smoke suite (task test-e2e)
remains the CI gate.
go build ./... # build
go vet ./... # vet
go test ./internal/... # unit tests (pipeline, dedup keys, controller helpers, template render)
task test-e2e # full kind-based e2eUnit tests cover the delivery pipeline (dedup, suppression, address resolution, send-failure retry), the opt-out resolver (cascade rules, category subsets, subject matching, workspace isolation), the dedup/profile key derivation, the membership/admin extraction helpers, and golden-ish assertions on the rendered email templates (including HTML escaping).
- Version digest (
NewServiceVersion): notify admins when aControlPlaneis running an outdated service or provider version. TheVersionDigestReconcileris scaffolded but disabled (see run.go) pending a catalog CRD that exposes available versions β analogous toManagedComponentsin the V1 offering. Once that resource exists on the onboarding cluster, the reconciler needs to be reimplemented as a scheduled comparison of installed vs. available versions, then re-enabled. - Slack
Notifier; MJML templates; a self-service opt-out link (needs a small HTTP endpoint). - OIDC
email-claim resolution (today: username-is-email, or aUserProfile.spec.emailoverride).
This project is open to feature requests/suggestions, bug reports etc. via GitHub issues. Contribution and feedback are encouraged and always welcome. For more information about how to contribute, the project structure, as well as additional contribution information, see our Contribution Guidelines.
If you find any bug that may be a security problem, please follow our instructions at in our security policy on how to report it. Please do not create GitHub issues for security-related doubts or problems.
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone. By participating in this project, you agree to abide by its Code of Conduct at all times.
Copyright OpenControlPlane contributors. Please see our LICENSE for copyright and license information. Detailed information including third-party components and their licensing/copyright information is available via the REUSE tool.
OpenControlPlane is part of ApeiroRA, an EU Important Project of Common European Interest (IPCEI-CIS).
Copyright Linux Foundation Europe. For web site terms of use, trademark policy and other project policies please see https://linuxfoundation.eu/en/policies.


