subs-free.openapps.xyz

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.
Owned app · rev 1ae4feb2bebf
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.
← All files
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
1ae4feb2be · 18 hours ago
Bind agent address in policy
99bbab3f0c · 19 hours ago
Use OpenApps service URLs and add contribution strip
781c3c23e5 · 19 hours ago
Bounded source sync
048e3257a5 · 1 day ago
Bounded source sync
7a358a3be6 · 1 day ago
Bounded source sync
0c833fcb0e · 1 day ago