diff --git a/config/config.exs b/config/config.exs
index ff01ab0f135985f8e102a108f6c86d0da37d53fa..16460450e900bc385fff25137994ab4bd150db36 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -8,9 +8,10 @@ config :plausible,
   ecto_repos: [Plausible.Repo],
   environment: System.get_env("ENVIRONMENT", "dev")
 
+
 config :plausible, :clickhouse,
   hostname: System.get_env("CLICKHOUSE_DATABASE_HOST", "localhost"),
-  database: System.get_env("CLICKHOUSE_DATABASE_NAME", "plausible_test"),
+  database: System.get_env("CLICKHOUSE_DATABASE_NAME", "plausible_dev"),
   username: System.get_env("CLICKHOUSE_DATABASE_USER"),
   password: System.get_env("CLICKHOUSE_DATABASE_PASSWORD"),
   pool_size: 10
diff --git a/config/test.exs b/config/test.exs
index f2a42bffc6e162c7a95d1b49d1a67773a9dfbff6..998a05f3442a0c2b72b1ae2cea644e426c0df509 100644
--- a/config/test.exs
+++ b/config/test.exs
@@ -22,6 +22,13 @@ config :plausible,
          ),
        pool: Ecto.Adapters.SQL.Sandbox
 
+config :plausible, :clickhouse,
+  hostname: System.get_env("CLICKHOUSE_DATABASE_HOST", "localhost"),
+  database: System.get_env("CLICKHOUSE_DATABASE_NAME", "plausible_test"),
+  username: System.get_env("CLICKHOUSE_DATABASE_USER"),
+  password: System.get_env("CLICKHOUSE_DATABASE_PASSWORD"),
+  pool_size: 10
+
 config :plausible, Plausible.Mailer, adapter: Bamboo.TestAdapter
 
 config :plausible, Oban, crontab: false, queues: false
diff --git a/lib/plausible/application.ex b/lib/plausible/application.ex
index bae4a975146030229d26e0b910ee78602ed8f4ad..0535fe513547d42ef38a91f003db6b2edf8e6319 100644
--- a/lib/plausible/application.ex
+++ b/lib/plausible/application.ex
@@ -5,7 +5,6 @@ defmodule Plausible.Application do
 
   def start(_type, _args) do
     clickhouse_config = Application.get_env(:plausible, :clickhouse)
-
     children = [
       Plausible.Repo,
       PlausibleWeb.Endpoint,
diff --git a/lib/plausible_web/controllers/tracker_controller.ex b/lib/plausible_web/controllers/tracker_controller.ex
new file mode 100644
index 0000000000000000000000000000000000000000..6e597bc9e71897b3e79f1d87410fd261a3beb623
--- /dev/null
+++ b/lib/plausible_web/controllers/tracker_controller.ex
@@ -0,0 +1,29 @@
+defmodule PlausibleWeb.TrackerController do
+  use PlausibleWeb, :controller
+  require EEx
+  EEx.function_from_file(:defp, :render_plausible, Application.app_dir(:plausible, "priv/tracker/js/plausible.js"), [:base_url])
+  EEx.function_from_file(:defp, :render_p, Application.app_dir(:plausible, "priv/tracker/js/p.js"), [:base_url])
+  @max_age 3600 # 1 hour
+
+  def plausible(conn, _params) do
+    conn
+    |> put_resp_header("cache-control", "max-age=#{@max_age},public")
+    |> send_resp(200, render_plausible(base_url()))
+  end
+
+  def analytics(conn, _params) do
+    conn
+    |> put_resp_header("cache-control", "max-age=#{@max_age},public")
+    |> send_resp(200, render_plausible(base_url()))
+  end
+
+  def p(conn, _params) do
+    conn
+    |> put_resp_header("cache-control", "max-age=#{@max_age},public")
+    |> send_resp(200, render_p(base_url()))
+  end
+
+  defp base_url() do
+    PlausibleWeb.Endpoint.clean_url()
+  end
+end
diff --git a/lib/plausible_web/router.ex b/lib/plausible_web/router.ex
index 9e5af38465e43af7c0fe65d4d1d11f485d7a0480..39484299810c919386b7b1e7f3e987cc7a18b1a7 100644
--- a/lib/plausible_web/router.ex
+++ b/lib/plausible_web/router.ex
@@ -33,6 +33,10 @@ defmodule PlausibleWeb.Router do
     forward "/sent-emails", Bamboo.SentEmailViewerPlug
   end
 
+  get "/js/plausible.js", PlausibleWeb.TrackerController, :plausible
+  get "/js/analytics.js", PlausibleWeb.TrackerController, :plausible
+  get "/js/p.js", PlausibleWeb.TrackerController, :p
+
   scope "/api/stats", PlausibleWeb.Api do
     pipe_through :stats_api
 
diff --git a/priv/tracker/js/analytics.js b/priv/tracker/js/analytics.js
new file mode 100644
index 0000000000000000000000000000000000000000..341f8f679afec81a282cd6838a134e4f8f589380
--- /dev/null
+++ b/priv/tracker/js/analytics.js
@@ -0,0 +1 @@
+!function(o,s){"use strict";var i=o.location,l=o.document,e=l.querySelector('[src*="'+s+'"]'),c={domain:e&&e.getAttribute("data-domain")||i.hostname};function u(e){console.warn("[Plausible] Ignore event: "+e)}function t(e,t){if(/^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$/.test(i.hostname)||"file:"===i.protocol)return u("running locally");if("prerender"===l.visibilityState)return u("prerendering");var n,a={};a.name=e,a.url=i.protocol+"//"+i.hostname+i.pathname+i.search,a.domain=c.domain,a.referrer=l.referrer||null,a.source=(n=i.search.match(/[?&](ref|source|utm_source)=([^?&]+)/))?n[2]:null,a.user_agent=o.navigator.userAgent,a.screen_width=o.innerWidth;var r=new XMLHttpRequest;r.open("POST",s+"/api/event",!0),r.setRequestHeader("Content-Type","text/plain"),r.send(JSON.stringify(a)),r.onreadystatechange=function(){4==r.readyState&&t&&t.callback&&t.callback()}}function n(){t("pageview")}try{var a,r=o.history;r.pushState&&(a=r.pushState,r.pushState=function(){a.apply(this,arguments),n()},o.addEventListener("popstate",n));var p=o.plausible&&o.plausible.q||[];o.plausible=t;for(var h=0;h<p.length;h++)t.apply(this,p[h]);n()}catch(e){(new Image).src=s+"/api/error?message="+encodeURIComponent(e.message)}}(window,"<%= base_url %>");
\ No newline at end of file
diff --git a/priv/tracker/js/p.js b/priv/tracker/js/p.js
new file mode 100644
index 0000000000000000000000000000000000000000..c859795f2b39efc2579c56b150597d6c4c899a1f
--- /dev/null
+++ b/priv/tracker/js/p.js
@@ -0,0 +1 @@
+!function(r,a){"use strict";function o(e){console.warn("[Plausible] Ignoring event because "+e)}function c(){var e=r.location.search.match(/[?&](ref|source|utm_source)=([^?&]+)/);return e?e[2]:null}function s(){var e,t,n=JSON.parse((e="plausible_user",(t=document.cookie.match(new RegExp("(?:^|; )"+e.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g,"\\$1")+"=([^;]*)")))?decodeURIComponent(t[1]):null));return n?{initial_referrer:n.initial_referrer&&decodeURIComponent(n.initial_referrer),initial_source:n.initial_source&&decodeURIComponent(n.initial_source)}:(n={initial_referrer:r.document.referrer||null,initial_source:c()},function(e,t){var n=new Date;n.setTime(n.getTime()+94608e6);var i="; expires="+n.toUTCString();document.cookie=e+"="+(t||"")+i+"; samesite=strict; path=/"}("plausible_user",JSON.stringify({initial_referrer:n.initial_referrer&&encodeURIComponent(n.initial_referrer),initial_source:n.initial_source&&encodeURIComponent(n.initial_source)})),n)}function t(e,t){if(/^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$/.test(r.location.hostname))return o("website is running locally");if("file:"===r.location.protocol)return o("website is running locally");if("prerender"===r.document.visibilityState)return o("document is prerendering");var n=l.trackAcquisition?s():{};n.name=e,n.url=r.location.protocol+"//"+r.location.hostname+r.location.pathname+r.location.search,n.domain=l.domain,n.referrer=r.document.referrer||null,n.source=c(),n.user_agent=r.navigator.userAgent,n.screen_width=r.innerWidth;var i=new XMLHttpRequest;i.open("POST",a+"/api/event",!0),i.setRequestHeader("Content-Type","text/plain"),i.send(JSON.stringify(n)),i.onreadystatechange=function(){i.readyState==XMLHttpRequest.DONE&&t&&t.callback&&t.callback()}}function n(e){t("pageview",e)}try{var l={domain:r.location.hostname},i={page:n,trigger:t,trackPushState:function(){var e,t=r.history;t.pushState&&(e=t.pushState,t.pushState=function(){e.apply(this,arguments),n()}),r.addEventListener("popstate",n)},configure:function(e,t){l[e]=t}},e=r.plausible.q||[];r.plausible=function(){var e=[].slice.call(arguments),t=e.shift();i[t].apply(this,e)};for(var u=0;u<e.length;u++)r.plausible.apply(this,e[u])}catch(e){(new Image).src=a+"/api/error?message="+encodeURIComponent(e.message)}}(window,"<%= base_url %>");
\ No newline at end of file
diff --git a/priv/tracker/js/plausible.js b/priv/tracker/js/plausible.js
new file mode 100644
index 0000000000000000000000000000000000000000..341f8f679afec81a282cd6838a134e4f8f589380
--- /dev/null
+++ b/priv/tracker/js/plausible.js
@@ -0,0 +1 @@
+!function(o,s){"use strict";var i=o.location,l=o.document,e=l.querySelector('[src*="'+s+'"]'),c={domain:e&&e.getAttribute("data-domain")||i.hostname};function u(e){console.warn("[Plausible] Ignore event: "+e)}function t(e,t){if(/^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$/.test(i.hostname)||"file:"===i.protocol)return u("running locally");if("prerender"===l.visibilityState)return u("prerendering");var n,a={};a.name=e,a.url=i.protocol+"//"+i.hostname+i.pathname+i.search,a.domain=c.domain,a.referrer=l.referrer||null,a.source=(n=i.search.match(/[?&](ref|source|utm_source)=([^?&]+)/))?n[2]:null,a.user_agent=o.navigator.userAgent,a.screen_width=o.innerWidth;var r=new XMLHttpRequest;r.open("POST",s+"/api/event",!0),r.setRequestHeader("Content-Type","text/plain"),r.send(JSON.stringify(a)),r.onreadystatechange=function(){4==r.readyState&&t&&t.callback&&t.callback()}}function n(){t("pageview")}try{var a,r=o.history;r.pushState&&(a=r.pushState,r.pushState=function(){a.apply(this,arguments),n()},o.addEventListener("popstate",n));var p=o.plausible&&o.plausible.q||[];o.plausible=t;for(var h=0;h<p.length;h++)t.apply(this,p[h]);n()}catch(e){(new Image).src=s+"/api/error?message="+encodeURIComponent(e.message)}}(window,"<%= base_url %>");
\ No newline at end of file
diff --git a/tracker/compile.js b/tracker/compile.js
index b3182c101e5ae09a2d486f7db63e3a4e3d802e1f..9ec485626e306ab74aef5eca4462899c3eaf3849 100644
--- a/tracker/compile.js
+++ b/tracker/compile.js
@@ -2,22 +2,16 @@ const uglify = require("uglify-js");
 const fs = require('fs')
 const path = require('path')
 
-const scheme = process.env.SCHEME || "https"
-const host = process.env.HOST || "plausible.io"
-const baseUrl = scheme + "://" + host
-
 function relPath(segment) {
   return path.join(__dirname, segment)
 }
 
 function compilefile(input, output) {
-  const code = fs.readFileSync(input)
-    .toString()
-    .replace('BASE_URL', "'" + baseUrl + "'")
+  const code = fs.readFileSync(input).toString()
   const result = uglify.minify(code)
   fs.writeFileSync(output, result.code)
 }
 
-compilefile(relPath('src/plausible.js'), relPath('../priv/static/js/plausible.js'))
-compilefile(relPath('src/p.js'), relPath('../priv/static/js/p.js'))
-fs.copyFileSync(relPath('../priv/static/js/plausible.js'), relPath('../priv/static/js/analytics.js'))
+compilefile(relPath('src/plausible.js'), relPath('../priv/tracker/js/plausible.js'))
+compilefile(relPath('src/p.js'), relPath('../priv/tracker/js/p.js'))
+fs.copyFileSync(relPath('../priv/tracker/js/plausible.js'), relPath('../priv/tracker/js/analytics.js'))
diff --git a/tracker/src/p.js b/tracker/src/p.js
index 459904734df8bcb8a41e1ba78233badd4b693025..57c296fb6c3df21b30202ca4367991ca13b5774b 100644
--- a/tracker/src/p.js
+++ b/tracker/src/p.js
@@ -124,4 +124,4 @@
   } catch (e) {
     new Image().src = plausibleHost + '/api/error?message=' +  encodeURIComponent(e.message);
   }
-})(window, "https://plausible.io");
+})(window, '<%= base_url %>');
diff --git a/tracker/src/plausible.js b/tracker/src/plausible.js
index bdeded53daff3e8dc7fe49ec863436c754d8fbe3..1c63fc21dcaaa861f7a7be465ee8facf39994049 100644
--- a/tracker/src/plausible.js
+++ b/tracker/src/plausible.js
@@ -72,4 +72,4 @@
   } catch (e) {
     new Image().src = plausibleHost + '/api/error?message=' +  encodeURIComponent(e.message);
   }
-})(window, BASE_URL);
+})(window, '<%= base_url %>');