diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..61a7393
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,45 @@
+# This file excludes paths from the Docker build context.
+#
+# By default, Docker's build context includes all files (and folders) in the
+# current directory. Even if a file isn't copied into the container it is still sent to
+# the Docker daemon.
+#
+# There are multiple reasons to exclude files from the build context:
+#
+# 1. Prevent nested folders from being copied into the container (ex: exclude
+#    /assets/node_modules when copying /assets)
+# 2. Reduce the size of the build context and improve build time (ex. /build, /deps, /doc)
+# 3. Avoid sending files containing sensitive information
+#
+# More information on using .dockerignore is available here:
+# https://docs.docker.com/engine/reference/builder/#dockerignore-file
+
+.dockerignore
+
+# Ignore git, but keep git HEAD and refs to access current commit hash if needed:
+#
+# $ cat .git/HEAD | awk '{print ".git/"$2}' | xargs cat
+# d0b8727759e1e0e7aa3d41707d12376e373d5ecc
+.git
+!.git/HEAD
+!.git/refs
+
+# Common development/test artifacts
+/cover/
+/doc/
+/test/
+/tmp/
+.elixir_ls
+
+# Mix artifacts
+/_build/
+/deps/
+*.ez
+
+# Generated on crash by the VM
+erl_crash.dump
+
+# Static artifacts - These should be fetched and built inside the Docker image
+/assets/node_modules/
+/priv/static/assets/
+/priv/static/cache_manifest.json
diff --git a/.kamal/hooks/docker-setup.sample b/.kamal/hooks/docker-setup.sample
new file mode 100755
index 0000000..2fb07d7
--- /dev/null
+++ b/.kamal/hooks/docker-setup.sample
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+echo "Docker set up on $KAMAL_HOSTS..."
diff --git a/.kamal/hooks/post-app-boot.sample b/.kamal/hooks/post-app-boot.sample
new file mode 100755
index 0000000..70f9c4b
--- /dev/null
+++ b/.kamal/hooks/post-app-boot.sample
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+echo "Booted app version $KAMAL_VERSION on $KAMAL_HOSTS..."
diff --git a/.kamal/hooks/post-deploy.sample b/.kamal/hooks/post-deploy.sample
new file mode 100755
index 0000000..75efafc
--- /dev/null
+++ b/.kamal/hooks/post-deploy.sample
@@ -0,0 +1,14 @@
+#!/bin/sh
+
+# A sample post-deploy hook
+#
+# These environment variables are available:
+# KAMAL_RECORDED_AT
+# KAMAL_PERFORMER
+# KAMAL_VERSION
+# KAMAL_HOSTS
+# KAMAL_ROLE (if set)
+# KAMAL_DESTINATION (if set)
+# KAMAL_RUNTIME
+
+echo "$KAMAL_PERFORMER deployed $KAMAL_VERSION to $KAMAL_DESTINATION in $KAMAL_RUNTIME seconds"
diff --git a/.kamal/hooks/post-proxy-reboot.sample b/.kamal/hooks/post-proxy-reboot.sample
new file mode 100755
index 0000000..1435a67
--- /dev/null
+++ b/.kamal/hooks/post-proxy-reboot.sample
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+echo "Rebooted kamal-proxy on $KAMAL_HOSTS"
diff --git a/.kamal/hooks/pre-app-boot.sample b/.kamal/hooks/pre-app-boot.sample
new file mode 100755
index 0000000..45f7355
--- /dev/null
+++ b/.kamal/hooks/pre-app-boot.sample
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+echo "Booting app version $KAMAL_VERSION on $KAMAL_HOSTS..."
diff --git a/.kamal/hooks/pre-build.sample b/.kamal/hooks/pre-build.sample
new file mode 100755
index 0000000..f87d811
--- /dev/null
+++ b/.kamal/hooks/pre-build.sample
@@ -0,0 +1,51 @@
+#!/bin/sh
+
+# A sample pre-build hook
+#
+# Checks:
+# 1. We have a clean checkout
+# 2. A remote is configured
+# 3. The branch has been pushed to the remote
+# 4. The version we are deploying matches the remote
+#
+# These environment variables are available:
+# KAMAL_RECORDED_AT
+# KAMAL_PERFORMER
+# KAMAL_VERSION
+# KAMAL_HOSTS
+# KAMAL_ROLE (if set)
+# KAMAL_DESTINATION (if set)
+
+if [ -n "$(git status --porcelain)" ]; then
+  echo "Git checkout is not clean, aborting..." >&2
+  git status --porcelain >&2
+  exit 1
+fi
+
+first_remote=$(git remote)
+
+if [ -z "$first_remote" ]; then
+  echo "No git remote set, aborting..." >&2
+  exit 1
+fi
+
+current_branch=$(git branch --show-current)
+
+if [ -z "$current_branch" ]; then
+  echo "Not on a git branch, aborting..." >&2
+  exit 1
+fi
+
+remote_head=$(git ls-remote $first_remote --tags $current_branch | cut -f1)
+
+if [ -z "$remote_head" ]; then
+  echo "Branch not pushed to remote, aborting..." >&2
+  exit 1
+fi
+
+if [ "$KAMAL_VERSION" != "$remote_head" ]; then
+  echo "Version ($KAMAL_VERSION) does not match remote HEAD ($remote_head), aborting..." >&2
+  exit 1
+fi
+
+exit 0
diff --git a/.kamal/hooks/pre-connect.sample b/.kamal/hooks/pre-connect.sample
new file mode 100755
index 0000000..18e61d7
--- /dev/null
+++ b/.kamal/hooks/pre-connect.sample
@@ -0,0 +1,47 @@
+#!/usr/bin/env ruby
+
+# A sample pre-connect check
+#
+# Warms DNS before connecting to hosts in parallel
+#
+# These environment variables are available:
+# KAMAL_RECORDED_AT
+# KAMAL_PERFORMER
+# KAMAL_VERSION
+# KAMAL_HOSTS
+# KAMAL_ROLE (if set)
+# KAMAL_DESTINATION (if set)
+# KAMAL_RUNTIME
+
+hosts = ENV["KAMAL_HOSTS"].split(",")
+results = nil
+max = 3
+
+elapsed = Benchmark.realtime do
+  results = hosts.map do |host|
+    Thread.new do
+      tries = 1
+
+      begin
+        Socket.getaddrinfo(host, 0, Socket::AF_UNSPEC, Socket::SOCK_STREAM, nil, Socket::AI_CANONNAME)
+      rescue SocketError
+        if tries < max
+          puts "Retrying DNS warmup: #{host}"
+          tries += 1
+          sleep rand
+          retry
+        else
+          puts "DNS warmup failed: #{host}"
+          host
+        end
+      end
+
+      tries
+    end
+  end.map(&:value)
+end
+
+retries = results.sum - hosts.size
+nopes = results.count { |r| r == max }
+
+puts "Prewarmed %d DNS lookups in %.2f sec: %d retries, %d failures" % [ hosts.size, elapsed, retries, nopes ]
diff --git a/.kamal/hooks/pre-deploy.sample b/.kamal/hooks/pre-deploy.sample
new file mode 100755
index 0000000..1b280c7
--- /dev/null
+++ b/.kamal/hooks/pre-deploy.sample
@@ -0,0 +1,109 @@
+#!/usr/bin/env ruby
+
+# A sample pre-deploy hook
+#
+# Checks the Github status of the build, waiting for a pending build to complete for up to 720 seconds.
+#
+# Fails unless the combined status is "success"
+#
+# These environment variables are available:
+# KAMAL_RECORDED_AT
+# KAMAL_PERFORMER
+# KAMAL_VERSION
+# KAMAL_HOSTS
+# KAMAL_COMMAND
+# KAMAL_SUBCOMMAND
+# KAMAL_ROLE (if set)
+# KAMAL_DESTINATION (if set)
+
+# Only check the build status for production deployments
+if ENV["KAMAL_COMMAND"] == "rollback" || ENV["KAMAL_DESTINATION"] != "production"
+  exit 0
+end
+
+require "bundler/inline"
+
+# true = install gems so this is fast on repeat invocations
+gemfile(true, quiet: true) do
+  source "https://rubygems.org"
+
+  gem "octokit"
+  gem "faraday-retry"
+end
+
+MAX_ATTEMPTS = 72
+ATTEMPTS_GAP = 10
+
+def exit_with_error(message)
+  $stderr.puts message
+  exit 1
+end
+
+class GithubStatusChecks
+  attr_reader :remote_url, :git_sha, :github_client, :combined_status
+
+  def initialize
+    @remote_url = `git config --get remote.origin.url`.strip.delete_prefix("https://github.com/")
+    @git_sha = `git rev-parse HEAD`.strip
+    @github_client = Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"])
+    refresh!
+  end
+
+  def refresh!
+    @combined_status = github_client.combined_status(remote_url, git_sha)
+  end
+
+  def state
+    combined_status[:state]
+  end
+
+  def first_status_url
+    first_status = combined_status[:statuses].find { |status| status[:state] == state }
+    first_status && first_status[:target_url]
+  end
+
+  def complete_count
+    combined_status[:statuses].count { |status| status[:state] != "pending"}
+  end
+
+  def total_count
+    combined_status[:statuses].count
+  end
+
+  def current_status
+    if total_count > 0
+      "Completed #{complete_count}/#{total_count} checks, see #{first_status_url} ..."
+    else
+      "Build not started..."
+    end
+  end
+end
+
+
+$stdout.sync = true
+
+puts "Checking build status..."
+attempts = 0
+checks = GithubStatusChecks.new
+
+begin
+  loop do
+    case checks.state
+    when "success"
+      puts "Checks passed, see #{checks.first_status_url}"
+      exit 0
+    when "failure"
+      exit_with_error "Checks failed, see #{checks.first_status_url}"
+    when "pending"
+      attempts += 1
+    end
+
+    exit_with_error "Checks are still pending, gave up after #{MAX_ATTEMPTS * ATTEMPTS_GAP} seconds" if attempts == MAX_ATTEMPTS
+
+    puts checks.current_status
+    sleep(ATTEMPTS_GAP)
+    checks.refresh!
+  end
+rescue Octokit::NotFound
+  exit_with_error "Build status could not be found"
+end
diff --git a/.kamal/hooks/pre-proxy-reboot.sample b/.kamal/hooks/pre-proxy-reboot.sample
new file mode 100755
index 0000000..061f805
--- /dev/null
+++ b/.kamal/hooks/pre-proxy-reboot.sample
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+echo "Rebooting kamal-proxy on $KAMAL_HOSTS..."
diff --git a/.kamal/secrets b/.kamal/secrets
new file mode 100644
index 0000000..5663118
--- /dev/null
+++ b/.kamal/secrets
@@ -0,0 +1,11 @@
+# Secrets defined here are available for reference under registry/password, env/secret, builder/secrets,
+# and accessories/*/env/secret in config/deploy.yml. All secrets should be pulled from either
+# password manager, ENV, or a file. DO NOT ENTER RAW CREDENTIALS HERE! This file needs to be safe for git.
+
+SECRETS=$(kamal secrets fetch --adapter 1password --account Perrault --from Private/sloanelybutsurely.com KAMAL_REGISTRY_PASSWORD POSTGRES_PASSWORD SECRET_KEY_BASE)
+
+KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD $SECRETS)
+POSTGRES_PASSWORD=$(kamal secrets extract POSTGRES_PASSWORD $SECRETS)
+SECRET_KEY_BASE=$(kamal secrets extract SECRET_KEY_BASE $SECRETS)
+
+DATABASE_URL="postgresql://sloanely_but_surely_prod:$POSTGRES_PASSWORD@sloanelybutsurely-db:5432/sloanely_but_surely_prod"
diff --git a/.mise.toml b/.mise.toml
index 18d0485..08324db 100644
--- a/.mise.toml
+++ b/.mise.toml
@@ -2,3 +2,4 @@
 elixir = "1.18.2-otp-27"
 erlang = "27.2.3"
 node = "22.14.0"
+ruby = "3.4.2"
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..10ffdac
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,97 @@
+# Find eligible builder and runner images on Docker Hub. We use Ubuntu/Debian
+# instead of Alpine to avoid DNS resolution issues in production.
+#
+# https://hub.docker.com/r/hexpm/elixir/tags?page=1&name=ubuntu
+# https://hub.docker.com/_/ubuntu?tab=tags
+#
+# This file is based on these images:
+#
+#   - https://hub.docker.com/r/hexpm/elixir/tags - for the build image
+#   - https://hub.docker.com/_/debian?tab=tags&page=1&name=bullseye-20250203-slim - for the release image
+#   - https://pkgs.org/ - resource for finding needed packages
+#   - Ex: hexpm/elixir:1.18.2-erlang-27.2.3-debian-bullseye-20250203-slim
+#
+ARG ELIXIR_VERSION=1.18.2
+ARG OTP_VERSION=27.2.3
+ARG DEBIAN_VERSION=bullseye-20250203-slim
+
+ARG BUILDER_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}"
+ARG RUNNER_IMAGE="debian:${DEBIAN_VERSION}"
+
+FROM ${BUILDER_IMAGE} as builder
+
+# install build dependencies
+RUN apt-get update -y && apt-get install -y build-essential git \
+    && apt-get clean && rm -f /var/lib/apt/lists/*_*
+
+# prepare build dir
+WORKDIR /app
+
+# install hex + rebar
+RUN mix local.hex --force && \
+    mix local.rebar --force
+
+# set build ENV
+ENV MIX_ENV="prod"
+
+# install mix dependencies
+COPY mix.exs mix.lock ./
+RUN mix deps.get --only $MIX_ENV
+RUN mkdir config
+
+# copy compile-time config files before we compile dependencies
+# to ensure any relevant config change will trigger the dependencies
+# to be re-compiled.
+COPY config/config.exs config/${MIX_ENV}.exs config/
+RUN mix deps.compile
+
+COPY priv priv
+
+COPY lib lib
+
+COPY assets assets
+
+# compile assets
+RUN mix assets.deploy
+
+# Compile the release
+RUN mix compile
+
+# Changes to config/runtime.exs don't require recompiling the code
+COPY config/runtime.exs config/
+
+COPY rel rel
+RUN mix release
+
+# start a new build stage so that the final image will only contain
+# the compiled release and other runtime necessities
+FROM ${RUNNER_IMAGE}
+
+RUN apt-get update -y && \
+  apt-get install -y libstdc++6 openssl libncurses5 locales ca-certificates \
+  && apt-get clean && rm -f /var/lib/apt/lists/*_*
+
+# Set the locale
+RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen
+
+ENV LANG en_US.UTF-8
+ENV LANGUAGE en_US:en
+ENV LC_ALL en_US.UTF-8
+
+WORKDIR "/app"
+RUN chown nobody /app
+
+# set runner ENV
+ENV MIX_ENV="prod"
+
+# Only copy the final release from the build stage
+COPY --from=builder --chown=nobody:root /app/_build/${MIX_ENV}/rel/sloanely_but_surely ./
+
+USER nobody
+
+# If using an environment that doesn't automatically reap zombie processes, it is
+# advised to add an init process such as tini via `apt-get install`
+# above and adding an entrypoint. See https://github.com/krallin/tini for details
+# ENTRYPOINT ["/tini", "--"]
+
+CMD ["/app/bin/server"]
diff --git a/Gemfile b/Gemfile
new file mode 100644
index 0000000..2fbb4f5
--- /dev/null
+++ b/Gemfile
@@ -0,0 +1,3 @@
+source 'https://rubygems.org'
+
+gem 'kamal'
diff --git a/Gemfile.lock b/Gemfile.lock
new file mode 100644
index 0000000..eda9e1b
--- /dev/null
+++ b/Gemfile.lock
@@ -0,0 +1,72 @@
+GEM
+  remote: https://rubygems.org/
+  specs:
+    activesupport (8.0.2)
+      base64
+      benchmark (>= 0.3)
+      bigdecimal
+      concurrent-ruby (~> 1.0, >= 1.3.1)
+      connection_pool (>= 2.2.5)
+      drb
+      i18n (>= 1.6, < 2)
+      logger (>= 1.4.2)
+      minitest (>= 5.1)
+      securerandom (>= 0.3)
+      tzinfo (~> 2.0, >= 2.0.5)
+      uri (>= 0.13.1)
+    base64 (0.2.0)
+    bcrypt_pbkdf (1.1.1)
+    bcrypt_pbkdf (1.1.1-arm64-darwin)
+    bcrypt_pbkdf (1.1.1-x86_64-darwin)
+    benchmark (0.4.0)
+    bigdecimal (3.1.9)
+    concurrent-ruby (1.3.5)
+    connection_pool (2.5.0)
+    dotenv (3.1.8)
+    drb (2.2.1)
+    ed25519 (1.3.0)
+    i18n (1.14.7)
+      concurrent-ruby (~> 1.0)
+    kamal (2.5.3)
+      activesupport (>= 7.0)
+      base64 (~> 0.2)
+      bcrypt_pbkdf (~> 1.0)
+      concurrent-ruby (~> 1.2)
+      dotenv (~> 3.1)
+      ed25519 (~> 1.2)
+      net-ssh (~> 7.3)
+      sshkit (>= 1.23.0, < 2.0)
+      thor (~> 1.3)
+      zeitwerk (>= 2.6.18, < 3.0)
+    logger (1.7.0)
+    minitest (5.25.5)
+    net-scp (4.1.0)
+      net-ssh (>= 2.6.5, < 8.0.0)
+    net-sftp (4.0.0)
+      net-ssh (>= 5.0.0, < 8.0.0)
+    net-ssh (7.3.0)
+    ostruct (0.6.1)
+    securerandom (0.4.1)
+    sshkit (1.24.0)
+      base64
+      logger
+      net-scp (>= 1.1.2)
+      net-sftp (>= 2.1.2)
+      net-ssh (>= 2.8.0)
+      ostruct
+    thor (1.3.2)
+    tzinfo (2.0.6)
+      concurrent-ruby (~> 1.0)
+    uri (1.0.3)
+    zeitwerk (2.7.2)
+
+PLATFORMS
+  arm64-darwin
+  ruby
+  x86_64-darwin
+
+DEPENDENCIES
+  kamal
+
+BUNDLED WITH
+   2.6.5
diff --git a/config/deploy.yml b/config/deploy.yml
new file mode 100644
index 0000000..ffbb3a3
--- /dev/null
+++ b/config/deploy.yml
@@ -0,0 +1,49 @@
+service: sloanelybutsurely
+image: sloanelybutsurely/sloanelybutsurely.com
+
+servers:
+  web:
+    - sloanelybutsurely
+
+env:
+  clear:
+    PHX_HOST: sloanelybutsurely.com
+    PORT: 4000
+  secret:
+    - DATABASE_URL
+    - SECRET_KEY_BASE
+
+# Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server.
+# Remove this section when using multiple web servers and ensure you terminate SSL at your load balancer.
+#
+# Note: If using Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption.
+proxy:
+  ssl: false
+  hosts:
+    - sloanelybutsurely.com
+    - sloanelybutsurely
+  app_port: 4000
+
+registry:
+  username: sloanelybutsurely
+  password:
+    - KAMAL_REGISTRY_PASSWORD
+
+builder:
+  arch: amd64
+  remote: ssh://root@sloanelybutsurely
+  local: false
+
+accessories:
+  db:
+    image: postgres:17-alpine
+    host: sloanelybutsurely
+    port: 5432
+    env:
+      clear:
+        POSTGRES_USER: sloanely_but_surely_prod
+        POSTGRES_DB: sloanely_but_surely_prod
+      secret:
+        - POSTGRES_PASSWORD
+    volumes:
+      - postgres_data:/var/lib/postgresql/data
diff --git a/lib/core.ex b/lib/core.ex
index 3ce5c5e..73576ec 100644
--- a/lib/core.ex
+++ b/lib/core.ex
@@ -1,4 +1,4 @@
 defmodule Core do
   @moduledoc false
-  use Boundary, deps: [Schema], exports: [Accounts, Posts, Author, DateTime]
+  use Boundary, deps: [Schema], exports: [Accounts, Posts, Author, DateTime, Release]
 end
diff --git a/lib/core/release.ex b/lib/core/release.ex
new file mode 100644
index 0000000..d91f9a0
--- /dev/null
+++ b/lib/core/release.ex
@@ -0,0 +1,30 @@
+defmodule Core.Release do
+  @moduledoc """
+  Used for executing DB release tasks when run in production without Mix
+  installed.
+  """
+  @app :sloanely_but_surely
+
+  def migrate do
+    load_app()
+
+    for repo <- repos() do
+      {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
+    end
+
+    :ok
+  end
+
+  def rollback(repo, version) do
+    load_app()
+    {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))
+  end
+
+  defp repos do
+    Application.fetch_env!(@app, :ecto_repos)
+  end
+
+  defp load_app do
+    Application.load(@app)
+  end
+end
diff --git a/lib/sloanely_but_surely/application.ex b/lib/sloanely_but_surely/application.ex
index c21a035..58d2b83 100644
--- a/lib/sloanely_but_surely/application.ex
+++ b/lib/sloanely_but_surely/application.ex
@@ -4,6 +4,8 @@ defmodule SloanelyButSurely.Application do
 
   @impl Application
   def start(_type, _args) do
+    :ok = Core.Release.migrate()
+
     children = [
       Core.Repo,
       {Phoenix.PubSub, name: Core.PubSub},
diff --git a/lib/sloanely_but_surely/mix.ex b/lib/sloanely_but_surely/mix.ex
deleted file mode 100644
index f34549e..0000000
--- a/lib/sloanely_but_surely/mix.ex
+++ /dev/null
@@ -1,4 +0,0 @@
-defmodule SloanelyButSurely.Mix do
-  @moduledoc false
-  use Boundary, deps: [], exports: []
-end
diff --git a/lib/web/endpoint.ex b/lib/web/endpoint.ex
index 80608e7..875a36c 100644
--- a/lib/web/endpoint.ex
+++ b/lib/web/endpoint.ex
@@ -1,6 +1,8 @@
 defmodule Web.Endpoint do
   use Phoenix.Endpoint, otp_app: :sloanely_but_surely
 
+  plug Web.HealthCheck
+
   # The session will be stored in the cookie and signed,
   # this means its contents can be read but not tampered with.
   # Set :encryption_salt if you would also like to encrypt it.
diff --git a/lib/web/health_check.ex b/lib/web/health_check.ex
new file mode 100644
index 0000000..3de1b3d
--- /dev/null
+++ b/lib/web/health_check.ex
@@ -0,0 +1,14 @@
+defmodule Web.HealthCheck do
+  @moduledoc "Silent HTTP health check plug"
+  import Plug.Conn
+
+  def init(opts), do: opts
+
+  def call(%Plug.Conn{request_path: "/up"} = conn, _opts) do
+    conn
+    |> send_resp(200, "")
+    |> halt()
+  end
+
+  def call(conn, _opts), do: conn
+end
diff --git a/mix.exs b/mix.exs
index 18337c0..fa6bf4f 100644
--- a/mix.exs
+++ b/mix.exs
@@ -84,10 +84,10 @@ defmodule SlaonelyButSurely.MixProject do
       "ecto.reset": ["ecto.drop", "ecto.setup"],
       test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"],
       "assets.setup": ["tailwind.install --if-missing", "esbuild.install --if-missing"],
-      "assets.build": ["tailwind cms", "esbuild cms"],
+      "assets.build": ["tailwind sloanely_but_surely", "esbuild sloanely_but_surely"],
       "assets.deploy": [
-        "tailwind cms --minify",
-        "esbuild cms --minify",
+        "tailwind sloanely_but_surely --minify",
+        "esbuild sloanely_but_surely --minify",
         "phx.digest"
       ]
     ]
diff --git a/priv/static/favicon-91f37b602a111216f1eef3aa337ad763.ico b/priv/static/favicon-91f37b602a111216f1eef3aa337ad763.ico
new file mode 100644
index 0000000..7f372bf
Binary files /dev/null and b/priv/static/favicon-91f37b602a111216f1eef3aa337ad763.ico differ
diff --git a/priv/static/images/logo-06a11be1f2cdde2c851763d00bdd2e80.svg b/priv/static/images/logo-06a11be1f2cdde2c851763d00bdd2e80.svg
new file mode 100644
index 0000000..9f26bab
--- /dev/null
+++ b/priv/static/images/logo-06a11be1f2cdde2c851763d00bdd2e80.svg
@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 71 48" fill="currentColor" aria-hidden="true">
+  <path
+    d="m26.371 33.477-.552-.1c-3.92-.729-6.397-3.1-7.57-6.829-.733-2.324.597-4.035 3.035-4.148 1.995-.092 3.362 1.055 4.57 2.39 1.557 1.72 2.984 3.558 4.514 5.305 2.202 2.515 4.797 4.134 8.347 3.634 3.183-.448 5.958-1.725 8.371-3.828.363-.316.761-.592 1.144-.886l-.241-.284c-2.027.63-4.093.841-6.205.735-3.195-.16-6.24-.828-8.964-2.582-2.486-1.601-4.319-3.746-5.19-6.611-.704-2.315.736-3.934 3.135-3.6.948.133 1.746.56 2.463 1.165.583.493 1.143 1.015 1.738 1.493 2.8 2.25 6.712 2.375 10.265-.068-5.842-.026-9.817-3.24-13.308-7.313-1.366-1.594-2.7-3.216-4.095-4.785-2.698-3.036-5.692-5.71-9.79-6.623C12.8-.623 7.745.14 2.893 2.361 1.926 2.804.997 3.319 0 4.149c.494 0 .763.006 1.032 0 2.446-.064 4.28 1.023 5.602 3.024.962 1.457 1.415 3.104 1.761 4.798.513 2.515.247 5.078.544 7.605.761 6.494 4.08 11.026 10.26 13.346 2.267.852 4.591 1.135 7.172.555ZM10.751 3.852c-.976.246-1.756-.148-2.56-.962 1.377-.343 2.592-.476 3.897-.528-.107.848-.607 1.306-1.336 1.49Zm32.002 37.924c-.085-.626-.62-.901-1.04-1.228-1.857-1.446-4.03-1.958-6.333-2-1.375-.026-2.735-.128-4.031-.61-.595-.22-1.26-.505-1.244-1.272.015-.78.693-1 1.31-1.184.505-.15 1.026-.247 1.6-.382-1.46-.936-2.886-1.065-4.787-.3-2.993 1.202-5.943 1.06-8.926-.017-1.684-.608-3.179-1.563-4.735-2.408l-.077.057c1.29 2.115 3.034 3.817 5.004 5.271 3.793 2.8 7.936 4.471 12.784 3.73A66.714 66.714 0 0 1 37 40.877c1.98-.16 3.866.398 5.753.899Zm-9.14-30.345c-.105-.076-.206-.266-.42-.069 1.745 2.36 3.985 4.098 6.683 5.193 4.354 1.767 8.773 2.07 13.293.51 3.51-1.21 6.033-.028 7.343 3.38.19-3.955-2.137-6.837-5.843-7.401-2.084-.318-4.01.373-5.962.94-5.434 1.575-10.485.798-15.094-2.553Zm27.085 15.425c.708.059 1.416.123 2.124.185-1.6-1.405-3.55-1.517-5.523-1.404-3.003.17-5.167 1.903-7.14 3.972-1.739 1.824-3.31 3.87-5.903 4.604.043.078.054.117.066.117.35.005.699.021 1.047.005 3.768-.17 7.317-.965 10.14-3.7.89-.86 1.685-1.817 2.544-2.71.716-.746 1.584-1.159 2.645-1.07Zm-8.753-4.67c-2.812.246-5.254 1.409-7.548 2.943-1.766 1.18-3.654 1.738-5.776 1.37-.374-.066-.75-.114-1.124-.17l-.013.156c.135.07.265.151.405.207.354.14.702.308 1.07.395 4.083.971 7.992.474 11.516-1.803 2.221-1.435 4.521-1.707 7.013-1.336.252.038.503.083.756.107.234.022.479.255.795.003-2.179-1.574-4.526-2.096-7.094-1.872Zm-10.049-9.544c1.475.051 2.943-.142 4.486-1.059-.452.04-.643.04-.827.076-2.126.424-4.033-.04-5.733-1.383-.623-.493-1.257-.974-1.889-1.457-2.503-1.914-5.374-2.555-8.514-2.5.05.154.054.26.108.315 3.417 3.455 7.371 5.836 12.369 6.008Zm24.727 17.731c-2.114-2.097-4.952-2.367-7.578-.537 1.738.078 3.043.632 4.101 1.728a13 13 0 0 0 1.182 1.106c1.6 1.29 4.311 1.352 5.896.155-1.861-.726-1.861-.726-3.601-2.452Zm-21.058 16.06c-1.858-3.46-4.981-4.24-8.59-4.008a9.667 9.667 0 0 1 2.977 1.39c.84.586 1.547 1.311 2.243 2.055 1.38 1.473 3.534 2.376 4.962 2.07-.656-.412-1.238-.848-1.592-1.507Zl-.006.006-.036-.004.021.018.012.053Za.127.127 0 0 0 .015.043c.005.008.038 0 .058-.002Zl-.008.01.005.026.024.014Z"
+    fill="#FD4F00"
+  />
+</svg>
diff --git a/priv/static/images/logo-06a11be1f2cdde2c851763d00bdd2e80.svg.gz b/priv/static/images/logo-06a11be1f2cdde2c851763d00bdd2e80.svg.gz
new file mode 100644
index 0000000..d8d1f38
Binary files /dev/null and b/priv/static/images/logo-06a11be1f2cdde2c851763d00bdd2e80.svg.gz differ
diff --git a/priv/static/images/logo.svg.gz b/priv/static/images/logo.svg.gz
new file mode 100644
index 0000000..d8d1f38
Binary files /dev/null and b/priv/static/images/logo.svg.gz differ
diff --git a/priv/static/robots-9e2c81b0855bbff2baa8371bc4a78186.txt b/priv/static/robots-9e2c81b0855bbff2baa8371bc4a78186.txt
new file mode 100644
index 0000000..26e06b5
--- /dev/null
+++ b/priv/static/robots-9e2c81b0855bbff2baa8371bc4a78186.txt
@@ -0,0 +1,5 @@
+# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
+#
+# To ban all spiders from the entire site uncomment the next two lines:
+# User-agent: *
+# Disallow: /
diff --git a/priv/static/robots-9e2c81b0855bbff2baa8371bc4a78186.txt.gz b/priv/static/robots-9e2c81b0855bbff2baa8371bc4a78186.txt.gz
new file mode 100644
index 0000000..a1d6ca8
Binary files /dev/null and b/priv/static/robots-9e2c81b0855bbff2baa8371bc4a78186.txt.gz differ
diff --git a/priv/static/robots.txt.gz b/priv/static/robots.txt.gz
new file mode 100644
index 0000000..a1d6ca8
Binary files /dev/null and b/priv/static/robots.txt.gz differ
diff --git a/rel/overlays/bin/migrate b/rel/overlays/bin/migrate
new file mode 100755
index 0000000..dc5607c
--- /dev/null
+++ b/rel/overlays/bin/migrate
@@ -0,0 +1,5 @@
+#!/bin/sh
+set -eu
+
+cd -P -- "$(dirname -- "$0")"
+exec ./sloanely_but_surely eval Core.Release.migrate
diff --git a/rel/overlays/bin/migrate.bat b/rel/overlays/bin/migrate.bat
new file mode 100755
index 0000000..2f36a80
--- /dev/null
+++ b/rel/overlays/bin/migrate.bat
@@ -0,0 +1 @@
+call "%~dp0\sloanely_but_surely" eval Core.Release.migrate
diff --git a/rel/overlays/bin/server b/rel/overlays/bin/server
new file mode 100755
index 0000000..6efe4df
--- /dev/null
+++ b/rel/overlays/bin/server
@@ -0,0 +1,5 @@
+#!/bin/sh
+set -eu
+
+cd -P -- "$(dirname -- "$0")"
+PHX_SERVER=true exec ./sloanely_but_surely start
diff --git a/rel/overlays/bin/server.bat b/rel/overlays/bin/server.bat
new file mode 100755
index 0000000..1b63628
--- /dev/null
+++ b/rel/overlays/bin/server.bat
@@ -0,0 +1,2 @@
+set PHX_SERVER=true
+call "%~dp0\sloanely_but_surely" start