diff --git a/README.md b/README.md
index 98437a7abbf4fa5609eaa0552d874e3acbd85d7e..b3c55e775da481c2f923429ae5409789f2e53ff5 100644
--- a/README.md
+++ b/README.md
@@ -13,12 +13,12 @@ Metabase is the easy, open source way for everyone in your company to ask questi
 
 # Features
 - 5 minute [setup](http://www.metabase.com/docs/latest/setting-up-metabase) (We're not kidding)
-- Let anyone on your team [ask questions](http://www.metabase.com/docs/latest/users-guide/03-asking-questions) without knowing SQL
-- Rich beautiful [dashboards](http://www.metabase.com/docs/latest/users-guide/05-sharing-answers) with auto refresh and fullscreen
+- Let anyone on your team [ask questions](http://www.metabase.com/docs/latest/users-guide/04-asking-questions) without knowing SQL
+- Rich beautiful [dashboards](http://www.metabase.com/docs/latest/users-guide/06-sharing-answers) with auto refresh and fullscreen
 - SQL Mode for analysts and data pros
 - Create canonical [segments and metrics](http://www.metabase.com/docs/latest/administration-guide/07-segments-and-metrics) for your team to use
-- Send data to Slack or email on a schedule with [Pulses](http://www.metabase.com/docs/latest/users-guide/09-pulses)
-- View data in Slack anytime with [MetaBot](http://www.metabase.com/docs/latest/users-guide/10-metabot)
+- Send data to Slack or email on a schedule with [Pulses](http://www.metabase.com/docs/latest/users-guide/10-pulses)
+- View data in Slack anytime with [MetaBot](http://www.metabase.com/docs/latest/users-guide/11-metabot)
 - [Humanize data](http://www.metabase.com/docs/latest/administration-guide/03-metadata-editing) for your team by renaming, annotating and hiding fields
 
 For more information check out [metabase.com](http://www.metabase.com)
diff --git a/bin/osx-release b/bin/osx-release
index 868747e5ccc48f7c21adce59a7819fe7b1f42743..0165e4b915613d628c3e1b25a8c609e8d930525e 100755
--- a/bin/osx-release
+++ b/bin/osx-release
@@ -187,8 +187,10 @@ sub create_dmg_from_source_dir {
            '-fs', 'HFS+',
            '-fsargs', '-c c=64,a=16,e=16',
            '-format', 'UDRW',
-           '-size', '256MB',          # it looks like this can be whatever size we want; compression slims it down
+           '-size', '512MB',          # has to be big enough to hold everything uncompressed, but doesn't matter if there's extra space -- compression slims it down
            $dmg_filename) == 0 or die $!;
+
+    announce "$dmg_filename created.";
 }
 
 # Mount the disk image, return the device name
diff --git a/bin/version b/bin/version
index aa97490e03a8c5e395fc40901d99fe812929133a..8f8564b7acdd9298a03c847856b3540774b54092 100755
--- a/bin/version
+++ b/bin/version
@@ -1,6 +1,6 @@
 #!/usr/bin/env bash
 
-VERSION="v0.24.1"
+VERSION="v0.25.0-snapshot"
 
 # dynamically pull more interesting stuff from latest git commit
 HASH=$(git show-ref --head --hash=7 head)            # first 7 letters of hash should be enough; that's what GitHub uses
diff --git a/frontend/src/metabase/admin/settings/components/SettingsSetupList.jsx b/frontend/src/metabase/admin/settings/components/SettingsSetupList.jsx
index b6b58ff722f001194d5e491afccd265dc3065511..8730041bb3bb9a60234ceaaae3efb12acb4c06d1 100644
--- a/frontend/src/metabase/admin/settings/components/SettingsSetupList.jsx
+++ b/frontend/src/metabase/admin/settings/components/SettingsSetupList.jsx
@@ -2,6 +2,7 @@ import React, { Component } from "react";
 import { Link } from "react-router";
 import Icon from "metabase/components/Icon.jsx";
 import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.jsx";
+import { SetupApi } from "metabase/services";
 
 const TaskList = ({tasks}) =>
   <ol>
@@ -57,11 +58,11 @@ export default class SettingsSetupList extends Component {
     }
 
     async componentWillMount() {
-        let response = await fetch("/api/setup/admin_checklist", { credentials: 'same-origin' });
-        if (response.status !== 200) {
-            this.setState({ error: await response.json() })
-        } else {
-            this.setState({ tasks: await response.json() });
+        try {
+            const tasks = await SetupApi.admin_checklist();
+            this.setState({ tasks: tasks });
+        } catch (e) {
+            this.setState({ error: e });
         }
     }
 
diff --git a/frontend/src/metabase/admin/settings/components/SettingsSlackForm.jsx b/frontend/src/metabase/admin/settings/components/SettingsSlackForm.jsx
index fb111c719574dc4977fa6a391735cc6b18ef4e56..221d5e6514ec57ed6526949da21fcfcdf2accf7d 100644
--- a/frontend/src/metabase/admin/settings/components/SettingsSlackForm.jsx
+++ b/frontend/src/metabase/admin/settings/components/SettingsSlackForm.jsx
@@ -200,7 +200,7 @@ export default class SettingsSlackForm extends Component {
                         Metabase
                         <RetinaImage
                             className="mx1"
-                            src="/app/img/slack_emoji.png"
+                            src="app/assets/img/slack_emoji.png"
                             width={79}
                             forceOriginalDimensions={false /* broken in React v0.13 */}
                         />
diff --git a/frontend/src/metabase/admin/settings/components/widgets/CustomGeoJSONWidget.jsx b/frontend/src/metabase/admin/settings/components/widgets/CustomGeoJSONWidget.jsx
index 5308fcd1cd408de0d28ea7ed4a9d872750b7403c..c60d36ce788cb8a551b088a4b3a9f9a6e7f3835b 100644
--- a/frontend/src/metabase/admin/settings/components/widgets/CustomGeoJSONWidget.jsx
+++ b/frontend/src/metabase/admin/settings/components/widgets/CustomGeoJSONWidget.jsx
@@ -11,8 +11,9 @@ import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.j
 
 import SettingHeader from "../SettingHeader.jsx";
 
+import { SettingsApi, GeoJSONApi } from "metabase/services";
+
 import cx from "classnames";
-import fetch from 'isomorphic-fetch';
 
 import LeafletChoropleth from "metabase/visualizations/components/LeafletChoropleth.jsx";
 
@@ -52,11 +53,9 @@ export default class CustomGeoJSONWidget extends Component {
             delete value[id];
         }
 
-        await fetch("/api/setting/custom-geojson", {
-            method: "PUT",
-            headers: { "Content-Type": "application/json" },
-            body: JSON.stringify({ value }),
-            credentials: "same-origin",
+        await SettingsApi.put({
+            key: "custom-geojson",
+            value: value
         });
 
         await this.props.reloadSettings();
@@ -88,11 +87,9 @@ export default class CustomGeoJSONWidget extends Component {
                 geoJsonError: null,
             });
             await this._saveMap(map.id, map);
-            let geoJsonResponse = await fetch("/api/geojson/" + map.id, {
-                credentials: "same-origin"
-            });
+            let geoJson = await GeoJSONApi.get({ id: map.id });
             this.setState({
-                geoJson: await geoJsonResponse.json(),
+                geoJson: geoJson,
                 geoJsonLoading: false,
                 geoJsonError: null,
             });
diff --git a/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing.jsx b/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing.jsx
index 892ba549ca6aeb38110a651b9fa711153f6b7e2c..49f5dfc9e885c0c2b04a0cc467443bc1df27f61d 100644
--- a/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing.jsx
+++ b/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing.jsx
@@ -152,7 +152,7 @@ export const PublicLinksDashboardListing = () =>
         revoke={DashboardApi.deletePublicLink}
         type='Public Dashboard Listing'
         getUrl={({ id }) => Urls.dashboard(id)}
-        getPublicUrl={({ public_uuid }) => window.location.origin + Urls.publicDashboard(public_uuid)}
+        getPublicUrl={({ public_uuid }) => Urls.publicDashboard(public_uuid)}
         noLinksMessage="No dashboards have been publicly shared yet."
     />;
 
@@ -162,7 +162,7 @@ export const PublicLinksQuestionListing = () =>
         revoke={CardApi.deletePublicLink}
         type='Public Card Listing'
         getUrl={({ id }) => Urls.question(id)}
-        getPublicUrl={({ public_uuid }) => window.location.origin + Urls.publicCard(public_uuid)}
+        getPublicUrl={({ public_uuid }) => Urls.publicCard(public_uuid)}
         noLinksMessage="No questions have been publicly shared yet."
     />;
 
diff --git a/frontend/src/metabase/app-main.js b/frontend/src/metabase/app-main.js
index fa077d93f3d26c873c6f552bf80bfe6b9a032961..169ad090a05c8749f8c21dff82506369a2b95353 100644
--- a/frontend/src/metabase/app-main.js
+++ b/frontend/src/metabase/app-main.js
@@ -26,7 +26,7 @@ const WHITELIST_FORBIDDEN_URLS = [
 init(reducers, getRoutes, (store) => {
     // received a 401 response
     api.on("401", (url) => {
-        if (url === "/api/user/current") {
+        if (url.indexOf("/api/user/current") >= 0) {
             return
         }
         store.dispatch(clearCurrentUser());
diff --git a/frontend/src/metabase/app.js b/frontend/src/metabase/app.js
index 57b95bb27fba89bca34c9f20f7aaf8d944443b6a..5cefe8294d390108453703d02b6d256ff8d348cf 100644
--- a/frontend/src/metabase/app.js
+++ b/frontend/src/metabase/app.js
@@ -10,13 +10,24 @@ import { Provider } from 'react-redux'
 import MetabaseAnalytics, { registerAnalyticsClickListener } from "metabase/lib/analytics";
 import MetabaseSettings from "metabase/lib/settings";
 
+import api from "metabase/lib/api";
+
 import { getStore } from './store'
 
 import { refreshSiteSettings } from "metabase/redux/settings";
 
-import { Router, browserHistory } from "react-router";
-import { syncHistoryWithStore } from 'react-router-redux'
+import { Router, useRouterHistory } from "react-router";
+import { createHistory } from 'history'
+import { syncHistoryWithStore } from 'react-router-redux';
+
+// remove trailing slash
+const BASENAME = window.MetabaseRoot.replace(/\/+$/, "");
+
+api.basename = BASENAME;
 
+const browserHistory = useRouterHistory(createHistory)({
+    basename: BASENAME
+});
 
 function _init(reducers, getRoutes, callback) {
     const store = getStore(reducers, browserHistory);
diff --git a/frontend/src/metabase/components/DatabaseDetailsForm.jsx b/frontend/src/metabase/components/DatabaseDetailsForm.jsx
index c72bc0c2ec44840c164e09edb6458a2e46843165..3adc7254bd39e3781c24c8a4a222c92e28608f24 100644
--- a/frontend/src/metabase/components/DatabaseDetailsForm.jsx
+++ b/frontend/src/metabase/components/DatabaseDetailsForm.jsx
@@ -165,7 +165,7 @@ export default class DatabaseDetailsForm extends Component {
                             <div style={{maxWidth: "40rem"}} className="pt1">
                                  Some database installations can only be accessed by connecting through an SSH bastion host.
                                  This option also provides an extra layer of security when a VPN is not available.
-                                 Enabling this is usually slower than a dirrect connection.
+                                 Enabling this is usually slower than a direct connection.
                             </div>
                         </div>
                     </div>
diff --git a/frontend/src/metabase/components/Logs.jsx b/frontend/src/metabase/components/Logs.jsx
index eb1521b1b217a7412375917ad9f1f5a9b0fc110d..c6acd12eec6228c4d9c8e3961f1cc27bd2304ab8 100644
--- a/frontend/src/metabase/components/Logs.jsx
+++ b/frontend/src/metabase/components/Logs.jsx
@@ -1,6 +1,7 @@
 import React, { Component } from "react";
 import ReactDOM from "react-dom";
-import fetch from 'isomorphic-fetch';
+
+import { UtilApi } from "metabase/services";
 
 import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.jsx";
 
@@ -32,7 +33,7 @@ export default class Logs extends Component {
 
     componentWillMount() {
         this.timer = setInterval(async () => {
-            let response = await fetch("/api/util/logs", { credentials: 'same-origin' });
+            let response = await UtilApi.logs();
             let logs = await response.json()
             this.setState({ logs: logs.reverse() })
         }, 1000);
diff --git a/frontend/src/metabase/css/query_builder.css b/frontend/src/metabase/css/query_builder.css
index f298eff4caf824a7d90164dc5ec9cfb288b2c88e..b85584090cd85144d7b1c0b46300bc51eb79320e 100644
--- a/frontend/src/metabase/css/query_builder.css
+++ b/frontend/src/metabase/css/query_builder.css
@@ -243,25 +243,25 @@
 .QueryError-image--noRows {
   width: 120px;
   height: 120px;
-  background-image: url('/app/img/no_results.svg');
+  background-image: url('../assets/img/no_results.svg');
 }
 
 .QueryError-image--queryError {
   width: 120px;
   height: 120px;
-  background-image: url('/app/img/no_understand.svg');
+  background-image: url('../assets/img/no_understand.svg');
 }
 
 .QueryError-image--serverError {
   width: 120px;
   height: 148px;
-  background-image: url('/app/img/blown_up.svg');
+  background-image: url('../assets/img/blown_up.svg');
 }
 
 .QueryError-image--timeout {
   width: 120px;
   height: 120px;
-  background-image: url('/app/img/stopwatch.svg');
+  background-image: url('../assets/img/stopwatch.svg');
 }
 
 .QueryError-message {
diff --git a/frontend/src/metabase/dashboard/components/DashCard.jsx b/frontend/src/metabase/dashboard/components/DashCard.jsx
index b24fd3a65369c5882233f8ab2d57214271d541df..b6b0fc202a3908d23b6c564f91e9eb3883a1fc1c 100644
--- a/frontend/src/metabase/dashboard/components/DashCard.jsx
+++ b/frontend/src/metabase/dashboard/components/DashCard.jsx
@@ -168,7 +168,7 @@ const ChartSettingsButton = ({ series, onReplaceAllVisualizationSettings }) =>
     </ModalWithTrigger>
 
 const RemoveButton = ({ onRemove }) =>
-    <a className="text-grey-2 text-grey-4-hover " data-metabase-event="Dashboard;Remove Card Modal" href="#" onClick={onRemove} style={HEADER_ACTION_STYLE}>
+    <a className="text-grey-2 text-grey-4-hover " data-metabase-event="Dashboard;Remove Card Modal" onClick={onRemove} style={HEADER_ACTION_STYLE}>
         <Icon name="close" size={HEADER_ICON_SIZE} />
     </a>
 
diff --git a/frontend/src/metabase/dashboard/containers/DashboardEmbedWidget.jsx b/frontend/src/metabase/dashboard/containers/DashboardEmbedWidget.jsx
index cd76a516159bb1b31fe14cd12ee93e2eaf917491..72dd16754d8024a047a8b6f72fb33fcfabac24c6 100644
--- a/frontend/src/metabase/dashboard/containers/DashboardEmbedWidget.jsx
+++ b/frontend/src/metabase/dashboard/containers/DashboardEmbedWidget.jsx
@@ -32,7 +32,7 @@ export default class DashboardEmbedWidget extends Component {
                 onDisablePublicLink={() => deletePublicLink(dashboard)}
                 onUpdateEnableEmbedding={(enableEmbedding) => updateEnableEmbedding(dashboard, enableEmbedding)}
                 onUpdateEmbeddingParams={(embeddingParams) => updateEmbeddingParams(dashboard, embeddingParams)}
-                getPublicUrl={({ public_uuid }) => window.location.origin + Urls.publicDashboard(public_uuid)}
+                getPublicUrl={({ public_uuid }) => Urls.publicDashboard(public_uuid)}
             />
         );
     }
diff --git a/frontend/src/metabase/home/components/NewUserOnboardingModal.jsx b/frontend/src/metabase/home/components/NewUserOnboardingModal.jsx
index 12e9b1c812fda901b8595a2b0df77ce893a4150f..00f10e96a74480fd75e9c2cef36e2f0ce1fc7a37 100644
--- a/frontend/src/metabase/home/components/NewUserOnboardingModal.jsx
+++ b/frontend/src/metabase/home/components/NewUserOnboardingModal.jsx
@@ -64,11 +64,11 @@ export default class NewUserOnboardingModal extends Component {
                         <div className="pl4 pr4 pt4 pb1 border-bottom">
                             <h2>Just 3 things worth knowing</h2>
 
-                            <p className="clearfix pt1"><img className="float-left mr2" width="40" height="40" src="/app/home/partials/onboarding_illustration_tables.png" />All of your data is organized in Tables. Think of them in terms of Excel spreadsheets with columns and rows.</p>
+                            <p className="clearfix pt1"><img className="float-left mr2" width="40" height="40" src="app/assets/img/onboarding_illustration_tables.png" />All of your data is organized in Tables. Think of them in terms of Excel spreadsheets with columns and rows.</p>
 
-                            <p className="clearfix"><img className="float-left mr2" width="40" height="40" src="/app/home/partials/onboarding_illustration_questions.png" />To get answers, you Ask Questions by picking a table and a few other parameters. You can visualize the answer in many ways, including cool charts.</p>
+                            <p className="clearfix"><img className="float-left mr2" width="40" height="40" src="app/assets/img/onboarding_illustration_questions.png" />To get answers, you Ask Questions by picking a table and a few other parameters. You can visualize the answer in many ways, including cool charts.</p>
 
-                            <p className="clearfix"><img className="float-left mr2" width="40" height="40" src="/app/home/partials/onboarding_illustration_dashboards.png" />You (and anyone on your team) can save answers in Dashboards, so you can check them often. It's a great way to quickly see a snapshot of your business.</p>
+                            <p className="clearfix"><img className="float-left mr2" width="40" height="40" src="app/assets/img/onboarding_illustration_dashboards.png" />You (and anyone on your team) can save answers in Dashboards, so you can check them often. It's a great way to quickly see a snapshot of your business.</p>
                         </div>
                         <div className="px4 py2 text-grey-2 flex align-center">
                             {this.renderStep()}
diff --git a/frontend/src/metabase/home/components/NextStep.jsx b/frontend/src/metabase/home/components/NextStep.jsx
index 95424080c7806ae205ac52eedfcc2c450582d26b..7f90bec8f3b6deaa6441360ec63cb9978718ad74 100644
--- a/frontend/src/metabase/home/components/NextStep.jsx
+++ b/frontend/src/metabase/home/components/NextStep.jsx
@@ -1,6 +1,6 @@
 import React, { Component } from "react";
 import { Link } from "react-router";
-import fetch from 'isomorphic-fetch';
+import { SetupApi } from "metabase/services";
 
 import SidebarSection from "./SidebarSection.jsx";
 
@@ -13,15 +13,12 @@ export default class NextStep extends Component {
     }
 
     async componentWillMount() {
-        let response = await fetch("/api/setup/admin_checklist", { credentials: 'same-origin' });
-        if (response.status === 200) {
-            let sections = await response.json();
-            for (let section of sections) {
-                for (let task of section.tasks) {
-                    if (task.is_next_step) {
-                        this.setState({ next: task });
-                        break;
-                    }
+        const sections = await SetupApi.admin_checklist(null, { noEvent: true });
+        for (let section of sections) {
+            for (let task of section.tasks) {
+                if (task.is_next_step) {
+                    this.setState({ next: task });
+                    break;
                 }
             }
         }
diff --git a/frontend/src/metabase/home/components/Smile.jsx b/frontend/src/metabase/home/components/Smile.jsx
index 1ab7fffc833e42bcba8a42943b20c8d0ee23c8e2..fedae4489c98220381f5452158b4a14cd0eed310 100644
--- a/frontend/src/metabase/home/components/Smile.jsx
+++ b/frontend/src/metabase/home/components/Smile.jsx
@@ -5,7 +5,7 @@ export default class Smile extends Component {
         const styles = {
             width: '48px',
             height: '48px',
-            backgroundImage: 'url("app/components/icons/assets/smile.svg")',
+            backgroundImage: 'url("app/assets/img/smile.svg")',
         }
         return <div style={styles}></div>
     }
diff --git a/frontend/src/metabase/icon_paths.js b/frontend/src/metabase/icon_paths.js
index 6b55243deca7e0866774fdaae008857f769b3725..5bfb5e3f3ff3e0d1508bc9753d0cf0cbd4c7ca76 100644
--- a/frontend/src/metabase/icon_paths.js
+++ b/frontend/src/metabase/icon_paths.js
@@ -207,7 +207,7 @@ export var ICON_PATHS = {
     x: 'm11.271709,16 l-3.19744231e-13,4.728291 l4.728291,0 l16,11.271709 l27.271709,2.39808173e-13 l32,4.728291 l20.728291,16 l31.1615012,26.4332102 l26.4332102,31.1615012 l16,20.728291 l5.56678976,31.1615012 l0.838498756,26.4332102 l11.271709,16 z',
     zoom: 'M12.416 12.454V8.37h3.256v4.083h4.07v3.266h-4.07v4.083h-3.256V15.72h-4.07v-3.266h4.07zm10.389 13.28c-5.582 4.178-13.543 3.718-18.632-1.37-5.58-5.581-5.595-14.615-.031-20.179 5.563-5.563 14.597-5.55 20.178.031 5.068 5.068 5.545 12.985 1.422 18.563l5.661 5.661a2.08 2.08 0 0 1 .003 2.949 2.085 2.085 0 0 1-2.95-.003l-5.651-5.652zm-1.486-4.371c3.895-3.895 3.885-10.218-.021-14.125-3.906-3.906-10.23-3.916-14.125-.021-3.894 3.894-3.885 10.218.022 14.124 3.906 3.907 10.23 3.916 14.124.022z',
     "slack": {
-        img: "/app/img/slack.png"
+        img: "app/assets/img/slack.png"
     }
 };
 
diff --git a/frontend/src/metabase/lib/api.js b/frontend/src/metabase/lib/api.js
index 53369187fac44790b8b79d009a1e9b01b8b2909e..9c88348055f62b042448f1c59a4ba534a70ff9d7 100644
--- a/frontend/src/metabase/lib/api.js
+++ b/frontend/src/metabase/lib/api.js
@@ -4,88 +4,115 @@ import querystring from "querystring";
 
 import EventEmitter from "events";
 
-let events = new EventEmitter();
-
-type ParamsMap = { [key:string]: any };
 type TransformFn = (o: any) => any;
 
-function makeMethod(method: string, hasBody: boolean = false) {
-    return function(
-        urlTemplate: string,
-        params: ParamsMap|TransformFn = {},
-        transformResponse: TransformFn = (o) => o
-    ) {
-        if (typeof params === "function") {
-            transformResponse = params;
-            params = {};
-        }
-        return function(
-            data?: { [key:string]: any },
-            options?: { [key:string]: any } = {}
-        ): Promise<any> {
-            let url = urlTemplate;
-            data = { ...data };
-            for (let tag of (url.match(/:\w+/g) || [])) {
-                let value = data[tag.slice(1)];
-                if (value === undefined) {
-                    console.warn("Warning: calling", method, "without", tag);
-                    value = "";
-                }
-                url = url.replace(tag, encodeURIComponent(data[tag.slice(1)]))
-                delete data[tag.slice(1)];
-            }
+type Options = {
+    noEvent?: boolean,
+    transformResponse?: TransformFn,
+    cancelled?: Promise<any>
+}
+type Data = {
+    [key:string]: any
+};
+
+const DEFAULT_OPTIONS: Options = {
+    noEvent: false,
+    transformResponse: (o) => o
+}
 
-            let headers: { [key:string]: string } = {
-                "Accept": "application/json",
-            };
+class Api extends EventEmitter {
+    basename: "";
 
-            let body;
-            if (hasBody) {
-                headers["Content-Type"] = "application/json";
-                body = JSON.stringify(data);
-            } else {
-                let qs = querystring.stringify(data);
-                if (qs) {
-                    url += (url.indexOf("?") >= 0 ? "&" : "?") + qs;
-                }
-            }
+    GET:    (t: string, o?: Options|TransformFn) => (d?: Data, o?: Options) => Promise<any>;
+    POST:   (t: string, o?: Options|TransformFn) => (d?: Data, o?: Options) => Promise<any>;
+    PUT:    (t: string, o?: Options|TransformFn) => (d?: Data, o?: Options) => Promise<any>;
+    DELETE: (t: string, o?: Options|TransformFn) => (d?: Data, o?: Options) => Promise<any>;
 
-            return new Promise((resolve, reject) => {
-                let xhr = new XMLHttpRequest();
-                xhr.open(method, url);
-                for (let headerName in headers) {
-                    xhr.setRequestHeader(headerName, headers[headerName])
-                }
-                xhr.onreadystatechange = function() {
-                    // $FlowFixMe
-                    if (xhr.readyState === XMLHttpRequest.DONE) {
-                        let body = xhr.responseText;
-                        try { body = JSON.parse(body); } catch (e) {}
-                        if (xhr.status >= 200 && xhr.status <= 299) {
-                            resolve(transformResponse(body, { data }));
-                        } else {
-                            reject({
-                                status: xhr.status,
-                                data: body
-                            });
-                        }
-                        events.emit(xhr.status, url);
+    constructor() {
+        super();
+        this.GET = this._makeMethod("GET").bind(this);
+        this.DELETE = this._makeMethod("DELETE").bind(this);
+        this.POST = this._makeMethod("POST", true).bind(this);
+        this.PUT = this._makeMethod("PUT", true).bind(this);
+    }
+
+    _makeMethod(method: string, hasBody: boolean = false) {
+        return (
+            urlTemplate: string,
+            methodOptions?: Options|TransformFn = {}
+        ) => {
+            if (typeof methodOptions === "function") {
+                methodOptions = { transformResponse: methodOptions };
+            }
+            const defaultOptions = { ...DEFAULT_OPTIONS, ...methodOptions };
+            return (
+                data?: Data,
+                invocationOptions?: Options = {}
+            ): Promise<any> => {
+                const options: Options = { ...defaultOptions, ...invocationOptions };
+                let url = urlTemplate;
+                data = { ...data };
+                for (let tag of (url.match(/:\w+/g) || [])) {
+                    let value = data[tag.slice(1)];
+                    if (value === undefined) {
+                        console.warn("Warning: calling", method, "without", tag);
+                        value = "";
                     }
+                    url = url.replace(tag, encodeURIComponent(data[tag.slice(1)]))
+                    delete data[tag.slice(1)];
                 }
-                xhr.send(body);
 
-                if (options.cancelled) {
-                    options.cancelled.then(() => xhr.abort());
+                let headers: { [key:string]: string } = {
+                    "Accept": "application/json",
+                };
+
+                let body;
+                if (hasBody) {
+                    headers["Content-Type"] = "application/json";
+                    body = JSON.stringify(data);
+                } else {
+                    let qs = querystring.stringify(data);
+                    if (qs) {
+                        url += (url.indexOf("?") >= 0 ? "&" : "?") + qs;
+                    }
                 }
-            })
 
+                return new Promise((resolve, reject) => {
+                    let xhr = new XMLHttpRequest();
+                    xhr.open(method, this.basename + url);
+                    for (let headerName in headers) {
+                        xhr.setRequestHeader(headerName, headers[headerName])
+                    }
+                    xhr.onreadystatechange = () => {
+                        // $FlowFixMe
+                        if (xhr.readyState === XMLHttpRequest.DONE) {
+                            let body = xhr.responseText;
+                            try { body = JSON.parse(body); } catch (e) {}
+                            if (xhr.status >= 200 && xhr.status <= 299) {
+                                if (options.transformResponse) {
+                                    body = options.transformResponse(body, { data });
+                                }
+                                resolve(body);
+                            } else {
+                                reject({
+                                    status: xhr.status,
+                                    data: body
+                                });
+                            }
+                            if (!options.noEvent) {
+                                this.emit(xhr.status, url);
+                            }
+                        }
+                    }
+                    xhr.send(body);
+
+                    if (options.cancelled) {
+                        options.cancelled.then(() => xhr.abort());
+                    }
+                });
+            }
         }
     }
 }
 
-export const GET = makeMethod("GET");
-export const DELETE = makeMethod("DELETE");
-export const POST = makeMethod("POST", true);
-export const PUT = makeMethod("PUT", true);
-
-export default events;
+export default new Api();
diff --git a/frontend/src/metabase/lib/cookies.js b/frontend/src/metabase/lib/cookies.js
index 083ba20d512bc6e0f5b40cf481f9b1a5e3d8d943..705fd7bfd5a02882b4c02a7a32150e1570223c3d 100644
--- a/frontend/src/metabase/lib/cookies.js
+++ b/frontend/src/metabase/lib/cookies.js
@@ -10,7 +10,7 @@ var MetabaseCookies = {
     // set the session cookie.  if sessionId is null, clears the cookie
     setSessionCookie: function(sessionId) {
         const options = {
-            path: '/',
+            path: window.MetabaseRoot || '/',
             expires: 14,
             secure: window.location.protocol === "https:"
         };
diff --git a/frontend/src/metabase/lib/urls.js b/frontend/src/metabase/lib/urls.js
index f63f47403069f448d5f6a7e1a4da1c7669420d5a..3a83c230e064af2ca68e7209d85f102577e56c48 100644
--- a/frontend/src/metabase/lib/urls.js
+++ b/frontend/src/metabase/lib/urls.js
@@ -1,4 +1,5 @@
 import { serializeCardForUrl } from "metabase/lib/card";
+import MetabaseSettings from "metabase/lib/settings"
 
 // provides functions for building urls to things we care about
 
@@ -69,11 +70,13 @@ export function label(label) {
 }
 
 export function publicCard(uuid, type = null) {
-    return `/public/question/${uuid}` + (type ? `.${type}` : ``);
+    const siteUrl = MetabaseSettings.get("site-url");
+    return `${siteUrl}/public/question/${uuid}` + (type ? `.${type}` : ``);
 }
 
 export function publicDashboard(uuid) {
-    return `/public/dashboard/${uuid}`;
+    const siteUrl = MetabaseSettings.get("site-url");
+    return `${siteUrl}/public/dashboard/${uuid}`;
 }
 
 export function embedCard(token, type = null) {
diff --git a/frontend/src/metabase/public/components/widgets/SharingPane.jsx b/frontend/src/metabase/public/components/widgets/SharingPane.jsx
index 783a46001e5756099bcd572225f91f5b5a60d2c6..5e89500a23e8d6a05c620ad4fea74cd95f930579 100644
--- a/frontend/src/metabase/public/components/widgets/SharingPane.jsx
+++ b/frontend/src/metabase/public/components/widgets/SharingPane.jsx
@@ -115,7 +115,7 @@ export default class SharingPane extends Component<*, Props, State> {
                 <div className={cx("mb4 flex align-center", { disabled: !resource.public_uuid })}>
                     <RetinaImage
                         width={98}
-                        src="/app/img/simple_embed.png"
+                        src="app/assets/img/simple_embed.png"
                         forceOriginalDimensions={false}
                     />
                     <div className="ml2 flex-full">
@@ -131,7 +131,7 @@ export default class SharingPane extends Component<*, Props, State> {
                     >
                         <RetinaImage
                             width={100}
-                            src="/app/img/secure_embed.png"
+                            src="app/assets/img/secure_embed.png"
                             forceOriginalDimensions={false}
                         />
                         <div className="ml2 flex-full">
diff --git a/frontend/src/metabase/pulse/components/WhatsAPulse.jsx b/frontend/src/metabase/pulse/components/WhatsAPulse.jsx
index 0d44604a99758b1ed9ba13be70b6a39cde1619f4..85ec9f8cef1816e9d139f6badfba3db75d329b4a 100644
--- a/frontend/src/metabase/pulse/components/WhatsAPulse.jsx
+++ b/frontend/src/metabase/pulse/components/WhatsAPulse.jsx
@@ -17,7 +17,7 @@ export default class WhatsAPulse extends Component {
                 <div className="mx4">
                     <RetinaImage
                         width={574}
-                        src="/app/img/pulse_empty_illustration.png"
+                        src="app/assets/img/pulse_empty_illustration.png"
                         forceOriginalDimensions={false}
                     />
                 </div>
diff --git a/frontend/src/metabase/query_builder/components/QueryDownloadWidget.jsx b/frontend/src/metabase/query_builder/components/QueryDownloadWidget.jsx
index 2f9a29423a9c4f6cdcecf43b341e5268b132288e..b5532b94c8b21313e686cbe5b336dbb52764ccff 100644
--- a/frontend/src/metabase/query_builder/components/QueryDownloadWidget.jsx
+++ b/frontend/src/metabase/query_builder/components/QueryDownloadWidget.jsx
@@ -52,7 +52,7 @@ const QueryDownloadWidget = ({ className, card, result, uuid, token }) =>
 const UnsavedQueryButton = ({ className, type, result: { json_query }, card }) =>
     <DownloadButton
         className={className}
-        url={`/api/dataset/${type}`}
+        url={`api/dataset/${type}`}
         params={{ query: JSON.stringify(_.omit(json_query, "constraints")) }}
         extensions={[type]}
     >
@@ -62,7 +62,7 @@ const UnsavedQueryButton = ({ className, type, result: { json_query }, card }) =
 const SavedQueryButton = ({ className, type, result: { json_query }, card }) =>
     <DownloadButton
         className={className}
-        url={`/api/card/${card.id}/query/${type}`}
+        url={`api/card/${card.id}/query/${type}`}
         params={{ parameters: JSON.stringify(json_query.parameters) }}
         extensions={[type]}
     >
diff --git a/frontend/src/metabase/query_builder/containers/QuestionEmbedWidget.jsx b/frontend/src/metabase/query_builder/containers/QuestionEmbedWidget.jsx
index 072267f530518a6f57963585944334e61723a1eb..a96a68b57e4481a3d00510dfa422304e0e2f1470 100644
--- a/frontend/src/metabase/query_builder/containers/QuestionEmbedWidget.jsx
+++ b/frontend/src/metabase/query_builder/containers/QuestionEmbedWidget.jsx
@@ -32,7 +32,7 @@ export default class QuestionEmbedWidget extends Component {
                 onDisablePublicLink={() => deletePublicLink(card)}
                 onUpdateEnableEmbedding={(enableEmbedding) => updateEnableEmbedding(card, enableEmbedding)}
                 onUpdateEmbeddingParams={(embeddingParams) => updateEmbeddingParams(card, embeddingParams)}
-                getPublicUrl={({ public_uuid }, extension) => window.location.origin + Urls.publicCard(public_uuid, extension)}
+                getPublicUrl={({ public_uuid }, extension) => Urls.publicCard(public_uuid, extension)}
                 extensions={["csv", "xlsx", "json"]}
             />
         );
diff --git a/frontend/src/metabase/reference/containers/ReferenceGettingStartedGuide.jsx b/frontend/src/metabase/reference/containers/ReferenceGettingStartedGuide.jsx
index 346b08be2b47f5f40374b039263e8016f9f93d43..d0a637dcb108746d81f88279a5188131d8cfd869 100644
--- a/frontend/src/metabase/reference/containers/ReferenceGettingStartedGuide.jsx
+++ b/frontend/src/metabase/reference/containers/ReferenceGettingStartedGuide.jsx
@@ -285,7 +285,7 @@ export default class ReferenceGettingStartedGuide extends Component {
                             collapsedTitle="Do you have any commonly referenced metrics?"
                             collapsedIcon="ruler"
                             linkMessage="Learn how to define a metric"
-                            link="http://www.metabase.com/docs/latest/administration-guide/05-segments-and-metrics#creating-a-metric"
+                            link="http://www.metabase.com/docs/latest/administration-guide/07-segments-and-metrics.html#creating-a-metric"
                             expand={() => important_metrics.addField({id: null, caveats: null, points_of_interest: null, important_fields: null})}
                         >
                             <div className="my2">
@@ -338,7 +338,7 @@ export default class ReferenceGettingStartedGuide extends Component {
                             collapsedTitle="Do you have any commonly referenced segments or tables?"
                             collapsedIcon="table2"
                             linkMessage="Learn how to create a segment"
-                            link="http://www.metabase.com/docs/latest/administration-guide/05-segments-and-metrics#creating-a-segment"
+                            link="http://www.metabase.com/docs/latest/administration-guide/07-segments-and-metrics.html#creating-a-segment"
                             expand={() => important_segments_and_tables.addField({id: null, type: null, caveats: null, points_of_interest: null})}
                         >
                             <div>
diff --git a/frontend/src/metabase/reference/selectors.js b/frontend/src/metabase/reference/selectors.js
index f9f1a17b2f9401827d830793fc57ef6ab6bd5b2e..e8bc19961dbdf7191258f23e9e654afc8cdedcad 100644
--- a/frontend/src/metabase/reference/selectors.js
+++ b/frontend/src/metabase/reference/selectors.js
@@ -50,7 +50,7 @@ const referenceSections = {
             title: "Metrics are the official numbers that your team cares about",
             adminMessage: "Defining common metrics for your team makes it even easier to ask questions",
             message: "Metrics will appear here once your admins have created some",
-            image: "/app/img/metrics-list",
+            image: "app/assets/img/metrics-list",
             adminAction: "Learn how to create metrics",
             adminLink: "http://www.metabase.com/docs/latest/administration-guide/06-segments-and-metrics.html"
         },
@@ -70,7 +70,7 @@ const referenceSections = {
             title: "Segments are interesting subsets of tables",
             adminMessage: "Defining common segments for your team makes it even easier to ask questions",
             message: "Segments will appear here once your admins have created some",
-            image: "/app/img/segments-list",
+            image: "app/assets/img/segments-list",
             adminAction: "Learn how to create segments",
             adminLink: "http://www.metabase.com/docs/latest/administration-guide/06-segments-and-metrics.html"
         },
@@ -89,7 +89,7 @@ const referenceSections = {
             title: "Metabase is no fun without any data",
             adminMessage: "Your databses will appear here once you connect one",
             message: "Databases will appear here once your admins have added some",
-            image: "/app/img/databases-list",
+            image: "app/assets/img/databases-list",
             adminAction: "Connect a database",
             adminLink: "/admin/databases/create"
         },
diff --git a/frontend/src/metabase/routes.jsx b/frontend/src/metabase/routes.jsx
index 3f227810f1a29b63227ea86c35f77158d845d779..2ef7fbfb05c7e85b604686a1e6d0bff60298d5c8 100644
--- a/frontend/src/metabase/routes.jsx
+++ b/frontend/src/metabase/routes.jsx
@@ -15,7 +15,8 @@ import App from "metabase/App.jsx";
 // auth containers
 import ForgotPasswordApp from "metabase/auth/containers/ForgotPasswordApp.jsx";
 import LoginApp from "metabase/auth/containers/LoginApp.jsx";
-import LogoutApp from "metabase/auth/containers/LogoutApp.jsx"; import PasswordResetApp from "metabase/auth/containers/PasswordResetApp.jsx";
+import LogoutApp from "metabase/auth/containers/LogoutApp.jsx";
+import PasswordResetApp from "metabase/auth/containers/PasswordResetApp.jsx";
 import GoogleNoAccount from "metabase/auth/components/GoogleNoAccount.jsx";
 
 // main app containers
diff --git a/frontend/src/metabase/services.js b/frontend/src/metabase/services.js
index 89c74c9b490eb9ce141df480b8d49f6a3d6f8bec..dcac3910d58d2fc00017a8d0e6a8976396f69db6 100644
--- a/frontend/src/metabase/services.js
+++ b/frontend/src/metabase/services.js
@@ -1,6 +1,7 @@
 /* @flow */
 
-import { GET, PUT, POST, DELETE } from "metabase/lib/api";
+import api from "metabase/lib/api";
+const { GET, PUT, POST, DELETE } = api;
 
 import { IS_EMBED_PREVIEW } from "metabase/lib/embed";
 
@@ -208,6 +209,7 @@ export const GettingStartedApi = {
 export const SetupApi = {
     create:                     POST("/api/setup"),
     validate_db:                POST("/api/setup/validate"),
+    admin_checklist:             GET("/api/setup/admin_checklist"),
 };
 
 export const UserApi = {
@@ -225,6 +227,11 @@ export const UserApi = {
 export const UtilApi = {
     password_check:             POST("/api/util/password_check"),
     random_token:                GET("/api/util/random_token"),
+    logs:                        GET("/api/util/logs"),
+};
+
+export const GeoJSONApi = {
+    get:                         GET("/api/geojson/:id"),
 };
 
 global.services = exports;
diff --git a/frontend/src/metabase/setup/components/DatabaseStep.jsx b/frontend/src/metabase/setup/components/DatabaseStep.jsx
index 9ec39c01afd0de087400196c1d138cb70e9317f8..d847d74c6f11164cebf17d047b70ae3b40e441f5 100644
--- a/frontend/src/metabase/setup/components/DatabaseStep.jsx
+++ b/frontend/src/metabase/setup/components/DatabaseStep.jsx
@@ -151,7 +151,7 @@ export default class DatabaseStep extends Component {
                           : null }
 
                           <div className="Form-field Form-offset">
-                              <a className="link" href="#" onClick={this.skipDatabase.bind(this)}>I'll add my data later</a>
+                              <a className="link" onClick={this.skipDatabase.bind(this)}>I'll add my data later</a>
                           </div>
                     </div>
                 </section>
diff --git a/frontend/src/metabase/tutorial/QueryBuilderTutorial.jsx b/frontend/src/metabase/tutorial/QueryBuilderTutorial.jsx
index 24638a3850d7ec602f9cc23d1af9a0047c4473cc..2dc826cae1a60e15f0ba3adaec321a47e2e396b7 100644
--- a/frontend/src/metabase/tutorial/QueryBuilderTutorial.jsx
+++ b/frontend/src/metabase/tutorial/QueryBuilderTutorial.jsx
@@ -11,7 +11,7 @@ const QUERY_BUILDER_STEPS = [
         getPortalTarget: () => qs(".GuiBuilder"),
         getModal: (props) =>
             <div className="text-centered">
-                <RetinaImage className="mb2" forceOriginalDimensions={false} src="/app/img/qb_tutorial/question_builder.png" width={186} />
+                <RetinaImage className="mb2" forceOriginalDimensions={false} src="app/assets/img/qb_tutorial/question_builder.png" width={186} />
                 <h3>Welcome to the Query Builder!</h3>
                 <p>The Query Builder lets you assemble questions (or "queries") to ask about your data.</p>
                 <a className="Button Button--primary" onClick={props.onNext}>Tell me more</a>
@@ -22,7 +22,7 @@ const QUERY_BUILDER_STEPS = [
         getModalTarget: () => qs(".GuiBuilder-data"),
         getModal: (props) =>
             <div className="text-centered">
-                <RetinaImage id="QB-TutorialTableImg" className="mb2" forceOriginalDimensions={false} src="/app/img/qb_tutorial/table.png" width={157} />
+                <RetinaImage id="QB-TutorialTableImg" className="mb2" forceOriginalDimensions={false} src="app/assets/img/qb_tutorial/table.png" width={157} />
                 <h3>Start by picking the table with the data that you have a question about.</h3>
                 <p>Go ahead and select the "Orders" table from the dropdown menu.</p>
             </div>,
@@ -48,7 +48,7 @@ const QUERY_BUILDER_STEPS = [
                     className="mb2"
                     forceOriginalDimensions={false}
                     id="QB-TutorialFunnelImg"
-                    src="/app/img/qb_tutorial/funnel.png"
+                    src="app/assets/img/qb_tutorial/funnel.png"
                     width={135}
                 />
                 <h3>Filter your data to get just what you want.</h3>
@@ -81,7 +81,7 @@ const QUERY_BUILDER_STEPS = [
                     className="mb2"
                     forceOriginalDimensions={false}
                     id="QB-TutorialCalculatorImg"
-                    src="/app/img/qb_tutorial/calculator.png"
+                    src="app/assets/img/qb_tutorial/calculator.png"
                     width={115}
                 />
                 <h3>Here's where you can choose to add or average your data, count the number of rows in the table, or just view the raw data.</h3>
@@ -103,7 +103,7 @@ const QUERY_BUILDER_STEPS = [
                     className="mb2"
                     forceOriginalDimensions={false}
                     id="QB-TutorialBananaImg"
-                    src="/app/img/qb_tutorial/banana.png"
+                    src="app/assets/img/qb_tutorial/banana.png"
                     width={232}
                 />
                 <h3>Add a grouping to break out your results by category, day, month, and more.</h3>
@@ -131,7 +131,7 @@ const QUERY_BUILDER_STEPS = [
                     className="mb2"
                     forceOriginalDimensions={false}
                     id="QB-TutorialRocketImg"
-                    src="/app/img/qb_tutorial/rocket.png"
+                    src="app/assets/img/qb_tutorial/rocket.png"
                     width={217}
                 />
                 <h3>Run Your Query.</h3>
@@ -148,7 +148,7 @@ const QUERY_BUILDER_STEPS = [
                     className="mb2"
                     forceOriginalDimensions={false}
                     id="QB-TutorialChartImg"
-                    src="/app/img/qb_tutorial/chart.png"
+                    src="app/assets/img/qb_tutorial/chart.png"
                     width={160}
                 />
                 <h3>You can view your results as a chart instead of a table.</h3>
@@ -169,7 +169,7 @@ const QUERY_BUILDER_STEPS = [
                     className="mb2"
                     forceOriginalDimensions={false}
                     id="QB-TutorialBoatImg"
-                    src="/app/img/qb_tutorial/boat.png" width={190}
+                    src="app/assets/img/qb_tutorial/boat.png" width={190}
                 />
                 <h3>Well done!</h3>
                 <p>That's all! If you still have questions, check out our <a className="link" target="_blank" href="http://www.metabase.com/docs/latest/users-guide/start">User's Guide</a>. Have fun exploring your data!</p>
diff --git a/frontend/src/metabase/visualizations/components/ChoroplethMap.jsx b/frontend/src/metabase/visualizations/components/ChoroplethMap.jsx
index d685c95e81c535cbe0aafa3ba32783b0b8adf5f1..a415d447fca4cefd734e833f8860dea050075168 100644
--- a/frontend/src/metabase/visualizations/components/ChoroplethMap.jsx
+++ b/frontend/src/metabase/visualizations/components/ChoroplethMap.jsx
@@ -92,7 +92,7 @@ export default class ChoroplethMap extends Component {
             if (details.builtin) {
                 geoJsonPath = details.url;
             } else {
-                geoJsonPath = "/api/geojson/" + nextProps.settings["map.region"]
+                geoJsonPath = "api/geojson/" + nextProps.settings["map.region"]
             }
             if (this.state.geoJsonPath !== geoJsonPath) {
                 this.setState({
diff --git a/frontend/src/metabase/visualizations/components/LeafletMarkerPinMap.jsx b/frontend/src/metabase/visualizations/components/LeafletMarkerPinMap.jsx
index fabd69a39fdf6b55a855ab6ea5593271ca5a7028..3518c697998b14156dee653bb525f5181569196c 100644
--- a/frontend/src/metabase/visualizations/components/LeafletMarkerPinMap.jsx
+++ b/frontend/src/metabase/visualizations/components/LeafletMarkerPinMap.jsx
@@ -7,7 +7,7 @@ import L from "leaflet";
 import { formatValue } from "metabase/lib/formatting";
 
 const MARKER_ICON = L.icon({
-    iconUrl: "/app/img/pin.png",
+    iconUrl: "app/assets/img/pin.png",
     iconSize: [28, 32],
     iconAnchor: [15, 24],
     popupAnchor: [0, -13]
diff --git a/frontend/src/metabase/visualizations/components/LeafletTilePinMap.jsx b/frontend/src/metabase/visualizations/components/LeafletTilePinMap.jsx
index 9bcf98d86b3422035472c58b3a7969285904b916..d1862f14adf5bcf57b939465bc3dd5eed42df7d4 100644
--- a/frontend/src/metabase/visualizations/components/LeafletTilePinMap.jsx
+++ b/frontend/src/metabase/visualizations/components/LeafletTilePinMap.jsx
@@ -36,7 +36,7 @@ export default class LeafletTilePinMap extends LeafletMap {
             return;
         }
 
-        return '/api/tiles/' + zoom + '/' + coord.x + '/' + coord.y + '/' +
+        return 'api/tiles/' + zoom + '/' + coord.x + '/' + coord.y + '/' +
             latitudeField.id + '/' + longitudeField.id + '/' +
             latitudeIndex + '/' + longitudeIndex + '/' +
             '?query=' + encodeURIComponent(JSON.stringify(dataset_query))
diff --git a/frontend/src/metabase/visualizations/components/Visualization.jsx b/frontend/src/metabase/visualizations/components/Visualization.jsx
index c740ea684b457633a6dfa912e29a1abdc9985c49..f81cef63a66e7fb26828ba62f603f21ad3430c98 100644
--- a/frontend/src/metabase/visualizations/components/Visualization.jsx
+++ b/frontend/src/metabase/visualizations/components/Visualization.jsx
@@ -349,7 +349,7 @@ export default class Visualization extends Component<*, Props, State> {
                 : isDashboard && noResults ?
                     <div className={"flex-full px1 pb1 text-centered flex flex-column layout-centered " + (isDashboard ? "text-slate-light" : "text-slate")}>
                         <Tooltip tooltip="No results!" isEnabled={small}>
-                            <img src="/app/img/no_results.svg" />
+                            <img src="app/assets/img/no_results.svg" />
                         </Tooltip>
                         { !small &&
                             <span className="h4 text-bold">
diff --git a/package.json b/package.json
index b95511323528d9bffedd33103c45169ce39ef74d..b2c69f1d26fcc5b155d5aa77dcc6bbb23f2523fd 100644
--- a/package.json
+++ b/package.json
@@ -20,7 +20,7 @@
     "d3": "^3.5.17",
     "dc": "^2.0.0",
     "diff": "^3.2.0",
-    "history": "^4.5.0",
+    "history": "3",
     "humanize-plus": "^1.8.1",
     "icepick": "^1.1.0",
     "iframe-resizer": "^3.5.11",
diff --git a/project.clj b/project.clj
index d0ac83ab26ed9259318b337b7e43c3b3a7282461..db982ea4083b96cf23faf6cec520072d08129564 100644
--- a/project.clj
+++ b/project.clj
@@ -76,7 +76,8 @@
                  [postgresql "9.3-1102.jdbc41"]                       ; Postgres driver
                  [io.crate/crate-jdbc "2.1.6"]                        ; Crate JDBC driver
                  [prismatic/schema "1.1.5"]                           ; Data schema declaration and validation library
-                 [ring/ring-jetty-adapter "1.5.1"]                    ; Ring adapter using Jetty webserver (used to run a Ring server for unit tests)
+                 [ring/ring-core "1.6.0"]
+                 [ring/ring-jetty-adapter "1.6.0"]                    ; Ring adapter using Jetty webserver (used to run a Ring server for unit tests)
                  [ring/ring-json "0.4.0"]                             ; Ring middleware for reading/writing JSON automatically
                  [stencil "0.5.0"]                                    ; Mustache templates for Clojure
                  [toucan "1.0.3"                                      ; Model layer, hydration, and DB utilities
diff --git a/resources/frontend_client/app/charts/us-states.json b/resources/frontend_client/app/assets/geojson/us-states.json
similarity index 100%
rename from resources/frontend_client/app/charts/us-states.json
rename to resources/frontend_client/app/assets/geojson/us-states.json
diff --git a/resources/frontend_client/app/charts/world.json b/resources/frontend_client/app/assets/geojson/world.json
similarity index 100%
rename from resources/frontend_client/app/charts/world.json
rename to resources/frontend_client/app/assets/geojson/world.json
diff --git a/resources/frontend_client/app/img/.gitkeep b/resources/frontend_client/app/assets/img/.gitkeep
similarity index 100%
rename from resources/frontend_client/app/img/.gitkeep
rename to resources/frontend_client/app/assets/img/.gitkeep
diff --git a/resources/frontend_client/app/img/blown_up.svg b/resources/frontend_client/app/assets/img/blown_up.svg
similarity index 100%
rename from resources/frontend_client/app/img/blown_up.svg
rename to resources/frontend_client/app/assets/img/blown_up.svg
diff --git a/resources/frontend_client/app/components/icons/assets/dash_empty_state.svg b/resources/frontend_client/app/assets/img/dash_empty_state.svg
similarity index 100%
rename from resources/frontend_client/app/components/icons/assets/dash_empty_state.svg
rename to resources/frontend_client/app/assets/img/dash_empty_state.svg
diff --git a/resources/frontend_client/app/img/databases-list.png b/resources/frontend_client/app/assets/img/databases-list.png
similarity index 100%
rename from resources/frontend_client/app/img/databases-list.png
rename to resources/frontend_client/app/assets/img/databases-list.png
diff --git a/resources/frontend_client/app/img/databases-list@2x.png b/resources/frontend_client/app/assets/img/databases-list@2x.png
similarity index 100%
rename from resources/frontend_client/app/img/databases-list@2x.png
rename to resources/frontend_client/app/assets/img/databases-list@2x.png
diff --git a/resources/frontend_client/app/img/disconnect.svg b/resources/frontend_client/app/assets/img/disconnect.svg
similarity index 100%
rename from resources/frontend_client/app/img/disconnect.svg
rename to resources/frontend_client/app/assets/img/disconnect.svg
diff --git a/resources/frontend_client/app/img/external_link.png b/resources/frontend_client/app/assets/img/external_link.png
similarity index 100%
rename from resources/frontend_client/app/img/external_link.png
rename to resources/frontend_client/app/assets/img/external_link.png
diff --git a/resources/frontend_client/app/img/external_link@2x.png b/resources/frontend_client/app/assets/img/external_link@2x.png
similarity index 100%
rename from resources/frontend_client/app/img/external_link@2x.png
rename to resources/frontend_client/app/assets/img/external_link@2x.png
diff --git a/resources/frontend_client/app/components/icons/assets/illustration_ask_question.png b/resources/frontend_client/app/assets/img/illustration_ask_question.png
similarity index 100%
rename from resources/frontend_client/app/components/icons/assets/illustration_ask_question.png
rename to resources/frontend_client/app/assets/img/illustration_ask_question.png
diff --git a/resources/frontend_client/app/components/icons/assets/illustration_dashboard.png b/resources/frontend_client/app/assets/img/illustration_dashboard.png
similarity index 100%
rename from resources/frontend_client/app/components/icons/assets/illustration_dashboard.png
rename to resources/frontend_client/app/assets/img/illustration_dashboard.png
diff --git a/resources/frontend_client/app/components/icons/assets/illustration_home.png b/resources/frontend_client/app/assets/img/illustration_home.png
similarity index 100%
rename from resources/frontend_client/app/components/icons/assets/illustration_home.png
rename to resources/frontend_client/app/assets/img/illustration_home.png
diff --git a/resources/frontend_client/app/components/icons/assets/illustration_question.png b/resources/frontend_client/app/assets/img/illustration_question.png
similarity index 100%
rename from resources/frontend_client/app/components/icons/assets/illustration_question.png
rename to resources/frontend_client/app/assets/img/illustration_question.png
diff --git a/resources/frontend_client/app/components/icons/assets/illustration_tables.png b/resources/frontend_client/app/assets/img/illustration_tables.png
similarity index 100%
rename from resources/frontend_client/app/components/icons/assets/illustration_tables.png
rename to resources/frontend_client/app/assets/img/illustration_tables.png
diff --git a/resources/frontend_client/app/components/icons/assets/illustration_visualization_area.png b/resources/frontend_client/app/assets/img/illustration_visualization_area.png
similarity index 100%
rename from resources/frontend_client/app/components/icons/assets/illustration_visualization_area.png
rename to resources/frontend_client/app/assets/img/illustration_visualization_area.png
diff --git a/resources/frontend_client/app/components/icons/assets/illustration_visualization_bar.png b/resources/frontend_client/app/assets/img/illustration_visualization_bar.png
similarity index 100%
rename from resources/frontend_client/app/components/icons/assets/illustration_visualization_bar.png
rename to resources/frontend_client/app/assets/img/illustration_visualization_bar.png
diff --git a/resources/frontend_client/app/components/icons/assets/illustration_visualization_country.png b/resources/frontend_client/app/assets/img/illustration_visualization_country.png
similarity index 100%
rename from resources/frontend_client/app/components/icons/assets/illustration_visualization_country.png
rename to resources/frontend_client/app/assets/img/illustration_visualization_country.png
diff --git a/resources/frontend_client/app/components/icons/assets/illustration_visualization_line.png b/resources/frontend_client/app/assets/img/illustration_visualization_line.png
similarity index 100%
rename from resources/frontend_client/app/components/icons/assets/illustration_visualization_line.png
rename to resources/frontend_client/app/assets/img/illustration_visualization_line.png
diff --git a/resources/frontend_client/app/components/icons/assets/illustration_visualization_pie.png b/resources/frontend_client/app/assets/img/illustration_visualization_pie.png
similarity index 100%
rename from resources/frontend_client/app/components/icons/assets/illustration_visualization_pie.png
rename to resources/frontend_client/app/assets/img/illustration_visualization_pie.png
diff --git a/resources/frontend_client/app/components/icons/assets/illustration_visualization_scalar.png b/resources/frontend_client/app/assets/img/illustration_visualization_scalar.png
similarity index 100%
rename from resources/frontend_client/app/components/icons/assets/illustration_visualization_scalar.png
rename to resources/frontend_client/app/assets/img/illustration_visualization_scalar.png
diff --git a/resources/frontend_client/app/components/icons/assets/illustration_visualization_state.png b/resources/frontend_client/app/assets/img/illustration_visualization_state.png
similarity index 100%
rename from resources/frontend_client/app/components/icons/assets/illustration_visualization_state.png
rename to resources/frontend_client/app/assets/img/illustration_visualization_state.png
diff --git a/resources/frontend_client/app/components/icons/assets/illustration_visualization_table.png b/resources/frontend_client/app/assets/img/illustration_visualization_table.png
similarity index 100%
rename from resources/frontend_client/app/components/icons/assets/illustration_visualization_table.png
rename to resources/frontend_client/app/assets/img/illustration_visualization_table.png
diff --git a/resources/frontend_client/app/img/lightbulb.png b/resources/frontend_client/app/assets/img/lightbulb.png
similarity index 100%
rename from resources/frontend_client/app/img/lightbulb.png
rename to resources/frontend_client/app/assets/img/lightbulb.png
diff --git a/resources/frontend_client/app/img/lightbulb@2x.png b/resources/frontend_client/app/assets/img/lightbulb@2x.png
similarity index 100%
rename from resources/frontend_client/app/img/lightbulb@2x.png
rename to resources/frontend_client/app/assets/img/lightbulb@2x.png
diff --git a/resources/frontend_client/app/img/metrics-list.png b/resources/frontend_client/app/assets/img/metrics-list.png
similarity index 100%
rename from resources/frontend_client/app/img/metrics-list.png
rename to resources/frontend_client/app/assets/img/metrics-list.png
diff --git a/resources/frontend_client/app/img/metrics-list@2x.png b/resources/frontend_client/app/assets/img/metrics-list@2x.png
similarity index 100%
rename from resources/frontend_client/app/img/metrics-list@2x.png
rename to resources/frontend_client/app/assets/img/metrics-list@2x.png
diff --git a/resources/frontend_client/app/img/no_results.svg b/resources/frontend_client/app/assets/img/no_results.svg
similarity index 100%
rename from resources/frontend_client/app/img/no_results.svg
rename to resources/frontend_client/app/assets/img/no_results.svg
diff --git a/resources/frontend_client/app/img/no_understand.svg b/resources/frontend_client/app/assets/img/no_understand.svg
similarity index 100%
rename from resources/frontend_client/app/img/no_understand.svg
rename to resources/frontend_client/app/assets/img/no_understand.svg
diff --git a/resources/frontend_client/app/home/partials/onboarding_illustration_dashboards.png b/resources/frontend_client/app/assets/img/onboarding_illustration_dashboards.png
similarity index 100%
rename from resources/frontend_client/app/home/partials/onboarding_illustration_dashboards.png
rename to resources/frontend_client/app/assets/img/onboarding_illustration_dashboards.png
diff --git a/resources/frontend_client/app/home/partials/onboarding_illustration_questions.png b/resources/frontend_client/app/assets/img/onboarding_illustration_questions.png
similarity index 100%
rename from resources/frontend_client/app/home/partials/onboarding_illustration_questions.png
rename to resources/frontend_client/app/assets/img/onboarding_illustration_questions.png
diff --git a/resources/frontend_client/app/home/partials/onboarding_illustration_tables.png b/resources/frontend_client/app/assets/img/onboarding_illustration_tables.png
similarity index 100%
rename from resources/frontend_client/app/home/partials/onboarding_illustration_tables.png
rename to resources/frontend_client/app/assets/img/onboarding_illustration_tables.png
diff --git a/resources/frontend_client/app/img/pin.png b/resources/frontend_client/app/assets/img/pin.png
similarity index 100%
rename from resources/frontend_client/app/img/pin.png
rename to resources/frontend_client/app/assets/img/pin.png
diff --git a/resources/frontend_client/app/img/pulse_empty_illustration.png b/resources/frontend_client/app/assets/img/pulse_empty_illustration.png
similarity index 100%
rename from resources/frontend_client/app/img/pulse_empty_illustration.png
rename to resources/frontend_client/app/assets/img/pulse_empty_illustration.png
diff --git a/resources/frontend_client/app/img/pulse_empty_illustration@2x.png b/resources/frontend_client/app/assets/img/pulse_empty_illustration@2x.png
similarity index 100%
rename from resources/frontend_client/app/img/pulse_empty_illustration@2x.png
rename to resources/frontend_client/app/assets/img/pulse_empty_illustration@2x.png
diff --git a/resources/frontend_client/app/img/pulse_no_results.png b/resources/frontend_client/app/assets/img/pulse_no_results.png
similarity index 100%
rename from resources/frontend_client/app/img/pulse_no_results.png
rename to resources/frontend_client/app/assets/img/pulse_no_results.png
diff --git a/resources/frontend_client/app/img/pulse_no_results@2x.png b/resources/frontend_client/app/assets/img/pulse_no_results@2x.png
similarity index 100%
rename from resources/frontend_client/app/img/pulse_no_results@2x.png
rename to resources/frontend_client/app/assets/img/pulse_no_results@2x.png
diff --git a/resources/frontend_client/app/img/qb_tutorial/banana.png b/resources/frontend_client/app/assets/img/qb_tutorial/banana.png
similarity index 100%
rename from resources/frontend_client/app/img/qb_tutorial/banana.png
rename to resources/frontend_client/app/assets/img/qb_tutorial/banana.png
diff --git a/resources/frontend_client/app/img/qb_tutorial/banana@2x.png b/resources/frontend_client/app/assets/img/qb_tutorial/banana@2x.png
similarity index 100%
rename from resources/frontend_client/app/img/qb_tutorial/banana@2x.png
rename to resources/frontend_client/app/assets/img/qb_tutorial/banana@2x.png
diff --git a/resources/frontend_client/app/img/qb_tutorial/boat.png b/resources/frontend_client/app/assets/img/qb_tutorial/boat.png
similarity index 100%
rename from resources/frontend_client/app/img/qb_tutorial/boat.png
rename to resources/frontend_client/app/assets/img/qb_tutorial/boat.png
diff --git a/resources/frontend_client/app/img/qb_tutorial/boat@2x.png b/resources/frontend_client/app/assets/img/qb_tutorial/boat@2x.png
similarity index 100%
rename from resources/frontend_client/app/img/qb_tutorial/boat@2x.png
rename to resources/frontend_client/app/assets/img/qb_tutorial/boat@2x.png
diff --git a/resources/frontend_client/app/img/qb_tutorial/calculator.png b/resources/frontend_client/app/assets/img/qb_tutorial/calculator.png
similarity index 100%
rename from resources/frontend_client/app/img/qb_tutorial/calculator.png
rename to resources/frontend_client/app/assets/img/qb_tutorial/calculator.png
diff --git a/resources/frontend_client/app/img/qb_tutorial/calculator@2x.png b/resources/frontend_client/app/assets/img/qb_tutorial/calculator@2x.png
similarity index 100%
rename from resources/frontend_client/app/img/qb_tutorial/calculator@2x.png
rename to resources/frontend_client/app/assets/img/qb_tutorial/calculator@2x.png
diff --git a/resources/frontend_client/app/img/qb_tutorial/chart.png b/resources/frontend_client/app/assets/img/qb_tutorial/chart.png
similarity index 100%
rename from resources/frontend_client/app/img/qb_tutorial/chart.png
rename to resources/frontend_client/app/assets/img/qb_tutorial/chart.png
diff --git a/resources/frontend_client/app/img/qb_tutorial/chart@2x.png b/resources/frontend_client/app/assets/img/qb_tutorial/chart@2x.png
similarity index 100%
rename from resources/frontend_client/app/img/qb_tutorial/chart@2x.png
rename to resources/frontend_client/app/assets/img/qb_tutorial/chart@2x.png
diff --git a/resources/frontend_client/app/img/qb_tutorial/funnel.png b/resources/frontend_client/app/assets/img/qb_tutorial/funnel.png
similarity index 100%
rename from resources/frontend_client/app/img/qb_tutorial/funnel.png
rename to resources/frontend_client/app/assets/img/qb_tutorial/funnel.png
diff --git a/resources/frontend_client/app/img/qb_tutorial/funnel@2x.png b/resources/frontend_client/app/assets/img/qb_tutorial/funnel@2x.png
similarity index 100%
rename from resources/frontend_client/app/img/qb_tutorial/funnel@2x.png
rename to resources/frontend_client/app/assets/img/qb_tutorial/funnel@2x.png
diff --git a/resources/frontend_client/app/img/qb_tutorial/question_builder.png b/resources/frontend_client/app/assets/img/qb_tutorial/question_builder.png
similarity index 100%
rename from resources/frontend_client/app/img/qb_tutorial/question_builder.png
rename to resources/frontend_client/app/assets/img/qb_tutorial/question_builder.png
diff --git a/resources/frontend_client/app/img/qb_tutorial/question_builder@2x.png b/resources/frontend_client/app/assets/img/qb_tutorial/question_builder@2x.png
similarity index 100%
rename from resources/frontend_client/app/img/qb_tutorial/question_builder@2x.png
rename to resources/frontend_client/app/assets/img/qb_tutorial/question_builder@2x.png
diff --git a/resources/frontend_client/app/img/qb_tutorial/rocket.png b/resources/frontend_client/app/assets/img/qb_tutorial/rocket.png
similarity index 100%
rename from resources/frontend_client/app/img/qb_tutorial/rocket.png
rename to resources/frontend_client/app/assets/img/qb_tutorial/rocket.png
diff --git a/resources/frontend_client/app/img/qb_tutorial/rocket@2x.png b/resources/frontend_client/app/assets/img/qb_tutorial/rocket@2x.png
similarity index 100%
rename from resources/frontend_client/app/img/qb_tutorial/rocket@2x.png
rename to resources/frontend_client/app/assets/img/qb_tutorial/rocket@2x.png
diff --git a/resources/frontend_client/app/img/qb_tutorial/table.png b/resources/frontend_client/app/assets/img/qb_tutorial/table.png
similarity index 100%
rename from resources/frontend_client/app/img/qb_tutorial/table.png
rename to resources/frontend_client/app/assets/img/qb_tutorial/table.png
diff --git a/resources/frontend_client/app/img/qb_tutorial/table@2x.png b/resources/frontend_client/app/assets/img/qb_tutorial/table@2x.png
similarity index 100%
rename from resources/frontend_client/app/img/qb_tutorial/table@2x.png
rename to resources/frontend_client/app/assets/img/qb_tutorial/table@2x.png
diff --git a/resources/frontend_client/app/img/secure_embed.png b/resources/frontend_client/app/assets/img/secure_embed.png
similarity index 100%
rename from resources/frontend_client/app/img/secure_embed.png
rename to resources/frontend_client/app/assets/img/secure_embed.png
diff --git a/resources/frontend_client/app/img/secure_embed@2x.png b/resources/frontend_client/app/assets/img/secure_embed@2x.png
similarity index 100%
rename from resources/frontend_client/app/img/secure_embed@2x.png
rename to resources/frontend_client/app/assets/img/secure_embed@2x.png
diff --git a/resources/frontend_client/app/img/segments-list.png b/resources/frontend_client/app/assets/img/segments-list.png
similarity index 100%
rename from resources/frontend_client/app/img/segments-list.png
rename to resources/frontend_client/app/assets/img/segments-list.png
diff --git a/resources/frontend_client/app/img/segments-list@2x.png b/resources/frontend_client/app/assets/img/segments-list@2x.png
similarity index 100%
rename from resources/frontend_client/app/img/segments-list@2x.png
rename to resources/frontend_client/app/assets/img/segments-list@2x.png
diff --git a/resources/frontend_client/app/img/simple_embed.png b/resources/frontend_client/app/assets/img/simple_embed.png
similarity index 100%
rename from resources/frontend_client/app/img/simple_embed.png
rename to resources/frontend_client/app/assets/img/simple_embed.png
diff --git a/resources/frontend_client/app/img/simple_embed@2x.png b/resources/frontend_client/app/assets/img/simple_embed@2x.png
similarity index 100%
rename from resources/frontend_client/app/img/simple_embed@2x.png
rename to resources/frontend_client/app/assets/img/simple_embed@2x.png
diff --git a/resources/frontend_client/app/img/slack.png b/resources/frontend_client/app/assets/img/slack.png
similarity index 100%
rename from resources/frontend_client/app/img/slack.png
rename to resources/frontend_client/app/assets/img/slack.png
diff --git a/resources/frontend_client/app/img/slack@2x.png b/resources/frontend_client/app/assets/img/slack@2x.png
similarity index 100%
rename from resources/frontend_client/app/img/slack@2x.png
rename to resources/frontend_client/app/assets/img/slack@2x.png
diff --git a/resources/frontend_client/app/img/slack_emoji.png b/resources/frontend_client/app/assets/img/slack_emoji.png
similarity index 100%
rename from resources/frontend_client/app/img/slack_emoji.png
rename to resources/frontend_client/app/assets/img/slack_emoji.png
diff --git a/resources/frontend_client/app/img/slack_emoji@2x.png b/resources/frontend_client/app/assets/img/slack_emoji@2x.png
similarity index 100%
rename from resources/frontend_client/app/img/slack_emoji@2x.png
rename to resources/frontend_client/app/assets/img/slack_emoji@2x.png
diff --git a/resources/frontend_client/app/components/icons/assets/smile.svg b/resources/frontend_client/app/assets/img/smile.svg
similarity index 100%
rename from resources/frontend_client/app/components/icons/assets/smile.svg
rename to resources/frontend_client/app/assets/img/smile.svg
diff --git a/resources/frontend_client/app/img/stopwatch.svg b/resources/frontend_client/app/assets/img/stopwatch.svg
similarity index 100%
rename from resources/frontend_client/app/img/stopwatch.svg
rename to resources/frontend_client/app/assets/img/stopwatch.svg
diff --git a/resources/frontend_client/app/img/test/pin-map-reference-image1.png b/resources/frontend_client/app/img/test/pin-map-reference-image1.png
deleted file mode 100644
index 4b6bee6d07bd367b580d99af04312152501b5f04..0000000000000000000000000000000000000000
Binary files a/resources/frontend_client/app/img/test/pin-map-reference-image1.png and /dev/null differ
diff --git a/resources/frontend_client/index_template.html b/resources/frontend_client/index_template.html
index 573608b6bd9945517b89250107362f6c3607076a..c2c0924cc3ddadc6bb3b0702e99f00e40767b781 100644
--- a/resources/frontend_client/index_template.html
+++ b/resources/frontend_client/index_template.html
@@ -11,8 +11,34 @@
 
         <title>Metabase</title>
 
+        <base href={{{base_href}}} />
+
         <script type="text/javascript">
-            window.MetabaseBootstrap = {{{bootstrap_json}}};
+            (function() {
+                window.MetabaseBootstrap = {{{bootstrap_json}}};
+
+                var configuredRoot = {{{base_href}}};
+                var actualRoot = "/";
+
+                // Add trailing slashes
+                var backendPathname = {{{uri}}}.replace(/\/*$/, "/");
+                // e.x. "/questions/"
+                var frontendPathname = window.location.pathname.replace(/\/*$/, "/");
+                // e.x. "/metabase/questions/"
+                if (backendPathname === frontendPathname.slice(-backendPathname.length)) {
+                    // Remove the backend pathname from the end of the frontend pathname
+                    actualRoot = frontendPathname.slice(0, -backendPathname.length) + "/";
+                    // e.x. "/metabase/"
+                }
+
+                if (actualRoot !== configuredRoot) {
+                    console.warn("Warning: the Metabase site URL basename \"" + configuredRoot + "\" does not match the actual basename \"" + actualRoot + "\".");
+                    console.warn("You probably want to update the Site URL setting to \"" + window.location.origin + actualRoot + "\"");
+                    document.getElementsByTagName("base")[0].href = actualRoot;
+                }
+
+                window.MetabaseRoot = actualRoot;
+            })();
         </script>
     </head>
 
diff --git a/src/metabase/api/card.clj b/src/metabase/api/card.clj
index 5863d4ebf813d986a1ba031eadd01c61f3696c35..12541c5dd35fff4213d8750dabf0008e5f72cc60 100644
--- a/src/metabase/api/card.clj
+++ b/src/metabase/api/card.clj
@@ -5,6 +5,7 @@
             [compojure.core :refer [DELETE GET POST PUT]]
             [metabase
              [events :as events]
+             [middleware :as middleware]
              [public-settings :as public-settings]
              [query-processor :as qp]
              [util :as u]]
@@ -12,6 +13,7 @@
              [common :as api]
              [dataset :as dataset-api]
              [label :as label-api]]
+            [metabase.api.common.internal :refer [route-fn-name]]
             [metabase.models
              [card :as card :refer [Card]]
              [card-favorite :refer [CardFavorite]]
@@ -467,5 +469,5 @@
   (api/check-embedding-enabled)
   (db/select [Card :name :id], :enable_embedding true, :archived false))
 
-
-(api/define-routes)
+(api/define-routes
+  (middleware/streaming-json-response (route-fn-name 'POST "/:card-id/query")))
diff --git a/src/metabase/api/common.clj b/src/metabase/api/common.clj
index 0e18189aa8075b6306801d0ece1df95d38337e2a..4b8514c881880868c17a05d7c98940a6083f1f18 100644
--- a/src/metabase/api/common.clj
+++ b/src/metabase/api/common.clj
@@ -264,7 +264,7 @@
                                                            (s/replace #"^metabase\." "")
                                                            (s/replace #"\." "/"))
                                                        (u/pprint-to-str (concat api-routes additional-routes))))
-       ~@api-routes ~@additional-routes)))
+       ~@additional-routes ~@api-routes)))
 
 
 ;;; ------------------------------------------------------------ PERMISSIONS CHECKING HELPER FNS ------------------------------------------------------------
diff --git a/src/metabase/api/dataset.clj b/src/metabase/api/dataset.clj
index 548e15d97b8309c874967b1843ee4b2d1dcf9d1b..7666d83e7fd55382770e080c7339684a33fe9961 100644
--- a/src/metabase/api/dataset.clj
+++ b/src/metabase/api/dataset.clj
@@ -6,9 +6,11 @@
             [compojure.core :refer [POST]]
             [dk.ative.docjure.spreadsheet :as spreadsheet]
             [metabase
+             [middleware :as middleware]
              [query-processor :as qp]
              [util :as u]]
             [metabase.api.common :as api]
+            [metabase.api.common.internal :refer [route-fn-name]]
             [metabase.models
              [database :refer [Database]]
              [query :as query]]
@@ -124,5 +126,5 @@
       (qp/dataset-query (dissoc query :constraints)
         {:executed-by api/*current-user-id*, :context (export-format->context export-format)}))))
 
-
-(api/define-routes)
+(api/define-routes
+  (middleware/streaming-json-response (route-fn-name 'POST "/")))
diff --git a/src/metabase/api/geojson.clj b/src/metabase/api/geojson.clj
index 1fc5e31143155fcb38318576a0c550c2806bd0bd..19494cd7be689705ed1926c8444477f5f043a851 100644
--- a/src/metabase/api/geojson.clj
+++ b/src/metabase/api/geojson.clj
@@ -17,11 +17,11 @@
   true)
 
 (defn- valid-json-resource?
-  "Does this RELATIVE-PATH point to a valid local JSON resource? (RELATIVE-PATH is something like \"app/charts/us-states.json\".)"
+  "Does this RELATIVE-PATH point to a valid local JSON resource? (RELATIVE-PATH is something like \"app/assets/geojson/us-states.json\".)"
   [relative-path]
   (when-let [^java.net.URI uri (u/ignore-exceptions (java.net.URI. relative-path))]
     (when-not (.isAbsolute uri)
-      (valid-json? (io/resource (str "frontend_client" uri))))))
+      (valid-json? (io/resource (str "frontend_client/" uri))))))
 
 (defn- valid-json-url?
   "Is URL a valid HTTP URL and does it point to valid JSON?"
@@ -47,12 +47,12 @@
 
 (def ^:private ^:const builtin-geojson
   {:us_states       {:name        "United States"
-                     :url         "/app/charts/us-states.json"
+                     :url         "app/assets/geojson/us-states.json"
                      :region_key  "name"
                      :region_name "name"
                      :builtin     true}
    :world_countries {:name        "World"
-                     :url         "/app/charts/world.json"
+                     :url         "app/assets/geojson/world.json"
                      :region_key  "ISO_A2"
                      :region_name "NAME"
                      :builtin     true}})
diff --git a/src/metabase/core.clj b/src/metabase/core.clj
index c36cfe2750630b5b58c2ce441bb1605d12bcbe9e..16cc4c3c09a137e5a9ce566e60b7cc9b4344dcbc 100644
--- a/src/metabase/core.clj
+++ b/src/metabase/core.clj
@@ -37,7 +37,7 @@
 
 (def ^:private app
   "The primary entry point to the Ring HTTP server."
-  (-> routes/routes
+  (-> #'routes/routes                    ; the #' is to allow tests to redefine endpoints
       mb-middleware/log-api-call
       mb-middleware/add-security-headers ; Add HTTP headers to API responses to prevent them from being cached
       (wrap-json-body                    ; extracts json POST body and makes it avaliable on request
diff --git a/src/metabase/middleware.clj b/src/metabase/middleware.clj
index a521f291aeee14404af9ccd4c6c5a8b1d2a0d07f..5e2467e44bff706c5269cb29b60395e045bfa05a 100644
--- a/src/metabase/middleware.clj
+++ b/src/metabase/middleware.clj
@@ -1,6 +1,10 @@
 (ns metabase.middleware
   "Metabase-specific middleware functions & configuration."
-  (:require [cheshire.generate :refer [add-encoder encode-nil encode-str]]
+  (:require [cheshire
+             [core :as json]
+             [generate :refer [add-encoder encode-nil encode-str]]]
+            [clojure.core.async :as async]
+            [clojure.java.io :as io]
             [clojure.tools.logging :as log]
             [metabase
              [config :as config]
@@ -15,10 +19,13 @@
              [setting :refer [defsetting]]
              [user :as user :refer [User]]]
             monger.json
+            [ring.core.protocols :as protocols]
+            [ring.util.response :as response]
             [toucan
              [db :as db]
              [models :as models]])
-  (:import com.fasterxml.jackson.core.JsonGenerator))
+  (:import com.fasterxml.jackson.core.JsonGenerator
+           java.io.OutputStream))
 
 ;;; # ------------------------------------------------------------ UTIL FNS ------------------------------------------------------------
 
@@ -354,3 +361,75 @@
            (handler request))
          (catch Throwable e
            {:status 400, :body (.getMessage e)}))))
+
+;;; ------------------------------------------------------------ EXCEPTION HANDLING ------------------------------------------------------------
+
+(def ^:private ^:const streaming-response-keep-alive-interval-ms
+  "Interval between sending newline characters to keep Heroku from terminating
+   requests like queries that take a long time to complete."
+  (* 1 1000))
+
+;; Handle ring response maps that contain a core.async chan in the :body key:
+;;
+;; {:status 200
+;;  :body (async/chan)}
+;;
+;; and send each string sent to that queue back to the browser as it arrives
+;; this avoids output buffering in the default stream handling which was not sending
+;; any responses until ~5k characters where in the queue.
+(extend-protocol protocols/StreamableResponseBody
+  clojure.core.async.impl.channels.ManyToManyChannel
+  (write-body-to-stream [output-queue _ ^OutputStream output-stream]
+    (log/debug (u/format-color 'green "starting streaming request"))
+    (with-open [out (io/writer output-stream)]
+      (loop [chunk (async/<!! output-queue)]
+        (when-not (= chunk ::EOF)
+          (.write out (str chunk))
+          (try
+            (.flush out)
+            (catch org.eclipse.jetty.io.EofException e
+              (log/info (u/format-color 'yellow "connection closed, canceling request %s" (type e)))
+              (async/close! output-queue)
+              (throw e)))
+          (recur (async/<!! output-queue)))))))
+
+(defn streaming-json-response
+  "This midelware assumes handlers fail early or return success
+   Run the handler in a future and send newlines to keep the connection open
+   and help detect when the browser is no longer listening for the response.
+   Waits for one second to see if the handler responds immediately, If it does
+   then there is no need to stream the response and it is sent back directly.
+   In cases where it takes longer than a second, assume the eventual result will
+   be a success and start sending newlines to keep the connection open."
+  [handler]
+  (fn [request]
+    (let [response            (future (handler request))
+          optimistic-response (deref response streaming-response-keep-alive-interval-ms ::no-immediate-response)]
+      (if (= optimistic-response ::no-immediate-response)
+        ;; if we didn't get a normal response in the first poling interval assume it's going to be slow
+        ;; and start sending keepalive packets.
+        (let [output (async/chan 1)]
+          ;; the output channel will be closed by the adapter when the incoming connection is closed.
+          (future
+            (loop []
+              (Thread/sleep streaming-response-keep-alive-interval-ms)
+              (when-not (realized? response)
+                (log/debug (u/format-color 'blue "Response not ready, writing one byte & sleeping..."))
+                ;; a newline padding character is used because it forces output flushing in jetty.
+                ;; if sending this character fails because the connection is closed, the chan will then close.
+                ;; Newlines are no-ops when reading JSON which this depends upon.
+                (when-not (async/>!! output "\n")
+                  (log/info (u/format-color 'yellow "canceled request %s" (future-cancel response)))
+                  (future-cancel response)) ;; try our best to kill the thread running the query.
+                (recur))))
+          (future
+            (try
+              ;; This is the part where we make this assume it's a JSON response we are sending.
+              (async/>!! output (json/encode (:body @response)))
+              (finally
+                (async/>!! output ::EOF)
+                (async/close! response))))
+          ;; here we assume a successful response will be written to the output channel.
+          (assoc (response/response output)
+            :content-type "applicaton/json"))
+          optimistic-response))))
diff --git a/src/metabase/pulse/render.clj b/src/metabase/pulse/render.clj
index e57965c17478cf74bcebafcec6a79d6ce22d9731..aeda60eff850892edae154ced2919ea14e104df0 100644
--- a/src/metabase/pulse/render.clj
+++ b/src/metabase/pulse/render.clj
@@ -376,7 +376,7 @@
 (defn- render:empty [_ _]
   [:div {:style (style {:text-align :center})}
    [:img {:style (style {:width :104px})
-          :src   (render-image-with-filename "frontend_client/app/img/pulse_no_results@2x.png")}]
+          :src   (render-image-with-filename "frontend_client/app/assets/img/pulse_no_results@2x.png")}]
    [:div {:style (style {:margin-top :8px
                          :color      color-gray-4})}
     "No results"]])
@@ -426,7 +426,7 @@
          (when *include-buttons*
            [:img {:style (style {:width :16px})
                   :width 16
-                  :src   (render-image-with-filename "frontend_client/app/img/external_link.png")}])]]]])
+                  :src   (render-image-with-filename "frontend_client/app/assets/img/external_link.png")}])]]]])
   (try
     (when error
       (throw (Exception. (str "Card has errors: " error))))
diff --git a/src/metabase/routes.clj b/src/metabase/routes.clj
index bfd8eb001f4efba61ccb817cc2f946de868f3f8a..469c73e3002cc7741c7cc8820ac843c87acb46fa 100644
--- a/src/metabase/routes.clj
+++ b/src/metabase/routes.clj
@@ -1,6 +1,7 @@
 (ns metabase.routes
   (:require [cheshire.core :as json]
             [clojure.java.io :as io]
+            [clojure.string :as str]
             [compojure
              [core :refer [context defroutes GET]]
              [route :as route]]
@@ -15,6 +16,14 @@
             [ring.util.response :as resp]
             [stencil.core :as stencil]))
 
+(defn- base-href []
+  (str (.getPath (io/as-url (public-settings/site-url))) "/"))
+
+(defn- escape-script [s]
+  ;; Escapes text to be included in an inline <script> tag, in particular the string '</script'
+  ;; https://stackoverflow.com/questions/14780858/escape-in-script-tag-contents/23983448#23983448
+  (str/replace s #"</script" "</scr\\\\ipt"))
+
 (defn- load-file-at-path [path]
   (slurp (or (io/resource path)
              (throw (Exception. (str "Cannot find '" path "'. Did you remember to build the Metabase frontend?"))))))
@@ -25,7 +34,9 @@
 (defn- entrypoint [entry embeddable? {:keys [uri]}]
   (-> (if (init-status/complete?)
         (load-template (str "frontend_client/" entry ".html")
-                       {:bootstrap_json (json/generate-string (public-settings/public-settings))
+                       {:bootstrap_json (escape-script (json/generate-string (public-settings/public-settings)))
+                        :uri            (escape-script (json/generate-string uri))
+                        :base_href      (escape-script (json/generate-string (base-href)))
                         :embed_code     (when embeddable? (embed/head uri))})
         (load-file-at-path "frontend_client/init.html"))
       resp/response
diff --git a/test/metabase/api/geojson_test.clj b/test/metabase/api/geojson_test.clj
index ad30b2e38fc4222169e7c4c874fc5fffd8c90018..72a6d767b7f61681b2b1886fb11f117be0972440 100644
--- a/test/metabase/api/geojson_test.clj
+++ b/test/metabase/api/geojson_test.clj
@@ -31,7 +31,7 @@
 
 ;;; test valid-json-resource?
 (expect
-  (valid-json-resource? "/app/charts/us-states.json"))
+  (valid-json-resource? "app/assets/geojson/us-states.json"))
 
 
 ;;; test the CustomGeoJSON schema
diff --git a/test/metabase/middleware_test.clj b/test/metabase/middleware_test.clj
index e67b97392181bfdeaf83a484eb69696b59158206..90175af670f6a37eb34c3cf14472ce4a174e0676 100644
--- a/test/metabase/middleware_test.clj
+++ b/test/metabase/middleware_test.clj
@@ -1,13 +1,20 @@
 (ns metabase.middleware-test
   (:require [cheshire.core :as json]
+            [clojure.core.async :as async]
+            [clojure.java.io :as io]
+            [clojure.tools.logging :as log]
+            [compojure.core :refer [GET]]
             [expectations :refer :all]
             [metabase
-             [middleware :refer :all]
+             [config :as config]
+             [middleware :as middleware :refer :all]
+             [routes :as routes]
              [util :as u]]
             [metabase.api.common :refer [*current-user* *current-user-id*]]
             [metabase.models.session :refer [Session]]
             [metabase.test.data.users :refer :all]
             [ring.mock.request :as mock]
+            [ring.util.response :as resp]
             [toucan.db :as db]))
 
 ;;  ===========================  TEST wrap-session-id middleware  ===========================
@@ -176,3 +183,95 @@
 (expect "{\"my-bytes\":\"0xC42360D7\"}"
         (json/generate-string {:my-bytes (byte-array [196 35  96 215  8 106 108 248 183 215 244 143  17 160 53 186
                                                       213 30 116  25 87  31 123 172 207 108  47 107 191 215 76  92])}))
+;;; stuff here
+
+(defn- streaming-fast-success [_]
+  (resp/response {:success true}))
+
+(defn- streaming-fast-failure [_]
+  (throw (Exception. "immediate failure")))
+
+(defn- streaming-slow-success [_]
+  (Thread/sleep 7000)
+  (resp/response {:success true}))
+
+(defn- streaming-slow-failure [_]
+  (Thread/sleep 7000)
+  (throw (Exception. "delayed failure")))
+
+(defn- test-streaming-endpoint [handler]
+  (let [path (str handler)]
+    (with-redefs [metabase.routes/routes (compojure.core/routes
+                                          (GET (str "/" path) [] (middleware/streaming-json-response
+                                                                  handler)))]
+      (let  [connection (async/chan 1000)
+             reader (io/input-stream (str "http://localhost:" (config/config-int :mb-jetty-port) "/" path))]
+        (async/go-loop [next-char (.read reader)]
+          (if (pos? next-char)
+            (do
+              (async/>! connection (char next-char))
+              (recur (.read reader)))
+            (async/close! connection)))
+        (let [_ (Thread/sleep 1500)
+              first-second (async/poll! connection)
+              _ (Thread/sleep 1000)
+              second-second (async/poll! connection)
+              eventually (apply str (async/<!! (async/into [] connection)))]
+          [first-second second-second eventually])))))
+
+
+;;slow success
+(expect
+  [\newline \newline "\n\n\n{\"success\":true}"]
+  (test-streaming-endpoint streaming-slow-success))
+
+;; immediate success should have no padding
+(expect
+  [\{ \" "success\":true}"]
+  (test-streaming-endpoint streaming-fast-success))
+
+;; we know delayed failures (exception thrown) will just drop the connection
+(expect
+  [\newline \newline "\n\n\n"]
+  (test-streaming-endpoint streaming-slow-failure))
+
+;; immediate failures (where an exception is thown will return a 500
+(expect
+  #"Server returned HTTP response code: 500 for URL:.*"
+  (try
+    (test-streaming-endpoint streaming-fast-failure)
+    (catch java.io.IOException e
+      (.getMessage e))))
+
+;; test that handler is killed when connection closes
+(def test-slow-handler-state (atom :unset))
+
+(defn- test-slow-handler [_]
+  (log/debug (u/format-color 'yellow "starting test-slow-handler"))
+  (Thread/sleep 7000)  ;; this is somewhat long to make sure the keepalive polling has time to kill it.
+  (reset! test-slow-handler-state :ran-to-compleation)
+  (log/debug (u/format-color 'yellow "finished test-slow-handler"))
+  (resp/response {:success true}))
+
+(defn- start-and-maybe-kill-test-request [kill?]
+  (reset! test-slow-handler-state :initial-state)
+  (let [path "test-slow-handler"]
+    (with-redefs [metabase.routes/routes (compojure.core/routes
+                                          (GET (str "/" path) [] (middleware/streaming-json-response
+                                                                  test-slow-handler)))]
+      (let  [reader (io/input-stream (str "http://localhost:" (config/config-int :mb-jetty-port) "/" path))]
+        (Thread/sleep 1500)
+        (when kill?
+          (.close reader))
+        (Thread/sleep 10000)))) ;; this is long enough to ensure that the handler has run to completion if it was not killed.
+  @test-slow-handler-state)
+
+;; In this first test we will close the connection before the test handler gets to change the state
+(expect
+  :initial-state
+  (start-and-maybe-kill-test-request true))
+
+;; and to make sure this test actually works, run the same test again and let it change the state.
+(expect
+  :ran-to-compleation
+  (start-and-maybe-kill-test-request false))
diff --git a/webpack.config.js b/webpack.config.js
index 1783c3ad8019320d11bfa160bc120de31f9c115f..4c0d5703592b660695dab359421ab2fdf83e941a 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -69,6 +69,7 @@ var CSS_CONFIG = {
         "[hash:base64:5]",
     restructuring: false,
     compatibility: true,
+    url: false, // disabled because we need to use relative url()
     importLoaders: 1
 }
 
@@ -89,7 +90,7 @@ var config = module.exports = {
         path: BUILD_PATH + '/app/dist',
         // NOTE: the filename on disk won't include "?[chunkhash]" but the URL in index.html generated by HtmlWebpackPlugin will:
         filename: '[name].bundle.js?[hash]',
-        publicPath: '/app/dist/'
+        publicPath: 'app/dist/'
     },
 
     module: {
@@ -200,7 +201,7 @@ if (NODE_ENV === "hot") {
     config.output.filename = "[name].hot.bundle.js?[hash]";
 
     // point the publicPath (inlined in index.html by HtmlWebpackPlugin) to the hot-reloading server
-    config.output.publicPath = "http://localhost:8080" + config.output.publicPath;
+    config.output.publicPath = "http://localhost:8080/" + config.output.publicPath;
 
     config.module.loaders.unshift({
         test: /\.jsx$/,
diff --git a/yarn.lock b/yarn.lock
index eac22ac2eea290c33f0e1bcad970bb3dc95bfe1d..dd1addc4e96816f9480a59cdc9f0feec58bffe54 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3313,7 +3313,7 @@ he@1.1.x:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd"
 
-history@^3.0.0:
+history@3, history@^3.0.0:
   version "3.3.0"
   resolved "https://registry.yarnpkg.com/history/-/history-3.3.0.tgz#fcedcce8f12975371545d735461033579a6dae9c"
   dependencies:
@@ -3322,16 +3322,6 @@ history@^3.0.0:
     query-string "^4.2.2"
     warning "^3.0.0"
 
-history@^4.5.0:
-  version "4.6.1"
-  resolved "https://registry.yarnpkg.com/history/-/history-4.6.1.tgz#911cf8eb65728555a94f2b12780a0c531a14d2fd"
-  dependencies:
-    invariant "^2.2.1"
-    loose-envify "^1.2.0"
-    resolve-pathname "^2.0.0"
-    value-equal "^0.2.0"
-    warning "^3.0.0"
-
 hmac-drbg@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
@@ -6686,10 +6676,6 @@ resolve-from@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226"
 
-resolve-pathname@^2.0.0:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-2.0.2.tgz#e55c016eb2e9df1de98e85002282bfb38c630436"
-
 resolve@1.1.7:
   version "1.1.7"
   resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
@@ -7598,10 +7584,6 @@ validate-npm-package-license@^3.0.1:
     spdx-correct "~1.0.0"
     spdx-expression-parse "~1.0.0"
 
-value-equal@^0.2.0:
-  version "0.2.0"
-  resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-0.2.0.tgz#4f41c60a3fc011139a2ec3d3340a8998ae8b69c0"
-
 vary@~1.1.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.1.tgz#67535ebb694c1d52257457984665323f587e8d37"