Cloud Integration
How OpenGPEX v2 connects to GPEX Cloud for file storage and sharing, and how the optional anonymous telemetry loop (update checks + usage statistics) is wired through a local BFF into the cloud.
Cloud Storage & Sharing
The editor integrates with GPEX Cloud through a small, stateless API surface and an explicit, user-driven sync model (carried over from v1):
- Authentication — email + one-time password, with OAuth popup as secondary; a short-lived access JWT held in memory and a refresh token persisted in localStorage, sent as Bearer headers and refreshed transparently on expiry.
- File operations — save, list, download, remove, share / unshare, and quota check, executed by the editor's cloud client against Cloudflare Workers with an R2 object store behind them.
- Sync model — the editor always works locally (IndexedDB). Cloud save is an explicit user action; there is no background sync or silent conflict resolution. Dirty detection is history-based: the menu derives a per-frame status (
OFFLINE/NEVER_SAVED/LOCAL_AHEAD/SYNCED) from the undo-history length recorded at last save, and skips redundant saves when already synced. Opening a cloud file that would replace an existing local frame requires explicit overwrite confirmation.
.gpex Payload Packaging
Cloud save/receive reuse the project container directly: the cloud menu commands build a ZIP payload from state.json + assets-manifest.json + the three asset classes (assets/, raw/, dec/), and receiving side unpacks it through the zero-decode, manifest-driven path described in File Format & Storage. A project uploaded to the cloud therefore round-trips its full color identity and high-precision pixels, not just an 8-bit flattening.
Telemetry & Update Checks
v2 adds a single outbound telemetry channel that doubles as the version-update service: checking for updates is what users want, and the same request yields an anonymous activity heartbeat.
Topology
┌───────────────────────────────────────────────┐
│ OpenGPEX local runtime (Next.js) │
│ │
│ Browser ──GET /api/system/check-update──► Local BFF
│ • client_id (localStorage, header) │ • validates opt-out state
│ • 12h throttle + cached response │ • collects platform self-report
│ • renders update toast │ • appends environment fields
│ │ • stateless, 3.5s timeout,
└───────────────────────────────────────────────┘ fails silent ("no update")
│
▼
gpex-cloud (Cloudflare Workers + Supabase)
• IP → /24 mask, then discarded
• single stored-procedure aggregate write
• GitHub Releases version match
The browser never talks to the cloud directly: the same-origin BFF relay is immune to ad blockers, lets the server contribute environment fields, and degrades silently on any failure.
Identity and Privacy
- Statistical entity = browser. A random UUID (
crypto.randomUUID()) is generated once per browser and sent via a request header — never in URLs or query strings. - Pseudonymization at the edge. The cloud stores only
SHA-256(client_id + server salt); the raw ID never leaves the browser. IPs are masked to /24 for abuse guarding and then dropped in memory; country comes from the edge header. - Low-cardinality dimensions only — client OS/arch (browser self-report via Client Hints), deployment kind (cloud vs. self-hosted, server-determined). No hostnames, file paths, document content, or high-cardinality fields. A negative list forbids them explicitly.
- Opt-out, two levels — an in-app preference stops all automatic checks; the server environment variable
DO_NOT_TRACK=1hard-disables outbound calls entirely (the BFF relay never performs the request).
Data Model
Three Supabase tables, maintained by a daily cron:
| Table | Contents | Retention |
|---|---|---|
tb_stats_daily_active |
Daily activity detail (pseudonymous client hash + dimensions) | 90 days |
tb_stats_ip_guards |
/24-subnet abuse counters | 2 days |
tb_stats_daily_rollup |
Day-grain aggregates, no identifier columns | permanent |
Daily/monthly active browsers and version distribution are computed off the rollup; cross-day deduplication queries the detail table. A per-subnet daily cap on new clients bounds statistical pollution (over-cap requests still get version answers, just no writes).
Version Source
Latest versions come from GitHub Releases (cached 1h server-side, with stale-cache fallback when GitHub is unavailable), so release publishing is the only operational step. Update checks are locked to the user's current major version — v1 users are never prompted into v2; major migration is announced on the website, not through the update toast. A release titled with [security] flags a critical update; responses always degrade to "no update" rather than failing.
Last updated: 2026-10-12