- TypeScript 83%
- CSS 17%
crosscredit #
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_creditsfor 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 #
- Create a Cloudflare account and keep Workers on the Free plan.
- Sign in with
pnpm exec cf auth login. - For your own deployment, change
worker.nameand remove or replaceworker.domainsandworker.triggersincloudflare.config.ts. The checked-in domain iscrosscredit.haltcase.dev; its explicit route takes precedence over the existing zone-wide redirect Worker. - Create an ignored
.env.production.localfile containing onlyTMDB_READ_ACCESS_TOKEN=your-token. - Run
pnpm deploy --secrets-file .env.production.localto 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.