diff --git a/lib/hooks/invite-links.js b/lib/hooks/invite-links.js
--- a/lib/hooks/invite-links.js
+++ b/lib/hooks/invite-links.js
@@ -3,6 +3,7 @@
 import invariant from 'invariant';
 import * as React from 'react';
 
+import { addKeyserverActionType } from '../actions/keyserver-actions.js';
 import {
   useCreateOrUpdatePublicLink,
   createOrUpdatePublicLinkActionTypes,
@@ -13,16 +14,23 @@
   joinThreadActionTypes,
   useJoinThread,
 } from '../actions/thread-actions.js';
+import { extractKeyserverIDFromID } from '../keyserver-conn/keyserver-call-utils.js';
 import { createLoadingStatusSelector } from '../selectors/loading-selectors.js';
+import { isLoggedInToKeyserver } from '../selectors/user-selectors.js';
+import type { KeyserverOverride } from '../shared/invite-links.js';
+import { useIsKeyserverURLValid } from '../shared/keyserver-utils.js';
+import { callSingleKeyserverEndpointTimeout } from '../shared/timeouts.js';
 import type { CalendarQuery } from '../types/entry-types.js';
 import type { SetState } from '../types/hook-types.js';
+import { defaultKeyserverInfo } from '../types/keyserver-types.js';
 import type {
   InviteLink,
   InviteLinkVerificationResponse,
 } from '../types/link-types.js';
 import type { LoadingStatus } from '../types/loading-types.js';
+import type { ThreadJoinPayload } from '../types/thread-types.js';
 import { useDispatchActionPromise } from '../utils/redux-promise-utils.js';
-import { useSelector } from '../utils/redux-utils.js';
+import { useDispatch, useSelector } from '../utils/redux-utils.js';
 
 const createOrUpdatePublicLinkStatusSelector = createLoadingStatusSelector(
   createOrUpdatePublicLinkActionTypes,
@@ -116,6 +124,7 @@
 type AcceptInviteLinkParams = {
   +verificationResponse: InviteLinkVerificationResponse,
   +inviteSecret: string,
+  +keyserverOverride: ?KeyserverOverride,
   +calendarQuery: () => CalendarQuery,
   +onFinish: () => mixed,
   +onInvalidLinkDetected: () => mixed,
@@ -127,52 +136,165 @@
   const {
     verificationResponse,
     inviteSecret,
+    keyserverOverride,
     calendarQuery,
     onFinish,
     onInvalidLinkDetected,
   } = params;
 
   React.useEffect(() => {
-    if (verificationResponse.status === 'already_joined') {
-      onFinish();
-    }
+    return () => {
+      if (verificationResponse.status === 'already_joined') {
+        onFinish();
+      }
+    };
   }, [onFinish, verificationResponse.status]);
 
+  const dispatch = useDispatch();
   const callJoinThread = useJoinThread();
+
   const communityID = verificationResponse.community?.id;
-  const createJoinCommunityAction = React.useCallback(async () => {
-    invariant(
-      communityID,
-      'CommunityID should be present while calling this function',
-    );
-    const query = calendarQuery();
-    try {
-      const result = await callJoinThread({
-        threadID: communityID,
-        calendarQuery: {
-          startDate: query.startDate,
-          endDate: query.endDate,
-          filters: [
-            ...query.filters,
-            { type: 'threads', threadIDs: [communityID] },
-          ],
+  let keyserverID = keyserverOverride?.keyserverID;
+  if (!keyserverID && communityID) {
+    keyserverID = extractKeyserverIDFromID(communityID);
+  }
+
+  const isKeyserverKnown = useSelector(state =>
+    keyserverID ? !!state.keyserverStore.keyserverInfos[keyserverID] : false,
+  );
+  const isAuthenticated = useSelector(isLoggedInToKeyserver(keyserverID));
+
+  const keyserverURL = keyserverOverride?.keyserverURL;
+  const isKeyserverURLValid = useIsKeyserverURLValid(keyserverURL);
+
+  const [joinCommunityPromiseFunctions, setJoinCommunityPromiseFunctions] =
+    React.useState<?{
+      +resolve: ThreadJoinPayload => mixed,
+      +reject: () => mixed,
+      +communityID: string,
+    }>(null);
+  const timeoutRef = React.useRef<?TimeoutID>();
+
+  React.useEffect(() => {
+    if (timeoutRef.current) {
+      clearTimeout(timeoutRef.current);
+    }
+  }, []);
+
+  const createJoinCommunityAction = React.useCallback(() => {
+    return new Promise<ThreadJoinPayload>((resolve, reject) => {
+      if (
+        !keyserverID ||
+        !communityID ||
+        keyserverID !== extractKeyserverIDFromID(communityID)
+      ) {
+        reject();
+        onInvalidLinkDetected();
+        setJoinCommunityPromiseFunctions(null);
+        return;
+      }
+      const timeoutID = setTimeout(() => {
+        reject();
+        setJoinCommunityPromiseFunctions(oldFunctions => {
+          if (oldFunctions) {
+            onInvalidLinkDetected();
+          }
+          return null;
+        });
+      }, callSingleKeyserverEndpointTimeout);
+      timeoutRef.current = timeoutID;
+      const resolveAndClearTimeout = (result: ThreadJoinPayload) => {
+        clearTimeout(timeoutID);
+        resolve(result);
+      };
+      const rejectAndClearTimeout = () => {
+        clearTimeout(timeoutID);
+        reject();
+      };
+      setJoinCommunityPromiseFunctions({
+        resolve: resolveAndClearTimeout,
+        reject: rejectAndClearTimeout,
+        communityID,
+      });
+    });
+  }, [communityID, keyserverID, onInvalidLinkDetected]);
+
+  React.useEffect(() => {
+    void (async () => {
+      if (!joinCommunityPromiseFunctions || isKeyserverKnown) {
+        return;
+      }
+      const isValid = await isKeyserverURLValid();
+      if (!isValid || !keyserverURL) {
+        onInvalidLinkDetected();
+        joinCommunityPromiseFunctions.reject();
+        setJoinCommunityPromiseFunctions(null);
+        return;
+      }
+      dispatch({
+        type: addKeyserverActionType,
+        payload: {
+          keyserverAdminUserID: keyserverID,
+          newKeyserverInfo: defaultKeyserverInfo(keyserverURL),
         },
-        inviteLinkSecret: inviteSecret,
       });
-      onFinish();
-      return result;
-    } catch (e) {
-      onInvalidLinkDetected();
-      throw e;
-    }
+    })();
+  }, [
+    dispatch,
+    isKeyserverKnown,
+    isKeyserverURLValid,
+    joinCommunityPromiseFunctions,
+    keyserverID,
+    keyserverURL,
+    onInvalidLinkDetected,
+  ]);
+
+  React.useEffect(() => {
+    void (async () => {
+      if (!joinCommunityPromiseFunctions || !isAuthenticated) {
+        return;
+      }
+      invariant(communityID, 'CommunityID should be set');
+      const query = calendarQuery();
+
+      if (communityID !== joinCommunityPromiseFunctions.communityID) {
+        joinCommunityPromiseFunctions.reject();
+        return;
+      }
+
+      try {
+        const result = await callJoinThread({
+          threadID: communityID,
+          calendarQuery: {
+            startDate: query.startDate,
+            endDate: query.endDate,
+            filters: [
+              ...query.filters,
+              { type: 'threads', threadIDs: [communityID] },
+            ],
+          },
+          inviteLinkSecret: inviteSecret,
+        });
+        onFinish();
+        joinCommunityPromiseFunctions.resolve(result);
+      } catch (e) {
+        onInvalidLinkDetected();
+        joinCommunityPromiseFunctions.reject();
+      } finally {
+        setJoinCommunityPromiseFunctions(null);
+      }
+    })();
   }, [
     calendarQuery,
     callJoinThread,
     communityID,
     inviteSecret,
+    isAuthenticated,
+    joinCommunityPromiseFunctions,
     onFinish,
     onInvalidLinkDetected,
   ]);
+
   const dispatchActionPromise = useDispatchActionPromise();
   const joinCommunity = React.useCallback(() => {
     void dispatchActionPromise(
diff --git a/lib/selectors/user-selectors.js b/lib/selectors/user-selectors.js
--- a/lib/selectors/user-selectors.js
+++ b/lib/selectors/user-selectors.js
@@ -143,9 +143,12 @@
   );
 
 const isLoggedInToKeyserver: (
-  keyserverID: string,
+  keyserverID: ?string,
 ) => (state: BaseAppState<>) => boolean = _memoize(
-  (keyserverID: string) => (state: BaseAppState<>) => {
+  (keyserverID: ?string) => (state: BaseAppState<>) => {
+    if (!keyserverID) {
+      return false;
+    }
     const cookie = state.keyserverStore.keyserverInfos[keyserverID]?.cookie;
     return !!cookie && cookie.startsWith('user=');
   },
diff --git a/native/navigation/deep-links-context-provider.react.js b/native/navigation/deep-links-context-provider.react.js
--- a/native/navigation/deep-links-context-provider.react.js
+++ b/native/navigation/deep-links-context-provider.react.js
@@ -161,6 +161,7 @@
         params: {
           invitationDetails: result,
           secret,
+          keyserverOverride,
         },
       });
     })();
diff --git a/native/navigation/invite-link-modal.react.js b/native/navigation/invite-link-modal.react.js
--- a/native/navigation/invite-link-modal.react.js
+++ b/native/navigation/invite-link-modal.react.js
@@ -4,6 +4,7 @@
 import { View, Text, ActivityIndicator } from 'react-native';
 
 import { useAcceptInviteLink } from 'lib/hooks/invite-links.js';
+import type { KeyserverOverride } from 'lib/shared/invite-links';
 import type { InviteLinkVerificationResponse } from 'lib/types/link-types.js';
 
 import { nonThreadCalendarQuery } from './nav-selectors.js';
@@ -18,6 +19,7 @@
 export type InviteLinkModalParams = {
   +invitationDetails: InviteLinkVerificationResponse,
   +secret: string,
+  +keyserverOverride?: ?KeyserverOverride,
 };
 
 type Props = {
@@ -27,7 +29,7 @@
 
 function InviteLinkModal(props: Props): React.Node {
   const styles = useStyles(unboundStyles);
-  const { invitationDetails, secret } = props.route.params;
+  const { invitationDetails, secret, keyserverOverride } = props.route.params;
 
   const navContext = React.useContext(NavContext);
   const calendarQuery = useSelector(state =>
@@ -49,6 +51,7 @@
   const { joinCommunity, joinThreadLoadingStatus } = useAcceptInviteLink({
     verificationResponse: invitationDetails,
     inviteSecret: secret,
+    keyserverOverride,
     calendarQuery,
     onFinish: props.navigation.goBack,
     onInvalidLinkDetected,
diff --git a/web/invite-links/accept-invite-modal.react.js b/web/invite-links/accept-invite-modal.react.js
--- a/web/invite-links/accept-invite-modal.react.js
+++ b/web/invite-links/accept-invite-modal.react.js
@@ -5,6 +5,7 @@
 import ModalOverlay from 'lib/components/modal-overlay.react.js';
 import { useModalContext } from 'lib/components/modal-provider.react.js';
 import { useAcceptInviteLink } from 'lib/hooks/invite-links.js';
+import type { KeyserverOverride } from 'lib/shared/invite-links.js';
 import { type InviteLinkVerificationResponse } from 'lib/types/link-types.js';
 
 import css from './accept-invite-modal.css';
@@ -15,10 +16,11 @@
 type Props = {
   +verificationResponse: InviteLinkVerificationResponse,
   +inviteSecret: string,
+  +keyserverOverride?: ?KeyserverOverride,
 };
 
 function AcceptInviteModal(props: Props): React.Node {
-  const { verificationResponse, inviteSecret } = props;
+  const { verificationResponse, inviteSecret, keyserverOverride } = props;
   const [isLinkValid, setIsLinkValid] = React.useState(
     verificationResponse.status === 'valid',
   );
@@ -32,6 +34,7 @@
   const { joinCommunity, joinThreadLoadingStatus } = useAcceptInviteLink({
     verificationResponse,
     inviteSecret,
+    keyserverOverride,
     calendarQuery,
     onFinish: popModal,
     onInvalidLinkDetected,
diff --git a/web/invite-links/invite-link-handler.react.js b/web/invite-links/invite-link-handler.react.js
--- a/web/invite-links/invite-link-handler.react.js
+++ b/web/invite-links/invite-link-handler.react.js
@@ -89,6 +89,7 @@
         <AcceptInviteModal
           verificationResponse={result}
           inviteSecret={secret}
+          keyserverOverride={keyserverOverride}
         />,
       );
     })();