diff --git a/.forgejo/workflows/push.yaml b/.forgejo/workflows/push.yaml
index 28f49f6..984c5d6 100644
--- a/.forgejo/workflows/push.yaml
+++ b/.forgejo/workflows/push.yaml
@@ -19,6 +19,10 @@ jobs:
     steps:
       # get the repo first
       - uses: https://code.forgejo.org/actions/checkout@v4
+      - uses: https://forgejo.skynet.ie/Skynet/actions/get_lfs@v3
+        with:
+          repository: ${{ gitea.repository }}
+          ref_name: ${{ gitea.ref_name }}
       - run: nix build .#fmt --verbose
 
   # clippy is incredibly useful for making yer code better
@@ -30,6 +34,10 @@ jobs:
     steps:
       # get the repo first
       - uses: https://code.forgejo.org/actions/checkout@v4
+      - uses: https://forgejo.skynet.ie/Skynet/actions/get_lfs@v3
+        with:
+          repository: ${{ gitea.repository }}
+          ref_name: ${{ gitea.ref_name }}
       - run: nix build .#clippy --verbose
   
   build:
@@ -39,6 +47,10 @@ jobs:
     steps:
       # get the repo first
       - uses: https://code.forgejo.org/actions/checkout@v4
+      - uses: https://forgejo.skynet.ie/Skynet/actions/get_lfs@v3
+        with:
+          repository: ${{ gitea.repository }}
+          ref_name: ${{ gitea.ref_name }}
       - name: "Build it locally"
         run: nix build --verbose
 
@@ -49,7 +61,7 @@ jobs:
     needs: [ build ]
     steps:
       - name: "Deploy to Skynet"
-        uses: https://forgejo.skynet.ie/Skynet/actions-deploy-to-skynet@v2
+        uses: https://forgejo.skynet.ie/Skynet/actions/deploy@v3
         with:
           input: 'skynet_discord_bot'
           token: ${{ secrets.API_TOKEN_FORGEJO }}
\ No newline at end of file
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..6c7adef
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,37 @@
+# Git config here
+* text eol=lf
+
+#############################################
+# Git lfs stuff
+# Documents
+*.pdf filter=lfs diff=lfs merge=lfs -text
+*.doc filter=lfs diff=lfs merge=lfs -text
+*.docx filter=lfs diff=lfs merge=lfs -text
+# Excel
+*.xls filter=lfs diff=lfs merge=lfs -text
+*.xlsx filter=lfs diff=lfs merge=lfs -text
+*.xlsm filter=lfs diff=lfs merge=lfs -text
+# Powerpoints
+*.ppt filter=lfs diff=lfs merge=lfs -text
+*.pptx filter=lfs diff=lfs merge=lfs -text
+*.ppsx filter=lfs diff=lfs merge=lfs -text
+# Images
+*.png filter=lfs diff=lfs merge=lfs -text
+*.jpg filter=lfs diff=lfs merge=lfs -text
+# Video
+*.mkv filter=lfs diff=lfs merge=lfs -text
+*.mp4 filter=lfs diff=lfs merge=lfs -text
+*.wmv filter=lfs diff=lfs merge=lfs -text
+# Misc
+*.zip filter=lfs diff=lfs merge=lfs -text
+# ET4011
+*.cbe filter=lfs diff=lfs merge=lfs -text
+*.pbs filter=lfs diff=lfs merge=lfs -text
+# Open/Libre office
+# from https://www.libreoffice.org/discover/what-is-opendocument/
+*.odt filter=lfs diff=lfs merge=lfs -text
+*.ods filter=lfs diff=lfs merge=lfs -text
+*.odp filter=lfs diff=lfs merge=lfs -text
+*.odg filter=lfs diff=lfs merge=lfs -text
+# QT
+*.ui filter=lfs diff=lfs merge=lfs -text
diff --git a/Cargo.lock b/Cargo.lock
index af719e5..23bef46 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -107,9 +107,9 @@ dependencies = [
 
 [[package]]
 name = "anyhow"
-version = "1.0.89"
+version = "1.0.93"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6"
+checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775"
 
 [[package]]
 name = "async-channel"
@@ -143,7 +143,7 @@ dependencies = [
  "async-task",
  "concurrent-queue",
  "fastrand 2.1.1",
- "futures-lite 2.3.0",
+ "futures-lite 2.5.0",
  "slab",
 ]
 
@@ -158,21 +158,21 @@ dependencies = [
  "async-io",
  "async-lock",
  "blocking",
- "futures-lite 2.3.0",
+ "futures-lite 2.5.0",
  "once_cell",
 ]
 
 [[package]]
 name = "async-io"
-version = "2.3.4"
+version = "2.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "444b0228950ee6501b3568d3c93bf1176a1fdbc3b758dcd9475046d30f4dc7e8"
+checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059"
 dependencies = [
  "async-lock",
  "cfg-if",
  "concurrent-queue",
  "futures-io",
- "futures-lite 2.3.0",
+ "futures-lite 2.5.0",
  "parking",
  "polling",
  "rustix",
@@ -206,7 +206,7 @@ dependencies = [
  "futures-channel",
  "futures-core",
  "futures-io",
- "futures-lite 2.3.0",
+ "futures-lite 2.5.0",
  "gloo-timers",
  "kv-log-macro",
  "log",
@@ -381,7 +381,7 @@ dependencies = [
  "async-channel 2.3.1",
  "async-task",
  "futures-io",
- "futures-lite 2.3.0",
+ "futures-lite 2.5.0",
  "piper",
 ]
 
@@ -586,9 +586,9 @@ dependencies = [
 
 [[package]]
 name = "curl"
-version = "0.4.46"
+version = "0.4.47"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e2161dd6eba090ff1594084e95fd67aeccf04382ffea77999ea94ed42ec67b6"
+checksum = "d9fb4d13a1be2b58f14d60adba57c9834b78c62fd86c3e76a148f732686e9265"
 dependencies = [
  "curl-sys",
  "libc",
@@ -601,9 +601,9 @@ dependencies = [
 
 [[package]]
 name = "curl-sys"
-version = "0.4.75+curl-8.10.0"
+version = "0.4.78+curl-8.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2a4fd752d337342e4314717c0d9b6586b059a120c80029ebe4d49b11fec7875e"
+checksum = "8eec768341c5c7789611ae51cf6c459099f22e64a5d5d0ce4892434e33821eaf"
 dependencies = [
  "cc",
  "libc",
@@ -923,9 +923,9 @@ dependencies = [
 
 [[package]]
 name = "futures-lite"
-version = "2.3.0"
+version = "2.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5"
+checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1"
 dependencies = [
  "fastrand 2.1.1",
  "futures-core",
@@ -1056,9 +1056,9 @@ dependencies = [
 
 [[package]]
 name = "h2"
-version = "0.4.6"
+version = "0.4.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205"
+checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e"
 dependencies = [
  "atomic-waker",
  "bytes 1.7.1",
@@ -1307,14 +1307,14 @@ dependencies = [
 
 [[package]]
 name = "hyper"
-version = "1.5.0"
+version = "1.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a"
+checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f"
 dependencies = [
  "bytes 1.7.1",
  "futures-channel",
  "futures-util",
- "h2 0.4.6",
+ "h2 0.4.7",
  "http 1.1.0",
  "http-body 1.0.1",
  "httparse",
@@ -1347,9 +1347,9 @@ checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333"
 dependencies = [
  "futures-util",
  "http 1.1.0",
- "hyper 1.5.0",
+ "hyper 1.5.1",
  "hyper-util",
- "rustls 0.23.16",
+ "rustls 0.23.18",
  "rustls-pki-types",
  "tokio",
  "tokio-rustls 0.26.0",
@@ -1364,7 +1364,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
 dependencies = [
  "bytes 1.7.1",
  "http-body-util",
- "hyper 1.5.0",
+ "hyper 1.5.1",
  "hyper-util",
  "native-tls",
  "tokio",
@@ -1383,7 +1383,7 @@ dependencies = [
  "futures-util",
  "http 1.1.0",
  "http-body 1.0.1",
- "hyper 1.5.0",
+ "hyper 1.5.1",
  "pin-project-lite",
  "socket2 0.5.7",
  "tokio",
@@ -1907,18 +1907,18 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
 
 [[package]]
 name = "pin-project"
-version = "1.1.5"
+version = "1.1.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3"
+checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95"
 dependencies = [
  "pin-project-internal",
 ]
 
 [[package]]
 name = "pin-project-internal"
-version = "1.1.5"
+version = "1.1.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
+checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1977,9 +1977,9 @@ checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
 
 [[package]]
 name = "polling"
-version = "3.7.3"
+version = "3.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511"
+checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f"
 dependencies = [
  "cfg-if",
  "concurrent-queue",
@@ -2205,11 +2205,11 @@ dependencies = [
  "encoding_rs",
  "futures-core",
  "futures-util",
- "h2 0.4.6",
+ "h2 0.4.7",
  "http 1.1.0",
  "http-body 1.0.1",
  "http-body-util",
- "hyper 1.5.0",
+ "hyper 1.5.1",
  "hyper-rustls 0.27.3",
  "hyper-tls",
  "hyper-util",
@@ -2225,7 +2225,7 @@ dependencies = [
  "serde",
  "serde_json",
  "serde_urlencoded",
- "sync_wrapper 1.0.1",
+ "sync_wrapper 1.0.2",
  "system-configuration 0.6.1",
  "tokio",
  "tokio-native-tls",
@@ -2341,9 +2341,9 @@ dependencies = [
 
 [[package]]
 name = "rustls"
-version = "0.23.16"
+version = "0.23.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eee87ff5d9b36712a58574e12e9f0ea80f915a5b0ac518d322b24a465617925e"
+checksum = "9c9cc1d47e243d655ace55ed38201c19ae02c148ae56412ab8750e8f0166ab7f"
 dependencies = [
  "once_cell",
  "rustls-pki-types",
@@ -2628,15 +2628,6 @@ version = "1.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
 
-[[package]]
-name = "signal-hook-registry"
-version = "1.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
-dependencies = [
- "libc",
-]
-
 [[package]]
 name = "signature"
 version = "2.2.0"
@@ -2657,7 +2648,6 @@ dependencies = [
  "maud",
  "rand 0.8.5",
  "serde",
- "serde_json",
  "serenity",
  "sqlx",
  "surf",
@@ -3076,9 +3066,9 @@ checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
 
 [[package]]
 name = "sync_wrapper"
-version = "1.0.1"
+version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394"
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
 dependencies = [
  "futures-core",
 ]
@@ -3252,9 +3242,7 @@ dependencies = [
  "bytes 1.7.1",
  "libc",
  "mio",
- "parking_lot",
  "pin-project-lite",
- "signal-hook-registry",
  "socket2 0.5.7",
  "tokio-macros",
  "windows-sys 0.52.0",
@@ -3308,7 +3296,7 @@ version = "0.26.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
 dependencies = [
- "rustls 0.23.16",
+ "rustls 0.23.18",
  "rustls-pki-types",
  "tokio",
 ]
@@ -3533,9 +3521,9 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
 
 [[package]]
 name = "value-bag"
-version = "1.9.0"
+version = "1.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a84c137d37ab0142f0f2ddfe332651fdbf252e7b7dbb4e67b6c1f1b2e925101"
+checksum = "3ef4c4aa54d5d05a279399bfa921ec387b7aba77caf7a682ae8d86785b8fdad2"
 
 [[package]]
 name = "vcpkg"
@@ -3929,7 +3917,7 @@ dependencies = [
 [[package]]
 name = "wolves_oxidised"
 version = "0.1.0"
-source = "git+https://forgejo.skynet.ie/Skynet/wolves-oxidised.git#eee6a76c695a36f1fe220fdeafd5a43757e50527"
+source = "git+https://forgejo.skynet.ie/Skynet/wolves-oxidised.git#867778128c1ef580ebfded7808bbd4e86f22903b"
 dependencies = [
  "reqwest 0.12.9",
  "serde",
diff --git a/Cargo.toml b/Cargo.toml
index 92fa6c5..19cb9ae 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -23,6 +23,9 @@ tokio = { version = "1.0", features = ["macros", "rt-multi-thread", "full"] }
 # TODO: move off of unstable
 wolves_oxidised = { git = "https://forgejo.skynet.ie/Skynet/wolves-oxidised.git", features = ["unstable"]}
 
+# wolves api
+wolves_oxidised = { git = "https://forgejo.skynet.ie/Skynet/wolves-oxidised.git" }
+
 #  to make the http requests
 surf = "2.3.2"
 
@@ -42,4 +45,4 @@ chrono = "0.4.26"
 lettre = "0.10.4"
 maud = "0.25.0"
 
-serde = "1.0.188"
\ No newline at end of file
+serde = "1.0"
\ No newline at end of file
diff --git a/README.md b/README.md
index 068979a..01543e6 100644
--- a/README.md
+++ b/README.md
@@ -1,70 +1,10 @@
 # Skynet Discord Bot
-This bots core purpose is to give members roles based on their status on <ulwolves.ie>.  
-It uses an api key provided by wolves to get member lists.  
+The Skynet bot is designed to manage users on Discord.  
+It allows users to link their UL Wolves account with Wolves in a GDPR compliant manner.  
+Skynet (bot) is hosted is hosted by the Computer Society on Skynet (computer cluster).
 
-Users are able to link their wolves account to the bot and that works across discord servers.  
-For example is a user links on the CompSoc Discord then they will also get their roles (automagically) on Games Dev if they are a member there.
+## Documentation
+We have split up the documentation into different segments depending on who the user is.
 
-## Commands - Admin
-
-You need admin access to run any of the commands in this section.  
-Either the server owner or a suer with the ``Administrator`` permission 
-
-### Getting the Skynet Discord bot
-1. Email ``keith@assurememberships.com`` from committee email and say ye want an api key for ``193.1.99.74``
-2. Create a role for current members (maybe call it ``current-member`` ?)
-3. (Optional) create a role for all past and current members (ye can use the existing ``member`` role for this, )
-4. Invite the bot https://discord.com/api/oauth2/authorize?client_id=1145761669256069270&permissions=139855185984&scope=bot
-5. Make sure the bot role ``@skynet`` is above these two roles (so it can manage them)
-6. Make sure that you have a role that gives ye administrator powers
-7. Use the command ``/add`` and insert the api key, role current and role all (desktop recommended)
-
-The reason for both roles is ye have one for active members while the second is for all current and past members.
-
-### Minecraft
-The bot is able to manage the whitelist of a Minecraft server managed by the Computer Society.  
-Talk to us to get a server.
-
-#### Add
-This links a minecraft server with your club/society.
-
-``/minecraft_add SERVER_ID``
-
-
-#### List
-List the servers linked to your club/society.  
-It is possible to have more than one minecraft server
-
-``/minecraft_list``
-
-#### Delete
-This unlinks a minecraft server from your club/society.
-
-``/minecraft_delete SERVER_ID``
-
-## Commands - User
-
-### Setup
-* Start the process using ``/link_wolves WOLVES_EMAIL`` 
-  * The email that is in the Contact Email here: <https://ulwolves.ie/memberships/profile>
-* An email will be sent to them that they need to verify using ``/verify CODE``
-* This will only have to be done once.
-
-* If the user is an active member on wolves
-  * If they are in any servers with teh Skynet Bot
-    * They will get relevant roles.
-  * If they Join a server with teh bot enabled.
-    * They will be granted the roles automatically
-* If the user is **not** an active member on wolves.
-  * If they have no Roles
-    * No change
-  * If they have Past Member Role
-    * No change
-  * If they have both Roles
-    * The current-member role will be removed from them
-    * Past Member role will remain unchanged
-
-### Minecraft
-Users can link their Minecraft username to grant them access to any servers where teh whitelist is managed by teh bot.
-
-``/link_minecraft MINECRAFT_USERNAME``
+* [Committees](./doc/Committee.md)
+* [Member](./doc/User.md)
\ No newline at end of file
diff --git a/db/migrations/7_minecraft-bedrock.sql b/db/migrations/7_minecraft-bedrock.sql
new file mode 100644
index 0000000..71d8976
--- /dev/null
+++ b/db/migrations/7_minecraft-bedrock.sql
@@ -0,0 +1,3 @@
+
+-- Using this col we will be able to see if someone has a bedrock account (as well as a java one)
+ALTER TABLE wolves ADD COLUMN minecraft_uid TEXT;
\ No newline at end of file
diff --git a/doc/Committee.md b/doc/Committee.md
new file mode 100644
index 0000000..d53be13
--- /dev/null
+++ b/doc/Committee.md
@@ -0,0 +1,75 @@
+# Skynet Discord Bot
+This bots core purpose is to give members roles based on their status on <https://ulwolves.ie>.  
+It uses an api key provided by wolves to get member lists.
+
+Users are able to link their wolves account to the bot and that works across discord servers.  
+For example is a user links on the CompSoc Discord then they will also get their roles (automagically) on Games Dev if they are a member there.
+
+## Setup - Committee
+You need admin access to run any of the commands in this section.  
+Either the server owner or a user with the ``Administrator`` permission.
+
+### Get the API Key
+The ``api_key``  is used by the Bot in order to request information, it will be used later in the process.
+
+1. Email ``keith@assurememberships.com`` from committee email and say you want an ``api_key`` for ``193.1.99.74``
+   * The committee email is the one here: <https://cp.ulwolves.ie/mailbox/>
+   * This may take up to a week to get the key.
+
+### Setup Server
+The Bot reason for existing is being able to give members Roles.  
+So we have to create those.  
+
+1. Create a role for Current Members.
+   * You can call it whatever you want.
+   * ``member-current`` is a good choice.
+   * This should be a new role
+2. **Optional**: you can create a role that is given to folks who were but no longer a member.
+   * ``member`` would be a good choice for this
+   * If you have an existing member role this is also a good fit.
+
+The reason for both roles is ye have one for active members while the second is for all current and past members.
+
+### Invite Bot
+1. Invite the bot https://discord.com/api/oauth2/authorize?client_id=1145761669256069270&permissions=139855185984&scope=bot
+2. Make sure the bot role ``@skynet`` is above these two roles created in the previous step
+   * This is so it can manage the roles (give and remove them from users)
+
+### Setup Bot
+This is where the bot is configured.  
+You will need the ``api_key`` from the start of the process.  
+You (personally) will need a role with ``Administrator`` permission to be able to do this.  
+
+1. Use the command ``/add`` and a list of options will pop up.
+2. ``api_key`` is the key you got from Keith earlier.
+3. ``role_current`` is the ``member-current`` that you created earlier.
+4. ``role_past`` (optional) is the role for all current and past members.
+5. ``bot_channel`` is a channel that folks are recommended to use the bot.
+   * You can have it so folks cannot see message history
+6. ``server_name`` For example ``UL Computer Society`` 
+   * Will be removed in the future
+7. ``wolves_link`` for example <https://ulwolves.ie/society/computer>
+   * Will be removed in the future
+
+At this point the bot is set up and no further action is required.
+
+### Minecraft
+The bot is able to manage the whitelist of a Minecraft server managed by the Computer Society.  
+Talk to us to get a server.
+
+#### Add
+This links a minecraft server with your club/society.
+
+``/minecraft_add SERVER_ID``
+
+
+#### List
+List the servers linked to your club/society.  
+It is possible to have more than one minecraft server
+
+``/minecraft_list``
+
+#### Delete
+This unlinks a minecraft server from your club/society.
+
+``/minecraft_delete SERVER_ID``
diff --git a/doc/User.md b/doc/User.md
new file mode 100644
index 0000000..fec6abe
--- /dev/null
+++ b/doc/User.md
@@ -0,0 +1,26 @@
+# Skynet Discord Bot
+The Skynet bot is designed to make it easy to verify that you are a member of a Club/Society.  
+The bot will be able to give you member roles for any partnered servers.  
+It also provides secondary manifests such as granting access to minecraft servers managed by teh Computer Society.
+
+## Setup
+This is to link your Discord account with your UL Wolves account.  
+**You will only need to do this once**.
+
+### Setup
+1. In a Discord server with the Skynet Bot enter ``/link_wolves YOUR_WOLVES_CONTACT_EMAIL``  
+   <img src="../media/setup_user_01.png" alt="link process start" width="50%" height="50%">
+   * Your ``YOUR_WOLVES_CONTACT_EMAIL`` is the email in the Contact Email here: <https://ulwolves.ie/memberships/profile>
+   * This is most likely your student mail
+2. An email will be sent to you with a verification code.  
+   <img src="../media/setup_user_02.png" alt="signup email" width="50%" height="50%">
+3. Verify the code using ``/verify CODE_FROM_EMAIL`` in Discord.  
+   <img src="../media/setup_user_03.png" alt="verify in discord" width="50%" height="50%">
+4. Once complete your Wolves and Discord accounts will be linked.
+
+You will get member roles on any Discord that is using the bot that you are a member of.
+
+### Minecraft
+You can link your Minecraft username to grant you access to any Minecraft server run by UL Computer Society.
+
+``/link_minecraft MINECRAFT_USERNAME``
diff --git a/flake.nix b/flake.nix
index b48d90b..12ae36e 100644
--- a/flake.nix
+++ b/flake.nix
@@ -119,7 +119,7 @@
             # modify these
             scripts = {
               # every 20 min
-              "update_data" = "*:0,20,40";
+              "update_data" = "*:0,10,20,30,40,50";
               # groups are updated every hour, offset from teh ldap
               "update_users" = "*:05:00";
               # minecraft stuff is updated at 5am
diff --git a/media/setup_user_01.png b/media/setup_user_01.png
new file mode 100644
index 0000000..b5e207b
--- /dev/null
+++ b/media/setup_user_01.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f6440b2d302c5a7e46493687bb0bbf941c29c5552c71869303397da3c4f0a52b
+size 72163
diff --git a/media/setup_user_02.png b/media/setup_user_02.png
new file mode 100644
index 0000000..34898b9
--- /dev/null
+++ b/media/setup_user_02.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e64d8e2322990dcc6446a34e988c6754edc536c3ee1aa8c330e7dadb899152bc
+size 72884
diff --git a/media/setup_user_03.png b/media/setup_user_03.png
new file mode 100644
index 0000000..fda3bda
--- /dev/null
+++ b/media/setup_user_03.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f19d24686bb9da8857263d1f146e29413fd6bd316981281c18ef2b4ec3621596
+size 51740
diff --git a/src/commands/link_email.rs b/src/commands/link_email.rs
index 9d6328f..fdf82d4 100644
--- a/src/commands/link_email.rs
+++ b/src/commands/link_email.rs
@@ -163,7 +163,7 @@ pub mod link {
                 "h2, h4 { font-family: Arial, Helvetica, sans-serif; }"
             }
         }
-        div style="display: flex; flex-direction: column; align-items: center;" {
+        div {
             h2 { "Hello from Skynet!" }
             // Substitute in the name of our recipient.
             p { "Hi " (user) "," }
@@ -263,6 +263,7 @@ pub mod link {
     .await
   }
 
+
   #[derive(Serialize, Deserialize, Debug)]
   #[serde(untagged)]
   pub enum WolvesResultUserResult {
diff --git a/src/commands/minecraft.rs b/src/commands/minecraft.rs
index 61b304d..45c4643 100644
--- a/src/commands/minecraft.rs
+++ b/src/commands/minecraft.rs
@@ -15,6 +15,7 @@ pub(crate) mod user {
   pub(crate) mod add {
     use super::*;
     use crate::commands::link_email::link::get_server_member_discord;
+    use serde::{Deserialize, Serialize};
     use serenity::model::id::UserId;
     use skynet_discord_bot::common::database::Wolves;
     use skynet_discord_bot::common::minecraft::{whitelist_update, Minecraft};
@@ -22,13 +23,23 @@ pub(crate) mod user {
     use sqlx::Error;
 
     pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand {
-      command.name("link_minecraft").description("Link your minecraft account").create_option(|option| {
-        option
-          .name("minecraft-username")
-          .description("Your Minecraft username")
-          .kind(CommandOptionType::String)
-          .required(true)
-      })
+      command
+        .name("link_minecraft")
+        .description("Link your minecraft account")
+        .create_option(|option| {
+          option
+            .name("minecraft_username")
+            .description("Your Minecraft username")
+            .kind(CommandOptionType::String)
+            .required(true)
+        })
+        .create_option(|option| {
+          option
+            .name("bedrock_account")
+            .description("Is this a Bedrock account?")
+            .kind(CommandOptionType::Boolean)
+            .required(false)
+        })
     }
 
     pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> String {
@@ -63,19 +74,48 @@ pub(crate) mod user {
         return "Please provide a valid username".to_string();
       };
 
-      // insert the username into the database
-      match add_minecraft(&db, &command.user.id, username).await {
-        Ok(_) => {}
-        Err(e) => {
-          dbg!("{:?}", e);
-          return format!("Failure to minecraft username {:?}", username);
+      // this is always true unless they state its not
+      let mut java = true;
+      if let Some(x) = command.data.options.get(1) {
+        if let Some(CommandDataOptionValue::Boolean(z)) = x.to_owned().resolved {
+          java = !z;
+        }
+      }
+
+      let username_mc;
+      if java {
+        // insert the username into the database
+        match add_minecraft(&db, &command.user.id, username).await {
+          Ok(_) => {}
+          Err(e) => {
+            dbg!("{:?}", e);
+            return format!("Failure to minecraft username {:?}", username);
+          }
+        }
+        username_mc = username.to_string();
+      } else {
+        match get_minecraft_bedrock(username, &config.minecraft_mcprofile).await {
+          None => {
+            return format!("No UID found for {:?}", username);
+          }
+          Some(x) => {
+            match add_minecraft_bedrock(&db, &command.user.id, &x.floodgateuid).await {
+              Ok(_) => {}
+              Err(e) => {
+                dbg!("{:?}", e);
+                return format!("Failure to minecraft UID {:?}", &x.floodgateuid);
+              }
+            }
+
+            username_mc = x.floodgateuid;
+          }
         }
       }
 
       // get a list of servers that the user is a member of
       if let Ok(servers) = get_servers(&db, &command.user.id).await {
         for server in servers {
-          whitelist_update(&vec![username.to_string()], &server.minecraft, &config.discord_token_minecraft).await;
+          whitelist_update(&vec![(username_mc.to_owned(), java)], &server.minecraft, &config.discord_token_minecraft).await;
         }
       }
 
@@ -96,6 +136,51 @@ pub(crate) mod user {
       .await
     }
 
+    #[derive(Serialize, Deserialize, Debug)]
+    struct BedrockDetails {
+      pub gamertag: String,
+      pub xuid: String,
+      pub floodgateuid: String,
+      pub icon: String,
+      pub gamescore: String,
+      pub accounttier: String,
+      pub textureid: String,
+      pub skin: String,
+      pub linked: bool,
+      pub java_uuid: String,
+      pub java_name: String,
+    }
+
+    async fn get_minecraft_bedrock(username: &str, api_key: &str) -> Option<BedrockDetails> {
+      let url = format!("https://mcprofile.io/api/v1/bedrock/gamertag/{username}/");
+      match surf::get(url)
+        .header("x-api-key", api_key)
+        .header("User-Agent", "UL Computer Society")
+        .recv_json()
+        .await
+      {
+        Ok(res) => Some(res),
+        Err(e) => {
+          dbg!(e);
+          None
+        }
+      }
+    }
+
+    async fn add_minecraft_bedrock(db: &Pool<Sqlite>, user: &UserId, minecraft: &str) -> Result<Option<Wolves>, Error> {
+      sqlx::query_as::<_, Wolves>(
+        "
+      UPDATE wolves
+      SET minecraft_uid = ?2
+      WHERE discord = ?1;
+    ",
+      )
+      .bind(*user.as_u64() as i64)
+      .bind(minecraft)
+      .fetch_optional(db)
+      .await
+    }
+
     async fn get_servers(db: &Pool<Sqlite>, discord: &UserId) -> Result<Vec<Minecraft>, Error> {
       sqlx::query_as::<_, Minecraft>(
         "
@@ -249,7 +334,7 @@ pub(crate) mod server {
             ID:     {id}
             Online: {online}
             Info:   {description} 
-            Link:   <http://panel.games.skynet.ie/server/{id}>
+            Link:   <https://panel.games.skynet.ie/server/{id}>
             "#,
             name = &x.attributes.name,
             online = !x.attributes.is_suspended,
diff --git a/src/lib.rs b/src/lib.rs
index 09f60bf..1b5000d 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -9,6 +9,8 @@ use serenity::model::prelude::application_command::ApplicationCommandInteraction
 use serenity::prelude::TypeMapKey;
 use std::{env, sync::Arc};
 use tokio::sync::RwLock;
+
+#[derive(Debug)]
 pub struct Config {
   // manages where teh database is stored
   pub home: String,
@@ -17,6 +19,7 @@ pub struct Config {
   // tokens for discord and other API's
   pub discord_token: String,
   pub discord_token_minecraft: String,
+  pub minecraft_mcprofile: String,
 
   // email settings
   pub mail_smtp: String,
@@ -44,6 +47,7 @@ pub fn get_config() -> Config {
   let mut config = Config {
     discord_token: "".to_string(),
     discord_token_minecraft: "".to_string(),
+    minecraft_mcprofile: "".to_string(),
 
     home: ".".to_string(),
     database: "database.db".to_string(),
@@ -52,7 +56,6 @@ pub fn get_config() -> Config {
     mail_user: "".to_string(),
     mail_pass: "".to_string(),
     wolves_url: "".to_string(),
-    wolves_api: "".to_string(),
     committee_server: GuildId(0),
     committee_role: RoleId(0),
     committee_category: ChannelId(0),
@@ -71,6 +74,9 @@ pub fn get_config() -> Config {
   if let Ok(x) = env::var("DISCORD_TOKEN_MINECRAFT") {
     config.discord_token_minecraft = x.trim().to_string();
   }
+  if let Ok(x) = env::var("MINECRAFT_MCPROFILE_KEY") {
+    config.minecraft_mcprofile = x.trim().to_string();
+  }
 
   if let Ok(x) = env::var("EMAIL_SMTP") {
     config.mail_smtp = x.trim().to_string();
@@ -85,7 +91,6 @@ pub fn get_config() -> Config {
   if let Ok(x) = env::var("WOLVES_URL_BASE") {
     config.wolves_url = x.trim().to_string();
   }
-
   if let Ok(x) = env::var("WOLVES_API") {
     config.wolves_api = x.trim().to_string();
   }
@@ -109,6 +114,295 @@ pub fn get_config() -> Config {
   config
 }
 
+#[derive(Debug, Clone, Deserialize, Serialize)]
+pub struct ServerMembers {
+  pub server: GuildId,
+  pub id_wolves: i64,
+  pub expiry: String,
+}
+impl<'r> FromRow<'r, SqliteRow> for ServerMembers {
+  fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
+    let server_tmp: i64 = row.try_get("server")?;
+    let server = GuildId::from(server_tmp as u64);
+
+    Ok(Self {
+      server,
+      id_wolves: row.try_get("id_wolves")?,
+      expiry: row.try_get("expiry")?,
+    })
+  }
+}
+
+#[derive(Debug, Clone, Deserialize, Serialize)]
+pub struct ServerMembersWolves {
+  pub server: GuildId,
+  pub id_wolves: i64,
+  pub expiry: String,
+  pub email: String,
+  pub discord: Option<UserId>,
+  pub minecraft: Option<String>,
+  pub minecraft_uid: Option<String>,
+}
+impl<'r> FromRow<'r, SqliteRow> for ServerMembersWolves {
+  fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
+    let server_tmp: i64 = row.try_get("server")?;
+    let server = GuildId::from(server_tmp as u64);
+    let discord = match row.try_get("discord") {
+      Ok(x) => {
+        let tmp: i64 = x;
+        if tmp == 0 {
+          None
+        } else {
+          Some(UserId::from(tmp as u64))
+        }
+      }
+      _ => None,
+    };
+
+    Ok(Self {
+      server,
+      id_wolves: row.try_get("id_wolves")?,
+      expiry: row.try_get("expiry")?,
+      email: row.try_get("email")?,
+      discord,
+      minecraft: row.try_get("minecraft")?,
+      minecraft_uid: row.try_get("minecraft_uid")?,
+    })
+  }
+}
+
+#[derive(Debug, Clone, Deserialize, Serialize)]
+pub struct Wolves {
+  pub id_wolves: i64,
+  pub email: String,
+  pub discord: Option<UserId>,
+  pub minecraft: Option<String>,
+}
+impl<'r> FromRow<'r, SqliteRow> for Wolves {
+  fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
+    let discord = match row.try_get("discord") {
+      Ok(x) => {
+        let tmp: i64 = x;
+        if tmp == 0 {
+          None
+        } else {
+          Some(UserId::from(tmp as u64))
+        }
+      }
+      _ => None,
+    };
+
+    Ok(Self {
+      id_wolves: row.try_get("id_wolves")?,
+      email: row.try_get("email")?,
+      discord,
+      minecraft: row.try_get("minecraft")?,
+    })
+  }
+}
+
+#[derive(Debug, Clone, Deserialize, Serialize)]
+pub struct WolvesVerify {
+  pub email: String,
+  pub discord: UserId,
+  pub auth_code: String,
+  pub date_expiry: String,
+}
+impl<'r> FromRow<'r, SqliteRow> for WolvesVerify {
+  fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
+    let user_tmp: i64 = row.try_get("discord")?;
+    let discord = UserId::from(user_tmp as u64);
+
+    Ok(Self {
+      email: row.try_get("email")?,
+      discord,
+      auth_code: row.try_get("auth_code")?,
+      date_expiry: row.try_get("date_expiry")?,
+    })
+  }
+}
+
+#[derive(Debug, Clone, Deserialize, Serialize)]
+pub struct Committee {
+  pub email: String,
+  pub discord: UserId,
+  pub auth_code: String,
+  pub committee: i64,
+}
+impl<'r> FromRow<'r, SqliteRow> for Committee {
+  fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
+    let user_tmp: i64 = row.try_get("discord")?;
+    let discord = UserId::from(user_tmp as u64);
+
+    Ok(Self {
+      email: row.try_get("email")?,
+      discord,
+      auth_code: row.try_get("auth_code")?,
+      committee: row.try_get("committee")?,
+    })
+  }
+}
+
+#[derive(Debug, Clone, Deserialize, Serialize)]
+pub struct Servers {
+  pub server: GuildId,
+  pub wolves_api: String,
+  pub role_past: Option<RoleId>,
+  pub role_current: RoleId,
+  pub member_past: i64,
+  pub member_current: i64,
+  pub bot_channel_id: ChannelId,
+  // these can be removed in teh future with an API update
+  pub server_name: String,
+  pub wolves_link: String,
+}
+impl<'r> FromRow<'r, SqliteRow> for Servers {
+  fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
+    let server_tmp: i64 = row.try_get("server")?;
+    let server = GuildId::from(server_tmp as u64);
+    let role_past = match row.try_get("role_past") {
+      Ok(x) => {
+        let tmp: i64 = x;
+        if tmp == 0 {
+          None
+        } else {
+          Some(RoleId::from(tmp as u64))
+        }
+      }
+      _ => None,
+    };
+    let role_current = match row.try_get("role_current") {
+      Ok(x) => {
+        let tmp: i64 = x;
+        RoleId::from(tmp as u64)
+      }
+      _ => RoleId::from(0u64),
+    };
+
+    let bot_channel_tmp: i64 = row.try_get("bot_channel_id")?;
+    let bot_channel_id = ChannelId::from(bot_channel_tmp as u64);
+
+    Ok(Self {
+      server,
+      wolves_api: row.try_get("wolves_api")?,
+      role_past,
+      role_current,
+      member_past: row.try_get("member_past")?,
+      member_current: row.try_get("member_current")?,
+      bot_channel_id,
+      server_name: row.try_get("server_name")?,
+      wolves_link: row.try_get("wolves_link")?,
+    })
+  }
+}
+
+#[derive(Debug, Clone, Deserialize, Serialize)]
+pub struct Minecraft {
+  pub discord: GuildId,
+  pub minecraft: String,
+}
+impl<'r> FromRow<'r, SqliteRow> for Minecraft {
+  fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
+    let server_tmp: i64 = row.try_get("server_discord")?;
+    let discord = GuildId::from(server_tmp as u64);
+
+    Ok(Self {
+      discord,
+      minecraft: row.try_get("server_minecraft")?,
+    })
+  }
+}
+
+#[derive(Debug, Clone, Deserialize, Serialize)]
+pub struct RoleAdder {
+  pub server: GuildId,
+  pub role_a: RoleId,
+  pub role_b: RoleId,
+  pub role_c: RoleId,
+}
+impl<'r> FromRow<'r, SqliteRow> for RoleAdder {
+  fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
+    let server_tmp: i64 = row.try_get("server")?;
+    let server = GuildId::from(server_tmp as u64);
+
+    Ok(Self {
+      server,
+      role_a: get_role_from_row(row, "role_a"),
+      role_b: get_role_from_row(row, "role_b"),
+      role_c: get_role_from_row(row, "role_c"),
+    })
+  }
+}
+
+fn get_role_from_row(row: &SqliteRow, col: &str) -> RoleId {
+  match row.try_get(col) {
+    Ok(x) => {
+      let tmp: i64 = x;
+      RoleId(tmp as u64)
+    }
+    _ => RoleId::from(0u64),
+  }
+}
+
+pub async fn db_init(config: &Config) -> Result<Pool<Sqlite>, Error> {
+  let database = format!("{}/{}", &config.home, &config.database);
+
+  let pool = SqlitePoolOptions::new()
+    .max_connections(5)
+    .connect_with(
+      SqliteConnectOptions::from_str(&format!("sqlite://{}", database))?
+        .foreign_keys(true)
+        .create_if_missing(true),
+    )
+    .await?;
+
+  // migrations are amazing!
+  sqlx::migrate!("./db/migrations").run(&pool).await?;
+
+  Ok(pool)
+}
+
+pub async fn get_server_config(db: &Pool<Sqlite>, server: &GuildId) -> Option<Servers> {
+  sqlx::query_as::<_, Servers>(
+    r#"
+    SELECT *
+    FROM servers
+    WHERE server = ?
+    "#,
+  )
+  .bind(*server.as_u64() as i64)
+  .fetch_one(db)
+  .await
+  .ok()
+}
+
+pub async fn get_server_member(db: &Pool<Sqlite>, server: &GuildId, member: &guild::Member) -> Result<ServerMembersWolves, Error> {
+  sqlx::query_as::<_, ServerMembersWolves>(
+    r#"
+    SELECT *
+    FROM server_members
+    JOIN wolves USING (id_wolves)
+    WHERE server = ? AND discord = ?
+    "#,
+  )
+  .bind(*server.as_u64() as i64)
+  .bind(*member.user.id.as_u64() as i64)
+  .fetch_one(db)
+  .await
+}
+
+pub async fn get_server_config_bulk(db: &Pool<Sqlite>) -> Vec<Servers> {
+  sqlx::query_as::<_, Servers>(
+    r#"
+        SELECT *
+        FROM servers
+        "#,
+  )
+  .fetch_all(db)
+  .await
+  .unwrap_or_default()
+}
+
 pub fn get_now_iso(short: bool) -> String {
   let now = Utc::now();
   if short {
@@ -122,6 +416,255 @@ pub fn random_string(len: usize) -> String {
   thread_rng().sample_iter(&Alphanumeric).take(len).map(char::from).collect()
 }
 
+pub mod set_roles {
+  use super::*;
+  pub async fn update_server(ctx: &Context, server: &Servers, remove_roles: &[Option<RoleId>], members_changed: &[UserId]) {
+    let db_lock = {
+      let data_read = ctx.data.read().await;
+      data_read.get::<DataBase>().expect("Expected Database in TypeMap.").clone()
+    };
+
+    let db = db_lock.read().await;
+
+    let Servers {
+      server,
+      role_past,
+      role_current,
+      ..
+    } = server;
+
+    let mut roles_set = [0, 0, 0];
+    let mut members = vec![];
+
+    for member in get_server_member_bulk(&db, server).await {
+      if let Some(x) = member.discord {
+        members.push(x);
+      }
+    }
+    let mut members_all = members.len();
+
+    if let Ok(x) = server.members(ctx, None, None).await {
+      for mut member in x {
+        // members_changed acts as an override to only deal with teh users in it
+        if !members_changed.is_empty() && !members_changed.contains(&member.user.id) {
+          continue;
+        }
+
+        if members.contains(&member.user.id) {
+          let mut roles = vec![];
+
+          if let Some(role) = &role_past {
+            if !member.roles.contains(role) {
+              roles_set[0] += 1;
+              roles.push(role.to_owned());
+            }
+          }
+
+          if !member.roles.contains(role_current) {
+            roles_set[1] += 1;
+            roles.push(role_current.to_owned());
+          }
+
+          if let Err(e) = member.add_roles(ctx, &roles).await {
+            println!("{:?}", e);
+          }
+        } else {
+          // old and never
+
+          if let Some(role) = &role_past {
+            if member.roles.contains(role) {
+              members_all += 1;
+            }
+          }
+
+          if member.roles.contains(role_current) {
+            roles_set[2] += 1;
+            // if theya re not a current member and have the role then remove it
+            if let Err(e) = member.remove_role(ctx, role_current).await {
+              println!("{:?}", e);
+            }
+          }
+        }
+        for role in remove_roles.iter().flatten() {
+          if let Err(e) = member.remove_role(ctx, role).await {
+            println!("{:?}", e);
+          }
+        }
+      }
+    }
+
+    set_server_numbers(&db, server, members_all as i64, members.len() as i64).await;
+
+    // small bit of logging to note changes over time
+    println!("{:?} Changes: New: +{}, Current: +{}/-{}", server.as_u64(), roles_set[0], roles_set[1], roles_set[2]);
+  }
+
+  pub async fn get_server_member_bulk(db: &Pool<Sqlite>, server: &GuildId) -> Vec<ServerMembersWolves> {
+    sqlx::query_as::<_, ServerMembersWolves>(
+      r#"
+      SELECT * 
+      FROM server_members
+      JOIN wolves USING (id_wolves)
+      WHERE (
+        server = ? 
+        AND discord IS NOT NULL
+        AND expiry > ?
+      )
+      "#,
+    )
+    .bind(*server.as_u64() as i64)
+    .bind(get_now_iso(true))
+    .fetch_all(db)
+    .await
+    .unwrap_or_default()
+  }
+
+  async fn set_server_numbers(db: &Pool<Sqlite>, server: &GuildId, past: i64, current: i64) {
+    match sqlx::query_as::<_, Wolves>(
+      "
+      UPDATE servers
+      SET member_past = ?, member_current = ?
+      WHERE server = ?
+    ",
+    )
+    .bind(past)
+    .bind(current)
+    .bind(*server.as_u64() as i64)
+    .fetch_optional(db)
+    .await
+    {
+      Ok(_) => {}
+      Err(e) => {
+        println!("Failure to insert into {}", server.as_u64());
+        println!("{:?}", e);
+      }
+    }
+  }
+}
+
+pub mod get_data {
+  use super::*;
+  use crate::set_roles::update_server;
+  use std::collections::BTreeMap;
+  use wolves_oxidised::WolvesUser;
+
+  pub async fn get_wolves(ctx: &Context) {
+    let db_lock = {
+      let data_read = ctx.data.read().await;
+      data_read.get::<DataBase>().expect("Expected Database in TypeMap.").clone()
+    };
+    let db = db_lock.read().await;
+
+    let config_lock = {
+      let data_read = ctx.data.read().await;
+      data_read.get::<Config>().expect("Expected Config in TypeMap.").clone()
+    };
+    let config = config_lock.read().await;
+
+    // set up teh client
+    let wolves = wolves_oxidised::Client::new(&config.wolves_url, Some(&config.wolves_api));
+
+    for server_config in get_server_config_bulk(&db).await {
+      let Servers {
+        server,
+        wolves_api,
+        ..
+      } = &server_config;
+
+      let existing_tmp = get_server_member(&db, server).await;
+      let existing = existing_tmp.iter().map(|data| (data.id_wolves, data)).collect::<BTreeMap<_, _>>();
+
+      // list of users that need to be updated for this server
+      let mut user_to_update = vec![];
+      for user in wolves.get_members(wolves_api).await {
+        let id = user.member_id.parse::<u64>().unwrap_or_default();
+        match existing.get(&(id as i64)) {
+          None => {
+            // user does not exist already, add everything
+            add_users_wolves(&db, &user).await;
+            add_users_server_members(&db, server, &user).await;
+          }
+          Some(old) => {
+            // always update wolves table, in case data has changed
+            add_users_wolves(&db, &user).await;
+            if old.expiry != user.expiry {
+              add_users_server_members(&db, server, &user).await;
+
+              if let Some(discord_id) = old.discord {
+                user_to_update.push(discord_id);
+              }
+            }
+          }
+        }
+      }
+
+      if !user_to_update.is_empty() {
+        update_server(ctx, &server_config, &[], &user_to_update).await;
+      }
+    }
+  }
+
+  pub async fn get_server_member(db: &Pool<Sqlite>, server: &GuildId) -> Vec<ServerMembersWolves> {
+    sqlx::query_as::<_, ServerMembersWolves>(
+      r#"
+      SELECT *
+      FROM server_members
+      JOIN wolves USING (id_wolves)
+      WHERE (
+        server = ?
+        AND discord IS NOT NULL
+      )
+      "#,
+    )
+    .bind(*server.as_u64() as i64)
+    .fetch_all(db)
+    .await
+    .unwrap_or_default()
+  }
+
+  async fn add_users_wolves(db: &Pool<Sqlite>, user: &WolvesUser) {
+    // expiry
+    match sqlx::query_as::<_, Wolves>(
+      "
+      INSERT INTO wolves (id_wolves, email)
+      VALUES ($1, $2)
+      ON CONFLICT(id_wolves) DO UPDATE SET email = $2
+    ",
+    )
+    .bind(&user.member_id)
+    .bind(&user.contact_email)
+    .fetch_optional(db)
+    .await
+    {
+      Ok(_) => {}
+      Err(e) => {
+        println!("Failure to insert into Wolves {:?}", user);
+        println!("{:?}", e);
+      }
+    }
+  }
+  async fn add_users_server_members(db: &Pool<Sqlite>, server: &GuildId, user: &WolvesUser) {
+    match sqlx::query_as::<_, ServerMembers>(
+      "
+      INSERT OR REPLACE INTO server_members (server, id_wolves, expiry)
+      VALUES (?1, ?2, ?3)
+    ",
+    )
+    .bind(*server.as_u64() as i64)
+    .bind(&user.member_id)
+    .bind(&user.expiry)
+    .fetch_optional(db)
+    .await
+    {
+      Ok(_) => {}
+      Err(e) => {
+        println!("Failure to insert into ServerMembers {}  {:?}", server.as_u64(), user);
+        println!("{:?}", e);
+      }
+    }
+  }
+}
+
 /**
  For any time ye need to check if a user who calls a command has admin privlages
 */
@@ -159,3 +702,153 @@ pub async fn is_admin(command: &ApplicationCommandInteraction, ctx: &Context) ->
     None
   }
 }
+
+/**
+loop through all members of server
+get a list of folks with mc accounts that are members
+and a list that arent members
+ */
+pub async fn update_server(server_id: &str, db: &Pool<Sqlite>, g_id: &GuildId, config: &Config) {
+  let mut usernames = vec![];
+  for member in get_server_member_bulk(db, g_id).await {
+    if let Some(x) = member.minecraft {
+      usernames.push((x, true));
+    }
+    if let Some(x) = member.minecraft_uid {
+      usernames.push((x, false));
+    }
+  }
+  if !usernames.is_empty() {
+    whitelist_update(&usernames, server_id, &config.discord_token_minecraft).await;
+  }
+}
+
+async fn post<T: Serialize>(url: &str, bearer: &str, data: &T) {
+  match surf::post(url)
+    .header("Authorization", bearer)
+    .header("Content-Type", "application/json")
+    .header("Accept", "Application/vnd.pterodactyl.v1+json")
+    .body_json(&data)
+  {
+    Ok(req) => {
+      req.await.ok();
+    }
+    Err(e) => {
+      dbg!(e);
+    }
+  }
+}
+
+#[derive(Deserialize, Serialize, Debug)]
+pub struct ServerDetailsResSub {
+  pub identifier: String,
+  pub name: String,
+  pub description: String,
+  pub is_suspended: bool,
+}
+#[derive(Deserialize, Serialize, Debug)]
+pub struct ServerDetailsRes {
+  pub attributes: ServerDetailsResSub,
+}
+
+async fn get<T: Serialize + DeserializeOwned>(url: &str, bearer: &str) -> Option<T> {
+  match surf::get(url)
+    .header("Authorization", bearer)
+    .header("Content-Type", "application/json")
+    .header("Accept", "Application/vnd.pterodactyl.v1+json")
+    .recv_json()
+    .await
+  {
+    Ok(res) => Some(res),
+    Err(e) => {
+      dbg!(e);
+
+      None
+    }
+  }
+}
+
+#[derive(Deserialize, Serialize, Debug)]
+struct BodyCommand {
+  command: String,
+}
+
+#[derive(Deserialize, Serialize, Debug)]
+struct BodyDelete {
+  root: String,
+  files: Vec<String>,
+}
+
+pub async fn whitelist_update(add: &Vec<(String, bool)>, server: &str, token: &str) {
+  println!("Update whitelist for {}", server);
+  let url_base = format!("https://panel.games.skynet.ie/api/client/servers/{server}");
+  let bearer = format!("Bearer {token}");
+
+  for (name, java) in add {
+    let data = if *java {
+      BodyCommand {
+        command: format!("whitelist add {name}"),
+      }
+    } else {
+      BodyCommand {
+        command: format!("fwhitelist add {name}"),
+      }
+    };
+    post(&format!("{url_base}/command"), &bearer, &data).await;
+  }
+}
+
+pub async fn whitelist_wipe(server: &str, token: &str) {
+  println!("Wiping whitelist for {}", server);
+  let url_base = format!("https://panel.games.skynet.ie/api/client/servers/{server}");
+  let bearer = format!("Bearer {token}");
+
+  // delete whitelist
+  let deletion = BodyDelete {
+    root: "/".to_string(),
+    files: vec!["whitelist.json".to_string()],
+  };
+  post(&format!("{url_base}/files/delete"), &bearer, &deletion).await;
+
+  // recreate teh file, passing in the type here so the compiler knows what type of vec it is
+  post::<Vec<&str>>(&format!("{url_base}/files/write?file=%2Fwhitelist.json"), &bearer, &vec![]).await;
+
+  // reload the whitelist
+  let data = BodyCommand {
+    command: "whitelist reload".to_string(),
+  };
+  post(&format!("{url_base}/command"), &bearer, &data).await;
+}
+
+pub async fn server_information(server: &str, token: &str) -> Option<ServerDetailsRes> {
+  println!("Get server information for {}", server);
+  let url_base = format!("https://panel.games.skynet.ie/api/client/servers/{server}");
+  let bearer = format!("Bearer {token}");
+  get::<ServerDetailsRes>(&format!("{url_base}/"), &bearer).await
+}
+
+pub async fn get_minecraft_config(db: &Pool<Sqlite>) -> Vec<Minecraft> {
+  sqlx::query_as::<_, Minecraft>(
+    r#"
+        SELECT *
+        FROM minecraft
+        "#,
+  )
+  .fetch_all(db)
+  .await
+  .unwrap_or_default()
+}
+
+pub async fn get_minecraft_config_server(db: &Pool<Sqlite>, g_id: GuildId) -> Vec<Minecraft> {
+  sqlx::query_as::<_, Minecraft>(
+    r#"
+        SELECT *
+        FROM minecraft
+        WHERE server_discord = ?1
+        "#,
+  )
+  .bind(*g_id.as_u64() as i64)
+  .fetch_all(db)
+  .await
+  .unwrap_or_default()
+}