diff --git a/.formatter.exs b/.formatter.exs
index 98279b9..c5f250f 100644
--- a/.formatter.exs
+++ b/.formatter.exs
@@ -1,5 +1,5 @@
 [
-  import_deps: [:oban, :ecto, :ecto_sql, :phoenix, :tesla],
+  import_deps: [:oban, :oban_web, :ecto, :ecto_sql, :phoenix, :tesla],
   subdirectories: ["priv/*/migrations"],
   plugins: [Phoenix.LiveView.HTMLFormatter],
   inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"]
diff --git a/lib/core/syndication.ex b/lib/core/syndication.ex
index 981b95b..d5f1099 100644
--- a/lib/core/syndication.ex
+++ b/lib/core/syndication.ex
@@ -74,12 +74,22 @@ defmodule Core.Syndication do
   end
 
   def refresh_bluesky_account(%Schema.BlueskyAccount{} = bluesky_account) do
-    with {:ok, refresh_resp} <-
-           Syndication.BlueskyClient.refresh_session(bluesky_account),
-         {:ok, attrs} <- parse_bluesky_token_resp(refresh_resp) do
-      bluesky_account
-      |> Syndication.BlueskyAccount.refresh_changeset(attrs)
-      |> Core.Repo.update()
+    if DateTime.compare(DateTime.utc_now(), bluesky_account.access_jwt_exp) == :lt do
+      {:ok, bluesky_account}
+    else
+      with {:ok, refresh_resp} <-
+             Syndication.BlueskyClient.refresh_session(bluesky_account),
+           {:ok, attrs} <- parse_bluesky_token_resp(refresh_resp) do
+        %{"bluesky_account_id" => bluesky_account.id}
+        |> Core.Syndication.BlueskyRefreshWorker.new(
+          scheduled_at: DateTime.add(bluesky_account.refresh_jwt_exp, -1, :day)
+        )
+        |> Oban.insert()
+
+        bluesky_account
+        |> Syndication.BlueskyAccount.refresh_changeset(attrs)
+        |> Core.Repo.update()
+      end
     end
   end
 
@@ -87,7 +97,7 @@ defmodule Core.Syndication do
     post = Core.Repo.preload(post, [:bluesky_post])
 
     if post.syndicate_to_bluesky && is_nil(post.bluesky_post) do
-      bluesky_account = get_bluesky_account!()
+      {:ok, bluesky_account} = get_bluesky_account!() |> refresh_bluesky_account()
 
       with {:ok, resp} <- Syndication.BlueskyClient.post_status(bluesky_account, post.body) do
         post
diff --git a/lib/core/syndication/bluesky_refresh_worker.ex b/lib/core/syndication/bluesky_refresh_worker.ex
new file mode 100644
index 0000000..3b09544
--- /dev/null
+++ b/lib/core/syndication/bluesky_refresh_worker.ex
@@ -0,0 +1,12 @@
+defmodule Core.Syndication.BlueskyRefreshWorker do
+  use Oban.Worker
+
+  @impl true
+  def perform(%Oban.Job{args: %{"bluesky_account_id" => id}}) do
+    if bluesky_account = Core.Repo.get(Schema.BlueskyAccount, id) do
+      Core.Syndication.refresh_bluesky_account(bluesky_account)
+    else
+      {:cancel, "bluesky account not found"}
+    end
+  end
+end
diff --git a/lib/web/components/layouts/app.html.heex b/lib/web/components/layouts/app.html.heex
index 0330348..ac21cf0 100644
--- a/lib/web/components/layouts/app.html.heex
+++ b/lib/web/components/layouts/app.html.heex
@@ -9,6 +9,8 @@
       <nav>
         <ul class="flex flex-row gap-x-2">
           <li><.link navigate={~p"/admin/writing"}>admin</.link></li>
+          <li><.link navigate={~p"/admin/dashboard"}>dashboard</.link></li>
+          <li><.link navigate={~p"/admin/oban"}>oban</.link></li>
         </ul>
       </nav>
     </section>
diff --git a/lib/web/router.ex b/lib/web/router.ex
index 6c540e4..ea0893b 100644
--- a/lib/web/router.ex
+++ b/lib/web/router.ex
@@ -1,6 +1,8 @@
 defmodule Web.Router do
   use Web, :router
 
+  import Phoenix.LiveDashboard.Router
+  import Oban.Web.Router
   import Web.UserAuth
 
   pipeline :browser do
@@ -35,6 +37,9 @@ defmodule Web.Router do
   scope "/admin", Web do
     pipe_through [:browser, :require_authenticated_user]
 
+    live_dashboard "/dashboard"
+    oban_dashboard "/oban"
+
     live_session :require_authenticated_user, on_mount: [{Web.UserAuth, :ensure_authenticated}] do
       live "/users/settings", UserSettingsLive, :edit
 
diff --git a/mix.exs b/mix.exs
index 9492cf2..993bbba 100644
--- a/mix.exs
+++ b/mix.exs
@@ -65,6 +65,7 @@ defmodule SlaonelyButSurely.MixProject do
       {:flop, "~> 0.26.1"},
       {:flop_phoenix, "~> 0.24.1"},
       {:oban, "~> 2.19"},
+      {:oban_web, "~> 2.11"},
       {:igniter, "~> 0.5", only: [:dev]},
       {:ueberauth, "~> 0.10"},
       {:ueberauth_mastodon, "~> 0.3.0"},
diff --git a/mix.lock b/mix.lock
index b34d993..34dda9d 100644
--- a/mix.lock
+++ b/mix.lock
@@ -42,6 +42,8 @@
   "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
   "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
   "oban": {:hex, :oban, "2.19.4", "045adb10db1161dceb75c254782f97cdc6596e7044af456a59decb6d06da73c1", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:igniter, "~> 0.5", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5fcc6219e6464525b808d97add17896e724131f498444a292071bf8991c99f97"},
+  "oban_met": {:hex, :oban_met, "1.0.2", "6a94e0711f70ad4dded62309e103cd85f3500bf6de7bfd5234fc20fb7121b870", [:mix], [{:oban, "~> 2.19", [hex: :oban, repo: "hexpm", optional: false]}], "hexpm", "e9103a9f3ad5a1751143b490b1ee572855a93956ad958b119ccee62c70984473"},
+  "oban_web": {:hex, :oban_web, "2.11.3", "dc4991263fae8ce21391ebcaebfc0a144ef434bf65f0a9fdafc2dd00424069b8", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:oban, "~> 2.19", [hex: :oban, repo: "hexpm", optional: false]}, {:oban_met, "~> 1.0", [hex: :oban_met, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}], "hexpm", "ec414e84cc283c3a06309a1167428a7ece0a5cc179c92b835b77c878278adf9b"},
   "owl": {:hex, :owl, "0.12.2", "65906b525e5c3ef51bab6cba7687152be017aebe1da077bb719a5ee9f7e60762", [:mix], [{:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: true]}], "hexpm", "6398efa9e1fea70a04d24231e10dcd66c1ac1aa2da418d20ef5357ec61de2880"},
   "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
   "phoenix": {:hex, :phoenix, "1.7.19", "36617efe5afbd821099a8b994ff4618a340a5bfb25531a1802c4d4c634017a57", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "ba4dc14458278773f905f8ae6c2ec743d52c3a35b6b353733f64f02dfe096cd6"},