Find the cast and crew shared by two movies or TV shows crosscredit.haltcase.dev
README.md

crosscredit #

Live app · Source

Pick two movies or TV shows and find their shared cast and crew. A small hobby project built with React, TanStack Start, TanStack Query, and strict TypeScript. Vite+ supplies the dev server, build, Vitest, Oxlint, and Oxfmt.

Run locally #

Requires Node 22.18+ (Node 24 recommended) and pnpm 12.16.1. A global Vite+ install is optional; all project commands use the locked local toolchain.

pnpm install

Copy .env.example to .env, add your API Read Access Token from TMDB account settings, then run:

pnpm dev

Open http://localhost:3000. Without a token the page explains setup; it does not substitute fabricated results. Never prefix the token with VITE_ or commit .env.

pnpm check      # Oxfmt, Oxlint, type-aware lint, and TypeScript checks
pnpm typecheck  # Additional standalone TypeScript compiler check
pnpm test      # Matching and data normalization tests
pnpm build     # Production Cloudflare Worker and static assets
pnpm preview   # Preview the production build locally

Small by design #

TypeScript, Oxfmt, and Oxlint use @haltcase/style@9.0.0-canary.1. The Vite+ configuration enables the React, Node, TanStack Router, and TanStack Start lint presets, including type-aware linting and type checking through oxlint-tsgolint. TypeScript extends the package's bundler preset with the project's stricter compiler options. The typescript alias supplies TypeScript 6 for JS lint plugins; pnpm typecheck uses the separate typescript-7 compiler, as documented by the style package.

  • Start server functions validate inputs and TMDB responses with Zod. Only server code contacts TMDB; the token is never sent to the browser.
  • Every frontend request uses TanStack Query. Search debounces within its query function and cancels stale requests. Picking a title immediately starts loading its credits while the other title is chosen.
  • Query clients are created per router/SSR request. The server has a bounded, per-process response cache (200 entries) and upstream timeouts. In-flight requests stay independent because Workers bind I/O to each request. No database, accounts, or persistent infrastructure.
  • Selected titles live in the URL. Movie IDs and TV IDs include their media type, so equal numeric IDs do not collide. Credits intersect by person ID.
  • Select a shared person's name to open a keyboard-accessible credits drawer. It loads their combined movie/TV filmography on demand, merges acting roles and crew jobs per title, and keeps guest appearances. Search and type filters help browse long lists. Replace either comparison title to continue exploring with a shareable URL; Close or Escape returns focus to the opener. A separate TMDB link provides deeper person information.
  • Cast means acting in both titles; Crew means working on both; Everyone includes mixed acting/crew connections. Multiple roles/jobs are merged into one person.
  • Select two shared people and choose Where else? to explore their other collaborations in a drawer with the same collapsing header, search, and title-type filters as the person drawer. Both drawers share dialog focus handling and the filmography query/cache. Both filmographies use TMDB's combined movie/TV credits, validated on the server and cached for six hours. Acting roles and crew jobs are merged by media type and title ID, with no popularity or episode-count cutoff. The current two titles are excluded. Results show each person's contribution and can be compared with either original title. Shared TV credits do not establish shared episodes. Filmographies load only on request through TanStack Query; failures have a retry action.
  • Movies use /movie/{id}/credits; TV uses /tv/{id}/aggregate_credits for all recorded episodes, including single appearances. No billing, episode-count, or popularity cutoff is applied to credits. Missing or uncredited appearances still depend on TMDB's coverage. Episode counts describe each role, not an exact episode list.

Open source and data terms #

The application code is MIT licensed. TMDB data, images, and logos are not covered by the application's MIT license. Attribution is included in the site footer. See TMDB's API FAQ and API terms.

public/tmdb.svg is the unmodified official TMDB logo, included solely for attribution under TMDB's branding requirements.

The intended use is a free, noncommercial hobby site. Donations are not assumed exempt from TMDB's revenue rules; obtain confirmation from TMDB before adding a donation or sponsorship link.

Deployment: Cloudflare Workers Free #

The app targets Cloudflare Workers using the beta cf CLI and Cloudflare Vite plugin v2. cloudflare.config.ts defines the Worker; Vite+ remains the build, test, lint, and format toolchain. The beta dependencies are pinned. The compatibility date matches the installed local runtime. No database, KV, Durable Objects, or paid bindings are required. Start server functions need Workers; uploading only static files to Pages is insufficient.

Validate locally #

pnpm build
pnpm preview

Preview runs the production build locally in the Workers runtime. Local development and preview use the server-only TMDB_READ_ACCESS_TOKEN from your ignored .env file. Never prefix it with VITE_. Build output lives in .cloudflare/output/v0/ and is ignored, together with generated Cloudflare types and local state. A production build does not require the TMDB token.

pnpm deploy:check

This builds and validates deployment output without uploading anything or requiring Cloudflare credentials. It does not validate account quotas or production secrets.

First deployment #

  1. Create a Cloudflare account and keep Workers on the Free plan.
  2. Sign in with pnpm exec cf auth login.
  3. For your own deployment, change worker.name and remove or replace worker.domains and worker.triggers in cloudflare.config.ts. The checked-in domain is crosscredit.haltcase.dev; its explicit route takes precedence over the existing zone-wide redirect Worker.
  4. Create an ignored .env.production.local file containing only TMDB_READ_ACCESS_TOKEN=your-token.
  5. Run pnpm deploy --secrets-file .env.production.local to upload the token as a Worker secret with the deployment. Never commit this file or put the token in the Cloudflare config.

Cloudflare provides a workers.dev address, so a purchased domain is optional. For later code-only deployments, run pnpm deploy; existing secrets are retained. To rotate the TMDB secret, update the secret file and pass --secrets-file again. cf does not yet offer a single-secret setter. Deploy commands publish the app; they are not part of local validation.

For Cloudflare Workers Builds, use pnpm build as the build command and pnpm exec cf deploy --prebuilt --mode production as the deploy command. Configure TMDB_READ_ACCESS_TOKEN as a runtime Worker secret, rather than a public build variable.

The response cache is temporary and local to each Worker isolate. Cold starts and different isolates may fetch the same TMDB data again; correctness does not depend on a warm cache. Free hosting remains subject to Workers request, CPU, and bundle-size limits. The dry run checks build validity; exercise representative searches and large TV comparisons after deployment to check CPU usage under the Free plan.

See the Cloudflare CLI guide and Workers pricing.

Tangled CI deployments #

.tangled/workflows/deploy.yml uses Tangled Spindle's workflow syntax, with the microVM engine and a NixOS image. Select a compatible spindle in repository Settings → Pipelines, then add these repository secrets:

Secret Purpose
CLOUDFLARE_API_TOKEN Deploy production and manage native Previews, with Workers Routes and zone access for haltcase.dev.
CLOUDFLARE_ACCOUNT_ID The account that owns crosscredit.
TMDB_READ_ACCESS_TOKEN Uploaded as an encrypted runtime secret to production.

All three workflows run installation and CI inside steam-run, provided by Nixpkgs' steam-run-free compatibility package. The pinned pnpm 12 executable and downloaded native build tools need the standard Linux loader and libraries that a bare NixOS image does not provide. This also covers commands spawned by scripts/ci.ts, without disabling pnpm's version pin or changing the lockfile. See Nix's guidance on running non-Nix executables.

The production and PR Preview CI processes preload Nixpkgs' jemalloc. Oxlint's JavaScript plugins reserve large, aligned virtual-memory arenas; the default glibc allocator can reject those reservations on a small Linux VM before lint analysis starts. Jemalloc uses MAP_NORESERVE with Linux's default overcommit policy, so physical memory is needed only as pages are used. This keeps the full lint configuration and requires no privileged VM changes. The cleanup workflow does not run lint and needs no allocator override. See the upstream Oxlint issue.

Pushes to main run pnpm check, pnpm typecheck, pnpm test, and a production build, then deploy the crosscredit Worker using the pinned cf CLI with --prebuilt. The script checks the current remote SHA before building and publishing to skip superseded runs.

PR previews run on opened, reopened, and synchronize events for pull requests targeting main. They run the same checks, build with Cloudflare's CLOUDFLARE_PREVIEW_BUILD=true context, and call cf previews deploy --prebuilt. Each Preview belongs to the existing crosscredit Worker; production's custom domain and route are excluded from Preview configuration. CI logs the Preview URL. The native workers.dev Preview URLs include Cloudflare's X-Robots-Tag: noindex header.

PR preview comments #

After a successful Preview upload, CI can post a comment containing its stable workers.dev URL and the deployed commit SHA. Each successful upload adds a comment, including updates and retries. Production deploys, cleanup runs, skipped builds, and untrusted PRs do not post comments.

To enable comments, add TANGLED_COMMENT_APP_PASSWORD as a repository secret. Use an app password from the AT Protocol account that should author the comments, not that account's main password. The account must be allowed to comment on this repository. The workflow already contains these non-secret values under environment:

TANGLED_COMMENT_IDENTIFIER: haltcase-bot.tngl.sh
TANGLED_COMMENT_PDS_URL: https://tngl.sh
TANGLED_COMMENT_SPINDLE_URL: https://spindle.tangled.network

For another account or host, use its actual PDS and the repository's configured spindle. These must be HTTPS origins without paths. CI reads the triggering PR's record URI from the spindle's org.tangled.ci.getPipeline response, verifies the repository, branch, and commit, then reads the current PR record from the knot. Closed PRs are skipped. It creates an org.tangled.feed.comment using a short-lived service-auth token scoped to com.atproto.repo.createRecord; the account app password and PDS access token are sent only to the configured PDS. No tokens are written to disk. See Tangled's comment record schema.

Without the app-password secret, CI logs the Preview URL and skips commenting. Once enabled, a commenting error fails the workflow so it is visible; an already-uploaded Preview remains deployed. Read requests retry transient network errors, timeouts, and HTTP 408, 429, 500, 502, 503, and 504 responses up to three attempts, with a 30-second timeout per attempt and short backoff. Errors name the service and method without exposing response bodies or tokens. Writes are not automatically retried because a timed-out comment submission may already have succeeded.

Preview lifecycle and secrets #

Tangled documents the PR source branch but no PR number environment variable, so Preview names use pr-<source-branch-slug>-<12-character-hash>. Updates and reopened PRs reuse the same Preview. Cleanup deletes the native Preview through Cloudflare's API on closed and merged events, even if the source branch still exists. Already-missing Previews are treated as successfully cleaned up; other API errors fail the pipeline. There are no branch-deletion listeners, scheduled jobs, or account-wide Worker scans.

Before the first PR deployment, add TMDB_READ_ACCESS_TOKEN to the Worker's Previews Base secrets in the Cloudflare dashboard (Worker → Settings → Variables and secrets → Previews Base). The pinned cf previews deploy command does not accept --secrets-file; the production repository secret does not populate Preview secrets. New Previews inherit Base secrets. Updating a Base secret only affects new Previews, so rotate existing Preview secrets separately or delete and recreate those Previews. See Cloudflare's Preview secret configuration.

Tangled withholds repository secrets for fork/patch PRs and runs requiring approval, so those runs skip Preview deployment and deletion. Same-repository PRs from collaborators with push access can deploy Previews. No credentials are passed to the frontend.

Spindle must support the microVM engine with a NixOS image. Its default workflow timeout is five minutes; increase it if installation, checks and deployment exceed that budget. Treat push access to main as production deployment access.

There is no workflow concurrency lock. Remote SHA checks skip superseded builds, but an update during the final upload can still race another deployment. A PR close can also race an in-flight Preview upload; retry the PR cleanup pipeline if that happens. Rapidly reopening a PR while its cleanup is running can delete the reopened Preview; rerun its Preview pipeline. Avoid concurrent production pushes until the preceding deploy finishes. Cloudflare also automatically evicts the least recently deployed Preview at its per-Worker limit.

The pipeline needs its first hosted run to verify spindle packages and Cloudflare permissions. Run pnpm deploy:check locally to validate configuration and build output without publishing.

GitHub mirror #

Tangled is the source of truth for code and contributions. GitHub mirrors main after each push to Tangled. The mirror runs separately from the production deployment.

The mirror workflow uses the Tangled repository secret GITHUB_MIRROR_DEPLOY_KEY. Store the private SSH key there and add its public key to the GitHub mirror as a deploy key with write access. The workflow fetches the latest Tangled main and pushes without force. If GitHub has diverged, inspect the GitHub-only commits before repairing the mirror.

Contributing #

Keep TypeScript strict, including indexed access and exact optional-property checks. Use Query for frontend I/O; do not fetch in effects. Use Vite+/Oxlint/Oxfmt, with no ESLint or Prettier configuration. Run check, test, and build before proposing changes. Add regression tests for changes to normalization or matching, particularly guest appearances, multiple roles, and identity collisions.