> ## Documentation Index
> Fetch the complete documentation index at: https://spreecommerce.org/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Deploying the dashboard

> Ship the React Dashboard to production — served by the Spree server itself (the default), or as a static build on a CDN.

The dashboard deploys as **static files**. `vite build` produces `dist/` — HTML, JS, CSS — there is no Node server in production. What varies is who serves those files.

<Warning>
  `VITE_`-prefixed values are compiled into the client bundle — never put credentials in them. The dashboard authenticates admins interactively (JWT in memory + an httpOnly refresh cookie); no API keys are involved.
</Warning>

## Single node — the default

The Spree server serves the dashboard itself at **`/dashboard`**: one container, one origin, zero CORS or cookie configuration. The official Spree Docker image ships with the stock dashboard baked in — deploy the image anywhere Docker runs (Render, Railway, AWS, Azure, your own host) and `https://your-api-host/dashboard` just works.

Under the hood, the **`spree_dashboard` gem** (in the Gemfile of the starter and the official image) serves the directory in `Spree::Dashboard.dist_path` — or the `SPREE_DASHBOARD_DIST_PATH` env var — with SPA semantics: every dashboard route falls back to `index.html`, hashed assets get immutable caching. The bundle is built with a **relative API base** (`VITE_SPREE_API_URL` unset) and `base: /dashboard/`, so the same image works on every host and environment with no rebuild. API-only deployments simply omit the gem.

### Custom dashboard, single node

The official image contains the *stock* dashboard. Once you've customized yours (your own pages/plugins in `apps/dashboard/`), bake your build into your own image:

```bash theme={"theme":"night-owl"}
spree build --production            # optionally --tag registry/repo:tag
```

The command detects `apps/dashboard/`, selects the Dockerfile's `custom` dashboard stage (`--build-arg DASHBOARD_SOURCE=custom`), and passes your app as a named build context — the Node toolchain lives only in that throw-away stage, and the final image is Rails plus your built `dist/` served at `/dashboard`. Without the CLI, the equivalent is:

```bash theme={"theme":"night-owl"}
docker build backend/ -f backend/Dockerfile \
  --build-arg DASHBOARD_SOURCE=custom \
  --build-context dashboard-src=./apps/dashboard \
  -t my-shop-spree:latest
```

Inside the stage the build runs with `VITE_BASE_PATH=/dashboard/` (asset URLs resolve under the mount) and `VITE_SPREE_API_URL` unset (API calls stay origin-relative). Projects **without** `apps/dashboard/` build exactly what plain `docker build backend/` produces — and if your `backend/Dockerfile` predates dashboard support, the command warns instead of silently shipping the stock dashboard over your customized one.

## Static host / CDN — the alternative

For edge-served assets or an independent dashboard release cadence, deploy `dist/` to a static host on its own domain:

```bash theme={"theme":"night-owl"}
cd apps/dashboard
VITE_SPREE_API_URL=https://api.mystore.com pnpm build
```

This is the **cross-origin** topology, with three requirements:

1. **HTTPS on both sides.** The refresh cookie is issued with `SameSite=None; Secure` in production, and browsers drop `Secure` cookies over HTTP. There is no insecure-production mode.
2. **Allowed Origins.** Add the dashboard's URL (e.g. `https://dashboard.mystore.com`) to **Settings → Allowed Origins** in the Spree admin — the API rejects cross-origin requests until then.
3. **SPA fallback.** Every path must rewrite to `index.html` so the router owns the URL (`/* → /index.html` on your host: `_redirects` on Netlify, `vercel.json` rewrites, `try_files $uri /index.html;` on nginx).

A middle ground also works: serve `dist/` and proxy `/api/*` + `/rails/*` to Rails from one domain behind your own nginx/Caddy/ingress — same-origin simplicity without baking the bundle into the image.

## Render

If your project deploys to Render via the Blueprint that `create-spree-app` places at the project root, `spree add dashboard` wires the single-node topology automatically: it extends the backend service's `buildCommand` to also build `apps/dashboard` (Render's Ruby runtime includes Node, and the full repo is available during the build) and adds one env var so the server serves the result:

```yaml theme={"theme":"night-owl"}
buildCommand: bundle install && … && (cd ../apps/dashboard && corepack enable pnpm && pnpm install && VITE_BASE_PATH=/dashboard/ pnpm build)
envVars:
  - key: SPREE_DASHBOARD_DIST_PATH
    value: ../apps/dashboard/dist
```

That's the whole setup — one service, one origin, **your customized dashboard** (it builds from your `apps/dashboard`, not the stock bundle) at `https://your-service.onrender.com/dashboard`. No `VITE_SPREE_API_URL`, no Allowed Origins entry, no extra service. Redeploys rebuild on every push.

If you prefer the dashboard on Render's CDN instead, add a static-site service by hand using the cross-origin recipe above.

## Any other static host

The same three ingredients, by hand: build with `VITE_SPREE_API_URL` set, upload `dist/`, configure the `/* → /index.html` rewrite. Then add the origin to Allowed Origins.

## Checklist (cross-origin only)

Single-node deployments skip all of this — one origin needs none of it.

* [ ] `VITE_SPREE_API_URL` points at the production API (baked at build time — one build per environment)
* [ ] HTTPS on the dashboard *and* the API
* [ ] Dashboard origin added to **Settings → Allowed Origins**
* [ ] All paths rewrite to `index.html`
* [ ] No secrets in any `VITE_` variable
