Source
This app’s code and revision history are public. Download a copy or explore the files below.
Public
The app’s code and revision history are public.
Run it yourself
Take the whole tree and run it on your own machine. The download needs no account. Cloning needs a sign-in.
Download .zip
bounded clone 5af1a249bbbef1db1b4c5ec8
A copy you download is not bounded. The guarantees this app runs under - declared egress, proven invariants, writes that fail closed - live in the runtime serving this page, not in the files. What you get is auditable source you can read and run, with no runtime guarantees attached.
src/lib/alternatives.ts
// Free alternatives from the OpenApps store. Subs ships a copy of the store's catalog (catalog.json,
// refreshed with `npm run catalog`) and reads the live list from the store's MCP server at most once a
// day, so apps added to the store show up without a new release. The store's own catalog.json is not
// readable from another site (static pages send no CORS header); its MCP server is.
import { useSyncExternalStore } from "react";
import snapshot from "./catalog.json";
import { serviceNamed } from "./services";
export interface FreeApp { slug: string; name: string; url: string; tagline: string; freeAlternativeTo: string[] }
export const STORE_URL = "https://openapps.free";
const MCP = "https://functions.openapps.xyz/apps/82875c5f2cbb469c47e29ec8/mcp";
const CACHE = "subs:catalog";
const DAY = 86400000;
const valid = (a: Partial<FreeApp>): a is FreeApp =>
typeof a?.slug === "string" && typeof a.name === "string" && /^https:\/\//.test(a.url || "") && Array.isArray(a.freeAlternativeTo) && a.slug !== "subs";
function combine(live: FreeApp[]): FreeApp[] {
const bySlug = new Map<string, FreeApp>();
for (const a of [...snapshot.apps, ...live]) if (valid(a)) bySlug.set(a.slug, { ...a, freeAlternativeTo: a.freeAlternativeTo.map(String), tagline: typeof a.tagline === "string" ? a.tagline : "" });
return [...bySlug.values()];
}
let cached: { at: number; apps: FreeApp[] } | null = null;
try { cached = JSON.parse(localStorage.getItem(CACHE) || "null"); } catch { /* blocked storage: use the copy */ }
let apps = combine(cached?.apps || []);
const listeners = new Set<() => void>();
let asked = false;
async function refresh() {
if (asked || (cached && Date.now() - cached.at < DAY)) return;
asked = true;
try {
const res = await fetch(MCP, {
method: "POST",
headers: { "content-type": "application/json", accept: "application/json, text/event-stream" },
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "list_free_apps", arguments: {} } }),
});
const list = (await res.json())?.result?.structuredContent?.apps;
if (!Array.isArray(list)) return;
const fresh = list.filter(valid);
cached = { at: Date.now(), apps: fresh };
try { localStorage.setItem(CACHE, JSON.stringify(cached)); } catch { /* fine */ }
apps = combine(fresh);
for (const l of listeners) l();
} catch { /* offline or the store is down: the shipped copy still works */ }
}
/** Every free app in the store; reads the live list in the background, once a day. */
export function useFreeApps(): FreeApp[] {
void refresh();
return useSyncExternalStore((cb) => { listeners.add(cb); return () => listeners.delete(cb); }, () => apps);
}
const norm = (t: string) => ` ${t.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim()} `;
/** The names a subscription goes by: what the person typed, plus the listed service's other names. */
function namesOf(name: string): string[] {
const sv = serviceNamed(name);
return [name, ...(sv ? [sv.name, ...(sv.aka || [])] : [])].map(norm);
}
export interface Match { app: FreeApp; paid: string }
/** The store app that is a free alternative to this subscription, matching whole words of the paid
* app's name ("Duolingo Super" matches Duolingo). The longest matching name wins, so "Adobe Acrobat
* Sign" goes to Sign rather than PDF. */
export function freeAlternative(name: string, list: FreeApp[]): Match | null {
const names = namesOf(name);
let best: Match | null = null;
for (const app of list) {
for (const paid of app.freeAlternativeTo) {
const p = norm(paid);
if (names.some((n) => n.includes(p)) && (!best || paid.length > best.paid.length)) best = { app, paid };
}
}
return best;
}
Change history
Point store links to openapps.free
Bind agent address in policy
Use OpenApps service URLs and add contribution strip
Bounded source sync
Bounded source sync
Bounded source sync