Docs

Reference

Configuration reference

The API Worker (apps/api) reads all of its settings from apps/api/wrangler.jsonc and from Worker secrets. This page lists every binding, variable, and secret it reads. To change a variable, edit vars in wrangler.jsonc and deploy again. To set a secret, run wrangler secret put <NAME> in apps/api. Locally, put secrets in apps/api/.dev.vars, which git ignores.

Bindings

BindingTypeRequiredWhat it holds
CONTROLD1 databaseYesAccounts, sites, goals, API keys, daily salts, the shard registry, and the routing table (migrations/control).
SHARD_0 ... SHARD_nD1 databaseAt least oneEvents, visits, and imported history (migrations/shard). A migrated SHARD_* binding registers itself on the next request.
EVENTSQueue producer and consumerNoBuffers beacons so the response is 202 at once. The consumer writes them in batches of up to 100. Without a queue, the Worker writes each event in ctx.waitUntil.
ASSETSStatic assetsYesThe built dashboard (apps/dashboard/dist), served by the same Worker.
STATS_LIMITRate limitNo100 requests per minute. It counts /api/v1/stats/* calls per API key, or per IP address for signed-out callers. Signed-in dashboards and shared links aren't counted. It also counts sign-up, sign-in, password reset, invitation accept, and shared-link password attempts per IP address.
EMAILSend emailNoCloudflare Email Service. Sends invitations, password resets, reports, alerts, and the daily-cap notice from EMAIL_FROM, which must be on a domain onboarded to Email Service. Sending to any address needs the Workers Paid plan. The deploy bundle leaves it out.
BROWSERBrowser RunNoPrints the PDF attached to weekly and monthly reports, and the PDF preview (/reports/preview?format=pdf). Each edition is printed once per site, whoever subscribes; a print takes a few seconds of browser time. Workers Free includes 10 minutes of browser time a day, Workers Paid 10 hours a month, then $0.09 an hour (pricing, checked 2026-10-24). Without it, or when a print fails, reports go out without the PDF. wrangler dev cannot print: quick actions run on Cloudflare only. The deploy bundle keeps it.

The consumer retries a batch 5 times, then moves it to the dead-letter queue misomoon-events-dlq. The cron trigger (*/5 * * * *) checks traffic-spike alerts and moves data off retiring shards every 5 minutes. At minute 15 of every hour, it also sends email reports, checks traffic-drop alerts, applies retention, and deletes old salts and usage rows older than 400 days.

Variables

Set these in vars in apps/api/wrangler.jsonc.

VariableDefaultWhat it does
PUBLIC_URLunsetThe public origin of your dashboard, for example https://analytics.example.com. Links in emails use it. Invitations and password resets fall back to the address of the request. Email reports are sent by the cron job, which has no request, so they fall back to https://misomoon.com. Set it if you send reports.
SIGNUPSopeninvite turns off open sign-up after the first account. While no account exists, one person can sign up and own the instance. After that, new people join only through an invitation from a team owner or admin. closed turns off sign-up completely, including the first account. Invitations still work. The deploy bundle sets invite.
DEMO_SITEdemoThe id of a public site that the dashboard opens first for people who can see it, such as signed-out visitors. The deploy bundle sets it empty, so the dashboard opens your first site.
RETENTION_DAYS0Deletes events and visits older than this many days, on every site. 0 keeps everything. A site's own Keep stats for setting applies when it is shorter (privacy settings).
SHARD_SOFT_LIMIT_MB8000The size at which a shard stops taking new sites and starts draining. D1's hard limit is 10 GB per database.
INGEST_DAILY_CAPunsetThe most events a site can store per UTC day, unless the site has its own cap. Unset means no cap. A value that is not a positive whole number also means no cap. See Daily ingest cap.
EMAIL_PROVIDERunsetcloudflare or ses. Unset picks the EMAIL binding when it is bound, then SES. A provider named here that is not set up sends nothing; email does not go out through another provider.
EMAIL_FROMmisomoon <reports@misomoon.com>The sender of every email. Use an address on a domain that your provider verified.
AWS_SES_REGIONunsetAmazon SES: the region of your SES identity, for example us-east-1. Email goes through SES when this and both AWS_SES_* secrets are set.
DEMO_SEEDunset1 allows POST /api/v1/sites/:id/demo, which writes synthetic traffic. Use it only locally.
CF_ZONE_IDunsetManaged proxy: the zone that owns PROXY_CNAME_TARGET and has Cloudflare for SaaS turned on.
PROXY_CNAME_TARGETunsetManaged proxy: the fallback origin that customer hostnames point a CNAME at, for example proxy.example.com.

Secrets

SecretNeeded forNotes
AWS_SES_ACCESS_KEY_ID, AWS_SES_SECRET_ACCESS_KEYEmail through Amazon SESAn IAM key that may call ses:SendEmail. Requests use SES v2 with SigV4 signing.
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRETGoogle import and Search ConsoleAn OAuth web client. Add <your misomoon URL>/api/v1/google/callback as an authorized redirect URI, using the address you open the dashboard on: misomoon builds the redirect from the request's own origin, not from PUBLIC_URL. In the Google Cloud project, enable the Google Analytics Data API, the Google Analytics Admin API, and the Google Search Console API. The import asks for analytics.readonly, Search Console for webmasters.readonly. Bring your history walks through the setup.
CF_API_TOKENManaged proxyA Cloudflare API token with SSL and Certificates: Edit on CF_ZONE_ID.

Without an email provider, the dashboard shows invitation links to copy by hand, and reports, alerts, password resets, and the daily-cap notice are not sent.

The managed proxy is on only when CF_API_TOKEN, CF_ZONE_ID, and PROXY_CNAME_TARGET are all set. Serve the tracker from your own domain explains the proxy and its other options.

Daily ingest cap

The cap limits how many events one site stores per UTC day, so a bot flood or a broken script can't run up D1 writes. It counts every stored row. A pageview, a custom event, and an engagement ping each count as one. The queue consumer applies the cap before it writes to a shard, and keeps the earliest events of the day. Several consumers can run at once. Each adds the room it takes to the day's count in CONTROL in one statement, so together they never store more than the cap, and it gives back the room it didn't use once its write is done. If a consumer fails before it can give the room back, the day stores a little less than the cap, never more. A queue retry never counts an event twice or drops one that was already stored. Once a day is full, the edge also drops beacons for that site within a minute, before they reach the queue. A ref-only beacon is an engagement ping that carries only payment refs. The edge drops it for a site that takes no payments. For a site that takes payments, it counts as one stored row, not as an event, and a queue retry doesn't count it twice. It takes room under the cap like any other row, but once queued it is never dropped, so the payment keeps its visit. Those beacons are the one way a day can store more than its cap: one row each, for those queued before the edge saw the day full. Payments from webhooks are never capped. The day starts over at 00:00 UTC.

Each site has an ingest_cap column in the sites table of CONTROL:

ingest_capEffect
NULLThe site uses INGEST_DAILY_CAP.
A positive numberThe site may store this many events per UTC day.
0The site is blocked. Every beacon is dropped.

To give one site its own cap, run:

sh
wrangler d1 execute CONTROL --remote --command "UPDATE sites SET ingest_cap = 500000 WHERE id = 'shop-com'"

Workers pick up the change within a minute. The first time a cap drops an event on a given day, the team's owners and admins get one email. A block sends no email. GET /api/v1/sites/:id/usage returns the cap and the counts for the last 31 days. The dropped count includes only events that the consumer dropped. Beacons that the edge dropped are not counted, because counting them would cost a write each.

Tracker attributes

The tracker reads these attributes from its own <script> tag. The tracking guide shows them in use.

AttributeExampleWhat it does
?s= on the script URLsrc=".../m.js?s=shop"The site id. Required. The server also uses it to add the code for the site's ticked features.
data-apidata-api="/relay/api/event"Where to send events. Defaults to api/event next to the script URL.
data-disabledata-disable="forms,clicks"Turns features ticked in the site settings off on the pages that carry it. Values: outbound, downloads, forms, events, payments, vitals, clicks, errors, fields, media, content.
data-manualdata-manualSends no pageview on load. Call misomoon('pageview') yourself.
data-hashdata-hashCounts #/route changes as pages, for hash-based routers.
data-prop-<name>data-prop-plan="pro"Adds a custom property to every event.
data-download-extdata-download-ext="dmg,apk"More file extensions to count as downloads, with Downloads ticked.

Which paths count, the search parameters, and what the tracker collects are site settings, not attributes: see Choose what to track. A browser the tracker doesn't count says why in its console.

Attributes on a page's own elements (data-misomoon-content, data-misomoon-media, data-misomoon-id, data-misomoon-mask) are in the tracking guide.

Search every page and heading. ↑ ↓ to move, enter to open.