<!-- AI agents: this is the Markdown version of an AppNorth documentation page. Index: https://www.appnorth.app/llms.txt -->

# Deep links

Use `AppNorthDeepLinks` to create custom-scheme links to an idea, private report, or the current chat. The SDK parses a destination; your app owns URL registration, navigation, and authentication.

## Register and create links {#create}

Register your app's URL scheme in Xcode's URL Types. Pass that same scheme to the helper. The scheme below is an example; replace it with your app's registered scheme.

```swift title="AppNorthURLs.swift"
import Foundation
import AppNorth

func ideaURL(handle: String) throws -> URL {
    try AppNorthDeepLinks.idea(scheme: "myapp", handle: handle)
}

func reportURL(id: String) throws -> URL {
    try AppNorthDeepLinks.feedback(scheme: "myapp", id: id)
}

func chatURL() throws -> URL {
    try AppNorthDeepLinks.chat(scheme: "myapp")
}
```

These produce `myapp://appnorth/ideas/<encoded-handle>`, `myapp://appnorth/feedback/<encoded-id>`, and `myapp://appnorth/chat/current`. IDs and slugs are encoded as one path component. Empty values or invalid schemes throw `AppNorthError.invalidConfiguration`.

## Validate before routing {#route}

`parse(_:)` checks the `appnorth` host and the two-part path. It does **not** verify that the URL uses your app's scheme. Check that in the host before accepting a link:

```swift title="AppNorthRouter.swift"
import Foundation
import AppNorth

func routeAppNorthURL(_ url: URL, client: AppNorthClient) async throws -> AppNorthLink? {
    guard url.scheme?.lowercased() == "myapp",
          let link = AppNorthDeepLinks.parse(url) else { return nil }
    switch link.kind {
    case .idea:
        _ = try await client.idea(link.value)
    case .feedback:
        _ = try await client.feedbackThread(link.value)
    case .chat:
        guard try await client.chatStatus().available else { return nil }
    }
    return link
}
```

Call this from your SwiftUI `onOpenURL` handler or UIKit URL callback, then navigate according to the returned link. For `.chat`, present `AppNorthChatView(client: client)` with that same client; `link.value` is `"current"`, not a conversation ID. Show a sign-in flow before resolving a private report or entering a chat that requires identified users. An ID in a link grants no access; report ownership is checked by the server.

These custom-scheme links contain no project ID. Route them only through the intended project's client. The SDK does not register Universal Links or open screens automatically. Hosted roadmap URLs and identity handoff are separate server routes; there is no public Swift handoff helper.

## Open chat from a push notification {#push-chat}

AppNorth chat notifications carry `appnorth.url: "appnorth://appnorth/chat/current"`, with `appnorth.type: "chat"` and the public App ID in `appnorth.appId`. Use the push facade to validate that envelope before routing. This payload URL uses the SDK's `appnorth` scheme; it is handled directly from the notification response, so you do not need to register that scheme in URL Types.

```swift title="ChatPush.swift"
import Foundation
import AppNorth

@MainActor
func configureChatPush(
    client: AppNorthClient,
    environment: AppNorthPushEnvironment,
    updateBadge: @escaping @MainActor (Int) -> Void,
    showChat: @escaping @MainActor () -> Void
) {
    AppNorth.configurePush(
        client: client,
        environment: environment,
        badgeHandler: updateBadge,
        routeHandler: { link in
            if link.kind == .chat { showChat() }
        }
    )
}

@MainActor
func handleChatNotificationTap(_ userInfo: [AnyHashable: Any]) -> Bool {
    AppNorth.handleNotificationTap(userInfo)
}
```

Configure with the client used by your chat and the environment matching your APNs signing configuration. `showChat` is your app's presentation callback. From your `UNUserNotificationCenterDelegate` tap callback, forward `response.notification.request.content.userInfo` to `handleChatNotificationTap` on the main actor, and complete the delegate callback after handling. A false result leaves the notification for your other handlers. The helper rejects a different App ID or an invalid chat destination; it does not switch accounts or grant access.

Your app still requests notification permission and registers with APNs. Forward the resulting raw device token with `try await AppNorth.registerPushToken(deviceToken)`, and register again after an identity change. Call `try await AppNorth.syncUnreadBadge()` on foreground and after reads in a custom chat UI; its badge value is 0 or 1, not a message count. Before logout, await `AppNorth.unregisterPushToken(deviceToken)`, then call `AppNorth.resetPush()` and clear identity. Handle failures so you can retry removal before clearing that identity. See the [push API reference](/docs/swift-api#appnorth) for signatures and lifecycle details.

## Verify {#verify}

1. Open an idea URL using your registered scheme and confirm the correct idea loads.
2. Test an encoded handle and reject a URL using another scheme or host.
3. Open a report as its author, signed out, and as another account. Only the author should retrieve it.
4. Check missing and deleted destinations with a useful error and a way back to your app.
5. Tap a chat notification for the configured App ID and confirm it opens your current chat. A foreign App ID or malformed chat URL must not invoke your route handler.
