license-tilt
Live, public radar of OSS license changes across npm, PyPI and the GitHub repos that back them. When a popular package tilts from a permissive license (MIT / Apache-2.0) toward a source-available license (BUSL, SSPL, FSL, Elastic-2.0, Commons Clause) or proprietary, license-tilt catches it within an hour, classifies the move, computes the blast-radius, and publishes a public timeline.
Live: https://holyai.me/license-tilt/
Why this exists
Relicensing is the dev-tool boardroom topic of 2024–2026. MongoDB, Elastic, HashiCorp (Terraform / Vault / Consul / Nomad), Sentry, Redis, MariaDB MaxScale, Cockroach, Confluent — six out of nine large OSS-funded startups that crossed the same growth phase ended up restricting their license. Renovate / Snyk give you zero signal here; GitHub's license.spdx_id field is in the API but nobody publishes a live diff feed of it. We do.
The pain point in one sentence:
"I shipped on top of <library X>. It was MIT yesterday. Today it is BUSL with a 'no commercial host' grant. My CI is green, my SBOM is green, and I just lost my legal moat."
Quick start
npm install
cp .env.example .env
npm start
# open http://localhost:4861/license-tilt/
The first refresh cycle takes about 30 minutes — the dashboard fills in as packages get fetched. The boot sequence kicks the SPDX catalogue refresh immediately, then begins the rolling package refresh 5 seconds later.
Environment
| Variable | Default | Notes |
|-------------------|------------------------|-------|
| PORT | 4861 | HTTP port |
| BASE_PATH | /license-tilt | All routes mount under this prefix |
| NODE_ENV | production recommended | Boots the immediate refresh cycle |
| GITHUB_TOKEN | unset | Optional; raises GitHub REST rate from 60 to 5000/h |
| LICENSE_TILT_DB | ./license-tilt.db | SQLite path |
No auth env vars exist. Every endpoint, including POST /api/refresh, is public on purpose.
HTTP API (all public, no auth)
| Method | Path | Description |
|--------|-----------------------------------------------|-------------|
| GET | /license-tilt/health | liveness, returns last tilt timestamp |
| GET | /license-tilt/api/stats | dashboard hero stats |
| GET | /license-tilt/api/tilts?period=30d&direction=&ecosystem=&limit=&offset= | tilt feed |
| GET | /license-tilt/api/packages?ecosystem=&category=&sort=&q=&limit=&offset= | tracked packages |
| GET | /license-tilt/api/packages/:ecosystem/:name | per-package detail + timeline |
| GET | /license-tilt/api/risk?limit=50 | risk leaderboard |
| GET | /license-tilt/api/categories | per-category counts + examples |
| GET | /license-tilt/api/fetch-log?limit=100 | recent fetcher events (transparency) |
| GET | /license-tilt/api/sources | last 24h fetch ok/bad counts per source |
| POST | /license-tilt/api/refresh {ecosystem,name}| trigger an immediate refresh of one package (rate-limited 30/IP/min) |
Where every number on this page comes from
All license values are fetched live at runtime — no mocks, no seeds, no preset arrays.
| # | Source | URL | Auth | Refresh | Used for |
|---|------------------------------|--------------------------------------------------------------------------------------|--------------------------|----------------------------------|----------|
| 1 | npm registry | https://registry.npmjs.org/{package} | none | every 6h, top 200 by downloads | license field for npm packages |
| 2 | npm downloads | https://api.npmjs.org/downloads/point/last-week/{package} | none | daily | weekly download count (blast-radius weight) |
| 3 | PyPI JSON | https://pypi.org/pypi/{package}/json | none | every 6h, top 200 | info.license + classifiers |
| 4 | PyPI downloads | https://pypistats.org/api/packages/{package}/recent | none | daily | last-week downloads (blast-radius weight) |
| 5 | GitHub repo metadata | https://api.github.com/repos/{owner}/{repo} | optional GITHUB_TOKEN | every 4h, tracked repos | license.spdx_id, stars, pushed_at |
| 6 | GitHub LICENSE-file history | https://api.github.com/repos/{owner}/{repo}/commits?path=LICENSE (also LICENSE.md, LICENSE.txt, COPYING) | optional GITHUB_TOKEN | daily | exact tilt-event commit / date / message |
| 7 | SPDX license catalogue | https://raw.githubusercontent.com/spdx/license-list-data/main/json/licenses.json | none | weekly | canonical SPDX list + OSI / FSF flags |
| 8 | OpenSSF Scorecard | https://api.securityscorecards.dev/projects/github.com/{owner}/{repo} | none | every 7d | criticality (input to risk score) |
If a source is unreachable, the fetcher logs the failure into fetch_log and leaves the previous snapshot untouched. Nothing on the dashboard is invented when a fetch fails — the "Source freshness" panel on the overview shows the truth.
Static lookup tables (heuristics only)
These two files never affect license values. They feed only the risk_score heuristic:
data/commercial-backers.json— hand-maintained list of for-profit organizations behind popular OSS projects (Hashicorp, Sentry, Redis, MongoDB, Elastic, Grafana, Supabase, …).data/recent-funding.json— hand-maintained list of OSS companies that closed a funding round in the last ~18 months.
Update them quarterly; nothing else in the product references them.
Seeds — names only
seeds/npm-top.txt— ~200 high-traffic npm package namesseeds/pypi-top.txt— ~200 high-traffic PyPI package namesseeds/critical-repos.txt— ~150 high-blast-radius GitHub repos (owner/repo)
Every package's actual license string is fetched live; the seed files only say what to track.
Schema
packages (1 row per tracked artifact) → license_snapshots (1 row per refresh that saw a new value) → tilts (1 row per detected category change). Plus license_catalog (SPDX), fetch_log (audit trail), schema_meta. See db.js for the DDL.
Cron schedule
| Cron expression | Job |
|------------------|-----|
| /30 | refresh next 25 packages (full sweep in ~12h for 600 tracked) |
| 0 /4 | refresh GitHub repo metadata (stars, pushed_at) for top 100 |
| 0 1 | refresh weekly downloads for every npm + PyPI package |
| 0 2 | for tilts missing commit context, look up LICENSE-file commits |
| 30 3 | recompute risk scores |
| 0 6 0 | refresh SPDX licence catalogue (weekly) |
License
MIT