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/). - 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.