# Run misomoon in your Cloudflare account This guide deploys misomoon to your own Cloudflare account. Then it walks you through claiming your instance and tracking your first site. misomoon runs as one Worker with D1 databases and a queue. You have no server or container to manage. There are two ways to deploy: - [With the Deploy to Cloudflare button](#deploy-with-the-button). This takes about 5 minutes and needs only a browser. - [From the command line](#deploy-from-the-command-line). Use this to change the code, to run more shards, or to deploy from your own CI. ## Before you start You need a Cloudflare account. The Workers Free plan works for small sites. The Free plan's queue allows 10,000 operations a day, and each event uses about 3, so it handles about 3,000 events a day. For more traffic, use the Workers Paid plan. See [Workers pricing](https://developers.cloudflare.com/workers/platform/pricing/), [D1 pricing](https://developers.cloudflare.com/d1/platform/pricing/), and [Queues pricing](https://developers.cloudflare.com/queues/platform/pricing/). misomoon creates these resources in your account: | Resource | Name | What it is for | | --- | --- | --- | | Worker | `misomoon` or `misomoon-api` | The API, the tracker, and the dashboard | | D1 database | `misomoon-control` | Accounts, sites, goals, and keys | | D1 databases | `misomoon-shard-0`, `misomoon-shard-1` | Events and visits | | Queues | `misomoon-events`, `misomoon-events-dlq` | Event buffering, and failed batches | | Cron trigger | Every 5 minutes | Reports, alerts, and retention | ## Deploy with the button 1. Select the button: [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/jackzard/misomoon/tree/deploy/misomoon) 2. Sign in to Cloudflare, and connect your GitHub or GitLab account. Cloudflare copies the deploy bundle into a new repository that you own. 3. Keep the default names for the databases and the queues, or change them. The migrations use binding names, so any names work. 4. Select **Create and deploy**. The build creates the databases and the queues, applies the migrations, and deploys the Worker. 5. When the deploy is done, open the Worker's URL. It looks like `https://misomoon..workers.dev`. Continue with [claim your instance](#claim-your-instance). The button deploys the bundle that CI builds from `main` and publishes to the `deploy` branch. It sets `SIGNUPS` to `invite`, so only the first person can sign up. ## Deploy from the command line You need Node.js 22.5 or later, pnpm 10, and git. 1. Clone the repository and install the dependencies: ```sh git clone https://github.com/jackzard/misomoon.git cd misomoon pnpm install ``` 2. Sign in to Cloudflare: ```sh pnpm --filter @misomoon/api exec wrangler login ``` 3. Close open sign-up. In `apps/api/wrangler.jsonc`, set `SIGNUPS` in `vars` to `invite`. Change the line if `vars` already has one, so the key appears once: ```jsonc "SIGNUPS": "invite" ``` The first account can still sign up. Everyone after that needs an invitation. 4. Deploy: ```sh pnpm --filter @misomoon/api run deploy ``` The command builds the tracker and the dashboard. On the first run, it also creates the three D1 databases and the two queues, and writes the database ids into `apps/api/wrangler.jsonc`. Then it applies the migrations and deploys the Worker. Commit the changed `wrangler.jsonc`, so that the next deploy uses the same databases. 5. Open the URL that the deploy prints. To keep the data in the EU, set `D1_JURISDICTION=eu` on the first deploy. D1 then runs and stores the databases only in the EU. Cloudflare sets a database's jurisdiction when it is created and never changes it, so set it before the first deploy, and on every run that adds shards. [D1 data location](https://developers.cloudflare.com/d1/configuration/data-location/) has the details. To start with more shards, set `SHARD_COUNT`, for example `SHARD_COUNT=4 pnpm --filter @misomoon/api run deploy`. [Scale the shards](upgrading.md#scale-the-shards) explains when you need more. ## Claim your instance 1. Open your misomoon URL, and select **Create an account**. 2. Enter your email, name, and password. This account owns the instance. After this, the sign-up link is gone. To add people, open **Settings**, and invite them to your team. Claim the instance right after the deploy. Until the first account exists, anyone who knows the URL can sign up. ## Track your first site 1. In **Settings**, add your site's domain. 2. Copy the snippet, and paste it into the `` of every page: ```html ``` 3. Open your site. You appear in **Live** within a few seconds. The [tracking guide](tracking.md) covers custom events, revenue, and goals. ## Use your own domain To use a domain such as `analytics.example.com`: 1. Add the domain to Cloudflare as a zone, if it isn't one already. 2. In the Cloudflare dashboard, open **Workers & Pages**, and select the misomoon Worker. 3. Open **Settings** > **Domains & Routes**, and select **Add** > **Custom domain**. 4. Enter `analytics.example.com`, and save. 5. Set the `PUBLIC_URL` variable to `https://analytics.example.com`. Then the links in email reports point to the right place. Update the snippet on your sites to use the new domain. ## Turn on optional features Each feature needs a secret or a variable. In the Cloudflare dashboard, open the Worker, then **Settings** > **Variables and Secrets**. From the command line, run `wrangler secret put ` in `apps/api`. | Feature | What to set | | --- | --- | | Emails: invitations, password resets, reports, and alerts | One provider, and the variable `EMAIL_FROM` with an address on a domain that the provider verified. For [Cloudflare Email Service](https://developers.cloudflare.com/email-service/), add `"send_email": [{ "name": "EMAIL" }]` to `wrangler.jsonc`. For Amazon SES, set the variable `AWS_SES_REGION` and the secrets `AWS_SES_ACCESS_KEY_ID` and `AWS_SES_SECRET_ACCESS_KEY`. | | Email reports with the right links | The variable `PUBLIC_URL` | | Google Analytics import and Search Console | The secrets `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` | | A managed proxy subdomain for each site | The secret `CF_API_TOKEN`, and the variables `CF_ZONE_ID` and `PROXY_CNAME_TARGET` | | Deleting old data | The variable `RETENTION_DAYS` | | A limit on events per site per day | The variable `INGEST_DAILY_CAP` | The [configuration reference](configuration.md) explains each one. Without an email provider, invitations still work. The dashboard shows the invitation link, and you send it yourself. A variable that you set in the Cloudflare dashboard is replaced by the next deploy from the repository. To keep a variable, put it in `vars` in `wrangler.jsonc`. Secrets are not affected. ## Next steps - [Connect Claude, Cursor, or another MCP client](mcp.md). - [Serve the tracker from your own domain](PROXY.md) so that ad blockers don't drop events. - [Upgrade to a new version](upgrading.md). - [Fix common problems](troubleshooting.md). --- # Track your site This guide adds the misomoon tracker to a website and sends custom events and revenue. It assumes you already run misomoon, either on your own Cloudflare account ([self-hosting](self-hosting.md)) or locally. The core tracker is `m.js`, under 1 KB gzipped. Each feature you tick in the site settings adds its own code to the same file ([Choose what to track](#choose-what-to-track)). It sets no cookies and stores nothing on the visitor's device. Each visitor gets an id that is a hash of a daily salt, the site, the IP address, and the user agent. The salt changes every day at 00:00 UTC, and the IP address is never stored. [Architecture](ARCHITECTURE.md#ingest) explains the details. ## Add the script 1. In the dashboard, add your site's domain. A new account sees the form on its first screen; later sites go in **Settings** > **Sites**. The dashboard then opens the install snippet, which carries the site id. 2. Paste the snippet into the `` of every page: ```html ``` 3. Open your site in a browser. Within a few seconds, you appear in **Live** in the dashboard. The server counts a page only on the site's domain or one of its subdomains, so visits on `localhost` are left out. A browser the tracker doesn't count says why in its console, for example `misomoon skipped: bot`. Single-page apps need nothing extra. The tracker listens to `pushState` and `popstate` and counts each route change as a pageview. For a router that uses `#/routes`, add `data-hash`. Each route is then its own page, such as `/#/pricing`, without any query string inside the route. ## Send custom events Call `misomoon()` with an event name: ```js misomoon('Signup') misomoon('Signup', { props: { plan: 'pro', source: 'pricing' } }) ``` To send events from HTML without JavaScript, tick **Tagged events** in the [settings](#choose-what-to-track) and put `data-misomoon-event` on an element. A click sends the event. On a form, the submit sends it. ```html Buy ``` Each `data-misomoon-prop-` attribute adds the property `` to the event. `data-misomoon-revenue-amount` and `data-misomoon-revenue-currency` add revenue, as the `revenue` option of `misomoon()` does. An event name can be up to 120 characters of any text except `;` and `|`, which separate filters in the dashboard and the API. To count an event without letting it end a bounce, pass `interactive: false`. Use it for events that fire without a real action from the visitor, such as a video that plays by itself: ```js misomoon('Autoplay started', { interactive: false }) ``` To run code after the event is sent, for example before a navigation, pass `callback`: ```js misomoon('Download', { props: { file: 'report.pdf' }, callback: () => (location.href = '/report.pdf') }) ``` Calls made before `m.js` loads are lost unless you queue them. Add this line before the script tag. `m.js` sends the queued calls after its first pageview: ```html ``` ## Track revenue Pass the amount and an ISO 4217 currency code: ```js misomoon('Purchase', { revenue: { amount: 49.9, currency: 'EUR' }, props: { plan: 'yearly' } }) ``` Each site has one reporting currency. The dashboard guesses it from your timezone when you add the site, and USD is the fallback. To change it, open **Settings** > **Sites**, or send `PATCH /api/v1/sites/` with `{"currency": "EUR"}` ([API reference](api.md#sites)). Revenue totals include events in that currency and events sent without a currency. Events in other currencies are stored but left out of the totals, so the dashboard never adds euros to dollars. To count payments from Stripe, Polar, Lemon Squeezy, or Dodo Payments without calling `misomoon()`, connect the provider's webhook: [Connect payments](revenue.md). ## Turn goals into conversions In **Settings**, add a goal for an event name (`Signup`) or a page path (`/thank-you`, or `/docs*` for a prefix). The **What they did** panel then shows each goal's visitors, conversion rate, and revenue. Goals work on data you already collected. ## What the tracker sends by itself | Event | When | Turn off with | | --- | --- | --- | | `pageview` | On load, on each route change, and when the back button shows a page from the browser's cache | `data-manual` (send them yourself with `misomoon('pageview')`) | | `engagement` | When the page is hidden or the route changes: engaged time, which counts only while the page has focus, and the share of the page scrolled, plus what the ticked features collected | | The [configuration reference](configuration.md#tracker-attributes) lists every attribute the script tag accepts. ## Choose what to track Everything else is a checkbox under **Settings** > **Install** > **What to track**. A new site has every feature on. Three presets set the boxes for you: | Preset | Features | `m.js`, gzipped | | --- | --- | --- | | Everything (the default) | All of the table below | 4.8 KB | | Light | Outbound links, downloads, form submits, and tagged events | 1.8 KB | | Pageviews only | None | 998 bytes | The id in brackets is the name that `data-disable` on the script tag and the API's `features` field take. | Feature | What it collects | Adds, gzipped | | --- | --- | --- | | Outbound links (`outbound`) | An `Outbound link` event for a click or middle click on a link to another host, without its query string | 0.2 KB | | Downloads (`downloads`) | A `Download` event for a click or middle click on a link to a file such as `.pdf` or `.zip`, or on a link with a `download` attribute, without its query string; `data-download-ext="sketch,fig"` on the script tag adds extensions | 0.4 KB | | Form submits (`forms`) | A `Form sent` event for each form, named by its `id` or `name` | 0.2 KB | | Tagged events (`events`) | Clicks and submits on elements with `data-misomoon-event`, with props and revenue ([custom events](#send-custom-events)) | 0.3 KB | | Payments (`payments`) | Tags payment links and hands out `misomoon.ref()`, so a checkout finds its visit ([Connect payments](revenue.md)). Connecting a provider ticks it | 0.5 KB | | Web Vitals (`vitals`) | LCP, INP, CLS, FCP, and TTFB from real visits | 0.6 KB | | Clicks (`clicks`) | Each clicked element, with rage and dead clicks | 1.0 KB | | JavaScript errors (`errors`) | Uncaught errors and rejected promises. A script from another origin reports only `Script error.` unless its tag has `crossorigin`, and those are left out | 0.4 KB | | Form fields (`fields`) | The fields a visit used and the one it left a form at | 0.4 KB, plus 0.3 KB shared | | Video and audio (`media`) | Plays of `