diff --git a/api/resolvers/notifications.js b/api/resolvers/notifications.js
index 36d83658..4850aa29 100644
--- a/api/resolvers/notifications.js
+++ b/api/resolvers/notifications.js
@@ -168,6 +168,17 @@ export default {
           LIMIT ${LIMIT}+$3)`
       )
 
+      // territory transfers
+      queries.push(
+        `(SELECT "TerritoryTransfer".id::text, "TerritoryTransfer"."created_at" AS "sortTime", NULL as "earnedSats",
+          'TerritoryTransfer' AS type
+          FROM "TerritoryTransfer"
+          WHERE "TerritoryTransfer"."newUserId" = $1
+          AND "TerritoryTransfer"."created_at" <= $2
+          ORDER BY "sortTime" DESC
+          LIMIT ${LIMIT}+$3)`
+      )
+
       if (meFull.noteItemSats) {
         queries.push(
           `(SELECT "Item".id::TEXT, MAX("ItemAct".created_at) AS "sortTime",
@@ -367,6 +378,12 @@ export default {
   TerritoryPost: {
     item: async (n, args, { models, me }) => getItem(n, { id: n.id }, { models, me })
   },
+  TerritoryTransfer: {
+    sub: async (n, args, { models, me }) => {
+      const transfer = await models.territoryTransfer.findUnique({ where: { id: Number(n.id) }, include: { sub: true } })
+      return transfer.sub
+    }
+  },
   JobChanged: {
     item: async (n, args, { models, me }) => getItem(n, { id: n.id }, { models, me })
   },
diff --git a/api/resolvers/sub.js b/api/resolvers/sub.js
index 9d06c618..ead446c9 100644
--- a/api/resolvers/sub.js
+++ b/api/resolvers/sub.js
@@ -6,6 +6,7 @@ import { ssValidate, territorySchema } from '../../lib/validate'
 import { nextBilling, proratedBillingCost } from '../../lib/territory'
 import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
 import { subViewGroup } from './growth'
+import { notifyTerritoryTransfer } from '../../lib/push-notifications'
 
 export function paySubQueries (sub, models) {
   if (sub.billingType === 'ONCE') {
@@ -280,6 +281,40 @@ export default {
         await models.subSubscription.create({ data: lookupData })
         return true
       }
+    },
+    transferTerritory: async (parent, { subName, userName }, { me, models }) => {
+      if (!me) {
+        throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
+      }
+
+      const sub = await models.sub.findUnique({
+        where: {
+          name: subName
+        }
+      })
+      if (!sub) {
+        throw new GraphQLError('sub not found', { extensions: { code: 'BAD_INPUT' } })
+      }
+      if (sub.userId !== me.id) {
+        throw new GraphQLError('you do not own this sub', { extensions: { code: 'BAD_INPUT' } })
+      }
+
+      const user = await models.user.findFirst({ where: { name: userName } })
+      if (!user) {
+        throw new GraphQLError('user not found', { extensions: { code: 'BAD_INPUT' } })
+      }
+      if (user.id === me.id) {
+        throw new GraphQLError('cannot transfer territory to yourself', { extensions: { code: 'BAD_INPUT' } })
+      }
+
+      const [, updatedSub] = await models.$transaction([
+        models.territoryTransfer.create({ data: { subName, oldUserId: me.id, newUserId: user.id } }),
+        models.sub.update({ where: { name: subName }, data: { userId: user.id } })
+      ])
+
+      notifyTerritoryTransfer({ models, sub, to: user })
+
+      return updatedSub
     }
   },
   Sub: {
diff --git a/api/typeDefs/notifications.js b/api/typeDefs/notifications.js
index ed1c73f7..07d94701 100644
--- a/api/typeDefs/notifications.js
+++ b/api/typeDefs/notifications.js
@@ -108,9 +108,16 @@ export default gql`
     sortTime: Date!
   }
 
+  type TerritoryTransfer {
+    id: ID!
+    sub: Sub!
+    sortTime: Date!
+  }
+
   union Notification = Reply | Votification | Mention
     | Invitification | Earn | JobChanged | InvoicePaid | Referral
-    | Streak | FollowActivity | ForwardedVotification | Revenue | SubStatus | TerritoryPost
+    | Streak | FollowActivity | ForwardedVotification | Revenue | SubStatus
+    | TerritoryPost | TerritoryTransfer
 
   type Notifications {
     lastChecked: Date
diff --git a/api/typeDefs/sub.js b/api/typeDefs/sub.js
index 99b6e39b..806f139d 100644
--- a/api/typeDefs/sub.js
+++ b/api/typeDefs/sub.js
@@ -22,6 +22,7 @@ export default gql`
     paySub(name: String!, hash: String, hmac: String): Sub
     toggleMuteSub(name: String!): Boolean!
     toggleSubSubscription(name: String!): Boolean!
+    transferTerritory(subName: String!, userName: String!): Sub
   }
 
   type Sub {
diff --git a/components/notifications.js b/components/notifications.js
index 4683ba1d..a767c0aa 100644
--- a/components/notifications.js
+++ b/components/notifications.js
@@ -49,7 +49,8 @@ function Notification ({ n, fresh }) {
         (type === 'Reply' && ) ||
         (type === 'SubStatus' && ) ||
         (type === 'FollowActivity' && ) ||
-        (type === 'TerritoryPost' && )
+        (type === 'TerritoryPost' && ) ||
+        (type === 'TerritoryTransfer' && )
       }
     
   )
@@ -96,6 +97,7 @@ const defaultOnClick = n => {
   if (type === 'InvoicePaid') return { href: `/invoices/${n.invoice.id}` }
   if (type === 'Referral') return { href: '/referrals/month' }
   if (type === 'Streak') return {}
+  if (type === 'TerritoryTransfer') return { href: `/~${n.sub.name}` }
 
   // Votification, Mention, JobChanged, Reply all have item
   if (!n.item.title) {
@@ -426,6 +428,17 @@ function TerritoryPost ({ n }) {
   )
 }
 
+function TerritoryTransfer ({ n }) {
+  return (
+    <>
+      
+        ~{n.sub.name} was transferred to you
+        {timeSince(new Date(n.sortTime))}
+      
+    >
+  )
+}
+
 export function NotificationAlert () {
   const [showAlert, setShowAlert] = useState(false)
   const [hasSubscription, setHasSubscription] = useState(false)
diff --git a/components/territory-header.js b/components/territory-header.js
index 5f9ed124..83aa47b4 100644
--- a/components/territory-header.js
+++ b/components/territory-header.js
@@ -11,6 +11,7 @@ import Share from './share'
 import { gql, useMutation } from '@apollo/client'
 import { useToast } from './toast'
 import ActionDropdown from './action-dropdown'
+import { TerritoryTransferDropdownItem } from './territory-transfer'
 
 export function TerritoryDetails ({ sub }) {
   return (
@@ -72,6 +73,8 @@ export default function TerritoryHeader ({ sub }) {
     }
   )
 
+  const isMine = Number(sub.userId) === Number(me?.id)
+
   return (
     <>
       
@@ -83,7 +86,7 @@ export default function TerritoryHeader ({ sub }) {
           
           {me &&
             <>
-              {(Number(sub.userId) === Number(me?.id)
+              {(isMine
                 ? (
                   
                     
@@ -106,6 +109,12 @@ export default function TerritoryHeader ({ sub }) {
               )}
               
                 
+                {isMine && (
+                  <>
+                    
+                    
+                  >
+                )}
               
             >}
         
diff --git a/components/territory-transfer.js b/components/territory-transfer.js
new file mode 100644
index 00000000..3533cc01
--- /dev/null
+++ b/components/territory-transfer.js
@@ -0,0 +1,100 @@
+import { gql, useApolloClient, useMutation } from '@apollo/client'
+import { useShowModal } from './modal'
+import { useToast } from './toast'
+import { Button, Dropdown, InputGroup } from 'react-bootstrap'
+import { Form, InputUserSuggest, SubmitButton } from './form'
+import { territoryTransferSchema } from '../lib/validate'
+import { useCallback } from 'react'
+import Link from 'next/link'
+import { useMe } from './me'
+
+function TransferObstacle ({ sub, onClose, userName }) {
+  const toaster = useToast()
+  const [transfer] = useMutation(
+    gql`
+      mutation transferTerritory($subName: String!, $userName: String!) {
+        transferTerritory(subName: $subName, userName: $userName) {
+          name
+          user {
+            id
+          }
+        }
+      }
+    `
+  )
+
+  return (
+    
+      Do you really want to transfer your territory
+      
+        ~{sub.name}
+        {' '}to{' '}
+        @{userName}?
+      
+      
+        
+        
+      
+    
 
+  )
+}
+
+function TerritoryTransferForm ({ sub, onClose }) {
+  const showModal = useShowModal()
+  const client = useApolloClient()
+  const me = useMe()
+  const schema = territoryTransferSchema({ me, client })
+
+  const onSubmit = useCallback(async (values) => {
+    showModal(onClose => )
+  }, [])
+
+  return (
+    
+  )
+}
+
+export function TerritoryTransferDropdownItem ({ sub }) {
+  const showModal = useShowModal()
+  return (
+    
+      showModal(onClose =>
+        )}
+    >
+      transfer
+    
+  )
+}
diff --git a/fragments/notifications.js b/fragments/notifications.js
index a291384a..247506da 100644
--- a/fragments/notifications.js
+++ b/fragments/notifications.js
@@ -94,6 +94,13 @@ export const NOTIFICATIONS = gql`
             text
           }
         }
+        ... on TerritoryTransfer {
+          id
+          sortTime
+          sub {
+            ...SubFields
+          }
+        }
         ... on Invitification {
           id
           sortTime
diff --git a/lib/push-notifications.js b/lib/push-notifications.js
index 4ffaccba..57bf3ab7 100644
--- a/lib/push-notifications.js
+++ b/lib/push-notifications.js
@@ -130,3 +130,14 @@ export const notifyZapped = async ({ models, id }) => {
     console.error(err)
   }
 }
+
+export const notifyTerritoryTransfer = async ({ models, sub, to }) => {
+  try {
+    await sendUserNotification(to.id, {
+      title: `~${sub.name} was transferred to you`,
+      tag: `TERRITORY_TRANSFER-${sub.name}`
+    })
+  } catch (err) {
+    console.error(err)
+  }
+}
diff --git a/lib/validate.js b/lib/validate.js
index 571741e9..9e65b5e5 100644
--- a/lib/validate.js
+++ b/lib/validate.js
@@ -431,6 +431,25 @@ export function territorySchema (args) {
   })
 }
 
+export function territoryTransferSchema ({ me, ...args }) {
+  return object({
+    userName: nameValidator
+      .test({
+        name: 'name',
+        test: async name => {
+          if (!name || !name.length) return false
+          return await usernameExists(name, args)
+        },
+        message: 'user does not exist'
+      })
+      .test({
+        name: 'name',
+        test: name => !me || me.name !== name,
+        message: 'cannot transfer to yourself'
+      })
+  })
+}
+
 export function userSchema (args) {
   return object({
     name: nameValidator
diff --git a/prisma/migrations/20240305143404_territory_transfer/migration.sql b/prisma/migrations/20240305143404_territory_transfer/migration.sql
new file mode 100644
index 00000000..8f68b4df
--- /dev/null
+++ b/prisma/migrations/20240305143404_territory_transfer/migration.sql
@@ -0,0 +1,25 @@
+-- CreateTable
+CREATE TABLE "TerritoryTransfer" (
+    "id" SERIAL NOT NULL,
+    "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "oldUserId" INTEGER NOT NULL,
+    "newUserId" INTEGER NOT NULL,
+    "subName" CITEXT NOT NULL,
+
+    CONSTRAINT "TerritoryTransfer_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE INDEX "TerritoryTransfer.newUserId_index" ON "TerritoryTransfer"("created_at", "newUserId");
+
+-- CreateIndex
+CREATE INDEX "TerritoryTransfer.oldUserId_index" ON "TerritoryTransfer"("created_at", "oldUserId");
+
+-- AddForeignKey
+ALTER TABLE "TerritoryTransfer" ADD CONSTRAINT "TerritoryTransfer_oldUserId_fkey" FOREIGN KEY ("oldUserId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "TerritoryTransfer" ADD CONSTRAINT "TerritoryTransfer_newUserId_fkey" FOREIGN KEY ("newUserId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "TerritoryTransfer" ADD CONSTRAINT "TerritoryTransfer_subName_fkey" FOREIGN KEY ("subName") REFERENCES "Sub"("name") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 8abf72d1..6884165a 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -114,6 +114,8 @@ model User {
   SubAct                    SubAct[]
   MuteSub                   MuteSub[]
   Wallet                    Wallet[]
+  TerritoryTransfers        TerritoryTransfer[] @relation("TerritoryTransfer_oldUser")
+  TerritoryReceives         TerritoryTransfer[] @relation("TerritoryTransfer_newUser")
 
   @@index([photoId])
   @@index([createdAt], map: "users.created_at_index")
@@ -490,6 +492,7 @@ model Sub {
   SubAct          SubAct[]
   MuteSub         MuteSub[]
   SubSubscription SubSubscription[]
+  TerritoryTransfer TerritoryTransfer[]
 
   @@index([parentName])
   @@index([createdAt])
@@ -772,6 +775,20 @@ model Log {
   @@index([createdAt, name], map: "Log.name_index")
 }
 
+model TerritoryTransfer {
+  id          Int @id @default(autoincrement())
+  createdAt   DateTime @default(now()) @map("created_at")
+  oldUserId   Int
+  newUserId   Int
+  subName     String @db.Citext
+  oldUser     User @relation("TerritoryTransfer_oldUser", fields: [oldUserId], references: [id], onDelete: Cascade)
+  newUser     User @relation("TerritoryTransfer_newUser", fields: [newUserId], references: [id], onDelete: Cascade)
+  sub         Sub  @relation(fields: [subName], references: [name], onDelete: Cascade)
+
+  @@index([createdAt, newUserId], map: "TerritoryTransfer.newUserId_index")
+  @@index([createdAt, oldUserId], map: "TerritoryTransfer.oldUserId_index")
+}
+
 enum EarnType {
   POST
   COMMENT
diff --git a/sw/eventListener.js b/sw/eventListener.js
index f73511a5..a3d925df 100644
--- a/sw/eventListener.js
+++ b/sw/eventListener.js
@@ -91,7 +91,8 @@ export function onPush (sw) {
 
 // if there is no tag or it's a TIP, FORWARDEDTIP or EARN notification
 // we don't need to merge notifications and thus the notification should be immediately shown using `showNotification`
-const immediatelyShowNotification = (tag) => !tag || ['TIP', 'FORWARDEDTIP', 'EARN', 'STREAK'].includes(tag.split('-')[0])
+const immediatelyShowNotification = (tag) =>
+  !tag || ['TIP', 'FORWARDEDTIP', 'EARN', 'STREAK', 'TERRITORY_TRANSFER'].includes(tag.split('-')[0])
 
 const mergeAndShowNotification = async (sw, payload, currentNotifications, tag, nid) => {
   // sanity check