This document details the configuration, data schema, styling, and synchronization mechanisms powering the NullStore web catalog.
- Stack: Pure Vanilla HTML5, CSS3, and modern ECMAScript (zero runtime frameworks or bundled bloat).
- Hosting: GitHub Pages (
https://nullcpy.github.io/) — canonical. An outage mirror runs on Vercel (https://nullstore.vercel.app), git-connected straight to this repo throughvercel.json; Pages stays the shareable URL and the mirror isnoindexd so it never competes on search. - Data Source: Static
data.jsonfile serialized under Schema v2. - Decoupled Design: The build pipeline in
nullcpy/rvbpushes catalog updates over GitHub API. The website operates entirely as an independent client.
The catalog uses Schema v2. All legacy baggage (such as artificial composite keys variantKey, buildKey, and container patches) has been purged in favor of clean, distinct first-class fields.
{
"version": 2,
"updated_at": "2026-09-08T03:30:00.000Z",
"patchSets": [ ... ],
"changelogSets": [ ... ],
"patchSourceSets": [ ... ],
"apps": [ ... ]
}Applied-patch names, changelog URLs and patch-source slugs repeat heavily across
builds, so identical lists are stored once in a top-level table and each build
references the entry by integer index (patchSetRef / changelogRef /
patchSourceRef) instead of inlining its own copy.
[
["Unlock Premium", "Hide Ads", "Remove Tracking"],
["Theme ENABLER", "Custom Branding"]
]Dedup is keyed on the ordered list, so builds with genuinely different lists still get distinct entries — only byte-identical repeats collapse (no data loss). An empty list is omitted entirely (the build simply carries no ref). The client resolves each ref via
getBuildAppliedPatches()/getBuildChangelogs()/getBuildPatchSources().
Each application in the catalog contains:
{
"appKey": "youtube",
"appName": "YouTube",
"totalDownloads": 125430,
"latestPublishedAt": "2026-09-08T03:30:00.000Z",
"brands": [ ... ]
}Represents a patching engine or identity (e.g. Morphe, ReVanced, ReVanced Extended):
{
"brandKey": "morphe",
"brandName": "Morphe",
"latestVersion": "19.16.39",
"latestPublishedAt": "2026-09-08T03:30:00.000Z",
"totalDownloads": 89200,
"variants": [ ... ],
"builds": [ ... ]
}Models a distinct functional or packaging stream:
variant(string | null): Feature or UI variant (e.g."Android TV","Nord"). Standard/default builds usenull.subVariant(string | null): Packaging variant (e.g."clone","alt"). Standard builds usenull.packageName(string): Target Android package name (e.g."com.google.android.youtube").apkFilter(string): Regular expression used for asset filtering (e.g. in Obtainium).latestStable/latestBeta(string | null): Channel pointer storing only the referenced build'sbuildid (ornullwhen the channel has no build). The client resolvesversion/publishedAt/isArchiveFallbackby looking the id up in the brand'sbuildsarray (matchingvariant+subVariant+releaseType+build), so these fields are no longer duplicated per pointer. This is the only shape the client reads; a pointer written as an inline object will not resolve.
{
"variant": "Android TV",
"subVariant": "clone",
"packageName": "com.amazon.amazonvideo.livingroom",
"apkFilter": "^primevideo-morphe-androidtv-clone-v.*\\.apk$",
"latestStable": "380576727",
"latestBeta": null
}Represents an individual build artifact release:
{
"build": "380576727",
"releaseType": "stable",
"isArchive": false,
"version": "19.16.39",
"versions": ["19.16.39"],
"variant": null,
"subVariant": null,
"publishedAt": "2026-09-08T02:00:00.000Z",
"releaseUrl": "https://github.com/nullcpy/rvb/releases/tag/380576727",
"patchSourceRef": 0,
"changelogRef": 0,
"patchSetRef": 0,
"assets": [
{
"name": "youtube-morphe-v19.16.39-arm64-v8a.apk",
"size": 134217728,
"download_count": 4200,
"browser_download_url": "https://github.com/nullcpy/rvb/releases/download/380576727/youtube-morphe-v19.16.39-arm64-v8a.apk",
"arch": "arm64",
"version": "19.16.39",
"patchSetRef": 0
}
]
}Notes
patchSetRef/changelogRef/patchSourceRefare integer indices into the top-levelpatchSets/changelogSets/patchSourceSetstables (see above). Each build's applied-patch names, changelog URLs and patch-source slugs live once in the shared table; an empty list is omitted (no ref). Rendered in the Applied Patches modal, never on collapsed cards.- A numbered build groups every arch under one card. Because rvb publishes each arch independently, one build can carry several app versions (e.g.
arm64at the newest,armat a fallback).versionis the primary (highest) value used forlatestVersionand channel pointers;versionslists every distinct version the build published, newest first, and is shown on the build card (at most two joined by/, the remainder folded into a+ Noverflow, e.g.v6.12.18 / v6.12.15 + 3).assets[].versionrecords the version of that specific file, so download rows show their own.assets[].patchSetRefis a per-asset index into the same sharedpatchSetstable. It lets the Applied Patches modal expose per-arch tabs (mirroring the Stable/Beta channel tabs) whenever a build's arches resolve to different patch sets; arches sharing one list collapse to the same index and the modal shows a single button. Resolved viagetArchAppliedPatches(); the build-levelpatchSetRefremains as the default for older clients.releaseIdis omitted when it equalsbuild(true for all numbered releases; both are the tag). It is kept only when it differs — i.e. rolling archive entries, wherebuildis a version butreleaseIdisstable/beta. The client falls back tobuildwhen it is absent.assets[].archuses the compact keysarm64 | arm | all | x86 | other(seegroupAssetsByArchitecture), whileCONFIG.knownArchslists the raw filename tokens used for auto-detection.assets[].fileTypeis not stored — it is always derived client-side from the filename extension viagetFileType()(.apk→APK,.zip→Module).
All website settings, categories, and dynamic notices are configured directly in CONFIG at the top of script.js.
const CONFIG = {
owner: "nullcpy",
repo: "rvb",
knownArchs: [
"arm64-v8a", "arm64", "aarch64",
"armeabi-v7a", "arm-v7a", "arm32", "arm",
"x86_64", "x86", "universal", "all"
],
appCategories: {
"Adobe": ["adobe"],
"Android TV": [
"primevideo", "plutotv", "moviebox", "disneyplus", "disney",
"hbomax", "tubi", "vix", "at4klauncher", "projectivylauncher",
"peacock", "netflix", "nuvio"
],
"Browser": ["browser", "edge"],
"Google": ["youtube", "google", "gboard"],
"Launcher": ["launcher", "at4klauncher", "projectivylauncher"],
"Meta": ["threads", "instagram", "messenger", "facebook", "!plusmessenger"],
"Social": [
"threads", "instagram", "messenger", "facebook", "twitter",
"tiktok", "telegram", "reddit", "pinterest", "tumblr"
],
"VPN": ["cloudflarewarp", "vpnify", "vpn"],
"YouTube": ["youtube"]
},
appNotices: [ ... ]
};- Category Names: Keys in
CONFIG.appCategoriesdefine the exact chip labels displayed on the site in clean Title Case (e.g."Android TV","Google","Meta","VPN"). - Matching Rules:
- Matches against normalized app name, app key, and names with
+converted toplus. - Prefix with
!to create an exclusion filter (e.g."!plusmessenger"prevents Plus Messenger from being caught under the"messenger"rule). - No random or duplicate word chips: only the categories defined here are rendered.
- Matches against normalized app name, app key, and names with
Notices display contextual alerts inside expanded app cards (e.g. MicroG requirements or login advisories).
Notices are styled 100% dynamically via CSS variables—you never need to edit CSS when adding or tweaking notices.
appNotices: [
{
triggers: ["youtube", "google"],
color: "accent", // Uses theme accent green with glowing gradient
title: "Login Issue",
text: "Signing into Google account on APK (not Module) requires MicroG.",
links: [
{ label: "Morphe", url: "https://github.com/MorpheApp/MicroG-RE/releases/latest" },
{ label: "ReVanced", url: "https://github.com/ReVanced/GmsCore/releases/latest" }
]
},
{
triggers: ["twitter"],
color: "warning", // Uses warning yellow with subtle warning gradient
title: "Login Issue",
text: "Since October 2025, Twitter has started checking device integrity during login.",
links: [
{ label: "Workarounds", url: "https://t.me/pikopatches/1/59772" }
]
},
// Example with custom hex color:
{
triggers: ["spotify"],
color: "#1db954", // Automatically derives border, text color, and 12% glow background!
title: "Playback Notice",
text: "Use recommended version.",
links: []
}
]color:- Presets:
"accent","info","warning","danger","error","success". - Custom Color: Any hex (
"#3b82f6"), rgb, or CSS color string.
- Presets:
border(optional): Specific border override (e.g."1px solid red"). Defaults tocolor.bg(optional): Specific background override. If omitted with a hexcolor, a subtle12%alpha transparent glow gradient is generated automatically.
NullStore provides native integration with Obtainium:
-
Label Standard: Uses square brackets for subvariants to prevent nested double parentheses:
$$\text{Format: } \text{AppName} \text{ (Brand - Variant [SubVariant])}$$ Example:Prime Video (Morphe - Android TV [Clone]) -
Asset Filtering: Automatically derives regex matching the variant, subvariant, and channel (
includePrereleasesfor beta). -
One-Click URL: Encodes configuration into
obtainium://app/{...}link protocol for instant setup.
Live search operates dynamically in memory without static corpus strings or bloated tokens:
- Exact match:
0(instant first result) - Prefix match:
1 - App Name Word match:
2(exact word),3(word prefix) - Substring match on App Name/Key:
4 - Brand Name match:
5 - Category Name / Keyword match:
5 - Variant & Sub-Variant match:
6 - Package Name match:
7
Unmatched apps return Infinity and are filtered out instantly.
- Sole writer of
data.json:.github/scripts/rebuild_catalog.pyregenerates the entire catalog in this repo and is the only component that writesdata.json. Itsfinalize()step emits the deduped schema: a top-levelpatchSetstable with per-build integerpatchSetRef, channel pointers stored as bare build-id strings,releaseIdomitted when equal tobuild, and assetfileTypeomitted (derived client-side).- It runs via
rebuild-catalog.ymlonrepository_dispatch(from rvb releases), the ~6-hourly schedule safety-net, or manualworkflow_dispatch, then self-dispatches a Pages deploy.
- rvb is an input source only:
nullcpy/rvbdoes not write or patchdata.json.rebuild_catalog.pyreads it through the GitHub API — each release'sbuild.jsonmanifest plus the releases/assets listings — and derives every field locally.
- Vercel mirror (outage fallback) — git-connected to this repo:
- Vercel project
nullstore→https://nullstore.vercel.app. It must live on a Vercel account whose linked GitHub identity isnullcpy: Vercel supports one GitHub account per Vercel user and requires repository ownership to import a personal-account repo, which is why an unrelated account cannot connect this one. vercel.jsoncopies only the five published site files (index.html,style.css,script.js,data.json,social-preview.svg) intodist/, so repo docs and workflow files are never served. If a sixth file ever becomes part of the page, add it to that copy list. The same file is published by Pages at/vercel.json, where it is inert.- It also sends
X-Robots-Tag: noindexon every response, and cachesdata.jsonasmax-age=0, s-maxage=300, stale-while-revalidate=600(edge 5 minutes, browser always revalidates) so no client holds a stale ~1 MB catalog. - Site-file pushes deploy on their own through the Git integration — no CI step is involved, and
deploy-pages.ymldeliberately carries no Vercel work. - Catalog rebuilds need the hook. The rebuild commit carries
[skip ci], a token Vercel honours, so without help a mirror would freeze at whatever catalog it was imported with.rebuild-catalog.ymltherefore POSTs theVERCEL_DEPLOY_HOOKsecret immediately after it pushesdata.json, gated onCATALOG_PUSHED. - That hook step is best-effort and non-fatal (
exit 0on failure): Pages is canonical, and the next site-file push re-syncs the mirror anyway because that deploy ships the whole repo,data.jsonincluded. Recovery from a lost hook is a singlecurl -X POSTof the hook URL.
- Vercel project