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
| Binding | Type | Required | What it holds |
|---|---|---|---|
CONTROL | D1 database | Yes | Accounts, sites, goals, API keys, daily salts, the shard registry, and the routing table (migrations/control). |
SHARD_0 ... SHARD_n | D1 database | At least one | Events, visits, and imported history (migrations/shard). A migrated SHARD_* binding registers itself on the next request. |
EVENTS | Queue producer and consumer | No | Buffers 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. |
ASSETS | Static assets | Yes | The built dashboard (apps/dashboard/dist), served by the same Worker. |
STATS_LIMIT | Rate limit | No | 100 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. |
EMAIL | Send email | No | Cloudflare 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. |
BROWSER | Browser Run | No | Prints 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.
| Variable | Default | What it does |
|---|---|---|
PUBLIC_URL | unset | The 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. |
SIGNUPS | open | invite 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_SITE | demo | The 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_DAYS | 0 | Deletes 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_MB | 8000 | The size at which a shard stops taking new sites and starts draining. D1's hard limit is 10 GB per database. |
INGEST_DAILY_CAP | unset | The 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_PROVIDER | unset | cloudflare 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_FROM | misomoon <reports@misomoon.com> | The sender of every email. Use an address on a domain that your provider verified. |
AWS_SES_REGION | unset | Amazon 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_SEED | unset | 1 allows POST /api/v1/sites/:id/demo, which writes synthetic traffic. Use it only locally. |
CF_ZONE_ID | unset | Managed proxy: the zone that owns PROXY_CNAME_TARGET and has Cloudflare for SaaS turned on. |
PROXY_CNAME_TARGET | unset | Managed proxy: the fallback origin that customer hostnames point a CNAME at, for example proxy.example.com. |
Secrets
| Secret | Needed for | Notes |
|---|---|---|
AWS_SES_ACCESS_KEY_ID, AWS_SES_SECRET_ACCESS_KEY | Email through Amazon SES | An IAM key that may call ses:SendEmail. Requests use SES v2 with SigV4 signing. |
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET | Google import and Search Console | An 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_TOKEN | Managed proxy | A 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_cap | Effect |
|---|---|
NULL | The site uses INGEST_DAILY_CAP. |
| A positive number | The site may store this many events per UTC day. |
0 | The site is blocked. Every beacon is dropped. |
To give one site its own cap, run:
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.
| Attribute | Example | What it does |
|---|---|---|
?s= on the script URL | src=".../m.js?s=shop" | The site id. Required. The server also uses it to add the code for the site's ticked features. |
data-api | data-api="/relay/api/event" | Where to send events. Defaults to api/event next to the script URL. |
data-disable | data-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-manual | data-manual | Sends no pageview on load. Call misomoon('pageview') yourself. |
data-hash | data-hash | Counts #/route changes as pages, for hash-based routers. |
data-prop-<name> | data-prop-plan="pro" | Adds a custom property to every event. |
data-download-ext | data-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.