Skip to content

feat: deep link from web app to mobile app for plugin execution #300

@0xtsukino

Description

@0xtsukino

Problem

Users visiting demo.tlsnotary.org on their phone cannot generate proofs without the Chrome extension. The mobile app can generate proofs natively, but there's no way for the web app to invoke the mobile app and receive results back.

Proposed Solution

Enable the web app (demo.tlsnotary.org) to detect the TLSNotary mobile app, launch it via deep link to execute a specific plugin, and receive the proof result back — similar to how payment apps handle "pay with app" flows.

Architecture

demo.tlsnotary.org                   TLSN Mobile App                  Relay Server
       │                                    │                              │
       │ 1. Generate correlationId (UUID)   │                              │
       │                                    │                              │
       │ 2. POST /register                  │                              │
       │    {correlationId, callbackUrl}  ──┼──────────────────────────── │
       │                                    │                    Store mapping
       │ 3. Open deep link:                 │                              │
       │    tlsn-mobile://execute?          │                              │
       │      plugin=spotify&               │                              │
       │      correlationId=uuid&           │                              │
       │      verifierUrl=https://...&      │                              │
       │      callbackUrl=https://demo...   │                              │
       │ ──────────────────────────────────→│                              │
       │                                    │                              │
       │                         4. Parse deep link                        │
       │                            Load plugin by ID                      │
       │                            Show plugin UI                         │
       │                            User logs in + proves                  │
       │                                    │                              │
       │                         5. Proof complete                         │
       │                            POST /result                           │
       │                            {correlationId, proof}                 │
       │                                    │─────────────────────────────→│
       │                                    │                    Store result
       │                                    │                              │
       │                         6. Open callback URL:                     │
       │                            https://demo.../callback?              │
       │                              correlationId=uuid&                  │
       │                              status=complete                      │
       │←──────────────────────────────────│                              │
       │                                    │                              │
       │ 7. GET /result/:correlationId      │                              │
       │ ──────────────────────────────────┼──────────────────────────────→│
       │                                    │                    Return proof
       │←──────────────────────────────────┼──────────────────────────────│
       │                                    │                              │
       │ 8. Display verified result         │                              │

How the correlationId works

The correlationId is the glue between the web session and the mobile proof, identical to the pattern used in the EAS webhook flow (#266):

  1. Web app generates a UUID (correlationId) when the user clicks "Prove on Mobile"
  2. Web app registers the correlationId with a relay server (POST /register), associating it with a callback URL and optionally the user's session/wallet
  3. Web app opens the deep link with the correlationId embedded as a query parameter
  4. Mobile app receives the deep link, extracts the correlationId, and threads it through the plugin execution as sessionData (same as EAS: sessionData: { correlationId })
  5. Mobile app completes the proof and POSTs the result to the relay server with the correlationId
  6. Mobile app opens the callback URL in the system browser, returning the user to the web app
  7. Web app polls or receives the result from the relay server using the correlationId

The relay server is necessary because:

  • Deep link URL params have ~2KB size limits — proof results are larger
  • The web app may not be in the foreground when the proof completes
  • It decouples the mobile app from needing to know the web app's exact URL structure
  • It's the same relay pattern as the EAS webhook (can reuse the same server)

Deep Link Format

Custom URL Scheme

tlsn-mobile://execute?
  plugin=spotify&
  correlationId=550e8400-e29b-41d4-a716-446655440000&
  verifierUrl=https://demo.tlsnotary.org&
  relayUrl=https://demo.tlsnotary.org/api&
  callbackUrl=https://demo.tlsnotary.org/callback
Parameter Required Description
plugin Yes Plugin ID from the registry (e.g., spotify, swissbank)
correlationId Yes UUID linking this request to the web session
verifierUrl Yes Verifier server URL for proof generation
relayUrl Yes Relay server URL where the mobile app POSTs results
callbackUrl Yes URL to open after proof completes (returns user to web app)

Universal Links (iOS) / App Links (Android) — future

For seamless detection without the "Open in app?" prompt:

https://tlsn.app/execute?plugin=spotify&correlationId=...

Requires:

  • iOS: apple-app-site-association file on the domain
  • Android: assetlinks.json on the domain
  • Both map the URL pattern to the mobile app

App Detection

The web app needs to detect whether the mobile app is installed:

iOS

  • Custom scheme: window.location = 'tlsn-mobile://...' with a timeout fallback. If the app isn't installed, nothing happens — after 1.5s, show "App not installed" with an App Store link
  • Universal Links (better UX): The OS handles the routing transparently. If the app is installed, it opens. If not, the URL falls through to the web page

Android

  • Intent scheme (recommended):
    intent://execute?plugin=spotify&correlationId=...#Intent;
      scheme=tlsn-mobile;
      package=org.tlsnotary.mobile;
      S.browser_fallback_url=https://play.google.com/store/apps/details?id=org.tlsnotary.mobile;
    end
    
    This tries the app first. If not installed, opens the Play Store link automatically.
  • Custom scheme: Same timeout-based fallback as iOS

Relay Server API

The relay server can be an extension of the existing EAS webhook server or a separate lightweight service.

POST /register

Called by the web app before opening the deep link.

{
  "correlationId": "550e8400-e29b-41d4-a716-446655440000",
  "callbackUrl": "https://demo.tlsnotary.org/callback",
  "metadata": {
    "plugin": "spotify",
    "userAgent": "...",
    "timestamp": 1711900000
  }
}

POST /result

Called by the mobile app after proof generation.

{
  "correlationId": "550e8400-e29b-41d4-a716-446655440000",
  "status": "complete",
  "proof": {
    "results": [{ "value": "Radiohead" }],
    "response": {
      "status": 200,
      "headers": [...],
      "body": "..."
    }
  }
}

GET /result/:correlationId

Polled by the web app to check if the mobile app has submitted a result.

{
  "status": "complete",
  "proof": { ... }
}

Status values: pending (registered, waiting for mobile), complete (proof received), expired (TTL exceeded).

Implementation Plan

Phase 1: Deep link handler in mobile app

packages/mobile/:

  • Register tlsn-mobile:// scheme in app.json (already done: "scheme": "tlsn-mobile")
  • Add deep link handler using expo-linking (already a dependency)
  • Parse plugin, correlationId, verifierUrl, relayUrl, callbackUrl from URL params
  • Navigate to the plugin screen with overridden config (verifierUrl from deep link, not localhost)
  • On plugin completion, POST result to relayUrl and open callbackUrl in system browser

Phase 2: Web app "Prove on Mobile" button

packages/demo/:

  • Add mobile detection (check if user is on mobile browser)
  • Add "Prove on Mobile" button that generates deep link
  • Register correlationId with relay server
  • Poll for result or listen for callback navigation

Phase 3: Relay server

packages/eas-webhook/ or new packages/relay/:

  • /register — store correlationId + metadata
  • /result — receive proof from mobile
  • /result/:id — poll endpoint for web app
  • In-memory store with TTL (same pattern as EAS webhook)
  • Could extend the existing EAS webhook server since the pattern is identical

Phase 4: Universal Links / App Links (optional)

  • Host apple-app-site-association on tlsn.app or demo.tlsnotary.org
  • Host assetlinks.json for Android
  • Eliminates the "Open in app?" prompt and handles app-not-installed gracefully

Integration with EAS Webhook

The relay server pattern is identical to the EAS webhook correlation flow. The same server could handle both:

Endpoint EAS Flow Deep Link Flow
POST /register Register wallet + correlationId Register callbackUrl + correlationId
Proof submission Verifier webhook → /webhook Mobile app → POST /result
GET /attestation/:id Poll attestation status Poll proof result

The key difference: in the EAS flow, the verifier server POSTs the webhook. In the deep link flow, the mobile app POSTs the result directly. Both use correlationId as the linking key.

Security Considerations

  • correlationId should be a cryptographically random UUID (not sequential)
  • Relay server should enforce TTL on stored results (e.g., 1 hour) and rate-limit registrations
  • Proof results should be verified by the relay server or the web app before displaying (check that the proof came from a trusted verifier)
  • callbackUrl should be validated against an allowlist to prevent open redirects
  • HTTPS only for all relay server communication

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions