Node 22 or later.
npm installThe repository uses npm workspaces. Install once at the root; do not install inside a workspace.
Two packages have install scripts that fetch platform binaries, esbuild and workerd.
They are approved explicitly in the root package.json under allowScripts, so a new
dependency that wants to run install scripts has to be approved deliberately.
npm run devThis starts wrangler dev on http://localhost:8787 with the same bindings as production,
including the rate limiter.
curl 'http://localhost:8787/v1/roll?formula=4d6kh3'
curl 'http://localhost:8787/openapi.json'npm run dev --workspace @rollful/siteAstro serves on http://localhost:4321. The documentation is grouped by what each page is,
after Diátaxis: a lesson under Start here, a recipe under
Guides, a description under Reference, and a discussion under Explanation. A page
belongs to exactly one of those, and the sidebar group follows from the directory it sits in.
Two routes are fixed rather than free. /docs/ and /docs/api/ are the two tutorials,
because the site header, the marketing page, the 404 page and the Worker's own index all link
them. The generated endpoint reference stays under /docs/api/endpoints and is listed under
Reference regardless of that route.
Moving a page means adding its old address to redirects in astro.config.mjs. A published
link should not break because the shelving changed.
The reference builds from the committed document, so it always describes this checkout. The
home page roller calls the real API, and by default that is production. To point it at a
local Worker, copy apps/site/.env.example to apps/site/.env and run both:
npm run dev --workspace @rollful/api
npm run dev --workspace @rollful/site.env is not committed.
npm run typecheck
npm test
npm run format:checknpm run typecheck regenerates apps/api/worker-configuration.d.ts with wrangler types
before compiling, so binding types always match wrangler.jsonc. That file is generated and
is not committed.
Tests run inside workerd through @cloudflare/vitest-pool-workers, not in Node. This is
deliberate: the API's whole premise is that OpenDice needs no Node compatibility layer, and
a test running in Node would not prove it.
npm test --workspace @rollful/api
cd apps/api && npx vitest # watch modeTests drive the Worker over HTTP through SELF, so middleware, validation, error mapping
and CORS are all covered rather than bypassed. Each request sends its own
cf-connecting-ip, because the rate limiter is real in local tests too and a shared address
would make one test spend another's budget.
Read limits from @rollful/schema in tests rather than repeating the numbers.
The site's test is not one of those. It runs in Node and checks a single thing: that the
version beside the wordmark is the OpenDice version the lockfile installs. Nothing in the
dependency graph holds those two together — the site does not depend on opendice, the API
does — so a release that bumped the package and forgot the site would go out announcing a
version nobody is running.
Prettier settings are in .prettierrc.json: no semicolons, single quotes, 100 columns.
npm run formatProse is neutral and direct, readable at high school level. Comments are kept to a minimum; code should explain itself, and a comment is only worth adding where the code cannot express the point.
Every source file starts with the SPDX header:
// SPDX-License-Identifier: MIT
// Copyright (C) 2026 Nicola MustonePushes to main deploy through GitHub Actions: the API when apps/api or
packages/schema change, the site when apps/site or packages/schema change. Both need
the same two repository secrets:
| Secret | Value |
|---|---|
CLOUDFLARE_API_TOKEN |
a token created from the Edit Cloudflare Workers template |
CLOUDFLARE_ACCOUNT_ID |
the account owning rollful.dev, from npx wrangler whoami |
To deploy by hand:
npm run deploy # the API
npm run deploy --workspace @rollful/site # the siteBoth rollful.dev and api.rollful.dev are Worker Custom Domains. The site is a Worker
with an assets binding and no main, so it serves static files and runs no code of its own.
Cloudflare creates the DNS record and issues the certificate on the first deploy, so there
is no DNS record to add by hand and none to keep in step afterwards. Do not edit those
records in the dashboard. The rollful.dev zone must already exist in the account.
Both workflows fetch the deployed URL afterwards to prove it answers. Cloudflare's bot protection replies to GitHub's runners with a managed challenge, so that fetch gets an interstitial rather than the site. The workflows treat a challenge as a warning rather than a failure, because a challenge says nothing about whether the deploy worked — which leaves the deploy unverified.
To get a real check back, add a WAF custom rule on the zone that skips bot protection for requests carrying a header only the workflows send:
| Expression | http.request.headers["x-rollful-deploy-check"][0] eq "<secret>" |
| Action | Skip → Bot Fight Mode, and any other rule that would challenge |
Then store the same value as the DEPLOY_CHECK_TOKEN repository secret. Use a secret rather
than matching a user agent: a rule anyone can satisfy by setting a header they can guess is
a hole in the bot protection, not an exception for CI.
Add an entry to CHANGELOG.md under Unreleased for anything that changes
the API's behaviour, its limits, or its response shapes.