The OSS alternatives directory launched with a content problem: AlternativeTo and SaaSHub have years of user reviews. Mine went live in April with zero. Fabricating reviews is a policy violation. Soliciting them takes time I don't have in month two of a six-month experiment.
What I built instead is a decision score — four signals derived from GitHub data I already fetch during ETL, each converted into a labeled rating that differs per entry. The goal was per-page content that genuinely varies between two alternatives in the same category, sourced from public facts, with nothing invented.
1. License commercial-use risk
Source field: the license SPDX identifier from the GitHub API. I convert it to one of five buckets — permissive, weak, strong, network, restricted, or unknown — each with a risk level and a one-line note:
const COMMERCIAL_USE: Record<LicenseBucketKey, CommercialUseRating> = {
permissive: { level: "Low", note: "Embed in a proprietary product with no copyleft obligation" },
weak: { level: "Medium", note: "Changes to the licensed files themselves must stay open" },
strong: { level: "High", note: "Distributing a derived work obliges releasing its source" },
network: { level: "High", note: "Even a hosted/modified deployment can trigger source release" },
restricted: { level: "High", note: "Non-OSI terms restrict commercial or hosted use — read first" },
unknown: { level: "Unknown", note: "No clear SPDX id — treat as all-rights-reserved until verified" },
};
The parsing order matters. LGPL contains GPL as a substring, so I use word-boundary patterns and check in risk-priority order: network (AGPL/SSPL) first, then strong (GPL/EUPL), weak (LGPL/MPL/EPL/CDDL), restricted (BUSL/Elastic/PolyForm/Commons-Clause), then permissive.
One edge case that required a comment in the source: BSL-1.0 is the Boost Software License, which is permissive. BUSL-1.1 is the Business Source License, which is restricted. If BSL appears in the restricted pattern, every Boost-licensed repo gets a High commercial-risk label. The two identifiers need separate patterns — matching BUSL catches business-source without touching Boost.
2. Maintenance level from last-push date
GitHub's pushed_at field says when the repository last received a push. I convert it to one of four levels:
| Level | Condition |
|---|---|
| Active | ≤ 90 days since last push |
| Maintained | 91–365 days |
| Stale | > 365 days |
| Unknown | Invalid or missing date |
Tighter bands would produce finer distinctions but more false positives. A library that ships a stable v1.0 and receives no commits for eighteen months is not necessarily abandoned — it might just be done. The three active-range labels are defensible without knowing each project's actual release cadence.
The date validation uses a round-trip check: parse the ISO string, convert back to ISO, compare. Node's Date constructor silently normalizes invalid dates like 2026-02-31 to 2026-03-03 without throwing, so round-trip comparison is the only way to catch corrupt data from the API. Corrupt dates return Unknown rather than guessing.
3. Adoption tier from star count
Stars are easy to dismiss as a vanity metric, but for adoption risk they carry a specific signal: community size. A 40-star project and a 40,000-star project have different expectations around third-party integrations, Stack Overflow coverage, and long-term maintenance continuity. That's useful decision information even if it says nothing about code quality.
Four tiers: Flagship (≥ 30,000 stars), Mainstream (5,000–29,999), Established (1,000–4,999), Niche (< 1,000). None of these labels implies "better" — a Niche tool that exactly fits a use case is more valuable than a Flagship one that almost fits. The tier tells you what to expect from the ecosystem around the project.
4. A combined decision row per alternative
The three signals above combine into a DecisionRow record that renders as a sortable comparison table on each SaaS alternatives page:
export interface DecisionRow {
name: string;
repo: string;
language: string | null;
stars: number;
adoption: AdoptionTier;
maintenance: MaintenanceRating;
commercialUse: CommercialUseRating;
}
Two OSS tools in the same category — say, two self-hosted analytics platforms for Plausible — will have different license buckets, different maintenance levels, different star counts. The row for each is unique, derived from real public data, and doesn't require me to claim I've actually run either one.
The sort order is stars descending, with repo name as a stable tie-breaker. The tie-breaker matters because the database ORDER BY stars DESC doesn't guarantee a consistent order for equal values, and inconsistent ordering across builds would produce unnecessary diffs in version control.
What I explicitly left out
Docker readiness and time-to-first-run were in an early spec draft. Both would be genuinely useful signals. I left them out because I don't have that data from the GitHub API, and estimating it would be a guess.
A comment in the module says this directly:
Axes that require actually running the software aren't included here because there's no verified data. Don't fill gaps with guesses. Those belong in a separate evidence card layer once real test data exists.
That constraint is the point. The decision score is the thing I can be honest about: objective fields from public repos, converted to labeled buckets by deterministic logic. Runtime behavior is a different category of claim that requires actually running the software. When I have those results, they'll go in a separate section with a clear label about how they were collected.
Part of an ongoing 6-month experiment running three AI-curated directory sites. The technical claims here are real; this article was AI-assisted.
Top comments (2)
The "don't fill gaps with guesses" line is the whole post for me, refreshing to see. one flag on the maintenance signal tho — pushed_at gets bumped by dependabot and bot pushes, so 'active' can be noise. filtering to human commits would tighten it
The BSL-1.0 vs BUSL-1.1 edge case is exactly the kind of thing that bites people when they regex-match license strings. I've seen the same confusion with "Elastic License" vs "Elastic License 2.0" — different obligations, one character apart.
Your constraint of "don't fill gaps with guesses" is the most interesting design decision here. Most directories pad their pages with subjective assessments that look authoritative but aren't verifiable. You're effectively trading perceived richness for trust — and I think that's the right bet for a directory that needs to survive past month two.
Question: have you considered adding a "last contributor count" signal? GitHub's contributors endpoint gives you a rough sense of bus-factor risk. A 10K-star project with a single committer has a very different risk profile than one with 20 contributors, even if both are "Active" on your maintenance scale. It wouldn't require running anything — just another ETL field from the same API.