The README covers content authoring and local content previews. This guide covers backend development, validation, builds, and deployment.
Run the complete validation gate before handing off a code or configuration change:
pnpm run checkRoutine browser CI (pnpm run test:e2e) covers ongoing user flows. The static
publication audit (pnpm run test:e2e:astro) is opt-in: it builds synthetic public
data and checks populated layouts, light and dark artwork, and historical
conference display exports. Run it for public-site migrations or relevant
publication changes, rather than adding those temporary-page and detailed
geometry checks to every CI run. The catalog-wide contrast audit
(pnpm run test:e2e:contrast) is also opt-in. Wait for the rendered state or
resource being tested; do not add fixed sleeps.
Each Worker test run (pnpm run test:worker) builds the static site into its
own temporary directory under PKIC_BUILD_TMPDIR (else TMPDIR) and serves its
assets from there, with a copy-on-write clone of the shared Astro cache. Runs in
one checkout or across checkouts that share node_modules therefore never
replace each other's public/ or dist/. The directory is removed when the run
ends, fails, or is stopped; the next run removes any directory left by a killed
one.
Use focused tests while iterating. pnpm test:changed runs the tests related
to uncommitted changes in every suite (--since <ref> compares with a branch
instead). The selection follows imports, so it is advisory: Worker routes and
migrations are not always visible through them, and a change to package.json
or a Vite or Vitest config runs every test.
The optional pre-commit hook checks formatting and lint on staged files in a few
seconds and runs no tests. Enable it once per clone with
git config core.hooksPath .githooks.
The component gallery renders the design-system primitives and every pattern
catalog entry (.agents/skills/pkic-ui/patterns.md) with fixture data, served
by Vite alone in a second. pnpm gallery serves it on port 8796 (?only=<id>
shows one entry); pnpm gallery:shot <id> [--phone] [--dark] writes a
screenshot of one entry, with its popup open, to .gallery-shots/. The same
page is also served by the site at /design/.
For repeatable timing profiles, set
PKIC_TEST_PROFILE_PATH to a path outside the repository. The report records
test, import, environment, and setup durations without disabling test isolation:
PKIC_TEST_PROFILE_PATH=/path/outside/repository/worker.json pnpm run test:worker
pnpm exec vitest run --config vitest.config.ts --configLoader runner tests/d1-public-flow-profile.test.tsThe public-flow test emits D1_FLOW_PROFILE JSON containing SQL templates,
session constraints, round-trip counts, and EXPLAIN QUERY PLAN results against
synthetic fixtures. It checks directory/search batching, a single-round-trip
member profile/roster batch, and single-query image resolution, including
missing images and entity precedence. Durations are advisory; query-count and
plan regressions fail the test. Compare repeated runs under similar machine
load and keep profiles outside the repository.
Oxlint (pnpm run lint) and Oxfmt (pnpm run format:check) run as separate
gates so formatting is checked once. Oxlint's type-aware rules read the nearest
tsconfig.json of each file. The tsconfig.json in assets/ts, assets/design,
tests/frontend, tests/tools, scripts, and site only extends the project
that owns the directory, so every group is typed against its own compiler options.
The architecture rules live in scripts/oxlint/pkic-rules.mjs and are scoped in
.oxlintrc.json. The Worker suite has two projects that share bindings and migrations. d1 tests
explicitly import their real service or router without eagerly loading the
entire application during setup. Tests using SELF.fetch belong in the
workerFetchFiles list in vitest.config.ts; the worker-fetch project runs
them against the real Worker entry point. An accidental SELF.fetch in d1
fails explicitly rather than returning a mock response.
Large WebAuthn and MCP dependency graphs are prebundled for tests using Vitest's dependency optimizer. Node and Cloudflare built-ins remain external so workerd provides their real implementations; application modules and storage remain isolated per test file.
The Worker opens one D1 session per invocation. Public GET and HEAD requests
use first-unconstrained; authenticated, capability-bearing, and state-changing
requests start with first-primary. Scheduled and email invocations also start
with first-primary.
That constraint applies only to the first query. Subsequent reads can use a replica that satisfies the session bookmark, while writes still go to the primary. Keeping authentication in the same session establishes a fresh consistency boundary before authorization and data reads. Explicitly public member and sponsor directory, wall, display, and logo reads ignore incidental login cookies when choosing a replica; staff projections do not.
Enabling replicas in the dashboard is not sufficient: application reads must
use the D1 Sessions API.
Replica eligibility does not guarantee a particular serving region. Verify
remote query metadata (served_by_region and served_by_primary) after
deployment; local D1 tests cannot establish European placement or network
latency.
pnpm dev migrates and seeds its own synthetic database. To seed a remote
target (admin and event data, forms, terms, and default email templates in D1
and R2), run pnpm seed --preview or pnpm seed --production; add
--only <step> to run one seeder.
For interactive development, use pnpm dev or its alias pnpm dev:test (see
the README). This is the default local
environment for everyone. It keeps a migrated and seeded synthetic database in
.local-test/, generates its own local secrets, and captures all email in a
local inbox; no local command delivers email. It shares its serving code with
pnpm dev:rehearsal below.
Both hot-reload by default: the Vite dev server runs the Worker from source
against the state directory, and a watching client build rewrites the portal
bundle under stable development names. Pass --build to serve the bundled
Worker and built site instead. Development and built behavior differ in these
ways:
- Public pages are prerendered once at startup in both modes; only
--builduses optimized images and social images (unless--fast). Diagrams render in every mode, including hot reload and--fast, and reuse cached output. An uncached diagram starts the build-time browser renderer. - The portal bundle is unminified with inline source maps and no bundle budget
or subresource integrity;
--buildproduces the hashed, budgeted release. - The Worker runs through Vite's module runner instead of the bundled output, so
bundling problems show only with
--buildorpnpm run build.
Run --build before a push or release, not for UI iteration.
For an isolated disposable database with SendGrid delivery captured by a local interceptor, use:
pnpm run dev:interceptedThe intercepted server prints its capture URL and never sends messages to an
external mailbox. Playwright starts this same isolated server automatically; do
not start it manually before pnpm run test:e2e unless the test run sets
REUSE_SERVER.
If templates are missing or you want to reseed template versions only, run:
pnpm run seed:templates:localpnpm dev:rehearsal is opt-in and only for operators who are authorized to hold
a production database backup. Everyone else uses pnpm dev:test. The rehearsal
imports the backup into a new local directory, applies this branch's migrations
to it locally, and serves it with the same hot reload and captured inbox as
pnpm dev:test, so you can check a migration and the portal against real data
before anything reaches preview or production. Nothing it runs touches a remote
database, and no email leaves the computer.
The backup is a D1 SQL export, such as the file pnpm backup production
writes to backups/d1/. It holds the database only: R2 uploads such as logos
and portraits are not copied, and the published pages show those images as
missing. Keep the state directory outside the repository (backups/ is ignored
by Git), and delete the backup and the state directory when the rehearsal is
finished.
One command prepares and serves a fresh rehearsal. It exports the latest
production backup (read-only), imports it into a new state directory, applies
this branch's migrations, and serves it. With --plans, it then applies each
reviewed agenda migration plan (<event>/plan.json, kept outside the
repository) in directory-name order, authenticated by PKIC_AGENDA_IMPORT_TOKEN.
Public pages are published before the plans run, so restart with start to see
migrated agendas on them.
PKIC_AGENDA_IMPORT_TOKEN=… pnpm dev:rehearsal refresh --state ../rehearsal-<date> [--plans ../agenda-migration-plans]The individual steps are available on their own:
# Import the backup once into a new directory; no migrations or seeders run.
pnpm dev:rehearsal init --state ../rehearsal --dump backups/d1/production-<timestamp>.sql
# Apply this branch's pending migrations to the imported database.
pnpm dev:rehearsal migrate --state ../rehearsal
# Optionally run one SQL file against it, such as a data repair under review.
pnpm dev:rehearsal apply --state ../rehearsal --sql repair.sql
# Serve with hot reload; add --build [--fast] to serve the built release instead.
pnpm dev:rehearsal start --state ../rehearsal [--build [--fast]] [--port 8788] [--inbox-port 8799] [--host 127.0.0.1]To access the site and captured email inbox from another device, bind both to
an IP address assigned to the host (the default is 127.0.0.1):
pnpm dev:rehearsal start --state ../pkic-rehearsal --port 8788 --host 192.168.1.10Open http://192.168.1.10:8788/portal/ and http://192.168.1.10:8799/.
--inbox-port changes the inbox port. Generated sign-in links use the selected
host; --base-url <origin> overrides the application's public origin when needed.
Pass IPv6 addresses without brackets; printed URLs include the required brackets.
Wildcard hosts (0.0.0.0 or ::) bind all interfaces and need --base-url set to
a reachable address for remote sign-in links. Only expose a rehearsal containing
private data on a trusted network. Passkeys require a secure browser context;
use the email sign-in link when accessing a LAN IP over HTTP.
start prints the portal and captured inbox URLs; sign in with a link from the
inbox. It never migrates the database, so run migrate first when the branch
adds migrations. init refuses an existing directory: import every backup into
a new one.
Free and ISP mail, disposable mail, and the Public Suffix List are vendored in
functions/_lib/email-domains/generated/ from open-source lists (free mail:
Kikobeats/free-email-domains, MIT; disposable mail:
disposable-email-domains/disposable-email-domains, CC0-1.0; Public Suffix
List: publicsuffix/list, MPL-2.0). provenance.json records each source's
commit, counts and license. The Worker never fetches them and the browser never
bundles them; it asks GET /api/v1/email/domains/:domain.
To move them forward, run the refresh and review the diff like code:
pnpm run refresh:email-domainsCorrect a provider the lists miss, or an organization they list by mistake, in
functions/_lib/email-domains/overrides.ts. Migration 0038 embeds the free and
disposable set; until it is applied, regenerate its affiliation_free_email_domains
inserts when the lists change (the organization-domain migration test shows the
expected text). An applied migration stays as it is.
The Cloudflare Worker uses Vite with @cloudflare/vite-plugin. The build
(scripts/build-site.mjs) prepares the public shell, bundles the native
TypeScript Worker and interactive application with Vite, renders the Astro
publication and its Pagefind index, generates the OpenAPI document, and
assembles one static release with the deployable Wrangler output config in
dist.
pnpm run build
pnpm run build:preview
pnpm run build:productionMerges to main are automatically deployed to production. Branches and pull
requests are deployed as Workers Previews
of the production Worker (wrangler preview), at
https://<branch>-pkic-org.pkic.workers.dev. They replace the former separate
pkic-org-preview Worker. Previews inherit no production bindings: every
binding they use is declared in env.production.previews in
wrangler.jsonc and points at preview resources
(pkic-db-preview, pkic-assets-preview, pkic-speaker-uploads-preview).
tests/tools/workers-previews-config.test.ts fails if a production resource
appears there.
- Each Preview's
APP_BASE_URLandWEBAUTHN_ORIGINare its own branch URL, injected byvite.config.tsfromWORKERS_CI_BRANCHor the current git branch. A preview build without a non-mainbranch fails. - All Previews share the
pkic-db-previewD1 database, which is also the productionDBbinding'spreview_database_id. Deployments never apply migrations; apply them separately withpnpm migrate preview, which runs Wrangler with--env production --preview.pnpm seed --previewuses the same arguments, andpnpm backup previewexportspkic-db-previewby name becaused1 exporthas no--previewoption. Wrangler takes the account fromwrangler.jsonc. - Cron Triggers, routes, and inbound email do not run in Previews, so the email outbox and other scheduled jobs stay idle there. Run a job manually from the portal scheduler when a Preview test needs it.
- Preview URLs are public unless Cloudflare Access protects them. Cloudflare
adds
X-Robots-Tag: noindextoworkers.devPreviews. - MCP OAuth in Previews uses its own
OAUTH_KVnamespace, never the production one.
Never copy production personal data, credentials, secrets, or private uploads
into preview; use synthetic or purpose-created preview data. Preview secrets
belong to the Previews base configuration:
pnpm exec wrangler preview base-config secret put <NAME> --env production.
One-time Cloudflare dashboard steps for the switch:
- Set each secret the old
pkic-org-previewWorker used (pnpm exec wrangler secret list --name pkic-org-previewlists the names) in the Previews base configuration with its preview value, never a production value. - In Workers Builds for
pkic-org, complete the one-time Previews setup and set the non-production branch deploy command topnpm exec wrangler preview. Keep the build command unchanged. - Optionally protect
*-pkic-org.pkic.workers.devwith Cloudflare Access. - Repoint or remove any Stripe test-mode or SendGrid webhook that targets
pkic-org-preview.pkic.workers.dev. - After a Preview builds and works, disconnect and delete the
pkic-org-previewWorker.
Manual deployment is exceptional. If an explicitly approved recovery or
operational task requires it, build and deploy together, because the Vite
plugin applies env.production (with its previews block) during build time:
pnpm run deploy:preview
pnpm run deploy:productionThe MCP OAuth binding uses Wrangler automatic provisioning for OAUTH_KV. On
the first deployment for an environment, Wrangler creates the namespace and
writes the generated IDs back into wrangler.jsonc.
System role IDs and names identify product responsibilities and remain immutable; permission bundles are editable by authorized operators. An assignment keeps its scope and responsibilities (such as group leadership) when the bundle changes. Effective access remains the union of current role assignments and direct grants.
Future migrations must preserve operator-managed bundles. Seed a complete default bundle only when creating a new role. Adding a default permission to an existing system role requires an explicit rollout decision: add it only where the current bundle still matches the documented prior default, or require an audited operator change. Do not regenerate existing bundles, restore removed permissions with unconditional inserts, or treat a new permission as automatically granted. Document the prior/default comparison and the affected roles in that migration's review. Advance the role's timestamp revision whenever a migration changes its bundle so concurrent operator edits retain their compare-and-set protection. Applied migrations remain immutable; these rules govern future schema/reference data work rather than rewriting existing migration history.