Analytics
What the admin page answers, and what needs a separate tool.
Two different questions
Commerce numbers and visitor numbers come from different places, and mixing them is how a shop ends up with figures that reconcile against nothing.
| Question | Source | Status |
|---|---|---|
| Sales, orders, average order value, best sellers | Medusa's own order records | ✅ built — admin → Analytics |
| Settlement after fees | Curlec dashboard | External, deliberately not restated |
| Sessions, visitors, sources, conversion | Self-hosted Plausible | ✅ built — set the variables below |
Medusa has no concept of a session or a visitor. It records orders. So the Shopify screens showing "Sessions", "Conversion rate" and the Live View globe cannot be reproduced from order data at any effort — they need tracking on the storefront. That is what the Plausible setup below provides, and the admin Analytics page now shows both halves on one screen.
Autoxspare uses self-hosted Plausible rather than Google Analytics or PostHog. It is cookieless and stores no personal data, so the shop needs no consent banner under the PDPA — a cookie prompt in front of a parts catalogue costs conversions to answer a question nobody asked.
Nothing ships until it is configured. With the variables unset the storefront renders no third-party script at all, and the admin analytics page says which variables are missing rather than showing an empty chart.
1. Run Plausible
Add it as a service in the same Railway project as the backend. It needs its own Postgres and a ClickHouse, which the Plausible template provisions for you.
Give it a subdomain rather than leaving it on *.up.railway.app:
analytics.autoxspare.com CNAME <the target Railway gives you>Same shape as docs.autoxspare.com and api.autoxspare.com. Railway issues the
certificate once DNS resolves.
Railway will not issue a certificate while the edge returns a 404 for the hostname. Add the domain in Railway first, then point DNS at the target it gives you — the other order leaves the certificate pending indefinitely.
2. Register the site
In Plausible, add a site whose domain is exactly the hostname visitors use:
new.autoxspare.comtodayautoxspare.comafter the apex switches
This is the value that must match NEXT_PUBLIC_PLAUSIBLE_DOMAIN. If they differ,
events are accepted and then discarded — the most common way a self-hosted
install looks perfectly healthy and records nothing.
Turn on the extensions the storefront's script expects: outbound links, file downloads, hash, pageview props, revenue, tagged events.
Plausible encodes the enabled extensions into the script filename. The
storefront loads
script.file-downloads.hash.outbound-links.pageview-props.revenue.tagged-events.js.
Toggling an extension off in the dashboard without changing that string makes
the affected features silently inert — no error, just no data.
3. Connect the storefront (Vercel)
NEXT_PUBLIC_PLAUSIBLE_HOST https://analytics.autoxspare.com
NEXT_PUBLIC_PLAUSIBLE_DOMAIN new.autoxspare.comvercel env add marks variables sensitive by default, and a sensitive
variable is not exposed to the browser — a NEXT_PUBLIC_* value added that way
silently does nothing. Add these as plain variables.
4. Connect the backend (Railway)
PLAUSIBLE_HOST https://analytics.autoxspare.com
PLAUSIBLE_SITE_ID new.autoxspare.com
PLAUSIBLE_API_KEY Plausible → Settings → API KeysPLAUSIBLE_API_KEY is read server-side only. It is never returned to a caller
and never reaches the admin bundle.
The admin analytics page then shows visitors, sources, most-viewed pages and the shop's own events beneath the order figures.
What is tracked
Page views, plus five events chosen to answer this shop's actual questions rather than to fill a dashboard:
| Event | Why it exists |
|---|---|
Vehicle chosen | Whether the garage — the entire premise of the site — is used |
Add to cart | With revenue, so cart value is visible before checkout |
Begin checkout | Where the funnel narrows |
Courier chosen | If everyone takes the cheapest, eight couriers is complexity earning nothing |
Return requested | A cost signal, not a vanity one |
Every call is a no-op when analytics is unconfigured, blocked, or running during SSR. Analytics must never be able to break a checkout.
When the apex domain switches
NEXT_PUBLIC_PLAUSIBLE_DOMAIN, PLAUSIBLE_SITE_ID and the site registered in
Plausible all have to change together, in the same move. Split across two and
the numbers simply stop, with nothing on screen to say why.
The admin Analytics page
apps/backend/src/admin/routes/analytics/page.tsx +
apps/backend/src/api/admin/analytics/route.ts
- Date range: presets (today, yesterday, last 7/30/90 days, month/quarter/year to date) plus a custom range
- Comparison: previous period, previous year, previous year matching day of week
- Buckets adapt to the range — hourly for a day, daily up to ~3 months, then weekly
- Best sellers counted by units, so one large order does not read as popularity
Days are cut on Malaysian time, not UTC, so "today" means today in Jitra.
There is no currency selector. The shop supports MYR only — the store has one currency and Curlec settles MYR — so a switcher would either do nothing or imply a conversion that never happens. It reads as a label instead.
Aggregation runs in memory over the order list, which is simpler than SQL at this volume. Past roughly ten thousand orders, move the sums into a query.
Adding sessions and conversion
Medusa ships an Analytics module with a first-party PostHog provider (since v2.8.3), so the backend half needs no custom plumbing.
{
resolve: '@medusajs/analytics',
options: {
providers: [{
resolve: '@medusajs/analytics-posthog',
id: 'posthog',
options: {
posthogEventsKey: process.env.POSTHOG_EVENTS_KEY,
posthogHost: process.env.POSTHOG_HOST, // defaults to eu.i.posthog.com
},
}],
},
}Medusa does not track anything automatically. The module gives you a
track() method; you still write subscribers that listen for events and call
it. Budget for that rather than assuming events appear on their own.
Only one analytics provider can be registered at a time.
Choosing a provider
| Option | Fits because | Costs |
|---|---|---|
| PostHog (recommended) | First-party Medusa provider, plus sessions, funnels, geography and live events on the storefront. One tool answers both halves. | Cloud has a free tier; self-hosting is heavy (below). |
| Vercel Web Analytics | Already on Vercel, one line to add | No funnels, limited API for building custom admin widgets |
| Plausible / Umami | Light, privacy-friendly, self-hostable cheaply | No product funnels; no Medusa provider |
| Metabase / Grafana on Postgres | Excellent for commerce reporting, no rebuild | Cannot see sessions at all |
On self-hosting PostHog
It is open source and self-hostable under MIT, but the Docker Compose stack runs ClickHouse, Kafka, Zookeeper, Postgres, Redis and MinIO — around 2 GB idle, and PostHog document it as a "hobby" deploy targeting up to ~100k events/month with no support. Kubernetes deployment was dropped in 2023.
For a shop this size that is a second production system to keep alive in order to count visitors. Use PostHog Cloud (the EU region if data residency matters) unless there is a specific reason not to.
Sources: Medusa Analytics Module, PostHog provider, PostHog self-hosting