diff --git a/agent/agent.go b/agent/agent.go
index 8125bbc5f70d6..e3bbe7f07c984 100644
--- a/agent/agent.go
+++ b/agent/agent.go
@@ -155,35 +155,35 @@ func New(options Options) Agent {
 	hardCtx, hardCancel := context.WithCancel(context.Background())
 	gracefulCtx, gracefulCancel := context.WithCancel(hardCtx)
 	a := &agent{
-		tailnetListenPort:            options.TailnetListenPort,
-		reconnectingPTYTimeout:       options.ReconnectingPTYTimeout,
-		logger:                       options.Logger,
-		gracefulCtx:                  gracefulCtx,
-		gracefulCancel:               gracefulCancel,
-		hardCtx:                      hardCtx,
-		hardCancel:                   hardCancel,
-		coordDisconnected:            make(chan struct{}),
-		environmentVariables:         options.EnvironmentVariables,
-		client:                       options.Client,
-		exchangeToken:                options.ExchangeToken,
-		filesystem:                   options.Filesystem,
-		logDir:                       options.LogDir,
-		tempDir:                      options.TempDir,
-		scriptDataDir:                options.ScriptDataDir,
-		lifecycleUpdate:              make(chan struct{}, 1),
-		lifecycleReported:            make(chan codersdk.WorkspaceAgentLifecycle, 1),
-		lifecycleStates:              []agentsdk.PostLifecycleRequest{{State: codersdk.WorkspaceAgentLifecycleCreated}},
-		ignorePorts:                  options.IgnorePorts,
-		portCacheDuration:            options.PortCacheDuration,
-		reportMetadataInterval:       options.ReportMetadataInterval,
-		serviceBannerRefreshInterval: options.ServiceBannerRefreshInterval,
-		sshMaxTimeout:                options.SSHMaxTimeout,
-		subsystems:                   options.Subsystems,
-		addresses:                    options.Addresses,
-		syscaller:                    options.Syscaller,
-		modifiedProcs:                options.ModifiedProcesses,
-		processManagementTick:        options.ProcessManagementTick,
-		logSender:                    agentsdk.NewLogSender(options.Logger),
+		tailnetListenPort:                  options.TailnetListenPort,
+		reconnectingPTYTimeout:             options.ReconnectingPTYTimeout,
+		logger:                             options.Logger,
+		gracefulCtx:                        gracefulCtx,
+		gracefulCancel:                     gracefulCancel,
+		hardCtx:                            hardCtx,
+		hardCancel:                         hardCancel,
+		coordDisconnected:                  make(chan struct{}),
+		environmentVariables:               options.EnvironmentVariables,
+		client:                             options.Client,
+		exchangeToken:                      options.ExchangeToken,
+		filesystem:                         options.Filesystem,
+		logDir:                             options.LogDir,
+		tempDir:                            options.TempDir,
+		scriptDataDir:                      options.ScriptDataDir,
+		lifecycleUpdate:                    make(chan struct{}, 1),
+		lifecycleReported:                  make(chan codersdk.WorkspaceAgentLifecycle, 1),
+		lifecycleStates:                    []agentsdk.PostLifecycleRequest{{State: codersdk.WorkspaceAgentLifecycleCreated}},
+		ignorePorts:                        options.IgnorePorts,
+		portCacheDuration:                  options.PortCacheDuration,
+		reportMetadataInterval:             options.ReportMetadataInterval,
+		notificationBannersRefreshInterval: options.ServiceBannerRefreshInterval,
+		sshMaxTimeout:                      options.SSHMaxTimeout,
+		subsystems:                         options.Subsystems,
+		addresses:                          options.Addresses,
+		syscaller:                          options.Syscaller,
+		modifiedProcs:                      options.ModifiedProcesses,
+		processManagementTick:              options.ProcessManagementTick,
+		logSender:                          agentsdk.NewLogSender(options.Logger),
 
 		prometheusRegistry: prometheusRegistry,
 		metrics:            newAgentMetrics(prometheusRegistry),
@@ -193,7 +193,7 @@ func New(options Options) Agent {
 	// that gets closed on disconnection.  This is used to wait for graceful disconnection from the
 	// coordinator during shut down.
 	close(a.coordDisconnected)
-	a.serviceBanner.Store(new(codersdk.ServiceBannerConfig))
+	a.notificationBanners.Store(new([]codersdk.BannerConfig))
 	a.sessionToken.Store(new(string))
 	a.init()
 	return a
@@ -231,14 +231,14 @@ type agent struct {
 
 	environmentVariables map[string]string
 
-	manifest                     atomic.Pointer[agentsdk.Manifest] // manifest is atomic because values can change after reconnection.
-	reportMetadataInterval       time.Duration
-	scriptRunner                 *agentscripts.Runner
-	serviceBanner                atomic.Pointer[codersdk.ServiceBannerConfig] // serviceBanner is atomic because it is periodically updated.
-	serviceBannerRefreshInterval time.Duration
-	sessionToken                 atomic.Pointer[string]
-	sshServer                    *agentssh.Server
-	sshMaxTimeout                time.Duration
+	manifest                           atomic.Pointer[agentsdk.Manifest] // manifest is atomic because values can change after reconnection.
+	reportMetadataInterval             time.Duration
+	scriptRunner                       *agentscripts.Runner
+	notificationBanners                atomic.Pointer[[]codersdk.BannerConfig] // notificationBanners is atomic because it is periodically updated.
+	notificationBannersRefreshInterval time.Duration
+	sessionToken                       atomic.Pointer[string]
+	sshServer                          *agentssh.Server
+	sshMaxTimeout                      time.Duration
 
 	lifecycleUpdate            chan struct{}
 	lifecycleReported          chan codersdk.WorkspaceAgentLifecycle
@@ -272,11 +272,11 @@ func (a *agent) TailnetConn() *tailnet.Conn {
 func (a *agent) init() {
 	// pass the "hard" context because we explicitly close the SSH server as part of graceful shutdown.
 	sshSrv, err := agentssh.NewServer(a.hardCtx, a.logger.Named("ssh-server"), a.prometheusRegistry, a.filesystem, &agentssh.Config{
-		MaxTimeout:       a.sshMaxTimeout,
-		MOTDFile:         func() string { return a.manifest.Load().MOTDFile },
-		ServiceBanner:    func() *codersdk.ServiceBannerConfig { return a.serviceBanner.Load() },
-		UpdateEnv:        a.updateCommandEnv,
-		WorkingDirectory: func() string { return a.manifest.Load().Directory },
+		MaxTimeout:          a.sshMaxTimeout,
+		MOTDFile:            func() string { return a.manifest.Load().MOTDFile },
+		NotificationBanners: func() *[]codersdk.BannerConfig { return a.notificationBanners.Load() },
+		UpdateEnv:           a.updateCommandEnv,
+		WorkingDirectory:    func() string { return a.manifest.Load().Directory },
 	})
 	if err != nil {
 		panic(err)
@@ -709,23 +709,26 @@ func (a *agent) setLifecycle(state codersdk.WorkspaceAgentLifecycle) {
 // (and must be done before the session actually starts).
 func (a *agent) fetchServiceBannerLoop(ctx context.Context, conn drpc.Conn) error {
 	aAPI := proto.NewDRPCAgentClient(conn)
-	ticker := time.NewTicker(a.serviceBannerRefreshInterval)
+	ticker := time.NewTicker(a.notificationBannersRefreshInterval)
 	defer ticker.Stop()
 	for {
 		select {
 		case <-ctx.Done():
 			return ctx.Err()
 		case <-ticker.C:
-			sbp, err := aAPI.GetServiceBanner(ctx, &proto.GetServiceBannerRequest{})
+			bannersProto, err := aAPI.GetNotificationBanners(ctx, &proto.GetNotificationBannersRequest{})
 			if err != nil {
 				if ctx.Err() != nil {
 					return ctx.Err()
 				}
-				a.logger.Error(ctx, "failed to update service banner", slog.Error(err))
+				a.logger.Error(ctx, "failed to update notification banners", slog.Error(err))
 				return err
 			}
-			serviceBanner := agentsdk.ServiceBannerFromProto(sbp)
-			a.serviceBanner.Store(&serviceBanner)
+			banners := make([]codersdk.BannerConfig, 0, len(bannersProto.NotificationBanners))
+			for _, bannerProto := range bannersProto.NotificationBanners {
+				banners = append(banners, agentsdk.BannerConfigFromProto(bannerProto))
+			}
+			a.notificationBanners.Store(&banners)
 		}
 	}
 }
@@ -757,15 +760,18 @@ func (a *agent) run() (retErr error) {
 	// redial the coder server and retry.
 	connMan := newAPIConnRoutineManager(a.gracefulCtx, a.hardCtx, a.logger, conn)
 
-	connMan.start("init service banner", gracefulShutdownBehaviorStop,
+	connMan.start("init notification banners", gracefulShutdownBehaviorStop,
 		func(ctx context.Context, conn drpc.Conn) error {
 			aAPI := proto.NewDRPCAgentClient(conn)
-			sbp, err := aAPI.GetServiceBanner(ctx, &proto.GetServiceBannerRequest{})
+			bannersProto, err := aAPI.GetNotificationBanners(ctx, &proto.GetNotificationBannersRequest{})
 			if err != nil {
 				return xerrors.Errorf("fetch service banner: %w", err)
 			}
-			serviceBanner := agentsdk.ServiceBannerFromProto(sbp)
-			a.serviceBanner.Store(&serviceBanner)
+			banners := make([]codersdk.BannerConfig, 0, len(bannersProto.NotificationBanners))
+			for _, bannerProto := range bannersProto.NotificationBanners {
+				banners = append(banners, agentsdk.BannerConfigFromProto(bannerProto))
+			}
+			a.notificationBanners.Store(&banners)
 			return nil
 		},
 	)
diff --git a/agent/agent_test.go b/agent/agent_test.go
index 45ebf7b709199..c674a29ec35f6 100644
--- a/agent/agent_test.go
+++ b/agent/agent_test.go
@@ -614,12 +614,12 @@ func TestAgent_Session_TTY_MOTD_Update(t *testing.T) {
 			// Set new banner func and wait for the agent to call it to update the
 			// banner.
 			ready := make(chan struct{}, 2)
-			client.SetServiceBannerFunc(func() (codersdk.ServiceBannerConfig, error) {
+			client.SetNotificationBannersFunc(func() ([]codersdk.BannerConfig, error) {
 				select {
 				case ready <- struct{}{}:
 				default:
 				}
-				return test.banner, nil
+				return []codersdk.BannerConfig{test.banner}, nil
 			})
 			<-ready
 			<-ready // Wait for two updates to ensure the value has propagated.
@@ -2193,15 +2193,15 @@ func setupAgentSSHClient(ctx context.Context, t *testing.T) *ssh.Client {
 func setupSSHSession(
 	t *testing.T,
 	manifest agentsdk.Manifest,
-	serviceBanner codersdk.ServiceBannerConfig,
+	banner codersdk.BannerConfig,
 	prepareFS func(fs afero.Fs),
 	opts ...func(*agenttest.Client, *agent.Options),
 ) *ssh.Session {
 	ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
 	defer cancel()
 	opts = append(opts, func(c *agenttest.Client, o *agent.Options) {
-		c.SetServiceBannerFunc(func() (codersdk.ServiceBannerConfig, error) {
-			return serviceBanner, nil
+		c.SetNotificationBannersFunc(func() ([]codersdk.BannerConfig, error) {
+			return []codersdk.BannerConfig{banner}, nil
 		})
 	})
 	//nolint:dogsled
diff --git a/agent/agentssh/agentssh.go b/agent/agentssh/agentssh.go
index 48da6aa0296ca..4fcc6ab869c5b 100644
--- a/agent/agentssh/agentssh.go
+++ b/agent/agentssh/agentssh.go
@@ -63,7 +63,7 @@ type Config struct {
 	// file will be displayed to the user upon login.
 	MOTDFile func() string
 	// ServiceBanner returns the configuration for the Coder service banner.
-	ServiceBanner func() *codersdk.ServiceBannerConfig
+	NotificationBanners func() *[]codersdk.BannerConfig
 	// UpdateEnv updates the environment variables for the command to be
 	// executed. It can be used to add, modify or replace environment variables.
 	UpdateEnv func(current []string) (updated []string, err error)
@@ -123,8 +123,8 @@ func NewServer(ctx context.Context, logger slog.Logger, prometheusRegistry *prom
 	if config.MOTDFile == nil {
 		config.MOTDFile = func() string { return "" }
 	}
-	if config.ServiceBanner == nil {
-		config.ServiceBanner = func() *codersdk.ServiceBannerConfig { return &codersdk.ServiceBannerConfig{} }
+	if config.NotificationBanners == nil {
+		config.NotificationBanners = func() *[]codersdk.BannerConfig { return &[]codersdk.BannerConfig{} }
 	}
 	if config.WorkingDirectory == nil {
 		config.WorkingDirectory = func() string {
@@ -441,12 +441,15 @@ func (s *Server) startPTYSession(logger slog.Logger, session ptySession, magicTy
 	session.DisablePTYEmulation()
 
 	if isLoginShell(session.RawCommand()) {
-		serviceBanner := s.config.ServiceBanner()
-		if serviceBanner != nil {
-			err := showServiceBanner(session, serviceBanner)
-			if err != nil {
-				logger.Error(ctx, "agent failed to show service banner", slog.Error(err))
-				s.metrics.sessionErrors.WithLabelValues(magicTypeLabel, "yes", "service_banner").Add(1)
+		banners := s.config.NotificationBanners()
+		if banners != nil {
+			for _, banner := range *banners {
+				err := showNotificationBanner(session, banner)
+				if err != nil {
+					logger.Error(ctx, "agent failed to show service banner", slog.Error(err))
+					s.metrics.sessionErrors.WithLabelValues(magicTypeLabel, "yes", "notification_banner").Add(1)
+					break
+				}
 			}
 		}
 	}
@@ -891,9 +894,9 @@ func isQuietLogin(fs afero.Fs, rawCommand string) bool {
 	return err == nil
 }
 
-// showServiceBanner will write the service banner if enabled and not blank
+// showNotificationBanner will write the service banner if enabled and not blank
 // along with a blank line for spacing.
-func showServiceBanner(session io.Writer, banner *codersdk.ServiceBannerConfig) error {
+func showNotificationBanner(session io.Writer, banner codersdk.BannerConfig) error {
 	if banner.Enabled && banner.Message != "" {
 		// The banner supports Markdown so we might want to parse it but Markdown is
 		// still fairly readable in its raw form.
diff --git a/agent/agenttest/client.go b/agent/agenttest/client.go
index 22eba14483f92..b21a7444c6084 100644
--- a/agent/agenttest/client.go
+++ b/agent/agenttest/client.go
@@ -138,8 +138,8 @@ func (c *Client) GetStartupLogs() []agentsdk.Log {
 	return c.logs
 }
 
-func (c *Client) SetServiceBannerFunc(f func() (codersdk.ServiceBannerConfig, error)) {
-	c.fakeAgentAPI.SetServiceBannerFunc(f)
+func (c *Client) SetNotificationBannersFunc(f func() ([]codersdk.ServiceBannerConfig, error)) {
+	c.fakeAgentAPI.SetNotificationBannersFunc(f)
 }
 
 func (c *Client) PushDERPMapUpdate(update *tailcfg.DERPMap) error {
@@ -171,31 +171,39 @@ type FakeAgentAPI struct {
 	lifecycleStates []codersdk.WorkspaceAgentLifecycle
 	metadata        map[string]agentsdk.Metadata
 
-	getServiceBannerFunc func() (codersdk.ServiceBannerConfig, error)
+	getNotificationBannersFunc func() ([]codersdk.BannerConfig, error)
 }
 
 func (f *FakeAgentAPI) GetManifest(context.Context, *agentproto.GetManifestRequest) (*agentproto.Manifest, error) {
 	return f.manifest, nil
 }
 
-func (f *FakeAgentAPI) SetServiceBannerFunc(fn func() (codersdk.ServiceBannerConfig, error)) {
+func (*FakeAgentAPI) GetServiceBanner(context.Context, *agentproto.GetServiceBannerRequest) (*agentproto.ServiceBanner, error) {
+	return &agentproto.ServiceBanner{}, nil
+}
+
+func (f *FakeAgentAPI) SetNotificationBannersFunc(fn func() ([]codersdk.BannerConfig, error)) {
 	f.Lock()
 	defer f.Unlock()
-	f.getServiceBannerFunc = fn
-	f.logger.Info(context.Background(), "updated ServiceBannerFunc")
+	f.getNotificationBannersFunc = fn
+	f.logger.Info(context.Background(), "updated notification banners")
 }
 
-func (f *FakeAgentAPI) GetServiceBanner(context.Context, *agentproto.GetServiceBannerRequest) (*agentproto.ServiceBanner, error) {
+func (f *FakeAgentAPI) GetNotificationBanners(context.Context, *agentproto.GetNotificationBannersRequest) (*agentproto.GetNotificationBannersResponse, error) {
 	f.Lock()
 	defer f.Unlock()
-	if f.getServiceBannerFunc == nil {
-		return &agentproto.ServiceBanner{}, nil
+	if f.getNotificationBannersFunc == nil {
+		return &agentproto.GetNotificationBannersResponse{NotificationBanners: []*agentproto.BannerConfig{}}, nil
 	}
-	sb, err := f.getServiceBannerFunc()
+	banners, err := f.getNotificationBannersFunc()
 	if err != nil {
 		return nil, err
 	}
-	return agentsdk.ProtoFromServiceBanner(sb), nil
+	bannersProto := make([]*agentproto.BannerConfig, 0, len(banners))
+	for _, banner := range banners {
+		bannersProto = append(bannersProto, agentsdk.ProtoFromBannerConfig(banner))
+	}
+	return &agentproto.GetNotificationBannersResponse{NotificationBanners: bannersProto}, nil
 }
 
 func (f *FakeAgentAPI) UpdateStats(ctx context.Context, req *agentproto.UpdateStatsRequest) (*agentproto.UpdateStatsResponse, error) {
diff --git a/agent/proto/agent.pb.go b/agent/proto/agent.pb.go
index 20bd20460275f..41e8d061054a5 100644
--- a/agent/proto/agent.pb.go
+++ b/agent/proto/agent.pb.go
@@ -1859,6 +1859,154 @@ func (x *BatchCreateLogsResponse) GetLogLimitExceeded() bool {
 	return false
 }
 
+type GetNotificationBannersRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+}
+
+func (x *GetNotificationBannersRequest) Reset() {
+	*x = GetNotificationBannersRequest{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_agent_proto_agent_proto_msgTypes[22]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *GetNotificationBannersRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetNotificationBannersRequest) ProtoMessage() {}
+
+func (x *GetNotificationBannersRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_agent_proto_agent_proto_msgTypes[22]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetNotificationBannersRequest.ProtoReflect.Descriptor instead.
+func (*GetNotificationBannersRequest) Descriptor() ([]byte, []int) {
+	return file_agent_proto_agent_proto_rawDescGZIP(), []int{22}
+}
+
+type GetNotificationBannersResponse struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	NotificationBanners []*BannerConfig `protobuf:"bytes,1,rep,name=notification_banners,json=notificationBanners,proto3" json:"notification_banners,omitempty"`
+}
+
+func (x *GetNotificationBannersResponse) Reset() {
+	*x = GetNotificationBannersResponse{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_agent_proto_agent_proto_msgTypes[23]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *GetNotificationBannersResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetNotificationBannersResponse) ProtoMessage() {}
+
+func (x *GetNotificationBannersResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_agent_proto_agent_proto_msgTypes[23]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetNotificationBannersResponse.ProtoReflect.Descriptor instead.
+func (*GetNotificationBannersResponse) Descriptor() ([]byte, []int) {
+	return file_agent_proto_agent_proto_rawDescGZIP(), []int{23}
+}
+
+func (x *GetNotificationBannersResponse) GetNotificationBanners() []*BannerConfig {
+	if x != nil {
+		return x.NotificationBanners
+	}
+	return nil
+}
+
+type BannerConfig struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Enabled         bool   `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"`
+	Message         string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
+	BackgroundColor string `protobuf:"bytes,3,opt,name=background_color,json=backgroundColor,proto3" json:"background_color,omitempty"`
+}
+
+func (x *BannerConfig) Reset() {
+	*x = BannerConfig{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_agent_proto_agent_proto_msgTypes[24]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *BannerConfig) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*BannerConfig) ProtoMessage() {}
+
+func (x *BannerConfig) ProtoReflect() protoreflect.Message {
+	mi := &file_agent_proto_agent_proto_msgTypes[24]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use BannerConfig.ProtoReflect.Descriptor instead.
+func (*BannerConfig) Descriptor() ([]byte, []int) {
+	return file_agent_proto_agent_proto_rawDescGZIP(), []int{24}
+}
+
+func (x *BannerConfig) GetEnabled() bool {
+	if x != nil {
+		return x.Enabled
+	}
+	return false
+}
+
+func (x *BannerConfig) GetMessage() string {
+	if x != nil {
+		return x.Message
+	}
+	return ""
+}
+
+func (x *BannerConfig) GetBackgroundColor() string {
+	if x != nil {
+		return x.BackgroundColor
+	}
+	return ""
+}
+
 type WorkspaceApp_Healthcheck struct {
 	state         protoimpl.MessageState
 	sizeCache     protoimpl.SizeCache
@@ -1872,7 +2020,7 @@ type WorkspaceApp_Healthcheck struct {
 func (x *WorkspaceApp_Healthcheck) Reset() {
 	*x = WorkspaceApp_Healthcheck{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_agent_proto_agent_proto_msgTypes[22]
+		mi := &file_agent_proto_agent_proto_msgTypes[25]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -1885,7 +2033,7 @@ func (x *WorkspaceApp_Healthcheck) String() string {
 func (*WorkspaceApp_Healthcheck) ProtoMessage() {}
 
 func (x *WorkspaceApp_Healthcheck) ProtoReflect() protoreflect.Message {
-	mi := &file_agent_proto_agent_proto_msgTypes[22]
+	mi := &file_agent_proto_agent_proto_msgTypes[25]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -1936,7 +2084,7 @@ type WorkspaceAgentMetadata_Result struct {
 func (x *WorkspaceAgentMetadata_Result) Reset() {
 	*x = WorkspaceAgentMetadata_Result{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_agent_proto_agent_proto_msgTypes[23]
+		mi := &file_agent_proto_agent_proto_msgTypes[26]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -1949,7 +2097,7 @@ func (x *WorkspaceAgentMetadata_Result) String() string {
 func (*WorkspaceAgentMetadata_Result) ProtoMessage() {}
 
 func (x *WorkspaceAgentMetadata_Result) ProtoReflect() protoreflect.Message {
-	mi := &file_agent_proto_agent_proto_msgTypes[23]
+	mi := &file_agent_proto_agent_proto_msgTypes[26]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -2008,7 +2156,7 @@ type WorkspaceAgentMetadata_Description struct {
 func (x *WorkspaceAgentMetadata_Description) Reset() {
 	*x = WorkspaceAgentMetadata_Description{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_agent_proto_agent_proto_msgTypes[24]
+		mi := &file_agent_proto_agent_proto_msgTypes[27]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -2021,7 +2169,7 @@ func (x *WorkspaceAgentMetadata_Description) String() string {
 func (*WorkspaceAgentMetadata_Description) ProtoMessage() {}
 
 func (x *WorkspaceAgentMetadata_Description) ProtoReflect() protoreflect.Message {
-	mi := &file_agent_proto_agent_proto_msgTypes[24]
+	mi := &file_agent_proto_agent_proto_msgTypes[27]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -2086,7 +2234,7 @@ type Stats_Metric struct {
 func (x *Stats_Metric) Reset() {
 	*x = Stats_Metric{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_agent_proto_agent_proto_msgTypes[27]
+		mi := &file_agent_proto_agent_proto_msgTypes[30]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -2099,7 +2247,7 @@ func (x *Stats_Metric) String() string {
 func (*Stats_Metric) ProtoMessage() {}
 
 func (x *Stats_Metric) ProtoReflect() protoreflect.Message {
-	mi := &file_agent_proto_agent_proto_msgTypes[27]
+	mi := &file_agent_proto_agent_proto_msgTypes[30]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -2155,7 +2303,7 @@ type Stats_Metric_Label struct {
 func (x *Stats_Metric_Label) Reset() {
 	*x = Stats_Metric_Label{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_agent_proto_agent_proto_msgTypes[28]
+		mi := &file_agent_proto_agent_proto_msgTypes[31]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -2168,7 +2316,7 @@ func (x *Stats_Metric_Label) String() string {
 func (*Stats_Metric_Label) ProtoMessage() {}
 
 func (x *Stats_Metric_Label) ProtoReflect() protoreflect.Message {
-	mi := &file_agent_proto_agent_proto_msgTypes[28]
+	mi := &file_agent_proto_agent_proto_msgTypes[31]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -2210,7 +2358,7 @@ type BatchUpdateAppHealthRequest_HealthUpdate struct {
 func (x *BatchUpdateAppHealthRequest_HealthUpdate) Reset() {
 	*x = BatchUpdateAppHealthRequest_HealthUpdate{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_agent_proto_agent_proto_msgTypes[29]
+		mi := &file_agent_proto_agent_proto_msgTypes[32]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -2223,7 +2371,7 @@ func (x *BatchUpdateAppHealthRequest_HealthUpdate) String() string {
 func (*BatchUpdateAppHealthRequest_HealthUpdate) ProtoMessage() {}
 
 func (x *BatchUpdateAppHealthRequest_HealthUpdate) ProtoReflect() protoreflect.Message {
-	mi := &file_agent_proto_agent_proto_msgTypes[29]
+	mi := &file_agent_proto_agent_proto_msgTypes[32]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -2594,64 +2742,87 @@ var file_agent_proto_agent_proto_rawDesc = []byte{
 	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2c, 0x0a, 0x12, 0x6c, 0x6f, 0x67, 0x5f, 0x6c, 0x69,
 	0x6d, 0x69, 0x74, 0x5f, 0x65, 0x78, 0x63, 0x65, 0x65, 0x64, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01,
 	0x28, 0x08, 0x52, 0x10, 0x6c, 0x6f, 0x67, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x45, 0x78, 0x63, 0x65,
-	0x65, 0x64, 0x65, 0x64, 0x2a, 0x63, 0x0a, 0x09, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74,
-	0x68, 0x12, 0x1a, 0x0a, 0x16, 0x41, 0x50, 0x50, 0x5f, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x5f,
-	0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0c, 0x0a,
-	0x08, 0x44, 0x49, 0x53, 0x41, 0x42, 0x4c, 0x45, 0x44, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x49,
-	0x4e, 0x49, 0x54, 0x49, 0x41, 0x4c, 0x49, 0x5a, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x0b, 0x0a,
-	0x07, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x59, 0x10, 0x03, 0x12, 0x0d, 0x0a, 0x09, 0x55, 0x4e,
-	0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x59, 0x10, 0x04, 0x32, 0xf6, 0x05, 0x0a, 0x05, 0x41, 0x67,
-	0x65, 0x6e, 0x74, 0x12, 0x4b, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65,
-	0x73, 0x74, 0x12, 0x22, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74,
-	0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52,
-	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61,
-	0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74,
-	0x12, 0x5a, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x61,
-	0x6e, 0x6e, 0x65, 0x72, 0x12, 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65,
-	0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
-	0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e,
-	0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53,
-	0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x12, 0x56, 0x0a, 0x0b,
-	0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x22, 0x2e, 0x63, 0x6f,
-	0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64,
-	0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
-	0x23, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32,
-	0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70,
-	0x6f, 0x6e, 0x73, 0x65, 0x12, 0x54, 0x0a, 0x0f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4c, 0x69,
-	0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x12, 0x26, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e,
-	0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4c,
-	0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
-	0x19, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32,
-	0x2e, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x12, 0x72, 0x0a, 0x15, 0x42, 0x61,
-	0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c,
-	0x74, 0x68, 0x73, 0x12, 0x2b, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e,
+	0x65, 0x64, 0x65, 0x64, 0x22, 0x1f, 0x0a, 0x1d, 0x47, 0x65, 0x74, 0x4e, 0x6f, 0x74, 0x69, 0x66,
+	0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65,
+	0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x71, 0x0a, 0x1e, 0x47, 0x65, 0x74, 0x4e, 0x6f, 0x74, 0x69,
+	0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x52,
+	0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x14, 0x6e, 0x6f, 0x74, 0x69, 0x66,
+	0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x62, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x18,
+	0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67,
+	0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e,
+	0x66, 0x69, 0x67, 0x52, 0x13, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f,
+	0x6e, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x22, 0x6d, 0x0a, 0x0c, 0x42, 0x61, 0x6e, 0x6e,
+	0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62,
+	0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c,
+	0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20,
+	0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x29, 0x0a, 0x10,
+	0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x63, 0x6f, 0x6c, 0x6f, 0x72,
+	0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75,
+	0x6e, 0x64, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x2a, 0x63, 0x0a, 0x09, 0x41, 0x70, 0x70, 0x48, 0x65,
+	0x61, 0x6c, 0x74, 0x68, 0x12, 0x1a, 0x0a, 0x16, 0x41, 0x50, 0x50, 0x5f, 0x48, 0x45, 0x41, 0x4c,
+	0x54, 0x48, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00,
+	0x12, 0x0c, 0x0a, 0x08, 0x44, 0x49, 0x53, 0x41, 0x42, 0x4c, 0x45, 0x44, 0x10, 0x01, 0x12, 0x10,
+	0x0a, 0x0c, 0x49, 0x4e, 0x49, 0x54, 0x49, 0x41, 0x4c, 0x49, 0x5a, 0x49, 0x4e, 0x47, 0x10, 0x02,
+	0x12, 0x0b, 0x0a, 0x07, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x59, 0x10, 0x03, 0x12, 0x0d, 0x0a,
+	0x09, 0x55, 0x4e, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x59, 0x10, 0x04, 0x32, 0xef, 0x06, 0x0a,
+	0x05, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x12, 0x4b, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e,
+	0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x22, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67,
+	0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65,
+	0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x63, 0x6f, 0x64, 0x65,
+	0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x61, 0x6e, 0x69, 0x66,
+	0x65, 0x73, 0x74, 0x12, 0x5a, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
+	0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x12, 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e,
+	0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76,
+	0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
+	0x1a, 0x1d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76,
+	0x32, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x12,
+	0x56, 0x0a, 0x0b, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x22,
+	0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e,
+	0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65,
+	0x73, 0x74, 0x1a, 0x23, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74,
+	0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52,
+	0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x54, 0x0a, 0x0f, 0x55, 0x70, 0x64, 0x61, 0x74,
+	0x65, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x12, 0x26, 0x2e, 0x63, 0x6f, 0x64,
+	0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61,
+	0x74, 0x65, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
+	0x73, 0x74, 0x1a, 0x19, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74,
+	0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x12, 0x72, 0x0a,
+	0x15, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48,
+	0x65, 0x61, 0x6c, 0x74, 0x68, 0x73, 0x12, 0x2b, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61,
+	0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64,
+	0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75,
+	0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e,
 	0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65,
-	0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
-	0x1a, 0x2c, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76,
-	0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70,
-	0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4e,
-	0x0a, 0x0d, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x12,
-	0x24, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32,
-	0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x52, 0x65,
-	0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67,
-	0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x12, 0x6e,
-	0x0a, 0x13, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74,
-	0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x2a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67,
-	0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61,
-	0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
-	0x74, 0x1a, 0x2b, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e,
-	0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65,
-	0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x62,
-	0x0a, 0x0f, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67,
-	0x73, 0x12, 0x26, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e,
-	0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f,
-	0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65,
-	0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68,
-	0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
-	0x73, 0x65, 0x42, 0x27, 0x5a, 0x25, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
-	0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f,
-	0x61, 0x67, 0x65, 0x6e, 0x74, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f,
-	0x74, 0x6f, 0x33,
+	0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
+	0x65, 0x12, 0x4e, 0x0a, 0x0d, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74,
+	0x75, 0x70, 0x12, 0x24, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74,
+	0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75,
+	0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72,
+	0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75,
+	0x70, 0x12, 0x6e, 0x0a, 0x13, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65,
+	0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x2a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72,
+	0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55,
+	0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71,
+	0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65,
+	0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74,
+	0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
+	0x65, 0x12, 0x62, 0x0a, 0x0f, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65,
+	0x4c, 0x6f, 0x67, 0x73, 0x12, 0x26, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65,
+	0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74,
+	0x65, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x63,
+	0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61,
+	0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73,
+	0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x77, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x4e, 0x6f, 0x74, 0x69,
+	0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x12,
+	0x2d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32,
+	0x2e, 0x47, 0x65, 0x74, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e,
+	0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e,
+	0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e,
+	0x47, 0x65, 0x74, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42,
+	0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x27,
+	0x5a, 0x25, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64,
+	0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x61, 0x67, 0x65, 0x6e,
+	0x74, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
@@ -2667,7 +2838,7 @@ func file_agent_proto_agent_proto_rawDescGZIP() []byte {
 }
 
 var file_agent_proto_agent_proto_enumTypes = make([]protoimpl.EnumInfo, 7)
-var file_agent_proto_agent_proto_msgTypes = make([]protoimpl.MessageInfo, 30)
+var file_agent_proto_agent_proto_msgTypes = make([]protoimpl.MessageInfo, 33)
 var file_agent_proto_agent_proto_goTypes = []interface{}{
 	(AppHealth)(0),                             // 0: coder.agent.v2.AppHealth
 	(WorkspaceApp_SharingLevel)(0),             // 1: coder.agent.v2.WorkspaceApp.SharingLevel
@@ -2698,73 +2869,79 @@ var file_agent_proto_agent_proto_goTypes = []interface{}{
 	(*Log)(nil),                                // 26: coder.agent.v2.Log
 	(*BatchCreateLogsRequest)(nil),             // 27: coder.agent.v2.BatchCreateLogsRequest
 	(*BatchCreateLogsResponse)(nil),            // 28: coder.agent.v2.BatchCreateLogsResponse
-	(*WorkspaceApp_Healthcheck)(nil),           // 29: coder.agent.v2.WorkspaceApp.Healthcheck
-	(*WorkspaceAgentMetadata_Result)(nil),      // 30: coder.agent.v2.WorkspaceAgentMetadata.Result
-	(*WorkspaceAgentMetadata_Description)(nil), // 31: coder.agent.v2.WorkspaceAgentMetadata.Description
-	nil,                        // 32: coder.agent.v2.Manifest.EnvironmentVariablesEntry
-	nil,                        // 33: coder.agent.v2.Stats.ConnectionsByProtoEntry
-	(*Stats_Metric)(nil),       // 34: coder.agent.v2.Stats.Metric
-	(*Stats_Metric_Label)(nil), // 35: coder.agent.v2.Stats.Metric.Label
-	(*BatchUpdateAppHealthRequest_HealthUpdate)(nil), // 36: coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate
-	(*durationpb.Duration)(nil),                      // 37: google.protobuf.Duration
-	(*proto.DERPMap)(nil),                            // 38: coder.tailnet.v2.DERPMap
-	(*timestamppb.Timestamp)(nil),                    // 39: google.protobuf.Timestamp
+	(*GetNotificationBannersRequest)(nil),      // 29: coder.agent.v2.GetNotificationBannersRequest
+	(*GetNotificationBannersResponse)(nil),     // 30: coder.agent.v2.GetNotificationBannersResponse
+	(*BannerConfig)(nil),                       // 31: coder.agent.v2.BannerConfig
+	(*WorkspaceApp_Healthcheck)(nil),           // 32: coder.agent.v2.WorkspaceApp.Healthcheck
+	(*WorkspaceAgentMetadata_Result)(nil),      // 33: coder.agent.v2.WorkspaceAgentMetadata.Result
+	(*WorkspaceAgentMetadata_Description)(nil), // 34: coder.agent.v2.WorkspaceAgentMetadata.Description
+	nil,                        // 35: coder.agent.v2.Manifest.EnvironmentVariablesEntry
+	nil,                        // 36: coder.agent.v2.Stats.ConnectionsByProtoEntry
+	(*Stats_Metric)(nil),       // 37: coder.agent.v2.Stats.Metric
+	(*Stats_Metric_Label)(nil), // 38: coder.agent.v2.Stats.Metric.Label
+	(*BatchUpdateAppHealthRequest_HealthUpdate)(nil), // 39: coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate
+	(*durationpb.Duration)(nil),                      // 40: google.protobuf.Duration
+	(*proto.DERPMap)(nil),                            // 41: coder.tailnet.v2.DERPMap
+	(*timestamppb.Timestamp)(nil),                    // 42: google.protobuf.Timestamp
 }
 var file_agent_proto_agent_proto_depIdxs = []int32{
 	1,  // 0: coder.agent.v2.WorkspaceApp.sharing_level:type_name -> coder.agent.v2.WorkspaceApp.SharingLevel
-	29, // 1: coder.agent.v2.WorkspaceApp.healthcheck:type_name -> coder.agent.v2.WorkspaceApp.Healthcheck
+	32, // 1: coder.agent.v2.WorkspaceApp.healthcheck:type_name -> coder.agent.v2.WorkspaceApp.Healthcheck
 	2,  // 2: coder.agent.v2.WorkspaceApp.health:type_name -> coder.agent.v2.WorkspaceApp.Health
-	37, // 3: coder.agent.v2.WorkspaceAgentScript.timeout:type_name -> google.protobuf.Duration
-	30, // 4: coder.agent.v2.WorkspaceAgentMetadata.result:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Result
-	31, // 5: coder.agent.v2.WorkspaceAgentMetadata.description:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Description
-	32, // 6: coder.agent.v2.Manifest.environment_variables:type_name -> coder.agent.v2.Manifest.EnvironmentVariablesEntry
-	38, // 7: coder.agent.v2.Manifest.derp_map:type_name -> coder.tailnet.v2.DERPMap
+	40, // 3: coder.agent.v2.WorkspaceAgentScript.timeout:type_name -> google.protobuf.Duration
+	33, // 4: coder.agent.v2.WorkspaceAgentMetadata.result:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Result
+	34, // 5: coder.agent.v2.WorkspaceAgentMetadata.description:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Description
+	35, // 6: coder.agent.v2.Manifest.environment_variables:type_name -> coder.agent.v2.Manifest.EnvironmentVariablesEntry
+	41, // 7: coder.agent.v2.Manifest.derp_map:type_name -> coder.tailnet.v2.DERPMap
 	8,  // 8: coder.agent.v2.Manifest.scripts:type_name -> coder.agent.v2.WorkspaceAgentScript
 	7,  // 9: coder.agent.v2.Manifest.apps:type_name -> coder.agent.v2.WorkspaceApp
-	31, // 10: coder.agent.v2.Manifest.metadata:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Description
-	33, // 11: coder.agent.v2.Stats.connections_by_proto:type_name -> coder.agent.v2.Stats.ConnectionsByProtoEntry
-	34, // 12: coder.agent.v2.Stats.metrics:type_name -> coder.agent.v2.Stats.Metric
+	34, // 10: coder.agent.v2.Manifest.metadata:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Description
+	36, // 11: coder.agent.v2.Stats.connections_by_proto:type_name -> coder.agent.v2.Stats.ConnectionsByProtoEntry
+	37, // 12: coder.agent.v2.Stats.metrics:type_name -> coder.agent.v2.Stats.Metric
 	14, // 13: coder.agent.v2.UpdateStatsRequest.stats:type_name -> coder.agent.v2.Stats
-	37, // 14: coder.agent.v2.UpdateStatsResponse.report_interval:type_name -> google.protobuf.Duration
+	40, // 14: coder.agent.v2.UpdateStatsResponse.report_interval:type_name -> google.protobuf.Duration
 	4,  // 15: coder.agent.v2.Lifecycle.state:type_name -> coder.agent.v2.Lifecycle.State
-	39, // 16: coder.agent.v2.Lifecycle.changed_at:type_name -> google.protobuf.Timestamp
+	42, // 16: coder.agent.v2.Lifecycle.changed_at:type_name -> google.protobuf.Timestamp
 	17, // 17: coder.agent.v2.UpdateLifecycleRequest.lifecycle:type_name -> coder.agent.v2.Lifecycle
-	36, // 18: coder.agent.v2.BatchUpdateAppHealthRequest.updates:type_name -> coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate
+	39, // 18: coder.agent.v2.BatchUpdateAppHealthRequest.updates:type_name -> coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate
 	5,  // 19: coder.agent.v2.Startup.subsystems:type_name -> coder.agent.v2.Startup.Subsystem
 	21, // 20: coder.agent.v2.UpdateStartupRequest.startup:type_name -> coder.agent.v2.Startup
-	30, // 21: coder.agent.v2.Metadata.result:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Result
+	33, // 21: coder.agent.v2.Metadata.result:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Result
 	23, // 22: coder.agent.v2.BatchUpdateMetadataRequest.metadata:type_name -> coder.agent.v2.Metadata
-	39, // 23: coder.agent.v2.Log.created_at:type_name -> google.protobuf.Timestamp
+	42, // 23: coder.agent.v2.Log.created_at:type_name -> google.protobuf.Timestamp
 	6,  // 24: coder.agent.v2.Log.level:type_name -> coder.agent.v2.Log.Level
 	26, // 25: coder.agent.v2.BatchCreateLogsRequest.logs:type_name -> coder.agent.v2.Log
-	37, // 26: coder.agent.v2.WorkspaceApp.Healthcheck.interval:type_name -> google.protobuf.Duration
-	39, // 27: coder.agent.v2.WorkspaceAgentMetadata.Result.collected_at:type_name -> google.protobuf.Timestamp
-	37, // 28: coder.agent.v2.WorkspaceAgentMetadata.Description.interval:type_name -> google.protobuf.Duration
-	37, // 29: coder.agent.v2.WorkspaceAgentMetadata.Description.timeout:type_name -> google.protobuf.Duration
-	3,  // 30: coder.agent.v2.Stats.Metric.type:type_name -> coder.agent.v2.Stats.Metric.Type
-	35, // 31: coder.agent.v2.Stats.Metric.labels:type_name -> coder.agent.v2.Stats.Metric.Label
-	0,  // 32: coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate.health:type_name -> coder.agent.v2.AppHealth
-	11, // 33: coder.agent.v2.Agent.GetManifest:input_type -> coder.agent.v2.GetManifestRequest
-	13, // 34: coder.agent.v2.Agent.GetServiceBanner:input_type -> coder.agent.v2.GetServiceBannerRequest
-	15, // 35: coder.agent.v2.Agent.UpdateStats:input_type -> coder.agent.v2.UpdateStatsRequest
-	18, // 36: coder.agent.v2.Agent.UpdateLifecycle:input_type -> coder.agent.v2.UpdateLifecycleRequest
-	19, // 37: coder.agent.v2.Agent.BatchUpdateAppHealths:input_type -> coder.agent.v2.BatchUpdateAppHealthRequest
-	22, // 38: coder.agent.v2.Agent.UpdateStartup:input_type -> coder.agent.v2.UpdateStartupRequest
-	24, // 39: coder.agent.v2.Agent.BatchUpdateMetadata:input_type -> coder.agent.v2.BatchUpdateMetadataRequest
-	27, // 40: coder.agent.v2.Agent.BatchCreateLogs:input_type -> coder.agent.v2.BatchCreateLogsRequest
-	10, // 41: coder.agent.v2.Agent.GetManifest:output_type -> coder.agent.v2.Manifest
-	12, // 42: coder.agent.v2.Agent.GetServiceBanner:output_type -> coder.agent.v2.ServiceBanner
-	16, // 43: coder.agent.v2.Agent.UpdateStats:output_type -> coder.agent.v2.UpdateStatsResponse
-	17, // 44: coder.agent.v2.Agent.UpdateLifecycle:output_type -> coder.agent.v2.Lifecycle
-	20, // 45: coder.agent.v2.Agent.BatchUpdateAppHealths:output_type -> coder.agent.v2.BatchUpdateAppHealthResponse
-	21, // 46: coder.agent.v2.Agent.UpdateStartup:output_type -> coder.agent.v2.Startup
-	25, // 47: coder.agent.v2.Agent.BatchUpdateMetadata:output_type -> coder.agent.v2.BatchUpdateMetadataResponse
-	28, // 48: coder.agent.v2.Agent.BatchCreateLogs:output_type -> coder.agent.v2.BatchCreateLogsResponse
-	41, // [41:49] is the sub-list for method output_type
-	33, // [33:41] is the sub-list for method input_type
-	33, // [33:33] is the sub-list for extension type_name
-	33, // [33:33] is the sub-list for extension extendee
-	0,  // [0:33] is the sub-list for field type_name
+	31, // 26: coder.agent.v2.GetNotificationBannersResponse.notification_banners:type_name -> coder.agent.v2.BannerConfig
+	40, // 27: coder.agent.v2.WorkspaceApp.Healthcheck.interval:type_name -> google.protobuf.Duration
+	42, // 28: coder.agent.v2.WorkspaceAgentMetadata.Result.collected_at:type_name -> google.protobuf.Timestamp
+	40, // 29: coder.agent.v2.WorkspaceAgentMetadata.Description.interval:type_name -> google.protobuf.Duration
+	40, // 30: coder.agent.v2.WorkspaceAgentMetadata.Description.timeout:type_name -> google.protobuf.Duration
+	3,  // 31: coder.agent.v2.Stats.Metric.type:type_name -> coder.agent.v2.Stats.Metric.Type
+	38, // 32: coder.agent.v2.Stats.Metric.labels:type_name -> coder.agent.v2.Stats.Metric.Label
+	0,  // 33: coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate.health:type_name -> coder.agent.v2.AppHealth
+	11, // 34: coder.agent.v2.Agent.GetManifest:input_type -> coder.agent.v2.GetManifestRequest
+	13, // 35: coder.agent.v2.Agent.GetServiceBanner:input_type -> coder.agent.v2.GetServiceBannerRequest
+	15, // 36: coder.agent.v2.Agent.UpdateStats:input_type -> coder.agent.v2.UpdateStatsRequest
+	18, // 37: coder.agent.v2.Agent.UpdateLifecycle:input_type -> coder.agent.v2.UpdateLifecycleRequest
+	19, // 38: coder.agent.v2.Agent.BatchUpdateAppHealths:input_type -> coder.agent.v2.BatchUpdateAppHealthRequest
+	22, // 39: coder.agent.v2.Agent.UpdateStartup:input_type -> coder.agent.v2.UpdateStartupRequest
+	24, // 40: coder.agent.v2.Agent.BatchUpdateMetadata:input_type -> coder.agent.v2.BatchUpdateMetadataRequest
+	27, // 41: coder.agent.v2.Agent.BatchCreateLogs:input_type -> coder.agent.v2.BatchCreateLogsRequest
+	29, // 42: coder.agent.v2.Agent.GetNotificationBanners:input_type -> coder.agent.v2.GetNotificationBannersRequest
+	10, // 43: coder.agent.v2.Agent.GetManifest:output_type -> coder.agent.v2.Manifest
+	12, // 44: coder.agent.v2.Agent.GetServiceBanner:output_type -> coder.agent.v2.ServiceBanner
+	16, // 45: coder.agent.v2.Agent.UpdateStats:output_type -> coder.agent.v2.UpdateStatsResponse
+	17, // 46: coder.agent.v2.Agent.UpdateLifecycle:output_type -> coder.agent.v2.Lifecycle
+	20, // 47: coder.agent.v2.Agent.BatchUpdateAppHealths:output_type -> coder.agent.v2.BatchUpdateAppHealthResponse
+	21, // 48: coder.agent.v2.Agent.UpdateStartup:output_type -> coder.agent.v2.Startup
+	25, // 49: coder.agent.v2.Agent.BatchUpdateMetadata:output_type -> coder.agent.v2.BatchUpdateMetadataResponse
+	28, // 50: coder.agent.v2.Agent.BatchCreateLogs:output_type -> coder.agent.v2.BatchCreateLogsResponse
+	30, // 51: coder.agent.v2.Agent.GetNotificationBanners:output_type -> coder.agent.v2.GetNotificationBannersResponse
+	43, // [43:52] is the sub-list for method output_type
+	34, // [34:43] is the sub-list for method input_type
+	34, // [34:34] is the sub-list for extension type_name
+	34, // [34:34] is the sub-list for extension extendee
+	0,  // [0:34] is the sub-list for field type_name
 }
 
 func init() { file_agent_proto_agent_proto_init() }
@@ -3038,7 +3215,7 @@ func file_agent_proto_agent_proto_init() {
 			}
 		}
 		file_agent_proto_agent_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*WorkspaceApp_Healthcheck); i {
+			switch v := v.(*GetNotificationBannersRequest); i {
 			case 0:
 				return &v.state
 			case 1:
@@ -3050,7 +3227,7 @@ func file_agent_proto_agent_proto_init() {
 			}
 		}
 		file_agent_proto_agent_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*WorkspaceAgentMetadata_Result); i {
+			switch v := v.(*GetNotificationBannersResponse); i {
 			case 0:
 				return &v.state
 			case 1:
@@ -3062,7 +3239,31 @@ func file_agent_proto_agent_proto_init() {
 			}
 		}
 		file_agent_proto_agent_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*WorkspaceAgentMetadata_Description); i {
+			switch v := v.(*BannerConfig); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_agent_proto_agent_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*WorkspaceApp_Healthcheck); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_agent_proto_agent_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*WorkspaceAgentMetadata_Result); i {
 			case 0:
 				return &v.state
 			case 1:
@@ -3074,6 +3275,18 @@ func file_agent_proto_agent_proto_init() {
 			}
 		}
 		file_agent_proto_agent_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*WorkspaceAgentMetadata_Description); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_agent_proto_agent_proto_msgTypes[30].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*Stats_Metric); i {
 			case 0:
 				return &v.state
@@ -3085,7 +3298,7 @@ func file_agent_proto_agent_proto_init() {
 				return nil
 			}
 		}
-		file_agent_proto_agent_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} {
+		file_agent_proto_agent_proto_msgTypes[31].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*Stats_Metric_Label); i {
 			case 0:
 				return &v.state
@@ -3097,7 +3310,7 @@ func file_agent_proto_agent_proto_init() {
 				return nil
 			}
 		}
-		file_agent_proto_agent_proto_msgTypes[29].Exporter = func(v interface{}, i int) interface{} {
+		file_agent_proto_agent_proto_msgTypes[32].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*BatchUpdateAppHealthRequest_HealthUpdate); i {
 			case 0:
 				return &v.state
@@ -3116,7 +3329,7 @@ func file_agent_proto_agent_proto_init() {
 			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
 			RawDescriptor: file_agent_proto_agent_proto_rawDesc,
 			NumEnums:      7,
-			NumMessages:   30,
+			NumMessages:   33,
 			NumExtensions: 0,
 			NumServices:   1,
 		},
diff --git a/agent/proto/agent.proto b/agent/proto/agent.proto
index f09c836446a04..8432fe8ef7f2b 100644
--- a/agent/proto/agent.proto
+++ b/agent/proto/agent.proto
@@ -251,6 +251,18 @@ message BatchCreateLogsResponse {
 	bool log_limit_exceeded = 1;
 }
 
+message GetNotificationBannersRequest {}
+
+message GetNotificationBannersResponse {
+	repeated BannerConfig notification_banners = 1;
+}
+
+message BannerConfig {
+	bool enabled = 1;
+	string message = 2;
+	string background_color = 3;
+}
+
 service Agent {
 	rpc GetManifest(GetManifestRequest) returns (Manifest);
 	rpc GetServiceBanner(GetServiceBannerRequest) returns (ServiceBanner);
@@ -260,4 +272,5 @@ service Agent {
 	rpc UpdateStartup(UpdateStartupRequest) returns (Startup);
 	rpc BatchUpdateMetadata(BatchUpdateMetadataRequest) returns (BatchUpdateMetadataResponse);
 	rpc BatchCreateLogs(BatchCreateLogsRequest) returns (BatchCreateLogsResponse);
+	rpc GetNotificationBanners(GetNotificationBannersRequest) returns (GetNotificationBannersResponse);
 }
diff --git a/agent/proto/agent_drpc.pb.go b/agent/proto/agent_drpc.pb.go
index 4bbf980522dd1..0003a1fa4568a 100644
--- a/agent/proto/agent_drpc.pb.go
+++ b/agent/proto/agent_drpc.pb.go
@@ -46,6 +46,7 @@ type DRPCAgentClient interface {
 	UpdateStartup(ctx context.Context, in *UpdateStartupRequest) (*Startup, error)
 	BatchUpdateMetadata(ctx context.Context, in *BatchUpdateMetadataRequest) (*BatchUpdateMetadataResponse, error)
 	BatchCreateLogs(ctx context.Context, in *BatchCreateLogsRequest) (*BatchCreateLogsResponse, error)
+	GetNotificationBanners(ctx context.Context, in *GetNotificationBannersRequest) (*GetNotificationBannersResponse, error)
 }
 
 type drpcAgentClient struct {
@@ -130,6 +131,15 @@ func (c *drpcAgentClient) BatchCreateLogs(ctx context.Context, in *BatchCreateLo
 	return out, nil
 }
 
+func (c *drpcAgentClient) GetNotificationBanners(ctx context.Context, in *GetNotificationBannersRequest) (*GetNotificationBannersResponse, error) {
+	out := new(GetNotificationBannersResponse)
+	err := c.cc.Invoke(ctx, "/coder.agent.v2.Agent/GetNotificationBanners", drpcEncoding_File_agent_proto_agent_proto{}, in, out)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
 type DRPCAgentServer interface {
 	GetManifest(context.Context, *GetManifestRequest) (*Manifest, error)
 	GetServiceBanner(context.Context, *GetServiceBannerRequest) (*ServiceBanner, error)
@@ -139,6 +149,7 @@ type DRPCAgentServer interface {
 	UpdateStartup(context.Context, *UpdateStartupRequest) (*Startup, error)
 	BatchUpdateMetadata(context.Context, *BatchUpdateMetadataRequest) (*BatchUpdateMetadataResponse, error)
 	BatchCreateLogs(context.Context, *BatchCreateLogsRequest) (*BatchCreateLogsResponse, error)
+	GetNotificationBanners(context.Context, *GetNotificationBannersRequest) (*GetNotificationBannersResponse, error)
 }
 
 type DRPCAgentUnimplementedServer struct{}
@@ -175,9 +186,13 @@ func (s *DRPCAgentUnimplementedServer) BatchCreateLogs(context.Context, *BatchCr
 	return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented)
 }
 
+func (s *DRPCAgentUnimplementedServer) GetNotificationBanners(context.Context, *GetNotificationBannersRequest) (*GetNotificationBannersResponse, error) {
+	return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented)
+}
+
 type DRPCAgentDescription struct{}
 
-func (DRPCAgentDescription) NumMethods() int { return 8 }
+func (DRPCAgentDescription) NumMethods() int { return 9 }
 
 func (DRPCAgentDescription) Method(n int) (string, drpc.Encoding, drpc.Receiver, interface{}, bool) {
 	switch n {
@@ -253,6 +268,15 @@ func (DRPCAgentDescription) Method(n int) (string, drpc.Encoding, drpc.Receiver,
 						in1.(*BatchCreateLogsRequest),
 					)
 			}, DRPCAgentServer.BatchCreateLogs, true
+	case 8:
+		return "/coder.agent.v2.Agent/GetNotificationBanners", drpcEncoding_File_agent_proto_agent_proto{},
+			func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) {
+				return srv.(DRPCAgentServer).
+					GetNotificationBanners(
+						ctx,
+						in1.(*GetNotificationBannersRequest),
+					)
+			}, DRPCAgentServer.GetNotificationBanners, true
 	default:
 		return "", nil, nil, nil, false
 	}
@@ -389,3 +413,19 @@ func (x *drpcAgent_BatchCreateLogsStream) SendAndClose(m *BatchCreateLogsRespons
 	}
 	return x.CloseSend()
 }
+
+type DRPCAgent_GetNotificationBannersStream interface {
+	drpc.Stream
+	SendAndClose(*GetNotificationBannersResponse) error
+}
+
+type drpcAgent_GetNotificationBannersStream struct {
+	drpc.Stream
+}
+
+func (x *drpcAgent_GetNotificationBannersStream) SendAndClose(m *GetNotificationBannersResponse) error {
+	if err := x.MsgSend(m, drpcEncoding_File_agent_proto_agent_proto{}); err != nil {
+		return err
+	}
+	return x.CloseSend()
+}
diff --git a/coderd/agentapi/api.go b/coderd/agentapi/api.go
index acfe9145b2ad0..fa8563a141a45 100644
--- a/coderd/agentapi/api.go
+++ b/coderd/agentapi/api.go
@@ -35,7 +35,7 @@ import (
 type API struct {
 	opts Options
 	*ManifestAPI
-	*ServiceBannerAPI
+	*NotificationBannerAPI
 	*StatsAPI
 	*LifecycleAPI
 	*AppsAPI
@@ -107,7 +107,7 @@ func New(opts Options) *API {
 		},
 	}
 
-	api.ServiceBannerAPI = &ServiceBannerAPI{
+	api.NotificationBannerAPI = &NotificationBannerAPI{
 		appearanceFetcher: opts.AppearanceFetcher,
 	}
 
diff --git a/coderd/agentapi/notification_banners.go b/coderd/agentapi/notification_banners.go
new file mode 100644
index 0000000000000..ab4e7dda96741
--- /dev/null
+++ b/coderd/agentapi/notification_banners.go
@@ -0,0 +1,39 @@
+package agentapi
+
+import (
+	"context"
+	"sync/atomic"
+
+	"golang.org/x/xerrors"
+
+	"github.com/coder/coder/v2/agent/proto"
+	"github.com/coder/coder/v2/coderd/appearance"
+	"github.com/coder/coder/v2/codersdk/agentsdk"
+)
+
+type NotificationBannerAPI struct {
+	appearanceFetcher *atomic.Pointer[appearance.Fetcher]
+}
+
+// Deprecated: GetServiceBanner has been deprecated in favor of GetNotificationBanners.
+func (a *NotificationBannerAPI) GetServiceBanner(ctx context.Context, _ *proto.GetServiceBannerRequest) (*proto.ServiceBanner, error) {
+	cfg, err := (*a.appearanceFetcher.Load()).Fetch(ctx)
+	if err != nil {
+		return nil, xerrors.Errorf("fetch appearance: %w", err)
+	}
+	return agentsdk.ProtoFromServiceBanner(cfg.ServiceBanner), nil
+}
+
+func (a *NotificationBannerAPI) GetNotificationBanners(ctx context.Context, _ *proto.GetNotificationBannersRequest) (*proto.GetNotificationBannersResponse, error) {
+	cfg, err := (*a.appearanceFetcher.Load()).Fetch(ctx)
+	if err != nil {
+		return nil, xerrors.Errorf("fetch appearance: %w", err)
+	}
+	banners := make([]*proto.BannerConfig, 0, len(cfg.NotificationBanners))
+	for _, banner := range cfg.NotificationBanners {
+		banners = append(banners, agentsdk.ProtoFromBannerConfig(banner))
+	}
+	return &proto.GetNotificationBannersResponse{
+		NotificationBanners: banners,
+	}, nil
+}
diff --git a/coderd/agentapi/servicebanner_internal_test.go b/coderd/agentapi/notification_banners_internal_test.go
similarity index 57%
rename from coderd/agentapi/servicebanner_internal_test.go
rename to coderd/agentapi/notification_banners_internal_test.go
index 6098d7df5f3d9..87f4df2d21764 100644
--- a/coderd/agentapi/servicebanner_internal_test.go
+++ b/coderd/agentapi/notification_banners_internal_test.go
@@ -11,36 +11,30 @@ import (
 	agentproto "github.com/coder/coder/v2/agent/proto"
 	"github.com/coder/coder/v2/coderd/appearance"
 	"github.com/coder/coder/v2/codersdk"
+	"github.com/coder/coder/v2/codersdk/agentsdk"
 )
 
-func TestGetServiceBanner(t *testing.T) {
+func TestGetNotificationBanners(t *testing.T) {
 	t.Parallel()
 
 	t.Run("OK", func(t *testing.T) {
 		t.Parallel()
 
-		cfg := codersdk.ServiceBannerConfig{
+		cfg := []codersdk.BannerConfig{{
 			Enabled:         true,
-			Message:         "hello world",
-			BackgroundColor: "#000000",
-		}
+			Message:         "The beep-bop will be boop-beeped on Saturday at 12AM PST.",
+			BackgroundColor: "#00FF00",
+		}}
 
-		var ff appearance.Fetcher = fakeFetcher{cfg: codersdk.AppearanceConfig{ServiceBanner: cfg}}
+		var ff appearance.Fetcher = fakeFetcher{cfg: codersdk.AppearanceConfig{NotificationBanners: cfg}}
 		ptr := atomic.Pointer[appearance.Fetcher]{}
 		ptr.Store(&ff)
 
-		api := &ServiceBannerAPI{
-			appearanceFetcher: &ptr,
-		}
-
-		resp, err := api.GetServiceBanner(context.Background(), &agentproto.GetServiceBannerRequest{})
+		api := &NotificationBannerAPI{appearanceFetcher: &ptr}
+		resp, err := api.GetNotificationBanners(context.Background(), &agentproto.GetNotificationBannersRequest{})
 		require.NoError(t, err)
-
-		require.Equal(t, &agentproto.ServiceBanner{
-			Enabled:         cfg.Enabled,
-			Message:         cfg.Message,
-			BackgroundColor: cfg.BackgroundColor,
-		}, resp)
+		require.Len(t, resp.NotificationBanners, 1)
+		require.Equal(t, cfg[0], agentsdk.BannerConfigFromProto(resp.NotificationBanners[0]))
 	})
 
 	t.Run("FetchError", func(t *testing.T) {
@@ -51,11 +45,8 @@ func TestGetServiceBanner(t *testing.T) {
 		ptr := atomic.Pointer[appearance.Fetcher]{}
 		ptr.Store(&ff)
 
-		api := &ServiceBannerAPI{
-			appearanceFetcher: &ptr,
-		}
-
-		resp, err := api.GetServiceBanner(context.Background(), &agentproto.GetServiceBannerRequest{})
+		api := &NotificationBannerAPI{appearanceFetcher: &ptr}
+		resp, err := api.GetNotificationBanners(context.Background(), &agentproto.GetNotificationBannersRequest{})
 		require.Error(t, err)
 		require.ErrorIs(t, err, expectedErr)
 		require.Nil(t, resp)
diff --git a/coderd/agentapi/servicebanner.go b/coderd/agentapi/servicebanner.go
deleted file mode 100644
index 2e835003c79a4..0000000000000
--- a/coderd/agentapi/servicebanner.go
+++ /dev/null
@@ -1,24 +0,0 @@
-package agentapi
-
-import (
-	"context"
-	"sync/atomic"
-
-	"golang.org/x/xerrors"
-
-	"github.com/coder/coder/v2/agent/proto"
-	"github.com/coder/coder/v2/coderd/appearance"
-	"github.com/coder/coder/v2/codersdk/agentsdk"
-)
-
-type ServiceBannerAPI struct {
-	appearanceFetcher *atomic.Pointer[appearance.Fetcher]
-}
-
-func (a *ServiceBannerAPI) GetServiceBanner(ctx context.Context, _ *proto.GetServiceBannerRequest) (*proto.ServiceBanner, error) {
-	cfg, err := (*a.appearanceFetcher.Load()).Fetch(ctx)
-	if err != nil {
-		return nil, xerrors.Errorf("fetch appearance: %w", err)
-	}
-	return agentsdk.ProtoFromServiceBanner(cfg.ServiceBanner), nil
-}
diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go
index 9e746d2df6abf..3d14f4ec72726 100644
--- a/coderd/apidoc/docs.go
+++ b/coderd/apidoc/docs.go
@@ -8272,8 +8272,19 @@ const docTemplate = `{
                 "logo_url": {
                     "type": "string"
                 },
+                "notification_banners": {
+                    "type": "array",
+                    "items": {
+                        "$ref": "#/definitions/codersdk.BannerConfig"
+                    }
+                },
                 "service_banner": {
-                    "$ref": "#/definitions/codersdk.ServiceBannerConfig"
+                    "description": "Deprecated: ServiceBanner has been replaced by NotificationBanners.",
+                    "allOf": [
+                        {
+                            "$ref": "#/definitions/codersdk.BannerConfig"
+                        }
+                    ]
                 },
                 "support_links": {
                     "type": "array",
@@ -8530,6 +8541,20 @@ const docTemplate = `{
                 "AutomaticUpdatesNever"
             ]
         },
+        "codersdk.BannerConfig": {
+            "type": "object",
+            "properties": {
+                "background_color": {
+                    "type": "string"
+                },
+                "enabled": {
+                    "type": "boolean"
+                },
+                "message": {
+                    "type": "string"
+                }
+            }
+        },
         "codersdk.BuildInfoResponse": {
             "type": "object",
             "properties": {
@@ -11060,20 +11085,6 @@ const docTemplate = `{
                 }
             }
         },
-        "codersdk.ServiceBannerConfig": {
-            "type": "object",
-            "properties": {
-                "background_color": {
-                    "type": "string"
-                },
-                "enabled": {
-                    "type": "boolean"
-                },
-                "message": {
-                    "type": "string"
-                }
-            }
-        },
         "codersdk.SessionCountDeploymentStats": {
             "type": "object",
             "properties": {
@@ -11906,8 +11917,19 @@ const docTemplate = `{
                 "logo_url": {
                     "type": "string"
                 },
+                "notification_banners": {
+                    "type": "array",
+                    "items": {
+                        "$ref": "#/definitions/codersdk.BannerConfig"
+                    }
+                },
                 "service_banner": {
-                    "$ref": "#/definitions/codersdk.ServiceBannerConfig"
+                    "description": "Deprecated: ServiceBanner has been replaced by NotificationBanners.",
+                    "allOf": [
+                        {
+                            "$ref": "#/definitions/codersdk.BannerConfig"
+                        }
+                    ]
                 }
             }
         },
diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json
index d0e60d65aabfe..9f6a1833e995d 100644
--- a/coderd/apidoc/swagger.json
+++ b/coderd/apidoc/swagger.json
@@ -7341,8 +7341,19 @@
         "logo_url": {
           "type": "string"
         },
+        "notification_banners": {
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/codersdk.BannerConfig"
+          }
+        },
         "service_banner": {
-          "$ref": "#/definitions/codersdk.ServiceBannerConfig"
+          "description": "Deprecated: ServiceBanner has been replaced by NotificationBanners.",
+          "allOf": [
+            {
+              "$ref": "#/definitions/codersdk.BannerConfig"
+            }
+          ]
         },
         "support_links": {
           "type": "array",
@@ -7588,6 +7599,20 @@
       "enum": ["always", "never"],
       "x-enum-varnames": ["AutomaticUpdatesAlways", "AutomaticUpdatesNever"]
     },
+    "codersdk.BannerConfig": {
+      "type": "object",
+      "properties": {
+        "background_color": {
+          "type": "string"
+        },
+        "enabled": {
+          "type": "boolean"
+        },
+        "message": {
+          "type": "string"
+        }
+      }
+    },
     "codersdk.BuildInfoResponse": {
       "type": "object",
       "properties": {
@@ -9960,20 +9985,6 @@
         }
       }
     },
-    "codersdk.ServiceBannerConfig": {
-      "type": "object",
-      "properties": {
-        "background_color": {
-          "type": "string"
-        },
-        "enabled": {
-          "type": "boolean"
-        },
-        "message": {
-          "type": "string"
-        }
-      }
-    },
     "codersdk.SessionCountDeploymentStats": {
       "type": "object",
       "properties": {
@@ -10763,8 +10774,19 @@
         "logo_url": {
           "type": "string"
         },
+        "notification_banners": {
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/codersdk.BannerConfig"
+          }
+        },
         "service_banner": {
-          "$ref": "#/definitions/codersdk.ServiceBannerConfig"
+          "description": "Deprecated: ServiceBanner has been replaced by NotificationBanners.",
+          "allOf": [
+            {
+              "$ref": "#/definitions/codersdk.BannerConfig"
+            }
+          ]
         }
       }
     },
diff --git a/coderd/appearance/appearance.go b/coderd/appearance/appearance.go
index 1ac61dea21fe3..f9809036ec84b 100644
--- a/coderd/appearance/appearance.go
+++ b/coderd/appearance/appearance.go
@@ -32,7 +32,8 @@ type AGPLFetcher struct{}
 
 func (AGPLFetcher) Fetch(context.Context) (codersdk.AppearanceConfig, error) {
 	return codersdk.AppearanceConfig{
-		SupportLinks: DefaultSupportLinks,
+		NotificationBanners: []codersdk.BannerConfig{},
+		SupportLinks:        DefaultSupportLinks,
 	}, nil
 }
 
diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go
index a638b705a54f0..aaf623c7a70b5 100644
--- a/coderd/database/dbauthz/dbauthz.go
+++ b/coderd/database/dbauthz/dbauthz.go
@@ -1220,6 +1220,11 @@ func (q *querier) GetLogoURL(ctx context.Context) (string, error) {
 	return q.db.GetLogoURL(ctx)
 }
 
+func (q *querier) GetNotificationBanners(ctx context.Context) (string, error) {
+	// No authz checks
+	return q.db.GetNotificationBanners(ctx)
+}
+
 func (q *querier) GetOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) (database.OAuth2ProviderApp, error) {
 	if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceOAuth2ProviderApp); err != nil {
 		return database.OAuth2ProviderApp{}, err
@@ -1454,11 +1459,6 @@ func (q *querier) GetReplicasUpdatedAfter(ctx context.Context, updatedAt time.Ti
 	return q.db.GetReplicasUpdatedAfter(ctx, updatedAt)
 }
 
-func (q *querier) GetServiceBanner(ctx context.Context) (string, error) {
-	// No authz checks
-	return q.db.GetServiceBanner(ctx)
-}
-
 func (q *querier) GetTailnetAgents(ctx context.Context, id uuid.UUID) ([]database.TailnetAgent, error) {
 	if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceTailnetCoordinator); err != nil {
 		return nil, err
@@ -3364,6 +3364,13 @@ func (q *querier) UpsertLogoURL(ctx context.Context, value string) error {
 	return q.db.UpsertLogoURL(ctx, value)
 }
 
+func (q *querier) UpsertNotificationBanners(ctx context.Context, value string) error {
+	if err := q.authorizeContext(ctx, rbac.ActionCreate, rbac.ResourceDeploymentValues); err != nil {
+		return err
+	}
+	return q.db.UpsertNotificationBanners(ctx, value)
+}
+
 func (q *querier) UpsertOAuthSigningKey(ctx context.Context, value string) error {
 	if err := q.authorizeContext(ctx, rbac.ActionUpdate, rbac.ResourceSystem); err != nil {
 		return err
@@ -3382,13 +3389,6 @@ func (q *querier) UpsertProvisionerDaemon(ctx context.Context, arg database.Upse
 	return q.db.UpsertProvisionerDaemon(ctx, arg)
 }
 
-func (q *querier) UpsertServiceBanner(ctx context.Context, value string) error {
-	if err := q.authorizeContext(ctx, rbac.ActionCreate, rbac.ResourceDeploymentValues); err != nil {
-		return err
-	}
-	return q.db.UpsertServiceBanner(ctx, value)
-}
-
 func (q *querier) UpsertTailnetAgent(ctx context.Context, arg database.UpsertTailnetAgentParams) (database.TailnetAgent, error) {
 	if err := q.authorizeContext(ctx, rbac.ActionUpdate, rbac.ResourceTailnetCoordinator); err != nil {
 		return database.TailnetAgent{}, err
diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go
index 7be33d58c8dda..48435a0141c64 100644
--- a/coderd/database/dbauthz/dbauthz_test.go
+++ b/coderd/database/dbauthz/dbauthz_test.go
@@ -525,7 +525,7 @@ func (s *MethodTestSuite) TestLicense() {
 	s.Run("UpsertLogoURL", s.Subtest(func(db database.Store, check *expects) {
 		check.Args("value").Asserts(rbac.ResourceDeploymentValues, rbac.ActionCreate)
 	}))
-	s.Run("UpsertServiceBanner", s.Subtest(func(db database.Store, check *expects) {
+	s.Run("UpsertNotificationBanners", s.Subtest(func(db database.Store, check *expects) {
 		check.Args("value").Asserts(rbac.ResourceDeploymentValues, rbac.ActionCreate)
 	}))
 	s.Run("GetLicenseByID", s.Subtest(func(db database.Store, check *expects) {
@@ -556,8 +556,8 @@ func (s *MethodTestSuite) TestLicense() {
 		require.NoError(s.T(), err)
 		check.Args().Asserts().Returns("value")
 	}))
-	s.Run("GetServiceBanner", s.Subtest(func(db database.Store, check *expects) {
-		err := db.UpsertServiceBanner(context.Background(), "value")
+	s.Run("GetNotificationBanners", s.Subtest(func(db database.Store, check *expects) {
+		err := db.UpsertNotificationBanners(context.Background(), "value")
 		require.NoError(s.T(), err)
 		check.Args().Asserts().Returns("value")
 	}))
diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go
index fcc3140133c42..8a2ce25b34367 100644
--- a/coderd/database/dbmem/dbmem.go
+++ b/coderd/database/dbmem/dbmem.go
@@ -185,7 +185,7 @@ type data struct {
 	deploymentID            string
 	derpMeshKey             string
 	lastUpdateCheck         []byte
-	serviceBanner           []byte
+	notificationBanners     []byte
 	healthSettings          []byte
 	applicationName         string
 	logoURL                 string
@@ -2488,6 +2488,17 @@ func (q *FakeQuerier) GetLogoURL(_ context.Context) (string, error) {
 	return q.logoURL, nil
 }
 
+func (q *FakeQuerier) GetNotificationBanners(_ context.Context) (string, error) {
+	q.mutex.RLock()
+	defer q.mutex.RUnlock()
+
+	if q.notificationBanners == nil {
+		return "", sql.ErrNoRows
+	}
+
+	return string(q.notificationBanners), nil
+}
+
 func (q *FakeQuerier) GetOAuth2ProviderAppByID(_ context.Context, id uuid.UUID) (database.OAuth2ProviderApp, error) {
 	q.mutex.Lock()
 	defer q.mutex.Unlock()
@@ -3027,17 +3038,6 @@ func (q *FakeQuerier) GetReplicasUpdatedAfter(_ context.Context, updatedAt time.
 	return replicas, nil
 }
 
-func (q *FakeQuerier) GetServiceBanner(_ context.Context) (string, error) {
-	q.mutex.RLock()
-	defer q.mutex.RUnlock()
-
-	if q.serviceBanner == nil {
-		return "", sql.ErrNoRows
-	}
-
-	return string(q.serviceBanner), nil
-}
-
 func (*FakeQuerier) GetTailnetAgents(context.Context, uuid.UUID) ([]database.TailnetAgent, error) {
 	return nil, ErrUnimplemented
 }
@@ -8251,6 +8251,14 @@ func (q *FakeQuerier) UpsertLogoURL(_ context.Context, data string) error {
 	return nil
 }
 
+func (q *FakeQuerier) UpsertNotificationBanners(_ context.Context, data string) error {
+	q.mutex.RLock()
+	defer q.mutex.RUnlock()
+
+	q.notificationBanners = []byte(data)
+	return nil
+}
+
 func (q *FakeQuerier) UpsertOAuthSigningKey(_ context.Context, value string) error {
 	q.mutex.Lock()
 	defer q.mutex.Unlock()
@@ -8298,14 +8306,6 @@ func (q *FakeQuerier) UpsertProvisionerDaemon(_ context.Context, arg database.Up
 	return d, nil
 }
 
-func (q *FakeQuerier) UpsertServiceBanner(_ context.Context, data string) error {
-	q.mutex.RLock()
-	defer q.mutex.RUnlock()
-
-	q.serviceBanner = []byte(data)
-	return nil
-}
-
 func (*FakeQuerier) UpsertTailnetAgent(context.Context, database.UpsertTailnetAgentParams) (database.TailnetAgent, error) {
 	return database.TailnetAgent{}, ErrUnimplemented
 }
diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go
index 4cb81c1eded86..d92c60e8db09a 100644
--- a/coderd/database/dbmetrics/dbmetrics.go
+++ b/coderd/database/dbmetrics/dbmetrics.go
@@ -646,6 +646,13 @@ func (m metricsStore) GetLogoURL(ctx context.Context) (string, error) {
 	return url, err
 }
 
+func (m metricsStore) GetNotificationBanners(ctx context.Context) (string, error) {
+	start := time.Now()
+	r0, r1 := m.s.GetNotificationBanners(ctx)
+	m.queryLatencies.WithLabelValues("GetNotificationBanners").Observe(time.Since(start).Seconds())
+	return r0, r1
+}
+
 func (m metricsStore) GetOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) (database.OAuth2ProviderApp, error) {
 	start := time.Now()
 	r0, r1 := m.s.GetOAuth2ProviderAppByID(ctx, id)
@@ -849,13 +856,6 @@ func (m metricsStore) GetReplicasUpdatedAfter(ctx context.Context, updatedAt tim
 	return replicas, err
 }
 
-func (m metricsStore) GetServiceBanner(ctx context.Context) (string, error) {
-	start := time.Now()
-	banner, err := m.s.GetServiceBanner(ctx)
-	m.queryLatencies.WithLabelValues("GetServiceBanner").Observe(time.Since(start).Seconds())
-	return banner, err
-}
-
 func (m metricsStore) GetTailnetAgents(ctx context.Context, id uuid.UUID) ([]database.TailnetAgent, error) {
 	start := time.Now()
 	r0, r1 := m.s.GetTailnetAgents(ctx, id)
@@ -2186,6 +2186,13 @@ func (m metricsStore) UpsertLogoURL(ctx context.Context, value string) error {
 	return r0
 }
 
+func (m metricsStore) UpsertNotificationBanners(ctx context.Context, value string) error {
+	start := time.Now()
+	r0 := m.s.UpsertNotificationBanners(ctx, value)
+	m.queryLatencies.WithLabelValues("UpsertNotificationBanners").Observe(time.Since(start).Seconds())
+	return r0
+}
+
 func (m metricsStore) UpsertOAuthSigningKey(ctx context.Context, value string) error {
 	start := time.Now()
 	r0 := m.s.UpsertOAuthSigningKey(ctx, value)
@@ -2200,13 +2207,6 @@ func (m metricsStore) UpsertProvisionerDaemon(ctx context.Context, arg database.
 	return r0, r1
 }
 
-func (m metricsStore) UpsertServiceBanner(ctx context.Context, value string) error {
-	start := time.Now()
-	r0 := m.s.UpsertServiceBanner(ctx, value)
-	m.queryLatencies.WithLabelValues("UpsertServiceBanner").Observe(time.Since(start).Seconds())
-	return r0
-}
-
 func (m metricsStore) UpsertTailnetAgent(ctx context.Context, arg database.UpsertTailnetAgentParams) (database.TailnetAgent, error) {
 	start := time.Now()
 	r0, r1 := m.s.UpsertTailnetAgent(ctx, arg)
diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go
index 2bb62e8c92e84..e651c8301c933 100644
--- a/coderd/database/dbmock/dbmock.go
+++ b/coderd/database/dbmock/dbmock.go
@@ -1275,6 +1275,21 @@ func (mr *MockStoreMockRecorder) GetLogoURL(arg0 any) *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLogoURL", reflect.TypeOf((*MockStore)(nil).GetLogoURL), arg0)
 }
 
+// GetNotificationBanners mocks base method.
+func (m *MockStore) GetNotificationBanners(arg0 context.Context) (string, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetNotificationBanners", arg0)
+	ret0, _ := ret[0].(string)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetNotificationBanners indicates an expected call of GetNotificationBanners.
+func (mr *MockStoreMockRecorder) GetNotificationBanners(arg0 any) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotificationBanners", reflect.TypeOf((*MockStore)(nil).GetNotificationBanners), arg0)
+}
+
 // GetOAuth2ProviderAppByID mocks base method.
 func (m *MockStore) GetOAuth2ProviderAppByID(arg0 context.Context, arg1 uuid.UUID) (database.OAuth2ProviderApp, error) {
 	m.ctrl.T.Helper()
@@ -1710,21 +1725,6 @@ func (mr *MockStoreMockRecorder) GetReplicasUpdatedAfter(arg0, arg1 any) *gomock
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetReplicasUpdatedAfter", reflect.TypeOf((*MockStore)(nil).GetReplicasUpdatedAfter), arg0, arg1)
 }
 
-// GetServiceBanner mocks base method.
-func (m *MockStore) GetServiceBanner(arg0 context.Context) (string, error) {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "GetServiceBanner", arg0)
-	ret0, _ := ret[0].(string)
-	ret1, _ := ret[1].(error)
-	return ret0, ret1
-}
-
-// GetServiceBanner indicates an expected call of GetServiceBanner.
-func (mr *MockStoreMockRecorder) GetServiceBanner(arg0 any) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetServiceBanner", reflect.TypeOf((*MockStore)(nil).GetServiceBanner), arg0)
-}
-
 // GetTailnetAgents mocks base method.
 func (m *MockStore) GetTailnetAgents(arg0 context.Context, arg1 uuid.UUID) ([]database.TailnetAgent, error) {
 	m.ctrl.T.Helper()
@@ -4577,6 +4577,20 @@ func (mr *MockStoreMockRecorder) UpsertLogoURL(arg0, arg1 any) *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertLogoURL", reflect.TypeOf((*MockStore)(nil).UpsertLogoURL), arg0, arg1)
 }
 
+// UpsertNotificationBanners mocks base method.
+func (m *MockStore) UpsertNotificationBanners(arg0 context.Context, arg1 string) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "UpsertNotificationBanners", arg0, arg1)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// UpsertNotificationBanners indicates an expected call of UpsertNotificationBanners.
+func (mr *MockStoreMockRecorder) UpsertNotificationBanners(arg0, arg1 any) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertNotificationBanners", reflect.TypeOf((*MockStore)(nil).UpsertNotificationBanners), arg0, arg1)
+}
+
 // UpsertOAuthSigningKey mocks base method.
 func (m *MockStore) UpsertOAuthSigningKey(arg0 context.Context, arg1 string) error {
 	m.ctrl.T.Helper()
@@ -4606,20 +4620,6 @@ func (mr *MockStoreMockRecorder) UpsertProvisionerDaemon(arg0, arg1 any) *gomock
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertProvisionerDaemon", reflect.TypeOf((*MockStore)(nil).UpsertProvisionerDaemon), arg0, arg1)
 }
 
-// UpsertServiceBanner mocks base method.
-func (m *MockStore) UpsertServiceBanner(arg0 context.Context, arg1 string) error {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "UpsertServiceBanner", arg0, arg1)
-	ret0, _ := ret[0].(error)
-	return ret0
-}
-
-// UpsertServiceBanner indicates an expected call of UpsertServiceBanner.
-func (mr *MockStoreMockRecorder) UpsertServiceBanner(arg0, arg1 any) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertServiceBanner", reflect.TypeOf((*MockStore)(nil).UpsertServiceBanner), arg0, arg1)
-}
-
 // UpsertTailnetAgent mocks base method.
 func (m *MockStore) UpsertTailnetAgent(arg0 context.Context, arg1 database.UpsertTailnetAgentParams) (database.TailnetAgent, error) {
 	m.ctrl.T.Helper()
diff --git a/coderd/database/migrations/000208_notification_banners.down.sql b/coderd/database/migrations/000208_notification_banners.down.sql
new file mode 100644
index 0000000000000..30d149cb016b6
--- /dev/null
+++ b/coderd/database/migrations/000208_notification_banners.down.sql
@@ -0,0 +1 @@
+delete from site_configs where key = 'notification_banners';
diff --git a/coderd/database/migrations/000208_notification_banners.up.sql b/coderd/database/migrations/000208_notification_banners.up.sql
new file mode 100644
index 0000000000000..8f846b16dd509
--- /dev/null
+++ b/coderd/database/migrations/000208_notification_banners.up.sql
@@ -0,0 +1,4 @@
+update site_configs SET
+	key = 'notification_banners',
+	value = concat('[', value, ']')
+where key = 'service_banner';
diff --git a/coderd/database/querier.go b/coderd/database/querier.go
index 7d8f504cb50e7..405f86bf47688 100644
--- a/coderd/database/querier.go
+++ b/coderd/database/querier.go
@@ -135,6 +135,7 @@ type sqlcQuerier interface {
 	GetLicenseByID(ctx context.Context, id int32) (License, error)
 	GetLicenses(ctx context.Context) ([]License, error)
 	GetLogoURL(ctx context.Context) (string, error)
+	GetNotificationBanners(ctx context.Context) (string, error)
 	GetOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) (OAuth2ProviderApp, error)
 	GetOAuth2ProviderAppCodeByID(ctx context.Context, id uuid.UUID) (OAuth2ProviderAppCode, error)
 	GetOAuth2ProviderAppCodeByPrefix(ctx context.Context, secretPrefix []byte) (OAuth2ProviderAppCode, error)
@@ -164,7 +165,6 @@ type sqlcQuerier interface {
 	GetQuotaConsumedForUser(ctx context.Context, ownerID uuid.UUID) (int64, error)
 	GetReplicaByID(ctx context.Context, id uuid.UUID) (Replica, error)
 	GetReplicasUpdatedAfter(ctx context.Context, updatedAt time.Time) ([]Replica, error)
-	GetServiceBanner(ctx context.Context) (string, error)
 	GetTailnetAgents(ctx context.Context, id uuid.UUID) ([]TailnetAgent, error)
 	GetTailnetClientsForAgent(ctx context.Context, agentID uuid.UUID) ([]TailnetClient, error)
 	GetTailnetPeers(ctx context.Context, id uuid.UUID) ([]TailnetPeer, error)
@@ -421,9 +421,9 @@ type sqlcQuerier interface {
 	UpsertJFrogXrayScanByWorkspaceAndAgentID(ctx context.Context, arg UpsertJFrogXrayScanByWorkspaceAndAgentIDParams) error
 	UpsertLastUpdateCheck(ctx context.Context, value string) error
 	UpsertLogoURL(ctx context.Context, value string) error
+	UpsertNotificationBanners(ctx context.Context, value string) error
 	UpsertOAuthSigningKey(ctx context.Context, value string) error
 	UpsertProvisionerDaemon(ctx context.Context, arg UpsertProvisionerDaemonParams) (ProvisionerDaemon, error)
-	UpsertServiceBanner(ctx context.Context, value string) error
 	UpsertTailnetAgent(ctx context.Context, arg UpsertTailnetAgentParams) (TailnetAgent, error)
 	UpsertTailnetClient(ctx context.Context, arg UpsertTailnetClientParams) (TailnetClient, error)
 	UpsertTailnetClientSubscription(ctx context.Context, arg UpsertTailnetClientSubscriptionParams) error
diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go
index 41171a7473cab..e0fba2dad35bd 100644
--- a/coderd/database/queries.sql.go
+++ b/coderd/database/queries.sql.go
@@ -5615,23 +5615,23 @@ func (q *sqlQuerier) GetLogoURL(ctx context.Context) (string, error) {
 	return value, err
 }
 
-const getOAuthSigningKey = `-- name: GetOAuthSigningKey :one
-SELECT value FROM site_configs WHERE key = 'oauth_signing_key'
+const getNotificationBanners = `-- name: GetNotificationBanners :one
+SELECT value FROM site_configs WHERE key = 'notification_banners'
 `
 
-func (q *sqlQuerier) GetOAuthSigningKey(ctx context.Context) (string, error) {
-	row := q.db.QueryRowContext(ctx, getOAuthSigningKey)
+func (q *sqlQuerier) GetNotificationBanners(ctx context.Context) (string, error) {
+	row := q.db.QueryRowContext(ctx, getNotificationBanners)
 	var value string
 	err := row.Scan(&value)
 	return value, err
 }
 
-const getServiceBanner = `-- name: GetServiceBanner :one
-SELECT value FROM site_configs WHERE key = 'service_banner'
+const getOAuthSigningKey = `-- name: GetOAuthSigningKey :one
+SELECT value FROM site_configs WHERE key = 'oauth_signing_key'
 `
 
-func (q *sqlQuerier) GetServiceBanner(ctx context.Context) (string, error) {
-	row := q.db.QueryRowContext(ctx, getServiceBanner)
+func (q *sqlQuerier) GetOAuthSigningKey(ctx context.Context) (string, error) {
+	row := q.db.QueryRowContext(ctx, getOAuthSigningKey)
 	var value string
 	err := row.Scan(&value)
 	return value, err
@@ -5728,23 +5728,23 @@ func (q *sqlQuerier) UpsertLogoURL(ctx context.Context, value string) error {
 	return err
 }
 
-const upsertOAuthSigningKey = `-- name: UpsertOAuthSigningKey :exec
-INSERT INTO site_configs (key, value) VALUES ('oauth_signing_key', $1)
-ON CONFLICT (key) DO UPDATE set value = $1 WHERE site_configs.key = 'oauth_signing_key'
+const upsertNotificationBanners = `-- name: UpsertNotificationBanners :exec
+INSERT INTO site_configs (key, value) VALUES ('notification_banners', $1)
+ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'notification_banners'
 `
 
-func (q *sqlQuerier) UpsertOAuthSigningKey(ctx context.Context, value string) error {
-	_, err := q.db.ExecContext(ctx, upsertOAuthSigningKey, value)
+func (q *sqlQuerier) UpsertNotificationBanners(ctx context.Context, value string) error {
+	_, err := q.db.ExecContext(ctx, upsertNotificationBanners, value)
 	return err
 }
 
-const upsertServiceBanner = `-- name: UpsertServiceBanner :exec
-INSERT INTO site_configs (key, value) VALUES ('service_banner', $1)
-ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'service_banner'
+const upsertOAuthSigningKey = `-- name: UpsertOAuthSigningKey :exec
+INSERT INTO site_configs (key, value) VALUES ('oauth_signing_key', $1)
+ON CONFLICT (key) DO UPDATE set value = $1 WHERE site_configs.key = 'oauth_signing_key'
 `
 
-func (q *sqlQuerier) UpsertServiceBanner(ctx context.Context, value string) error {
-	_, err := q.db.ExecContext(ctx, upsertServiceBanner, value)
+func (q *sqlQuerier) UpsertOAuthSigningKey(ctx context.Context, value string) error {
+	_, err := q.db.ExecContext(ctx, upsertOAuthSigningKey, value)
 	return err
 }
 
diff --git a/coderd/database/queries/siteconfig.sql b/coderd/database/queries/siteconfig.sql
index a432b71e3a91d..b827c6e19e959 100644
--- a/coderd/database/queries/siteconfig.sql
+++ b/coderd/database/queries/siteconfig.sql
@@ -36,12 +36,12 @@ ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'last_update
 -- name: GetLastUpdateCheck :one
 SELECT value FROM site_configs WHERE key = 'last_update_check';
 
--- name: UpsertServiceBanner :exec
-INSERT INTO site_configs (key, value) VALUES ('service_banner', $1)
-ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'service_banner';
+-- name: UpsertNotificationBanners :exec
+INSERT INTO site_configs (key, value) VALUES ('notification_banners', $1)
+ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'notification_banners';
 
--- name: GetServiceBanner :one
-SELECT value FROM site_configs WHERE key = 'service_banner';
+-- name: GetNotificationBanners :one
+SELECT value FROM site_configs WHERE key = 'notification_banners';
 
 -- name: UpsertLogoURL :exec
 INSERT INTO site_configs (key, value) VALUES ('logo_url', $1)
diff --git a/codersdk/agentsdk/convert.go b/codersdk/agentsdk/convert.go
index 8671d9e0b51e3..adfabd1510768 100644
--- a/codersdk/agentsdk/convert.go
+++ b/codersdk/agentsdk/convert.go
@@ -277,15 +277,15 @@ func ProtoFromApp(a codersdk.WorkspaceApp) (*proto.WorkspaceApp, error) {
 	}, nil
 }
 
-func ServiceBannerFromProto(sbp *proto.ServiceBanner) codersdk.ServiceBannerConfig {
-	return codersdk.ServiceBannerConfig{
+func ServiceBannerFromProto(sbp *proto.ServiceBanner) codersdk.BannerConfig {
+	return codersdk.BannerConfig{
 		Enabled:         sbp.GetEnabled(),
 		Message:         sbp.GetMessage(),
 		BackgroundColor: sbp.GetBackgroundColor(),
 	}
 }
 
-func ProtoFromServiceBanner(sb codersdk.ServiceBannerConfig) *proto.ServiceBanner {
+func ProtoFromServiceBanner(sb codersdk.BannerConfig) *proto.ServiceBanner {
 	return &proto.ServiceBanner{
 		Enabled:         sb.Enabled,
 		Message:         sb.Message,
@@ -293,6 +293,22 @@ func ProtoFromServiceBanner(sb codersdk.ServiceBannerConfig) *proto.ServiceBanne
 	}
 }
 
+func BannerConfigFromProto(sbp *proto.BannerConfig) codersdk.BannerConfig {
+	return codersdk.BannerConfig{
+		Enabled:         sbp.GetEnabled(),
+		Message:         sbp.GetMessage(),
+		BackgroundColor: sbp.GetBackgroundColor(),
+	}
+}
+
+func ProtoFromBannerConfig(sb codersdk.BannerConfig) *proto.BannerConfig {
+	return &proto.BannerConfig{
+		Enabled:         sb.Enabled,
+		Message:         sb.Message,
+		BackgroundColor: sb.BackgroundColor,
+	}
+}
+
 func ProtoFromSubsystems(ss []codersdk.AgentSubsystem) ([]proto.Startup_Subsystem, error) {
 	ret := make([]proto.Startup_Subsystem, len(ss))
 	for i, s := range ss {
diff --git a/codersdk/deployment.go b/codersdk/deployment.go
index 2aa675727b72b..087ad660cbc68 100644
--- a/codersdk/deployment.go
+++ b/codersdk/deployment.go
@@ -2100,19 +2100,26 @@ func (c *Client) DeploymentStats(ctx context.Context) (DeploymentStats, error) {
 }
 
 type AppearanceConfig struct {
-	ApplicationName string              `json:"application_name"`
-	LogoURL         string              `json:"logo_url"`
-	ServiceBanner   ServiceBannerConfig `json:"service_banner"`
-	SupportLinks    []LinkConfig        `json:"support_links,omitempty"`
+	ApplicationName string `json:"application_name"`
+	LogoURL         string `json:"logo_url"`
+	// Deprecated: ServiceBanner has been replaced by NotificationBanners.
+	ServiceBanner       BannerConfig   `json:"service_banner"`
+	NotificationBanners []BannerConfig `json:"notification_banners"`
+	SupportLinks        []LinkConfig   `json:"support_links,omitempty"`
 }
 
 type UpdateAppearanceConfig struct {
-	ApplicationName string              `json:"application_name"`
-	LogoURL         string              `json:"logo_url"`
-	ServiceBanner   ServiceBannerConfig `json:"service_banner"`
+	ApplicationName string `json:"application_name"`
+	LogoURL         string `json:"logo_url"`
+	// Deprecated: ServiceBanner has been replaced by NotificationBanners.
+	ServiceBanner       BannerConfig   `json:"service_banner"`
+	NotificationBanners []BannerConfig `json:"notification_banners"`
 }
 
-type ServiceBannerConfig struct {
+// Deprecated: ServiceBannerConfig has been renamed to BannerConfig.
+type ServiceBannerConfig = BannerConfig
+
+type BannerConfig struct {
 	Enabled         bool   `json:"enabled"`
 	Message         string `json:"message,omitempty"`
 	BackgroundColor string `json:"background_color,omitempty"`
diff --git a/codersdk/organizations.go b/codersdk/organizations.go
index f887d5ea4de5a..441f4774f2441 100644
--- a/codersdk/organizations.go
+++ b/codersdk/organizations.go
@@ -34,7 +34,7 @@ func ProvisionerTypeValid[T ProvisionerType | string](pt T) error {
 	case string(ProvisionerTypeEcho), string(ProvisionerTypeTerraform):
 		return nil
 	default:
-		return fmt.Errorf("provisioner type '%s' is not supported", pt)
+		return xerrors.Errorf("provisioner type '%s' is not supported", pt)
 	}
 }
 
diff --git a/docs/api/enterprise.md b/docs/api/enterprise.md
index 0b05a9fffeee6..800e9e517196d 100644
--- a/docs/api/enterprise.md
+++ b/docs/api/enterprise.md
@@ -21,6 +21,13 @@ curl -X GET http://coder-server:8080/api/v2/appearance \
 {
   "application_name": "string",
   "logo_url": "string",
+  "notification_banners": [
+    {
+      "background_color": "string",
+      "enabled": true,
+      "message": "string"
+    }
+  ],
   "service_banner": {
     "background_color": "string",
     "enabled": true,
@@ -64,6 +71,13 @@ curl -X PUT http://coder-server:8080/api/v2/appearance \
 {
   "application_name": "string",
   "logo_url": "string",
+  "notification_banners": [
+    {
+      "background_color": "string",
+      "enabled": true,
+      "message": "string"
+    }
+  ],
   "service_banner": {
     "background_color": "string",
     "enabled": true,
@@ -86,6 +100,13 @@ curl -X PUT http://coder-server:8080/api/v2/appearance \
 {
   "application_name": "string",
   "logo_url": "string",
+  "notification_banners": [
+    {
+      "background_color": "string",
+      "enabled": true,
+      "message": "string"
+    }
+  ],
   "service_banner": {
     "background_color": "string",
     "enabled": true,
diff --git a/docs/api/schemas.md b/docs/api/schemas.md
index 26b38a7c1ec78..a6462a14ca29c 100644
--- a/docs/api/schemas.md
+++ b/docs/api/schemas.md
@@ -751,6 +751,13 @@
 {
   "application_name": "string",
   "logo_url": "string",
+  "notification_banners": [
+    {
+      "background_color": "string",
+      "enabled": true,
+      "message": "string"
+    }
+  ],
   "service_banner": {
     "background_color": "string",
     "enabled": true,
@@ -768,12 +775,13 @@
 
 ### Properties
 
-| Name               | Type                                                         | Required | Restrictions | Description |
-| ------------------ | ------------------------------------------------------------ | -------- | ------------ | ----------- |
-| `application_name` | string                                                       | false    |              |             |
-| `logo_url`         | string                                                       | false    |              |             |
-| `service_banner`   | [codersdk.ServiceBannerConfig](#codersdkservicebannerconfig) | false    |              |             |
-| `support_links`    | array of [codersdk.LinkConfig](#codersdklinkconfig)          | false    |              |             |
+| Name                   | Type                                                    | Required | Restrictions | Description                                                         |
+| ---------------------- | ------------------------------------------------------- | -------- | ------------ | ------------------------------------------------------------------- |
+| `application_name`     | string                                                  | false    |              |                                                                     |
+| `logo_url`             | string                                                  | false    |              |                                                                     |
+| `notification_banners` | array of [codersdk.BannerConfig](#codersdkbannerconfig) | false    |              |                                                                     |
+| `service_banner`       | [codersdk.BannerConfig](#codersdkbannerconfig)          | false    |              | Deprecated: ServiceBanner has been replaced by NotificationBanners. |
+| `support_links`        | array of [codersdk.LinkConfig](#codersdklinkconfig)     | false    |              |                                                                     |
 
 ## codersdk.ArchiveTemplateVersionsRequest
 
@@ -1172,6 +1180,24 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
 | `always` |
 | `never`  |
 
+## codersdk.BannerConfig
+
+```json
+{
+  "background_color": "string",
+  "enabled": true,
+  "message": "string"
+}
+```
+
+### Properties
+
+| Name               | Type    | Required | Restrictions | Description |
+| ------------------ | ------- | -------- | ------------ | ----------- |
+| `background_color` | string  | false    |              |             |
+| `enabled`          | boolean | false    |              |             |
+| `message`          | string  | false    |              |             |
+
 ## codersdk.BuildInfoResponse
 
 ```json
@@ -4264,24 +4290,6 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
 | `ssh_config_options` | object | false    |              |             |
 | » `[any property]`   | string | false    |              |             |
 
-## codersdk.ServiceBannerConfig
-
-```json
-{
-  "background_color": "string",
-  "enabled": true,
-  "message": "string"
-}
-```
-
-### Properties
-
-| Name               | Type    | Required | Restrictions | Description |
-| ------------------ | ------- | -------- | ------------ | ----------- |
-| `background_color` | string  | false    |              |             |
-| `enabled`          | boolean | false    |              |             |
-| `message`          | string  | false    |              |             |
-
 ## codersdk.SessionCountDeploymentStats
 
 ```json
@@ -5174,6 +5182,13 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
 {
   "application_name": "string",
   "logo_url": "string",
+  "notification_banners": [
+    {
+      "background_color": "string",
+      "enabled": true,
+      "message": "string"
+    }
+  ],
   "service_banner": {
     "background_color": "string",
     "enabled": true,
@@ -5184,11 +5199,12 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
 
 ### Properties
 
-| Name               | Type                                                         | Required | Restrictions | Description |
-| ------------------ | ------------------------------------------------------------ | -------- | ------------ | ----------- |
-| `application_name` | string                                                       | false    |              |             |
-| `logo_url`         | string                                                       | false    |              |             |
-| `service_banner`   | [codersdk.ServiceBannerConfig](#codersdkservicebannerconfig) | false    |              |             |
+| Name                   | Type                                                    | Required | Restrictions | Description                                                         |
+| ---------------------- | ------------------------------------------------------- | -------- | ------------ | ------------------------------------------------------------------- |
+| `application_name`     | string                                                  | false    |              |                                                                     |
+| `logo_url`             | string                                                  | false    |              |                                                                     |
+| `notification_banners` | array of [codersdk.BannerConfig](#codersdkbannerconfig) | false    |              |                                                                     |
+| `service_banner`       | [codersdk.BannerConfig](#codersdkbannerconfig)          | false    |              | Deprecated: ServiceBanner has been replaced by NotificationBanners. |
 
 ## codersdk.UpdateCheckResponse
 
diff --git a/enterprise/coderd/appearance.go b/enterprise/coderd/appearance.go
index 70ef238d6056c..7029340672b6e 100644
--- a/enterprise/coderd/appearance.go
+++ b/enterprise/coderd/appearance.go
@@ -6,6 +6,7 @@ import (
 	"encoding/hex"
 	"encoding/json"
 	"errors"
+	"fmt"
 	"net/http"
 
 	"golang.org/x/sync/errgroup"
@@ -53,9 +54,11 @@ func newAppearanceFetcher(store database.Store, links []codersdk.LinkConfig) agp
 
 func (f *appearanceFetcher) Fetch(ctx context.Context) (codersdk.AppearanceConfig, error) {
 	var eg errgroup.Group
-	var applicationName string
-	var logoURL string
-	var serviceBannerJSON string
+	var (
+		applicationName         string
+		logoURL                 string
+		notificationBannersJSON string
+	)
 	eg.Go(func() (err error) {
 		applicationName, err = f.database.GetApplicationName(ctx)
 		if err != nil && !errors.Is(err, sql.ErrNoRows) {
@@ -71,9 +74,9 @@ func (f *appearanceFetcher) Fetch(ctx context.Context) (codersdk.AppearanceConfi
 		return nil
 	})
 	eg.Go(func() (err error) {
-		serviceBannerJSON, err = f.database.GetServiceBanner(ctx)
+		notificationBannersJSON, err = f.database.GetNotificationBanners(ctx)
 		if err != nil && !errors.Is(err, sql.ErrNoRows) {
-			return xerrors.Errorf("get service banner: %w", err)
+			return xerrors.Errorf("get notification banners: %w", err)
 		}
 		return nil
 	})
@@ -83,21 +86,27 @@ func (f *appearanceFetcher) Fetch(ctx context.Context) (codersdk.AppearanceConfi
 	}
 
 	cfg := codersdk.AppearanceConfig{
-		ApplicationName: applicationName,
-		LogoURL:         logoURL,
+		ApplicationName:     applicationName,
+		LogoURL:             logoURL,
+		NotificationBanners: []codersdk.BannerConfig{},
+		SupportLinks:        agpl.DefaultSupportLinks,
 	}
-	if serviceBannerJSON != "" {
-		err = json.Unmarshal([]byte(serviceBannerJSON), &cfg.ServiceBanner)
+
+	if notificationBannersJSON != "" {
+		err = json.Unmarshal([]byte(notificationBannersJSON), &cfg.NotificationBanners)
 		if err != nil {
 			return codersdk.AppearanceConfig{}, xerrors.Errorf(
-				"unmarshal json: %w, raw: %s", err, serviceBannerJSON,
+				"unmarshal notification banners json: %w, raw: %s", err, notificationBannersJSON,
 			)
 		}
-	}
 
-	if len(f.supportLinks) == 0 {
-		cfg.SupportLinks = agpl.DefaultSupportLinks
-	} else {
+		// Redundant, but improves compatibility with slightly mismatched agent versions.
+		// Maybe we can remove this after a grace period? -Kayla, May 6th 2024
+		if len(cfg.NotificationBanners) > 0 {
+			cfg.ServiceBanner = cfg.NotificationBanners[0]
+		}
+	}
+	if len(f.supportLinks) > 0 {
 		cfg.SupportLinks = f.supportLinks
 	}
 
@@ -139,29 +148,32 @@ func (api *API) putAppearance(rw http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	if appearance.ServiceBanner.Enabled {
-		if err := validateHexColor(appearance.ServiceBanner.BackgroundColor); err != nil {
+	for _, banner := range appearance.NotificationBanners {
+		if err := validateHexColor(banner.BackgroundColor); err != nil {
 			httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
-				Message: "Invalid color format",
+				Message: fmt.Sprintf("Invalid color format: %q", banner.BackgroundColor),
 				Detail:  err.Error(),
 			})
 			return
 		}
 	}
 
-	serviceBannerJSON, err := json.Marshal(appearance.ServiceBanner)
+	if appearance.NotificationBanners == nil {
+		appearance.NotificationBanners = []codersdk.BannerConfig{}
+	}
+	notificationBannersJSON, err := json.Marshal(appearance.NotificationBanners)
 	if err != nil {
 		httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
-			Message: "Unable to marshal service banner",
+			Message: "Unable to marshal notification banners",
 			Detail:  err.Error(),
 		})
 		return
 	}
 
-	err = api.Database.UpsertServiceBanner(ctx, string(serviceBannerJSON))
+	err = api.Database.UpsertNotificationBanners(ctx, string(notificationBannersJSON))
 	if err != nil {
 		httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
-			Message: "Unable to set service banner",
+			Message: "Unable to set notification banners",
 			Detail:  err.Error(),
 		})
 		return
diff --git a/enterprise/coderd/appearance_test.go b/enterprise/coderd/appearance_test.go
index beab7c104f5e0..745f90e00d03b 100644
--- a/enterprise/coderd/appearance_test.go
+++ b/enterprise/coderd/appearance_test.go
@@ -6,7 +6,6 @@ import (
 	"net/http"
 	"testing"
 
-	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 
 	"github.com/coder/coder/v2/agent/proto"
@@ -56,7 +55,7 @@ func TestCustomLogoAndCompanyName(t *testing.T) {
 	require.Equal(t, uac.LogoURL, got.LogoURL)
 }
 
-func TestServiceBanners(t *testing.T) {
+func TestNotificationBanners(t *testing.T) {
 	t.Parallel()
 
 	t.Run("User", func(t *testing.T) {
@@ -68,10 +67,10 @@ func TestServiceBanners(t *testing.T) {
 		adminClient, adminUser := coderdenttest.New(t, &coderdenttest.Options{DontAddLicense: true})
 		basicUserClient, _ := coderdtest.CreateAnotherUser(t, adminClient, adminUser.OrganizationID)
 
-		// Even without a license, the banner should return as disabled.
+		// Without a license, there should be no banners.
 		sb, err := basicUserClient.Appearance(ctx)
 		require.NoError(t, err)
-		require.False(t, sb.ServiceBanner.Enabled)
+		require.Empty(t, sb.NotificationBanners)
 
 		coderdenttest.AddLicense(t, adminClient, coderdenttest.LicenseOptions{
 			Features: license.Features{
@@ -82,43 +81,42 @@ func TestServiceBanners(t *testing.T) {
 		// Default state
 		sb, err = basicUserClient.Appearance(ctx)
 		require.NoError(t, err)
-		require.False(t, sb.ServiceBanner.Enabled)
+		require.Empty(t, sb.NotificationBanners)
 
+		// Regular user should be unable to set the banner
 		uac := codersdk.UpdateAppearanceConfig{
-			ServiceBanner: sb.ServiceBanner,
+			NotificationBanners: []codersdk.BannerConfig{{Enabled: true}},
 		}
-		// Regular user should be unable to set the banner
-		uac.ServiceBanner.Enabled = true
-
 		err = basicUserClient.UpdateAppearance(ctx, uac)
 		require.Error(t, err)
 		var sdkError *codersdk.Error
 		require.True(t, errors.As(err, &sdkError))
+		require.ErrorAs(t, err, &sdkError)
 		require.Equal(t, http.StatusForbidden, sdkError.StatusCode())
 
 		// But an admin can
-		wantBanner := uac
-		wantBanner.ServiceBanner.Enabled = true
-		wantBanner.ServiceBanner.Message = "Hey"
-		wantBanner.ServiceBanner.BackgroundColor = "#00FF00"
+		wantBanner := codersdk.UpdateAppearanceConfig{
+			NotificationBanners: []codersdk.BannerConfig{{
+				Enabled:         true,
+				Message:         "The beep-bop will be boop-beeped on Saturday at 12AM PST.",
+				BackgroundColor: "#00FF00",
+			}},
+		}
 		err = adminClient.UpdateAppearance(ctx, wantBanner)
 		require.NoError(t, err)
 		gotBanner, err := adminClient.Appearance(ctx) //nolint:gocritic // we should assert at least once that the owner can get the banner
 		require.NoError(t, err)
-		gotBanner.SupportLinks = nil // clean "support links" before comparison
-		require.Equal(t, wantBanner.ServiceBanner, gotBanner.ServiceBanner)
+		require.Equal(t, wantBanner.NotificationBanners, gotBanner.NotificationBanners)
 
 		// But even an admin can't give a bad color
-		wantBanner.ServiceBanner.BackgroundColor = "#bad color"
+		wantBanner.NotificationBanners[0].BackgroundColor = "#bad color"
 		err = adminClient.UpdateAppearance(ctx, wantBanner)
 		require.Error(t, err)
-
 		var sdkErr *codersdk.Error
-		if assert.ErrorAs(t, err, &sdkErr) {
-			assert.Equal(t, http.StatusBadRequest, sdkErr.StatusCode())
-			assert.Contains(t, sdkErr.Message, "Invalid color format")
-			assert.Contains(t, sdkErr.Detail, "expected # prefix and 6 characters")
-		}
+		require.ErrorAs(t, err, &sdkErr)
+		require.Equal(t, http.StatusBadRequest, sdkErr.StatusCode())
+		require.Contains(t, sdkErr.Message, "Invalid color format")
+		require.Contains(t, sdkErr.Detail, "expected # prefix and 6 characters")
 	})
 
 	t.Run("Agent", func(t *testing.T) {
@@ -141,11 +139,11 @@ func TestServiceBanners(t *testing.T) {
 			},
 		})
 		cfg := codersdk.UpdateAppearanceConfig{
-			ServiceBanner: codersdk.ServiceBannerConfig{
+			NotificationBanners: []codersdk.BannerConfig{{
 				Enabled:         true,
-				Message:         "Hey",
+				Message:         "The beep-bop will be boop-beeped on Saturday at 12AM PST.",
 				BackgroundColor: "#00FF00",
-			},
+			}},
 		}
 		err := client.UpdateAppearance(ctx, cfg)
 		require.NoError(t, err)
@@ -157,34 +155,38 @@ func TestServiceBanners(t *testing.T) {
 
 		agentClient := agentsdk.New(client.URL)
 		agentClient.SetSessionToken(r.AgentToken)
-		banner := requireGetServiceBanner(ctx, t, agentClient)
-		require.Equal(t, cfg.ServiceBanner, banner)
+		banners := requireGetNotificationBanners(ctx, t, agentClient)
+		require.Equal(t, cfg.NotificationBanners, banners)
 
 		// Create an AGPL Coderd against the same database
 		agplClient := coderdtest.New(t, &coderdtest.Options{Database: store, Pubsub: ps})
 		agplAgentClient := agentsdk.New(agplClient.URL)
 		agplAgentClient.SetSessionToken(r.AgentToken)
-		banner = requireGetServiceBanner(ctx, t, agplAgentClient)
-		require.Equal(t, codersdk.ServiceBannerConfig{}, banner)
+		banners = requireGetNotificationBanners(ctx, t, agplAgentClient)
+		require.Equal(t, []codersdk.BannerConfig{}, banners)
 
 		// No license means no banner.
 		err = client.DeleteLicense(ctx, lic.ID)
 		require.NoError(t, err)
-		banner = requireGetServiceBanner(ctx, t, agentClient)
-		require.Equal(t, codersdk.ServiceBannerConfig{}, banner)
+		banners = requireGetNotificationBanners(ctx, t, agentClient)
+		require.Equal(t, []codersdk.BannerConfig{}, banners)
 	})
 }
 
-func requireGetServiceBanner(ctx context.Context, t *testing.T, client *agentsdk.Client) codersdk.ServiceBannerConfig {
+func requireGetNotificationBanners(ctx context.Context, t *testing.T, client *agentsdk.Client) []codersdk.BannerConfig {
 	cc, err := client.ConnectRPC(ctx)
 	require.NoError(t, err)
 	defer func() {
 		_ = cc.Close()
 	}()
 	aAPI := proto.NewDRPCAgentClient(cc)
-	sbp, err := aAPI.GetServiceBanner(ctx, &proto.GetServiceBannerRequest{})
+	bannersProto, err := aAPI.GetNotificationBanners(ctx, &proto.GetNotificationBannersRequest{})
 	require.NoError(t, err)
-	return agentsdk.ServiceBannerFromProto(sbp)
+	banners := make([]codersdk.BannerConfig, 0, len(bannersProto.NotificationBanners))
+	for _, bannerProto := range bannersProto.NotificationBanners {
+		banners = append(banners, agentsdk.BannerConfigFromProto(bannerProto))
+	}
+	return banners
 }
 
 func TestCustomSupportLinks(t *testing.T) {
diff --git a/provisionerd/provisionerd_test.go b/provisionerd/provisionerd_test.go
index bca072707f491..b3cf08fc1ca5a 100644
--- a/provisionerd/provisionerd_test.go
+++ b/provisionerd/provisionerd_test.go
@@ -611,7 +611,7 @@ func TestProvisionerd(t *testing.T) {
 		server := createProvisionerd(t, func(ctx context.Context) (proto.DRPCProvisionerDaemonClient, error) {
 			// This is the dial out to Coderd, which in this unit test will always fail.
 			connectAttemptedClose.Do(func() { close(connectAttempted) })
-			return nil, fmt.Errorf("client connection always fails")
+			return nil, xerrors.New("client connection always fails")
 		}, provisionerd.LocalProvisioners{
 			"someprovisioner": createProvisionerClient(t, done, provisionerTestServer{}),
 		})
diff --git a/site/src/api/api.ts b/site/src/api/api.ts
index 6b4102073b3d1..c677ffbcb1b3b 100644
--- a/site/src/api/api.ts
+++ b/site/src/api/api.ts
@@ -1357,6 +1357,7 @@ export const getAppearance = async (): Promise<TypesGen.AppearanceConfig> => {
         service_banner: {
           enabled: false,
         },
+        notification_banners: [],
       };
     }
     throw ex;
diff --git a/site/src/api/queries/appearance.ts b/site/src/api/queries/appearance.ts
index d9337bc39e79d..7fc6cd1a71b9d 100644
--- a/site/src/api/queries/appearance.ts
+++ b/site/src/api/queries/appearance.ts
@@ -4,12 +4,12 @@ import type { AppearanceConfig } from "api/typesGenerated";
 import type { MetadataState } from "hooks/useEmbeddedMetadata";
 import { cachedQuery } from "./util";
 
-const appearanceConfigKey = ["appearance"] as const;
+export const appearanceConfigKey = ["appearance"] as const;
 
 export const appearance = (metadata: MetadataState<AppearanceConfig>) => {
   return cachedQuery({
     metadata,
-    queryKey: ["appearance"],
+    queryKey: appearanceConfigKey,
     queryFn: () => API.getAppearance(),
   });
 };
diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts
index d91a6b430de27..c2e9b51b96a11 100644
--- a/site/src/api/typesGenerated.ts
+++ b/site/src/api/typesGenerated.ts
@@ -48,7 +48,8 @@ export interface AppHostResponse {
 export interface AppearanceConfig {
   readonly application_name: string;
   readonly logo_url: string;
-  readonly service_banner: ServiceBannerConfig;
+  readonly service_banner: BannerConfig;
+  readonly notification_banners: readonly BannerConfig[];
   readonly support_links?: readonly LinkConfig[];
 }
 
@@ -157,6 +158,13 @@ export interface AvailableExperiments {
   readonly safe: readonly Experiment[];
 }
 
+// From codersdk/deployment.go
+export interface BannerConfig {
+  readonly enabled: boolean;
+  readonly message?: string;
+  readonly background_color?: string;
+}
+
 // From codersdk/deployment.go
 export interface BuildInfoResponse {
   readonly external_url: string;
@@ -1281,7 +1289,8 @@ export interface UpdateActiveTemplateVersion {
 export interface UpdateAppearanceConfig {
   readonly application_name: string;
   readonly logo_url: string;
-  readonly service_banner: ServiceBannerConfig;
+  readonly service_banner: BannerConfig;
+  readonly notification_banners: readonly BannerConfig[];
 }
 
 // From codersdk/updatecheck.go
diff --git a/site/src/modules/dashboard/DashboardLayout.tsx b/site/src/modules/dashboard/DashboardLayout.tsx
index d698e77f001ca..2a1f545820115 100644
--- a/site/src/modules/dashboard/DashboardLayout.tsx
+++ b/site/src/modules/dashboard/DashboardLayout.tsx
@@ -7,7 +7,7 @@ import { Outlet } from "react-router-dom";
 import { Loader } from "components/Loader/Loader";
 import { useAuthenticated } from "contexts/auth/RequireAuth";
 import { LicenseBanner } from "modules/dashboard/LicenseBanner/LicenseBanner";
-import { ServiceBanner } from "modules/dashboard/ServiceBanner/ServiceBanner";
+import { NotificationBanners } from "modules/dashboard/NotificationBanners/NotificationBanners";
 import { dashboardContentBottomPadding } from "theme/constants";
 import { docs } from "utils/docs";
 import { DeploymentBanner } from "./DeploymentBanner/DeploymentBanner";
@@ -21,8 +21,8 @@ export const DashboardLayout: FC = () => {
 
   return (
     <>
-      <ServiceBanner />
       {canViewDeployment && <LicenseBanner />}
+      <NotificationBanners />
 
       <div
         css={{
diff --git a/site/src/modules/dashboard/DashboardProvider.tsx b/site/src/modules/dashboard/DashboardProvider.tsx
index 3cb85c2461c8d..19daf886f02f8 100644
--- a/site/src/modules/dashboard/DashboardProvider.tsx
+++ b/site/src/modules/dashboard/DashboardProvider.tsx
@@ -1,10 +1,4 @@
-import {
-  createContext,
-  type FC,
-  type PropsWithChildren,
-  useCallback,
-  useState,
-} from "react";
+import { createContext, type FC, type PropsWithChildren } from "react";
 import { useQuery } from "react-query";
 import { appearance } from "api/queries/appearance";
 import { entitlements } from "api/queries/entitlements";
@@ -14,21 +8,13 @@ import type {
   Entitlements,
   Experiments,
 } from "api/typesGenerated";
-import { displayError } from "components/GlobalSnackbar/utils";
 import { Loader } from "components/Loader/Loader";
 import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata";
-import { hslToHex, isHexColor, isHslColor } from "utils/colors";
-
-interface Appearance {
-  config: AppearanceConfig;
-  isPreview: boolean;
-  setPreview: (config: AppearanceConfig) => void;
-}
 
 export interface DashboardValue {
   entitlements: Entitlements;
   experiments: Experiments;
-  appearance: Appearance;
+  appearance: AppearanceConfig;
 }
 
 export const DashboardContext = createContext<DashboardValue | undefined>(
@@ -44,34 +30,6 @@ export const DashboardProvider: FC<PropsWithChildren> = ({ children }) => {
   const isLoading =
     !entitlementsQuery.data || !appearanceQuery.data || !experimentsQuery.data;
 
-  const [configPreview, setConfigPreview] = useState<AppearanceConfig>();
-
-  // Centralizing the logic for catching malformed configs in one spot, just to
-  // be on the safe side; don't want to expose raw setConfigPreview outside
-  // the provider
-  const setPreview = useCallback((newConfig: AppearanceConfig) => {
-    // Have runtime safety nets in place, just because so much of the codebase
-    // relies on HSL for formatting, but server expects hex values. Can't catch
-    // color format mismatches at the type level
-    const incomingBg = newConfig.service_banner.background_color;
-    let configForDispatch = newConfig;
-
-    if (typeof incomingBg === "string" && isHslColor(incomingBg)) {
-      configForDispatch = {
-        ...newConfig,
-        service_banner: {
-          ...newConfig.service_banner,
-          background_color: hslToHex(incomingBg),
-        },
-      };
-    } else if (typeof incomingBg === "string" && !isHexColor(incomingBg)) {
-      displayError(`The value ${incomingBg} is not a valid hex string`);
-      return;
-    }
-
-    setConfigPreview(configForDispatch);
-  }, []);
-
   if (isLoading) {
     return <Loader fullscreen />;
   }
@@ -81,11 +39,7 @@ export const DashboardProvider: FC<PropsWithChildren> = ({ children }) => {
       value={{
         entitlements: entitlementsQuery.data,
         experiments: experimentsQuery.data,
-        appearance: {
-          config: configPreview ?? appearanceQuery.data,
-          setPreview: setPreview,
-          isPreview: configPreview !== undefined,
-        },
+        appearance: appearanceQuery.data,
       }}
     >
       {children}
diff --git a/site/src/modules/dashboard/Navbar/Navbar.tsx b/site/src/modules/dashboard/Navbar/Navbar.tsx
index 388622fdf7636..8a0b473398a70 100644
--- a/site/src/modules/dashboard/Navbar/Navbar.tsx
+++ b/site/src/modules/dashboard/Navbar/Navbar.tsx
@@ -25,9 +25,9 @@ export const Navbar: FC = () => {
   return (
     <NavbarView
       user={me}
-      logo_url={appearance.config.logo_url}
+      logo_url={appearance.logo_url}
       buildInfo={buildInfoQuery.data}
-      supportLinks={appearance.config.support_links}
+      supportLinks={appearance.support_links}
       onSignOut={signOut}
       canViewAuditLog={canViewAuditLog}
       canViewDeployment={canViewDeployment}
diff --git a/site/src/modules/dashboard/NotificationBanners/NotificationBannerView.stories.tsx b/site/src/modules/dashboard/NotificationBanners/NotificationBannerView.stories.tsx
new file mode 100644
index 0000000000000..ee5f8dece47ca
--- /dev/null
+++ b/site/src/modules/dashboard/NotificationBanners/NotificationBannerView.stories.tsx
@@ -0,0 +1,24 @@
+import type { Meta, StoryObj } from "@storybook/react";
+import { NotificationBannerView } from "./NotificationBannerView";
+
+const meta: Meta<typeof NotificationBannerView> = {
+  title: "modules/dashboard/NotificationBannerView",
+  component: NotificationBannerView,
+};
+
+export default meta;
+type Story = StoryObj<typeof NotificationBannerView>;
+
+export const Production: Story = {
+  args: {
+    message: "Unfortunately, there's a radio connected to my brain.",
+    backgroundColor: "#ffaff3",
+  },
+};
+
+export const Preview: Story = {
+  args: {
+    message: "バアン バン バン バン バアン ブレイバアン!",
+    backgroundColor: "#4cd473",
+  },
+};
diff --git a/site/src/modules/dashboard/ServiceBanner/ServiceBannerView.tsx b/site/src/modules/dashboard/NotificationBanners/NotificationBannerView.tsx
similarity index 57%
rename from site/src/modules/dashboard/ServiceBanner/ServiceBannerView.tsx
rename to site/src/modules/dashboard/NotificationBanners/NotificationBannerView.tsx
index e907085cb2af4..4832ea93f6065 100644
--- a/site/src/modules/dashboard/ServiceBanner/ServiceBannerView.tsx
+++ b/site/src/modules/dashboard/NotificationBanners/NotificationBannerView.tsx
@@ -1,28 +1,30 @@
 import { css, type Interpolation, type Theme } from "@emotion/react";
 import type { FC } from "react";
 import { InlineMarkdown } from "components/Markdown/Markdown";
-import { Pill } from "components/Pill/Pill";
 import { readableForegroundColor } from "utils/colors";
 
-export interface ServiceBannerViewProps {
-  message: string;
-  backgroundColor: string;
-  isPreview: boolean;
+export interface NotificationBannerViewProps {
+  message?: string;
+  backgroundColor?: string;
 }
 
-export const ServiceBannerView: FC<ServiceBannerViewProps> = ({
+export const NotificationBannerView: FC<NotificationBannerViewProps> = ({
   message,
   backgroundColor,
-  isPreview,
 }) => {
+  if (!message || !backgroundColor) {
+    return null;
+  }
+
   return (
-    <div css={[styles.banner, { backgroundColor }]} className="service-banner">
-      {isPreview && <Pill type="info">Preview</Pill>}
+    <div
+      css={styles.banner}
+      style={{ backgroundColor }}
+      className="service-banner"
+    >
       <div
-        css={[
-          styles.wrapper,
-          { color: readableForegroundColor(backgroundColor) },
-        ]}
+        css={styles.wrapper}
+        style={{ color: readableForegroundColor(backgroundColor) }}
       >
         <InlineMarkdown>{message}</InlineMarkdown>
       </div>
diff --git a/site/src/modules/dashboard/NotificationBanners/NotificationBanners.tsx b/site/src/modules/dashboard/NotificationBanners/NotificationBanners.tsx
new file mode 100644
index 0000000000000..a8ab663721a46
--- /dev/null
+++ b/site/src/modules/dashboard/NotificationBanners/NotificationBanners.tsx
@@ -0,0 +1,28 @@
+import type { FC } from "react";
+import { useDashboard } from "modules/dashboard/useDashboard";
+import { NotificationBannerView } from "./NotificationBannerView";
+
+export const NotificationBanners: FC = () => {
+  const { appearance, entitlements } = useDashboard();
+  const notificationBanners = appearance.notification_banners;
+
+  const isEntitled =
+    entitlements.features.appearance.entitlement !== "not_entitled";
+  if (!isEntitled) {
+    return null;
+  }
+
+  return (
+    <>
+      {notificationBanners
+        .filter((banner) => banner.enabled)
+        .map((banner) => (
+          <NotificationBannerView
+            key={banner.message}
+            message={banner.message}
+            backgroundColor={banner.background_color}
+          />
+        ))}
+    </>
+  );
+};
diff --git a/site/src/modules/dashboard/ServiceBanner/ServiceBanner.tsx b/site/src/modules/dashboard/ServiceBanner/ServiceBanner.tsx
deleted file mode 100644
index cedd4ba4e77f0..0000000000000
--- a/site/src/modules/dashboard/ServiceBanner/ServiceBanner.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import type { FC } from "react";
-import { useDashboard } from "modules/dashboard/useDashboard";
-import { ServiceBannerView } from "./ServiceBannerView";
-
-export const ServiceBanner: FC = () => {
-  const { appearance } = useDashboard();
-  const { message, background_color, enabled } =
-    appearance.config.service_banner;
-
-  if (!enabled || message === undefined || background_color === undefined) {
-    return null;
-  }
-
-  return (
-    <ServiceBannerView
-      message={message}
-      backgroundColor={background_color}
-      isPreview={appearance.isPreview}
-    />
-  );
-};
diff --git a/site/src/modules/dashboard/ServiceBanner/ServiceBannerView.stories.tsx b/site/src/modules/dashboard/ServiceBanner/ServiceBannerView.stories.tsx
deleted file mode 100644
index 1f3df18b3a42a..0000000000000
--- a/site/src/modules/dashboard/ServiceBanner/ServiceBannerView.stories.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import type { Meta, StoryObj } from "@storybook/react";
-import { ServiceBannerView } from "./ServiceBannerView";
-
-const meta: Meta<typeof ServiceBannerView> = {
-  title: "modules/dashboard/ServiceBannerView",
-  component: ServiceBannerView,
-};
-
-export default meta;
-type Story = StoryObj<typeof ServiceBannerView>;
-
-export const Production: Story = {
-  args: {
-    message: "weeeee",
-    backgroundColor: "#FFFFFF",
-  },
-};
-
-export const Preview: Story = {
-  args: {
-    message: "weeeee",
-    backgroundColor: "#000000",
-    isPreview: true,
-  },
-};
diff --git a/site/src/modules/workspaces/WorkspaceStatusBadge/WorkspaceStatusBadge.stories.tsx b/site/src/modules/workspaces/WorkspaceStatusBadge/WorkspaceStatusBadge.stories.tsx
index a9c065c33b330..83da1063488b7 100644
--- a/site/src/modules/workspaces/WorkspaceStatusBadge/WorkspaceStatusBadge.stories.tsx
+++ b/site/src/modules/workspaces/WorkspaceStatusBadge/WorkspaceStatusBadge.stories.tsx
@@ -18,12 +18,6 @@ import {
 } from "testHelpers/entities";
 import { WorkspaceStatusBadge } from "./WorkspaceStatusBadge";
 
-const MockedAppearance = {
-  config: MockAppearanceConfig,
-  isPreview: false,
-  setPreview: () => {},
-};
-
 const meta: Meta<typeof WorkspaceStatusBadge> = {
   title: "modules/workspaces/WorkspaceStatusBadge",
   component: WorkspaceStatusBadge,
@@ -41,7 +35,7 @@ const meta: Meta<typeof WorkspaceStatusBadge> = {
         value={{
           entitlements: MockEntitlementsWithScheduling,
           experiments: MockExperiments,
-          appearance: MockedAppearance,
+          appearance: MockAppearanceConfig,
         }}
       >
         <Story />
diff --git a/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPage.tsx b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPage.tsx
index 2f05eacb9a20a..a99e04dd6b8e0 100644
--- a/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPage.tsx
+++ b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPage.tsx
@@ -2,7 +2,7 @@ import type { FC } from "react";
 import { Helmet } from "react-helmet-async";
 import { useMutation, useQueryClient } from "react-query";
 import { getErrorMessage } from "api/errors";
-import { updateAppearance } from "api/queries/appearance";
+import { appearanceConfigKey, updateAppearance } from "api/queries/appearance";
 import type { UpdateAppearanceConfig } from "api/typesGenerated";
 import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
 import { useDashboard } from "modules/dashboard/useDashboard";
@@ -20,16 +20,12 @@ const AppearanceSettingsPage: FC = () => {
 
   const onSaveAppearance = async (
     newConfig: Partial<UpdateAppearanceConfig>,
-    preview: boolean,
   ) => {
-    const newAppearance = { ...appearance.config, ...newConfig };
-    if (preview) {
-      appearance.setPreview(newAppearance);
-      return;
-    }
+    const newAppearance = { ...appearance, ...newConfig };
 
     try {
       await updateAppearanceMutation.mutateAsync(newAppearance);
+      await queryClient.invalidateQueries(appearanceConfigKey);
       displaySuccess("Successfully updated appearance settings!");
     } catch (error) {
       displayError(
@@ -45,7 +41,7 @@ const AppearanceSettingsPage: FC = () => {
       </Helmet>
 
       <AppearanceSettingsPageView
-        appearance={appearance.config}
+        appearance={appearance}
         onSaveAppearance={onSaveAppearance}
         isEntitled={
           entitlements.features.appearance.entitlement !== "not_entitled"
diff --git a/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.stories.tsx b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.stories.tsx
index 60e0581ddc661..ed27e6bddaf96 100644
--- a/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.stories.tsx
+++ b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.stories.tsx
@@ -9,10 +9,17 @@ const meta: Meta<typeof AppearanceSettingsPageView> = {
       application_name: "Foobar",
       logo_url: "https://github.com/coder.png",
       service_banner: {
-        enabled: true,
-        message: "hello world",
-        background_color: "white",
+        enabled: false,
+        message: "",
+        background_color: "#00ff00",
       },
+      notification_banners: [
+        {
+          enabled: true,
+          message: "The beep-bop will be boop-beeped on Saturday at 12AM PST.",
+          background_color: "#ffaff3",
+        },
+      ],
     },
     isEntitled: false,
   },
diff --git a/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx
index 784ccb94ac3b3..b62a20e923c89 100644
--- a/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx
+++ b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx
@@ -1,13 +1,8 @@
-import { useTheme } from "@emotion/react";
 import Button from "@mui/material/Button";
-import FormControlLabel from "@mui/material/FormControlLabel";
 import InputAdornment from "@mui/material/InputAdornment";
-import Link from "@mui/material/Link";
-import Switch from "@mui/material/Switch";
 import TextField from "@mui/material/TextField";
 import { useFormik } from "formik";
-import { type FC, useState } from "react";
-import { BlockPicker } from "react-color";
+import type { FC } from "react";
 import type { UpdateAppearanceConfig } from "api/typesGenerated";
 import {
   Badges,
@@ -15,35 +10,29 @@ import {
   EnterpriseBadge,
   EntitledBadge,
 } from "components/Badges/Badges";
-import { Stack } from "components/Stack/Stack";
-import colors from "theme/tailwindColors";
 import { getFormHelpers } from "utils/formUtils";
 import { Fieldset } from "../Fieldset";
 import { Header } from "../Header";
+import { NotificationBannerSettings } from "./NotificationBannerSettings";
 
 export type AppearanceSettingsPageViewProps = {
   appearance: UpdateAppearanceConfig;
   isEntitled: boolean;
   onSaveAppearance: (
     newConfig: Partial<UpdateAppearanceConfig>,
-    preview: boolean,
-  ) => void;
+  ) => Promise<void>;
 };
 
-const fallbackBgColor = colors.neutral[500];
-
 export const AppearanceSettingsPageView: FC<
   AppearanceSettingsPageViewProps
 > = ({ appearance, isEntitled, onSaveAppearance }) => {
-  const theme = useTheme();
-
   const applicationNameForm = useFormik<{
     application_name: string;
   }>({
     initialValues: {
       application_name: appearance.application_name,
     },
-    onSubmit: (values) => onSaveAppearance(values, false),
+    onSubmit: (values) => onSaveAppearance(values),
   });
   const applicationNameFieldHelpers = getFormHelpers(applicationNameForm);
 
@@ -53,33 +42,10 @@ export const AppearanceSettingsPageView: FC<
     initialValues: {
       logo_url: appearance.logo_url,
     },
-    onSubmit: (values) => onSaveAppearance(values, false),
+    onSubmit: (values) => onSaveAppearance(values),
   });
   const logoFieldHelpers = getFormHelpers(logoForm);
 
-  const serviceBannerForm = useFormik<UpdateAppearanceConfig["service_banner"]>(
-    {
-      initialValues: {
-        message: appearance.service_banner.message,
-        enabled: appearance.service_banner.enabled,
-        background_color:
-          appearance.service_banner.background_color ?? fallbackBgColor,
-      },
-      onSubmit: (values) =>
-        onSaveAppearance(
-          {
-            service_banner: values,
-          },
-          false,
-        ),
-    },
-  );
-  const serviceBannerFieldHelpers = getFormHelpers(serviceBannerForm);
-
-  const [backgroundColor, setBackgroundColor] = useState(
-    serviceBannerForm.values.background_color,
-  );
-
   return (
     <>
       <Header
@@ -159,123 +125,13 @@ export const AppearanceSettingsPageView: FC<
         />
       </Fieldset>
 
-      <Fieldset
-        title="Service Banner"
-        subtitle="Configure a banner that displays a message to all users."
-        onSubmit={serviceBannerForm.handleSubmit}
-        button={
-          !isEntitled && (
-            <Button
-              onClick={() => {
-                onSaveAppearance(
-                  {
-                    service_banner: {
-                      message:
-                        "👋 **This** is a service banner. The banner's color and text are editable.",
-                      background_color: "#004852",
-                      enabled: true,
-                    },
-                  },
-                  true,
-                );
-              }}
-            >
-              Show Preview
-            </Button>
-          )
-        }
-        validation={
-          !isEntitled && (
-            <p>
-              Your license does not include Service Banners.{" "}
-              <Link href="mailto:sales@coder.com">Contact sales</Link> to learn
-              more.
-            </p>
-          )
+      <NotificationBannerSettings
+        isEntitled={isEntitled}
+        notificationBanners={appearance.notification_banners || []}
+        onSubmit={(notificationBanners) =>
+          onSaveAppearance({ notification_banners: notificationBanners })
         }
-      >
-        {isEntitled && (
-          <Stack>
-            <FormControlLabel
-              control={
-                <Switch
-                  checked={serviceBannerForm.values.enabled}
-                  onChange={async () => {
-                    const newState = !serviceBannerForm.values.enabled;
-                    const newBanner = {
-                      ...serviceBannerForm.values,
-                      enabled: newState,
-                    };
-                    onSaveAppearance(
-                      {
-                        service_banner: newBanner,
-                      },
-                      false,
-                    );
-                    await serviceBannerForm.setFieldValue("enabled", newState);
-                  }}
-                  data-testid="switch-service-banner"
-                />
-              }
-              label="Enabled"
-            />
-            <Stack spacing={0}>
-              <TextField
-                {...serviceBannerFieldHelpers("message", {
-                  helperText:
-                    "Markdown bold, italics, and links are supported.",
-                })}
-                fullWidth
-                label="Message"
-                multiline
-                inputProps={{
-                  "aria-label": "Message",
-                }}
-              />
-            </Stack>
-
-            <Stack spacing={0}>
-              <h3>{"Background Color"}</h3>
-              <BlockPicker
-                color={backgroundColor}
-                onChange={async (color) => {
-                  setBackgroundColor(color.hex);
-                  await serviceBannerForm.setFieldValue(
-                    "background_color",
-                    color.hex,
-                  );
-                  onSaveAppearance(
-                    {
-                      service_banner: {
-                        ...serviceBannerForm.values,
-                        background_color: color.hex,
-                      },
-                    },
-                    true,
-                  );
-                }}
-                triangle="hide"
-                colors={["#004852", "#D65D0F", "#4CD473", "#D94A5D", "#5A00CF"]}
-                styles={{
-                  default: {
-                    input: {
-                      color: "white",
-                      backgroundColor: theme.palette.background.default,
-                    },
-                    body: {
-                      backgroundColor: "black",
-                      color: "white",
-                    },
-                    card: {
-                      backgroundColor: "black",
-                    },
-                  },
-                }}
-              />
-            </Stack>
-          </Stack>
-        )}
-      </Fieldset>
+      />
     </>
   );
 };
diff --git a/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/NotificationBannerDialog.stories.tsx b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/NotificationBannerDialog.stories.tsx
new file mode 100644
index 0000000000000..d9ae43a6d80d0
--- /dev/null
+++ b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/NotificationBannerDialog.stories.tsx
@@ -0,0 +1,24 @@
+import { action } from "@storybook/addon-actions";
+import type { Meta, StoryObj } from "@storybook/react";
+import { NotificationBannerDialog } from "./NotificationBannerDialog";
+
+const meta: Meta<typeof NotificationBannerDialog> = {
+  title: "pages/DeploySettingsPage/NotificationBannerDialog",
+  component: NotificationBannerDialog,
+  args: {
+    banner: {
+      enabled: true,
+      message: "The beep-bop will be boop-beeped on Saturday at 12AM PST.",
+      background_color: "#ffaff3",
+    },
+    onCancel: action("onCancel"),
+    onUpdate: () => Promise.resolve(void action("onUpdate")),
+  },
+};
+
+export default meta;
+type Story = StoryObj<typeof NotificationBannerDialog>;
+
+const Example: Story = {};
+
+export { Example as NotificationBannerDialog };
diff --git a/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/NotificationBannerDialog.tsx b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/NotificationBannerDialog.tsx
new file mode 100644
index 0000000000000..6b5ffaf6fc27b
--- /dev/null
+++ b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/NotificationBannerDialog.tsx
@@ -0,0 +1,138 @@
+import { type Interpolation, type Theme, useTheme } from "@emotion/react";
+import DialogActions from "@mui/material/DialogActions";
+import TextField from "@mui/material/TextField";
+import { useFormik } from "formik";
+import type { FC } from "react";
+import { BlockPicker } from "react-color";
+import type { BannerConfig } from "api/typesGenerated";
+import { Dialog, DialogActionButtons } from "components/Dialogs/Dialog";
+import { Stack } from "components/Stack/Stack";
+import { NotificationBannerView } from "modules/dashboard/NotificationBanners/NotificationBannerView";
+import { getFormHelpers } from "utils/formUtils";
+
+interface NotificationBannerDialogProps {
+  banner: BannerConfig;
+  onCancel: () => void;
+  onUpdate: (banner: Partial<BannerConfig>) => Promise<void>;
+}
+
+export const NotificationBannerDialog: FC<NotificationBannerDialogProps> = ({
+  banner,
+  onCancel,
+  onUpdate,
+}) => {
+  const theme = useTheme();
+
+  const bannerForm = useFormik<{
+    message: string;
+    background_color: string;
+  }>({
+    initialValues: {
+      message: banner.message ?? "",
+      background_color: banner.background_color ?? "#004852",
+    },
+    onSubmit: (banner) => onUpdate(banner),
+  });
+  const bannerFieldHelpers = getFormHelpers(bannerForm);
+
+  return (
+    <Dialog css={styles.dialogWrapper} open onClose={onCancel}>
+      {/* Banner preview */}
+      <div css={{ position: "fixed", top: 0, left: 0, right: 0 }}>
+        <NotificationBannerView
+          message={bannerForm.values.message}
+          backgroundColor={bannerForm.values.background_color}
+        />
+      </div>
+
+      <div css={styles.dialogContent}>
+        <h3 css={styles.dialogTitle}>Notification banner</h3>
+        <Stack>
+          <div>
+            <h4 css={styles.settingName}>Message</h4>
+            <TextField
+              {...bannerFieldHelpers("message", {
+                helperText: "Markdown bold, italics, and links are supported.",
+              })}
+              fullWidth
+              inputProps={{
+                "aria-label": "Message",
+                placeholder: "Enter a message for the banner",
+              }}
+            />
+          </div>
+          <div>
+            <h4 css={styles.settingName}>Background color</h4>
+            <BlockPicker
+              color={bannerForm.values.background_color}
+              onChange={async (color) => {
+                await bannerForm.setFieldValue("background_color", color.hex);
+              }}
+              triangle="hide"
+              colors={["#004852", "#D65D0F", "#4CD473", "#D94A5D", "#5A00CF"]}
+              styles={{
+                default: {
+                  input: {
+                    color: "white",
+                    backgroundColor: theme.palette.background.default,
+                  },
+                  body: {
+                    backgroundColor: "black",
+                    color: "white",
+                  },
+                  card: {
+                    backgroundColor: "black",
+                  },
+                },
+              }}
+            />
+          </div>
+        </Stack>
+      </div>
+
+      <DialogActions>
+        <DialogActionButtons
+          cancelText="Cancel"
+          confirmLoading={bannerForm.isSubmitting}
+          confirmText="Update"
+          disabled={bannerForm.isSubmitting}
+          onCancel={onCancel}
+          onConfirm={bannerForm.handleSubmit}
+        />
+      </DialogActions>
+    </Dialog>
+  );
+};
+
+const styles = {
+  dialogWrapper: (theme) => ({
+    "& .MuiPaper-root": {
+      background: theme.palette.background.paper,
+      border: `1px solid ${theme.palette.divider}`,
+      width: "100%",
+      maxWidth: 500,
+    },
+    "& .MuiDialogActions-spacing": {
+      padding: "0 40px 40px",
+    },
+  }),
+  dialogContent: (theme) => ({
+    color: theme.palette.text.secondary,
+    padding: "40px 40px 20px",
+  }),
+  dialogTitle: (theme) => ({
+    margin: 0,
+    marginBottom: 16,
+    color: theme.palette.text.primary,
+    fontWeight: 400,
+    fontSize: 20,
+  }),
+  settingName: (theme) => ({
+    marginTop: 0,
+    marginBottom: 8,
+    color: theme.palette.text.primary,
+    fontSize: 16,
+    lineHeight: "150%",
+    fontWeight: 600,
+  }),
+} satisfies Record<string, Interpolation<Theme>>;
diff --git a/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/NotificationBannerItem.tsx b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/NotificationBannerItem.tsx
new file mode 100644
index 0000000000000..76636a30c4492
--- /dev/null
+++ b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/NotificationBannerItem.tsx
@@ -0,0 +1,77 @@
+import type { Interpolation, Theme } from "@emotion/react";
+import Checkbox from "@mui/material/Checkbox";
+import TableCell from "@mui/material/TableCell";
+import TableRow from "@mui/material/TableRow";
+import type { FC } from "react";
+import type { BannerConfig } from "api/typesGenerated";
+import {
+  MoreMenu,
+  MoreMenuContent,
+  MoreMenuItem,
+  MoreMenuTrigger,
+  ThreeDotsButton,
+} from "components/MoreMenu/MoreMenu";
+
+interface NotificationBannerItemProps {
+  enabled: boolean;
+  backgroundColor?: string;
+  message?: string;
+  onUpdate: (banner: Partial<BannerConfig>) => Promise<void>;
+  onEdit: () => void;
+  onDelete: () => void;
+}
+
+export const NotificationBannerItem: FC<NotificationBannerItemProps> = ({
+  enabled,
+  backgroundColor = "#004852",
+  message,
+  onUpdate,
+  onEdit,
+  onDelete,
+}) => {
+  return (
+    <TableRow>
+      <TableCell>
+        <Checkbox
+          size="small"
+          checked={enabled}
+          onClick={() => void onUpdate({ enabled: !enabled })}
+        />
+      </TableCell>
+
+      <TableCell css={!enabled && styles.disabled}>
+        {message || <em>No message</em>}
+      </TableCell>
+
+      <TableCell>
+        <div css={styles.colorSample} style={{ backgroundColor }}></div>
+      </TableCell>
+
+      <TableCell>
+        <MoreMenu>
+          <MoreMenuTrigger>
+            <ThreeDotsButton />
+          </MoreMenuTrigger>
+          <MoreMenuContent>
+            <MoreMenuItem onClick={() => onEdit()}>Edit&hellip;</MoreMenuItem>
+            <MoreMenuItem onClick={() => onDelete()} danger>
+              Delete&hellip;
+            </MoreMenuItem>
+          </MoreMenuContent>
+        </MoreMenu>
+      </TableCell>
+    </TableRow>
+  );
+};
+
+const styles = {
+  disabled: (theme) => ({
+    color: theme.roles.inactive.fill.outline,
+  }),
+
+  colorSample: {
+    width: 24,
+    height: 24,
+    borderRadius: 4,
+  },
+} satisfies Record<string, Interpolation<Theme>>;
diff --git a/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/NotificationBannerSettings.tsx b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/NotificationBannerSettings.tsx
new file mode 100644
index 0000000000000..d5611af119614
--- /dev/null
+++ b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/NotificationBannerSettings.tsx
@@ -0,0 +1,202 @@
+import { type CSSObject, useTheme } from "@emotion/react";
+import AddIcon from "@mui/icons-material/AddOutlined";
+import Button from "@mui/material/Button";
+import Link from "@mui/material/Link";
+import Table from "@mui/material/Table";
+import TableBody from "@mui/material/TableBody";
+import TableCell from "@mui/material/TableCell";
+import TableContainer from "@mui/material/TableContainer";
+import TableHead from "@mui/material/TableHead";
+import TableRow from "@mui/material/TableRow";
+import { type FC, useState } from "react";
+import type { BannerConfig } from "api/typesGenerated";
+import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog";
+import { EmptyState } from "components/EmptyState/EmptyState";
+import { Stack } from "components/Stack/Stack";
+import { NotificationBannerDialog } from "./NotificationBannerDialog";
+import { NotificationBannerItem } from "./NotificationBannerItem";
+
+interface NotificationBannerSettingsProps {
+  isEntitled: boolean;
+  notificationBanners: readonly BannerConfig[];
+  onSubmit: (banners: readonly BannerConfig[]) => Promise<void>;
+}
+
+export const NotificationBannerSettings: FC<
+  NotificationBannerSettingsProps
+> = ({ isEntitled, notificationBanners, onSubmit }) => {
+  const theme = useTheme();
+  const [banners, setBanners] = useState(notificationBanners);
+  const [editingBannerId, setEditingBannerId] = useState<number | null>(null);
+  const [deletingBannerId, setDeletingBannerId] = useState<number | null>(null);
+
+  const addBanner = () => {
+    setBanners([
+      ...banners,
+      { enabled: true, message: "", background_color: "#004852" },
+    ]);
+    setEditingBannerId(banners.length);
+  };
+
+  const updateBanner = (i: number, banner: Partial<BannerConfig>) => {
+    const newBanners = [...banners];
+    newBanners[i] = { ...banners[i], ...banner };
+    setBanners(newBanners);
+    return newBanners;
+  };
+
+  const removeBanner = (i: number) => {
+    const newBanners = [...banners];
+    newBanners.splice(i, 1);
+    setBanners(newBanners);
+    return newBanners;
+  };
+
+  const editingBanner = editingBannerId !== null && banners[editingBannerId];
+  const deletingBanner = deletingBannerId !== null && banners[deletingBannerId];
+
+  // If we're not editing a new banner, remove all empty banners. This makes canceling the
+  // "new" dialog more intuitive, by not persisting an empty banner.
+  if (editingBannerId === null && banners.some((banner) => !banner.message)) {
+    setBanners(banners.filter((banner) => banner.message));
+  }
+
+  return (
+    <>
+      <div
+        css={{
+          borderRadius: 8,
+          border: `1px solid ${theme.palette.divider}`,
+          marginTop: 32,
+          overflow: "hidden",
+        }}
+      >
+        <div css={{ padding: "24px 24px 0" }}>
+          <Stack
+            direction="row"
+            justifyContent="space-between"
+            alignItems="center"
+          >
+            <h3
+              css={{
+                fontSize: 20,
+                margin: 0,
+                fontWeight: 600,
+              }}
+            >
+              Notification Banners
+            </h3>
+            <Button
+              disabled={!isEntitled}
+              onClick={() => addBanner()}
+              startIcon={<AddIcon />}
+            >
+              New
+            </Button>
+          </Stack>
+          <div
+            css={{
+              color: theme.palette.text.secondary,
+              fontSize: 14,
+              marginTop: 8,
+            }}
+          >
+            Display message banners to all users.
+          </div>
+
+          <div
+            css={[
+              theme.typography.body2 as CSSObject,
+              { paddingTop: 16, margin: "0 -32px" },
+            ]}
+          >
+            <TableContainer css={{ borderRadius: 0, borderBottom: "none" }}>
+              <Table>
+                <TableHead>
+                  <TableRow>
+                    <TableCell width="1%">Enabled</TableCell>
+                    <TableCell>Message</TableCell>
+                    <TableCell width="2%">Color</TableCell>
+                    <TableCell width="1%" />
+                  </TableRow>
+                </TableHead>
+                <TableBody>
+                  {!isEntitled || banners.length < 1 ? (
+                    <TableCell colSpan={999}>
+                      <EmptyState
+                        css={{ minHeight: 160 }}
+                        message="No notification banners"
+                      />
+                    </TableCell>
+                  ) : (
+                    banners.map((banner, i) => (
+                      <NotificationBannerItem
+                        key={banner.message}
+                        enabled={banner.enabled && Boolean(banner.message)}
+                        backgroundColor={banner.background_color}
+                        message={banner.message}
+                        onEdit={() => setEditingBannerId(i)}
+                        onUpdate={async (banner) => {
+                          const newBanners = updateBanner(i, banner);
+                          await onSubmit(newBanners);
+                        }}
+                        onDelete={() => setDeletingBannerId(i)}
+                      />
+                    ))
+                  )}
+                </TableBody>
+              </Table>
+            </TableContainer>
+          </div>
+        </div>
+
+        {!isEntitled && (
+          <footer
+            css={[
+              theme.typography.body2 as CSSObject,
+              {
+                background: theme.palette.background.paper,
+                padding: "16px 24px",
+              },
+            ]}
+          >
+            <div css={{ color: theme.palette.text.secondary }}>
+              <p>
+                Your license does not include Service Banners.{" "}
+                <Link href="mailto:sales@coder.com">Contact sales</Link> to
+                learn more.
+              </p>
+            </div>
+          </footer>
+        )}
+      </div>
+
+      {editingBanner && (
+        <NotificationBannerDialog
+          banner={editingBanner}
+          onCancel={() => setEditingBannerId(null)}
+          onUpdate={async (banner) => {
+            const newBanners = updateBanner(editingBannerId, banner);
+            setEditingBannerId(null);
+            await onSubmit(newBanners);
+          }}
+        />
+      )}
+
+      {deletingBanner && (
+        <ConfirmDialog
+          type="delete"
+          open
+          title="Delete this banner?"
+          description={deletingBanner.message}
+          onClose={() => setDeletingBannerId(null)}
+          onConfirm={async () => {
+            const newBanners = removeBanner(deletingBannerId);
+            setDeletingBannerId(null);
+            await onSubmit(newBanners);
+          }}
+        />
+      )}
+    </>
+  );
+};
diff --git a/site/src/pages/DeploySettingsPage/Fieldset.tsx b/site/src/pages/DeploySettingsPage/Fieldset.tsx
index d91ab95f23bae..5ef43a9f36c10 100644
--- a/site/src/pages/DeploySettingsPage/Fieldset.tsx
+++ b/site/src/pages/DeploySettingsPage/Fieldset.tsx
@@ -12,16 +12,15 @@ interface FieldsetProps {
   isSubmitting?: boolean;
 }
 
-export const Fieldset: FC<FieldsetProps> = (props) => {
-  const {
-    title,
-    subtitle,
-    children,
-    validation,
-    button,
-    onSubmit,
-    isSubmitting,
-  } = props;
+export const Fieldset: FC<FieldsetProps> = ({
+  title,
+  subtitle,
+  children,
+  validation,
+  button,
+  onSubmit,
+  isSubmitting,
+}) => {
   const theme = useTheme();
 
   return (
@@ -30,6 +29,7 @@ export const Fieldset: FC<FieldsetProps> = (props) => {
         borderRadius: 8,
         border: `1px solid ${theme.palette.divider}`,
         marginTop: 32,
+        overflow: "hidden",
       }}
       onSubmit={onSubmit}
     >
diff --git a/site/src/pages/WorkspacePage/Workspace.stories.tsx b/site/src/pages/WorkspacePage/Workspace.stories.tsx
index 7f4db0efb8888..c321366862264 100644
--- a/site/src/pages/WorkspacePage/Workspace.stories.tsx
+++ b/site/src/pages/WorkspacePage/Workspace.stories.tsx
@@ -8,12 +8,6 @@ import type { WorkspacePermissions } from "./permissions";
 import { Workspace } from "./Workspace";
 import { WorkspaceBuildLogsSection } from "./WorkspaceBuildLogsSection";
 
-const MockedAppearance = {
-  config: Mocks.MockAppearanceConfig,
-  isPreview: false,
-  setPreview: () => {},
-};
-
 const permissions: WorkspacePermissions = {
   readWorkspace: true,
   updateWorkspace: true,
@@ -43,7 +37,7 @@ const meta: Meta<typeof Workspace> = {
         value={{
           entitlements: Mocks.MockEntitlementsWithScheduling,
           experiments: Mocks.MockExperiments,
-          appearance: MockedAppearance,
+          appearance: Mocks.MockAppearanceConfig,
         }}
       >
         <ProxyContext.Provider
diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx
index e84dbe617dc84..0331f5290bb73 100644
--- a/site/src/pages/WorkspacePage/WorkspacePage.tsx
+++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx
@@ -13,7 +13,7 @@ import { Margins } from "components/Margins/Margins";
 import { useAuthenticated } from "contexts/auth/RequireAuth";
 import { useEffectEvent } from "hooks/hookPolyfills";
 import { Navbar } from "modules/dashboard/Navbar/Navbar";
-import { ServiceBanner } from "modules/dashboard/ServiceBanner/ServiceBanner";
+import { NotificationBanners } from "modules/dashboard/NotificationBanners/NotificationBanners";
 import { workspaceChecks, type WorkspacePermissions } from "./permissions";
 import { WorkspaceReadyPage } from "./WorkspaceReadyPage";
 
@@ -106,7 +106,7 @@ export const WorkspacePage: FC = () => {
 
   return (
     <>
-      <ServiceBanner />
+      <NotificationBanners />
       <div css={{ height: "100%", display: "flex", flexDirection: "column" }}>
         <Navbar />
         {pageError ? (
diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx
index 1c7b61558a8cf..11fc39b142448 100644
--- a/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx
+++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx
@@ -91,12 +91,6 @@ const allWorkspaces = [
   ...Object.values(additionalWorkspaces),
 ];
 
-const MockedAppearance = {
-  config: MockAppearanceConfig,
-  isPreview: false,
-  setPreview: () => {},
-};
-
 type FilterProps = ComponentProps<typeof WorkspacesPageView>["filterProps"];
 
 const defaultFilterProps = getDefaultFilterProps<FilterProps>({
@@ -153,7 +147,7 @@ const meta: Meta<typeof WorkspacesPageView> = {
         value={{
           entitlements: MockEntitlementsWithScheduling,
           experiments: MockExperiments,
-          appearance: MockedAppearance,
+          appearance: MockAppearanceConfig,
         }}
       >
         <Story />
diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts
index 17bb48c1d00bd..6cf97131aba67 100644
--- a/site/src/testHelpers/entities.ts
+++ b/site/src/testHelpers/entities.ts
@@ -2355,6 +2355,7 @@ export const MockAppearanceConfig: TypesGen.AppearanceConfig = {
   service_banner: {
     enabled: false,
   },
+  notification_banners: [],
 };
 
 export const MockWorkspaceBuildParameter1: TypesGen.WorkspaceBuildParameter = {
diff --git a/site/src/testHelpers/storybook.tsx b/site/src/testHelpers/storybook.tsx
index 666d8b91c5c98..4d601e0dd67ef 100644
--- a/site/src/testHelpers/storybook.tsx
+++ b/site/src/testHelpers/storybook.tsx
@@ -28,11 +28,7 @@ export const withDashboardProvider = (
       value={{
         entitlements,
         experiments,
-        appearance: {
-          config: MockAppearanceConfig,
-          isPreview: false,
-          setPreview: () => {},
-        },
+        appearance: MockAppearanceConfig,
       }}
     >
       <Story />
diff --git a/tailnet/proto/version.go b/tailnet/proto/version.go
index a6040a9feae47..16f324f74fa33 100644
--- a/tailnet/proto/version.go
+++ b/tailnet/proto/version.go
@@ -6,7 +6,7 @@ import (
 
 const (
 	CurrentMajor = 2
-	CurrentMinor = 0
+	CurrentMinor = 1
 )
 
 var CurrentVersion = apiversion.New(CurrentMajor, CurrentMinor).WithBackwardCompat(1)
diff --git a/tailnet/test/integration/integration_test.go b/tailnet/test/integration/integration_test.go
index 76b57fecae651..db7ccf12b6a51 100644
--- a/tailnet/test/integration/integration_test.go
+++ b/tailnet/test/integration/integration_test.go
@@ -136,7 +136,6 @@ func handleTestSubprocess(t *testing.T) {
 		testName += *clientName
 	}
 
-	//nolint:parralleltest
 	t.Run(testName, func(t *testing.T) {
 		log := slogtest.Make(t, nil).Leveled(slog.LevelDebug)
 		switch *role {