diff --git a/client/src/app/header/header.component.html b/client/src/app/header/header.component.html
index d24a4559a..f20148e66 100644
--- a/client/src/app/header/header.component.html
+++ b/client/src/app/header/header.component.html
@@ -1,7 +1,7 @@
 <div *ngIf="mobileMsg" class="mobile-msg">
   <div class="msg ellipsis me-auto" i18n>Open in the application?</div>
 
-  <a class="peertube-button-link secondary-button me-3" [href]="mobileAppUrl">Open</a>
+  <a class="peertube-button-link secondary-button me-3" [href]="androidAppUrl || iosAppUrl" (click)="onOpenClientClick()">Open</a>
 
   <button class="border-0 p-0" title="Close this message" i18n-title (click)="hideMobileMsg()">
     <my-global-icon iconName="cross"></my-global-icon>
diff --git a/client/src/app/header/header.component.ts b/client/src/app/header/header.component.ts
index 990a512a1..54cc83114 100644
--- a/client/src/app/header/header.component.ts
+++ b/client/src/app/header/header.component.ts
@@ -22,7 +22,7 @@ import { SignupLabelComponent } from '@app/shared/shared-main/users/signup-label
 import { NgbDropdown, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
 import { ServerConfig } from '@peertube/peertube-models'
 import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
-import { isAndroid } from '@root-helpers/web-browser'
+import { isAndroid, isIOS, isIphone } from '@root-helpers/web-browser'
 import { Subscription } from 'rxjs'
 import { GlobalIconComponent } from '../shared/shared-icons/global-icon.component'
 import { ButtonComponent } from '../shared/shared-main/buttons/button.component'
@@ -66,7 +66,8 @@ export class HeaderComponent implements OnInit, OnDestroy {
   currentInterfaceLanguage: string
 
   mobileMsg = false
-  mobileAppUrl = ''
+  androidAppUrl = ''
+  iosAppUrl = ''
 
   private serverConfig: ServerConfig
 
@@ -159,12 +160,15 @@ export class HeaderComponent implements OnInit, OnDestroy {
   private setupMobileMsg () {
     if (!this.isInMobileView()) return
     if (peertubeLocalStorage.getItem(HeaderComponent.LS_HIDE_MOBILE_MSG) === 'true') return
-    if (!isAndroid()) return
+
+    if (!isAndroid() && !isIphone()) return
 
     this.mobileMsg = true
     document.body.classList.add('mobile-app-msg')
 
     const host = window.location.host
+    const intentConfig = this.serverConfig.client.openInApp.android.intent
+    const iosConfig = this.serverConfig.client.openInApp.ios
 
     const getVideoId = (url: string) => {
       const matches = url.match(/^\/w\/([^/]+)$/)
@@ -183,19 +187,39 @@ export class HeaderComponent implements OnInit, OnDestroy {
 
       const url = event.url
 
+      const baseAndroid = `intent://${intentConfig.host}`
+      const fallbackAndroid = `#Intent;scheme=${intentConfig.scheme};S.browser_fallback_url=${intentConfig.fallbackUrl};end`
+
+      const baseIOS = `peertube://${iosConfig.host}`
+
       const videoId = getVideoId(url)
-      if (videoId) {
-        this.mobileAppUrl = `peertube://joinpeertube.org/video/${videoId}?host=${host}`
-        return
-      }
-
       const channelId = getChannelId(url)
-      if (channelId) {
-        this.mobileAppUrl = `peertube://joinpeertube.org/video-channel/${channelId}?host=${host}`
+
+      if (videoId) {
+        if (isAndroid()) {
+          this.androidAppUrl = `${baseAndroid}/video/${videoId}?host=${host}${fallbackAndroid}`
+        } else {
+          this.iosAppUrl = `${baseIOS}/video/${videoId}?host=${host}`
+        }
+
         return
       }
 
-      this.mobileAppUrl = `peertube://joinpeertube.org/?host=${host}`
+      if (channelId) {
+        if (isAndroid()) {
+          this.androidAppUrl = `${baseAndroid}/video-channel/${channelId}?host=${host}${fallbackAndroid}`
+        } else {
+          this.iosAppUrl = `${baseIOS}/video/${videoId}?host=${host}`
+        }
+
+        return
+      }
+
+      if (isAndroid()) {
+        this.androidAppUrl = `${baseAndroid}/?host=${host}${fallbackAndroid}`
+      } else {
+        this.iosAppUrl = `${baseIOS}/?host=${host}`
+      }
     })
   }
 
@@ -206,6 +230,14 @@ export class HeaderComponent implements OnInit, OnDestroy {
     peertubeLocalStorage.setItem(HeaderComponent.LS_HIDE_MOBILE_MSG, 'true')
   }
 
+  onOpenClientClick () {
+    if (!isIOS()) return
+
+    setTimeout(() => {
+      window.location.href = this.serverConfig.client.openInApp.ios.fallbackUrl
+    }, 2500)
+  }
+
   // ---------------------------------------------------------------------------
 
   isRegistrationAllowed () {
diff --git a/client/src/app/shared/shared-user-subscription/subscribe-button.component.ts b/client/src/app/shared/shared-user-subscription/subscribe-button.component.ts
index 2d8ca65d7..6f6af1976 100644
--- a/client/src/app/shared/shared-user-subscription/subscribe-button.component.ts
+++ b/client/src/app/shared/shared-user-subscription/subscribe-button.component.ts
@@ -1,12 +1,11 @@
 import { NgClass, NgIf, NgTemplateOutlet } from '@angular/common'
 import { Component, Input, OnChanges, ViewChild } from '@angular/core'
 import { AuthService, Notifier, RedirectService } from '@app/core'
-import { NgbDropdown, NgbDropdownItem, NgbDropdownMenu, NgbDropdownToggle } from '@ng-bootstrap/ng-bootstrap'
+import { NgbDropdown, NgbDropdownMenu, NgbDropdownToggle } from '@ng-bootstrap/ng-bootstrap'
 import { FeedFormat } from '@peertube/peertube-models'
 import { concat, forkJoin, merge } from 'rxjs'
 import { Account } from '../shared-main/account/account.model'
 import { VideoChannel } from '../shared-main/channel/video-channel.model'
-import { NumberFormatterPipe } from '../shared-main/common/number-formatter.pipe'
 import { VideoService } from '../shared-main/video/video.service'
 import { RemoteSubscribeComponent } from './remote-subscribe.component'
 import { UserSubscriptionService } from './user-subscription.service'
@@ -22,9 +21,7 @@ import { UserSubscriptionService } from './user-subscription.service'
     NgbDropdown,
     NgbDropdownToggle,
     NgbDropdownMenu,
-    NgbDropdownItem,
-    RemoteSubscribeComponent,
-    NumberFormatterPipe
+    RemoteSubscribeComponent
   ]
 })
 export class SubscribeButtonComponent implements OnChanges {
diff --git a/config/default.yaml b/config/default.yaml
index bbb023cac..6e5dae6f6 100644
--- a/config/default.yaml
+++ b/config/default.yaml
@@ -1077,6 +1077,29 @@ client:
       # You can automatically redirect your users on this external platform when they click on the login button
       redirect_on_single_external_auth: false
 
+  open_in_app:
+    android:
+      # Use an intent URL: https://developer.chrome.com/docs/android/intents
+      intent:
+        enabled: true
+        # Host registered by the mobile app
+        host: 'joinpeertube.org'
+        # Scheme registered by the mobile app
+        scheme: 'peertube'
+        # If not having the app on the mobile device, open this page
+        # F-Droid alternative: https://f-droid.org/packages/org.framasoft.peertube/
+        fallback_url: 'https://play.google.com/store/apps/details?id=org.framasoft.peertube'
+
+    ios:
+      # We use a timeout for iOS: if the app is not opened after a few seconds, open the fallback URL
+      enabled: true
+      # Host registered by the mobile app
+      host: 'joinpeertube.org'
+      # Scheme registered by the mobile app
+      scheme: 'peertube'
+      # If not having the app on the mobile device, open this page
+      fallback_url: 'https://apps.apple.com/fr/app/peertube/id6737834858'
+
 storyboards:
   # Generate storyboards of local videos using ffmpeg so users can see the video preview in the player while scrubbing the video
   enabled: true
diff --git a/config/production.yaml.example b/config/production.yaml.example
index 97e682c2a..e2fd77185 100644
--- a/config/production.yaml.example
+++ b/config/production.yaml.example
@@ -1087,6 +1087,29 @@ client:
       # You can automatically redirect your users on this external platform when they click on the login button
       redirect_on_single_external_auth: false
 
+  open_in_app:
+    android:
+      # Use an intent URL: https://developer.chrome.com/docs/android/intents
+      intent:
+        enabled: true
+        # Host registered by the mobile app
+        host: 'joinpeertube.org'
+        # Scheme registered by the mobile app
+        scheme: 'peertube'
+        # If not having the app on the mobile device, open this page
+        # F-Droid alternative: https://f-droid.org/packages/org.framasoft.peertube/
+        fallback_url: 'https://play.google.com/store/apps/details?id=org.framasoft.peertube'
+
+    ios:
+      # We use a timeout for iOS: if the app is not opened after a few seconds, open the fallback URL
+      enabled: true
+      # Host registered by the mobile app
+      host: 'joinpeertube.org'
+      # Scheme registered by the mobile app
+      scheme: 'peertube'
+      # If not having the app on the mobile device, open this page
+      fallback_url: 'https://apps.apple.com/fr/app/peertube/id6737834858'
+
 storyboards:
   # Generate storyboards of local videos using ffmpeg so users can see the video preview in the player while scrubbing the video
   enabled: true
diff --git a/packages/models/src/server/server-config.model.ts b/packages/models/src/server/server-config.model.ts
index 3060d711f..1e3108774 100644
--- a/packages/models/src/server/server-config.model.ts
+++ b/packages/models/src/server/server-config.model.ts
@@ -51,6 +51,24 @@ export interface ServerConfig {
         redirectOnSingleExternalAuth: boolean
       }
     }
+
+    openInApp: {
+      android: {
+        intent: {
+          enabled: boolean
+          host: string
+          scheme: string
+          fallbackUrl: string
+        }
+      }
+
+      ios: {
+        enabled: boolean
+        host: string
+        scheme: string
+        fallbackUrl: string
+      }
+    }
   }
 
   defaults: {
diff --git a/server/core/initializers/config.ts b/server/core/initializers/config.ts
index 7c016fb65..0976060c1 100644
--- a/server/core/initializers/config.ts
+++ b/server/core/initializers/config.ts
@@ -87,6 +87,22 @@ const CONFIG = {
       LOGIN: {
         get REDIRECT_ON_SINGLE_EXTERNAL_AUTH () { return config.get<boolean>('client.menu.login.redirect_on_single_external_auth') }
       }
+    },
+    OPEN_IN_APP: {
+      ANDROID: {
+        INTENT: {
+          get ENABLED () { return config.get<boolean>('client.open_in_app.android.intent.enabled') },
+          get HOST () { return config.get<string>('client.open_in_app.android.intent.host') },
+          get SCHEME () { return config.get<string>('client.open_in_app.android.intent.scheme') },
+          get FALLBACK_URL () { return config.get<string>('client.open_in_app.android.intent.fallback_url') }
+        }
+      },
+      IOS: {
+        get ENABLED () { return config.get<boolean>('client.open_in_app.ios.enabled') },
+        get HOST () { return config.get<string>('client.open_in_app.ios.host') },
+        get SCHEME () { return config.get<string>('client.open_in_app.ios.scheme') },
+        get FALLBACK_URL () { return config.get<string>('client.open_in_app.ios.fallback_url') }
+      }
     }
   },
 
diff --git a/server/core/lib/server-config-manager.ts b/server/core/lib/server-config-manager.ts
index 5dd99b55a..afc8ef8ae 100644
--- a/server/core/lib/server-config-manager.ts
+++ b/server/core/lib/server-config-manager.ts
@@ -66,6 +66,22 @@ class ServerConfigManager {
           login: {
             redirectOnSingleExternalAuth: CONFIG.CLIENT.MENU.LOGIN.REDIRECT_ON_SINGLE_EXTERNAL_AUTH
           }
+        },
+        openInApp: {
+          android: {
+            intent: {
+              enabled: CONFIG.CLIENT.OPEN_IN_APP.ANDROID.INTENT.ENABLED,
+              host: CONFIG.CLIENT.OPEN_IN_APP.ANDROID.INTENT.HOST,
+              scheme: CONFIG.CLIENT.OPEN_IN_APP.ANDROID.INTENT.SCHEME,
+              fallbackUrl: CONFIG.CLIENT.OPEN_IN_APP.ANDROID.INTENT.FALLBACK_URL
+            }
+          },
+          ios: {
+            enabled: CONFIG.CLIENT.OPEN_IN_APP.IOS.ENABLED,
+            host: CONFIG.CLIENT.OPEN_IN_APP.IOS.HOST,
+            scheme: CONFIG.CLIENT.OPEN_IN_APP.IOS.SCHEME,
+            fallbackUrl: CONFIG.CLIENT.OPEN_IN_APP.IOS.FALLBACK_URL
+          }
         }
       },