diff --git a/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift b/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift
index 4d787355..e2fe3abb 100644
--- a/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift
+++ b/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift
@@ -1,8 +1,10 @@
 import FluidMenuBarExtra
 import NetworkExtension
+import os
 import SDWebImageSVGCoder
 import SDWebImageSwiftUI
 import SwiftUI
+import UserNotifications
 import VPNLib
 
 @main
@@ -36,13 +38,16 @@ struct DesktopApp: App {
 
 @MainActor
 class AppDelegate: NSObject, NSApplicationDelegate {
+    private var logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "app-delegate")
     private var menuBar: MenuBarController?
     let vpn: CoderVPNService
     let state: AppState
     let fileSyncDaemon: MutagenDaemon
     let urlHandler: URLHandler
+    let notifDelegate: NotifDelegate
 
     override init() {
+        notifDelegate = NotifDelegate()
         vpn = CoderVPNService()
         let state = AppState(onChange: vpn.configureTunnelProviderProtocol)
         vpn.onStart = {
@@ -67,6 +72,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
         }
         self.fileSyncDaemon = fileSyncDaemon
         urlHandler = URLHandler(state: state, vpn: vpn)
+        // `delegate` is weak
+        UNUserNotificationCenter.current().delegate = notifDelegate
     }
 
     func applicationDidFinishLaunching(_: Notification) {
@@ -141,9 +148,14 @@ class AppDelegate: NSObject, NSApplicationDelegate {
             // We only accept one at time, for now
             return
         }
-        do { try urlHandler.handle(url) } catch {
-            // TODO: Push notification
-            print(error.description)
+        do { try urlHandler.handle(url) } catch let handleError {
+            Task {
+                do {
+                    try await sendNotification(title: "Failed to handle link", body: handleError.description)
+                } catch let notifError {
+                    logger.error("Failed to send notification (\(handleError.description)): \(notifError)")
+                }
+            }
         }
     }
 
diff --git a/Coder-Desktop/Coder-Desktop/Notifications.swift b/Coder-Desktop/Coder-Desktop/Notifications.swift
new file mode 100644
index 00000000..44a2afb8
--- /dev/null
+++ b/Coder-Desktop/Coder-Desktop/Notifications.swift
@@ -0,0 +1,28 @@
+import UserNotifications
+
+class NotifDelegate: NSObject, UNUserNotificationCenterDelegate {
+    override init() {
+        super.init()
+    }
+
+    // This function is required for notifications to appear as banners whilst the app is running.
+    // We're effectively forwarding the notification back to the OS
+    nonisolated func userNotificationCenter(
+        _: UNUserNotificationCenter,
+        willPresent _: UNNotification
+    ) async -> UNNotificationPresentationOptions {
+        [.banner]
+    }
+}
+
+func sendNotification(title: String, body: String) async throws {
+    let nc = UNUserNotificationCenter.current()
+    let granted = try await nc.requestAuthorization(options: [.alert, .badge])
+    guard granted else {
+        return
+    }
+    let content = UNMutableNotificationContent()
+    content.title = title
+    content.body = body
+    try await nc.add(.init(identifier: UUID().uuidString, content: content, trigger: nil))
+}
diff --git a/Coder-Desktop/project.yml b/Coder-Desktop/project.yml
index 9ec3ba44..f2c96fac 100644
--- a/Coder-Desktop/project.yml
+++ b/Coder-Desktop/project.yml
@@ -147,6 +147,7 @@ targets:
         com.apple.developer.system-extension.install: true
         com.apple.security.application-groups:
           - $(TeamIdentifierPrefix)com.coder.Coder-Desktop
+        aps-environment: development
     settings:
       base:
         ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon # Sets the app icon to "AppIcon".