feat: graduate web-push from experiment to always-on (#24310)

* Removes experiment `web-push`.
* Falls back to NoopWebpusher in case of error
* Checks browser capability in FE
* Adds note to agents getting-started docs regarding webpush without TLS

> 🤖
This commit is contained in:
Cian Johnston
2026-04-14 09:07:06 +01:00
committed by GitHub
parent 155e98914d
commit 116323d3cf
11 changed files with 49 additions and 45 deletions
+6 -19
View File
@@ -843,27 +843,14 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
}
// Manage push notifications.
experiments := coderd.ReadExperiments(options.Logger, options.DeploymentValues.Experiments.Value())
if experiments.Enabled(codersdk.ExperimentWebPush) || buildinfo.IsDev() {
if !strings.HasPrefix(options.AccessURL.String(), "https://") {
options.Logger.Warn(ctx, "access URL is not HTTPS, so web push notifications may not work on some browsers", slog.F("access_url", options.AccessURL.String()))
}
webpusher, err := webpush.New(ctx, ptr.Ref(options.Logger.Named("webpush")), options.Database, options.AccessURL.String())
if err != nil {
options.Logger.Error(ctx, "failed to create web push dispatcher", slog.Error(err))
options.Logger.Warn(ctx, "web push notifications will not work until the VAPID keys are regenerated")
webpusher = &webpush.NoopWebpusher{
Msg: "Web Push notifications are disabled due to a system error. Please contact your Coder administrator.",
}
}
options.WebPushDispatcher = webpusher
} else {
options.WebPushDispatcher = &webpush.NoopWebpusher{
// Users will likely not see this message as the endpoints return 404
// if not enabled. Just in case...
Msg: "Web Push notifications are an experimental feature and are disabled by default. Enable the 'web-push' experiment to use this feature.",
webpusher, err := webpush.New(ctx, ptr.Ref(options.Logger.Named("webpush")), options.Database, options.AccessURL.String())
if err != nil {
options.Logger.Error(ctx, "failed to create web push dispatcher", slog.Error(err))
webpusher = &webpush.NoopWebpusher{
Msg: "Web Push notifications are disabled due to a system error. Please contact your Coder administrator.",
}
}
options.WebPushDispatcher = webpusher
githubOAuth2ConfigParams, err := getGithubOAuth2ConfigParams(ctx, options.Database, vals)
if err != nil {
-4
View File
@@ -16176,7 +16176,6 @@ const docTemplate = `{
"auto-fill-parameters",
"notifications",
"workspace-usage",
"web-push",
"oauth2",
"agents",
"mcp-server-http",
@@ -16189,7 +16188,6 @@ const docTemplate = `{
"ExperimentMCPServerHTTP": "Enables the MCP HTTP server functionality.",
"ExperimentNotifications": "Sends notifications via SMTP and webhooks following certain events.",
"ExperimentOAuth2": "Enables OAuth2 provider functionality.",
"ExperimentWebPush": "Enables web push notifications through the browser.",
"ExperimentWorkspaceBuildUpdates": "Enables publishing workspace build updates to the all builds pubsub channel.",
"ExperimentWorkspaceUsage": "Enables the new workspace usage tracking."
},
@@ -16198,7 +16196,6 @@ const docTemplate = `{
"This should not be taken out of experiments until we have redesigned the feature.",
"Sends notifications via SMTP and webhooks following certain events.",
"Enables the new workspace usage tracking.",
"Enables web push notifications through the browser.",
"Enables OAuth2 provider functionality.",
"Enables agent-powered chat functionality.",
"Enables the MCP HTTP server functionality.",
@@ -16209,7 +16206,6 @@ const docTemplate = `{
"ExperimentAutoFillParameters",
"ExperimentNotifications",
"ExperimentWorkspaceUsage",
"ExperimentWebPush",
"ExperimentOAuth2",
"ExperimentAgents",
"ExperimentMCPServerHTTP",
-4
View File
@@ -14636,7 +14636,6 @@
"auto-fill-parameters",
"notifications",
"workspace-usage",
"web-push",
"oauth2",
"agents",
"mcp-server-http",
@@ -14649,7 +14648,6 @@
"ExperimentMCPServerHTTP": "Enables the MCP HTTP server functionality.",
"ExperimentNotifications": "Sends notifications via SMTP and webhooks following certain events.",
"ExperimentOAuth2": "Enables OAuth2 provider functionality.",
"ExperimentWebPush": "Enables web push notifications through the browser.",
"ExperimentWorkspaceBuildUpdates": "Enables publishing workspace build updates to the all builds pubsub channel.",
"ExperimentWorkspaceUsage": "Enables the new workspace usage tracking."
},
@@ -14658,7 +14656,6 @@
"This should not be taken out of experiments until we have redesigned the feature.",
"Sends notifications via SMTP and webhooks following certain events.",
"Enables the new workspace usage tracking.",
"Enables web push notifications through the browser.",
"Enables OAuth2 provider functionality.",
"Enables agent-powered chat functionality.",
"Enables the MCP HTTP server functionality.",
@@ -14669,7 +14666,6 @@
"ExperimentAutoFillParameters",
"ExperimentNotifications",
"ExperimentWorkspaceUsage",
"ExperimentWebPush",
"ExperimentOAuth2",
"ExperimentAgents",
"ExperimentMCPServerHTTP",
-1
View File
@@ -1624,7 +1624,6 @@ func New(options *Options) *API {
})
})
r.Route("/webpush", func(r chi.Router) {
r.Use(httpmw.RequireExperimentWithDevBypass(api.Experiments, codersdk.ExperimentWebPush))
r.Post("/subscription", api.postUserWebpushSubscription)
r.Delete("/subscription", api.deleteUserWebpushSubscription)
r.Post("/test", api.postUserPushNotificationTest)
+5 -3
View File
@@ -386,9 +386,11 @@ func (n *Webpusher) PublicKey() string {
return n.VAPIDPublicKey
}
// NoopWebpusher is a Dispatcher that does nothing except return an error.
// This is returned when web push notifications are disabled, or if there was an
// error generating the VAPID keys.
// NoopWebpusher is a Dispatcher that always fails, returning Msg as
// the error. It is used as a fallback when VAPID key setup fails.
// The underlying error is not included to avoid leaking internal
// details (e.g. database errors) in API responses; it is logged at
// the call site instead.
type NoopWebpusher struct {
Msg string
}
+18
View File
@@ -405,3 +405,21 @@ func setupPushTestWithOptions(ctx context.Context, t *testing.T, db database.Sto
return manager, db, server.URL
}
func TestNoopWebpusher(t *testing.T) {
t.Parallel()
noop := &webpush.NoopWebpusher{
Msg: "push disabled",
}
dispatchErr := noop.Dispatch(context.Background(), uuid.New(), codersdk.WebpushMessage{})
require.Error(t, dispatchErr)
require.Contains(t, dispatchErr.Error(), "push disabled")
testErr := noop.Test(context.Background(), codersdk.WebpushSubscription{})
require.Error(t, testErr)
require.Contains(t, testErr.Error(), "push disabled")
require.Empty(t, noop.PublicKey())
}
+1 -5
View File
@@ -4355,7 +4355,6 @@ const (
ExperimentAutoFillParameters Experiment = "auto-fill-parameters" // This should not be taken out of experiments until we have redesigned the feature.
ExperimentNotifications Experiment = "notifications" // Sends notifications via SMTP and webhooks following certain events.
ExperimentWorkspaceUsage Experiment = "workspace-usage" // Enables the new workspace usage tracking.
ExperimentWebPush Experiment = "web-push" // Enables web push notifications through the browser.
ExperimentOAuth2 Experiment = "oauth2" // Enables OAuth2 provider functionality.
ExperimentAgents Experiment = "agents" // Enables agent-powered chat functionality.
ExperimentMCPServerHTTP Experiment = "mcp-server-http" // Enables the MCP HTTP server functionality.
@@ -4372,8 +4371,6 @@ func (e Experiment) DisplayName() string {
return "SMTP and Webhook Notifications"
case ExperimentWorkspaceUsage:
return "Workspace Usage Tracking"
case ExperimentWebPush:
return "Browser Push Notifications"
case ExperimentOAuth2:
return "OAuth2 Provider Functionality"
case ExperimentAgents:
@@ -4384,7 +4381,7 @@ func (e Experiment) DisplayName() string {
return "Workspace Build Updates Channel"
default:
// Split on hyphen and convert to title case
// e.g. "web-push" -> "Web Push", "mcp-server-http" -> "Mcp Server Http"
// e.g. "mcp-server-http" -> "Mcp Server Http"
caser := cases.Title(language.English)
return caser.String(strings.ReplaceAll(string(e), "-", " "))
}
@@ -4396,7 +4393,6 @@ var ExperimentsKnown = Experiments{
ExperimentAutoFillParameters,
ExperimentNotifications,
ExperimentWorkspaceUsage,
ExperimentWebPush,
ExperimentOAuth2,
ExperimentAgents,
ExperimentMCPServerHTTP,
+12
View File
@@ -159,6 +159,18 @@ dedicated test or staging deployment to avoid disruption to production
developer workflows. See [Early Access](./early-access.md) for the full
set of expectations and limitations.
### Use HTTPS for push notifications
Coder Agents use browser push notifications to alert you when a task
completes or needs attention. Most browsers require a secure (HTTPS)
origin for the [Push API](https://developer.mozilla.org/en-US/docs/Web/API/Push_API)
to work. If your access URL uses plain HTTP,
push notifications may not function.
This does not affect agents themselves — only the browser notification
delivery. If you terminate TLS at a reverse proxy, ensure the
[access URL](../../admin/setup/index.md) is configured with an `https://` scheme.
### Set a deployment-wide system prompt
Administrators can set a system prompt that applies to all Coder Agents across the
+3 -3
View File
@@ -4558,9 +4558,9 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
#### Enumerated Values
| Value(s) |
|-----------------------------------------------------------------------------------------------------------------------------------------------------|
| `agents`, `auto-fill-parameters`, `example`, `mcp-server-http`, `notifications`, `oauth2`, `web-push`, `workspace-build-updates`, `workspace-usage` |
| Value(s) |
|-----------------------------------------------------------------------------------------------------------------------------------------|
| `agents`, `auto-fill-parameters`, `example`, `mcp-server-http`, `notifications`, `oauth2`, `workspace-build-updates`, `workspace-usage` |
## codersdk.ExternalAPIKeyScopes
-2
View File
@@ -3550,7 +3550,6 @@ export type Experiment =
| "mcp-server-http"
| "notifications"
| "oauth2"
| "web-push"
| "workspace-build-updates"
| "workspace-usage";
@@ -3561,7 +3560,6 @@ export const Experiments: Experiment[] = [
"mcp-server-http",
"notifications",
"oauth2",
"web-push",
"workspace-build-updates",
"workspace-usage",
];
+4 -4
View File
@@ -2,7 +2,6 @@ import { useEffect, useState } from "react";
import { useQuery } from "react-query";
import { API } from "#/api/api";
import { buildInfo } from "#/api/queries/buildInfo";
import { experiments } from "#/api/queries/experiments";
import { useEmbeddedMetadata } from "#/hooks/useEmbeddedMetadata";
interface WebpushNotifications {
@@ -17,11 +16,12 @@ interface WebpushNotifications {
export const useWebpushNotifications = (): WebpushNotifications => {
const { metadata } = useEmbeddedMetadata();
const buildInfoQuery = useQuery(buildInfo(metadata["build-info"]));
const enabledExperimentsQuery = useQuery(experiments(metadata.experiments));
const [subscribed, setSubscribed] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(true);
const enabled = enabledExperimentsQuery.data?.includes("web-push") ?? false;
const enabled =
"Notification" in window &&
"serviceWorker" in navigator &&
!!buildInfoQuery.data?.webpush_public_key;
useEffect(() => {
// Check if browser supports push notifications