Cloud Storage
How GPEX-Cloud persists, syncs, and serves OpenGPEX v2 projects — from the editor's serialization pipeline to the server-side storage model.
Architecture at a Glance
Editor (opengpex_v2) GPEX-Cloud
┌─────────────────────────┐ ┌──────────────────────────┐
│ core/cloud (client SDK) │ ──HTTPS──▶ │ API Routes (/api/user/*) │
│ auth/ · protocol.ts │ │ guard.ts (requireAuth) │
│ storage.ts · types.ts │ │ FileService (upsert) │
└──────────┬──────────────┘ │ Postgres + R2 objects │
│ └──────────────────────────┘
.gpex container (state + assets)
The cloud client lives entirely in core/cloud — the editor core has no knowledge of backend internals. All requests carry the authenticated session as an Authorization: Bearer header (the access token lives only in memory; the refresh token is persisted in localStorage with rotation detection). Server-side guard.ts (requireAuth) resolves the caller before any file operation.
Data Model
Cloud files live in tb_user_files, owned by the authenticated business user (sys_user_id):
| Field | Purpose |
|---|---|
file_local_id |
Client-generated upsert key; unique per user — re-saving the same project overwrites in place |
manifest (JSONB) |
Lightweight document metadata for listings (name, dimensions, layer count, timestamps) |
preview_b64 |
Base64 thumbnail for the Cloud Gallery, extracted server-side from the container |
r2_path |
Absolute address of the binary blob in R2 object storage |
file_size, version |
Quota accounting and per-save version counter |
fingerprint |
SHA-256 of the uploaded bytes, returned to the client after each save |
Binary payloads (the ZIP member of the .gpex container) go to R2; only the manifest, preview, and counters live in Postgres, keeping listing queries cheap.
Saving & Syncing
Sync to Cloud serializes the active document into a .gpex binary container (thumbnail + JSON manifest + ZIP of state and content-addressed assets) and uploads it to POST /api/user/files/save with the project's file_local_id. The server upserts on that key: a matching row keeps its file_id, bumps version, and overwrites the same R2 object (the path is derived from file_id), so there are no addressable historical snapshots — only the latest bytes plus a rising version counter. Limits enforced server-side: 20 MB per file, 200 MB storage quota per user.
Status Indicators
The UI derives a SyncStatus per frame (SYNCED / LOCAL_AHEAD / NEVER_SAVED / OFFLINE):
| Status | Meaning |
|---|---|
| NEVER_SAVED (Local only) | Never saved to cloud |
| LOCAL_AHEAD (Unsaved changes) | Local modifications since last sync |
| SYNCED | Up-to-date with cloud (shows version) |
| OFFLINE | Cloud service unavailable |
Cloud Gallery
The Cloud Browser (XTEND_SLOT → CloudMenu) lists the authenticated user's files from the manifest table:
- Open — download and hydrate into the editor
- Share — create (or idempotently re-fetch) the file's public share link and copy it to the clipboard
- Download — save locally as
.gpex - Delete — remove manifest row and R2 object after an explicit confirmation (associated share links are deactivated)
Public Share Links
Files can be published via a share_token (POST /api/user/files/[id]/share; creation is idempotent — re-sharing returns the existing token). External visitors download through GET /api/share/[token]/download — token alone, no account and no file id. Because the token is bound to the file's file_id, which survives re-saves, links keep working when a project is updated. Server-side limits: up to 20 active share links per user (HTTP 429 beyond that); links can be revoked via DELETE /api/user/files/[id]/share, and deleting the file deactivates its shares.
Conflict Handling
Loading a cloud project that collides with the active local document (same frame id) raises a confirmation dialog — Overwrite or Cancel — preventing accidental loss of local work.
Roadmap: Teams and Workspaces
A workspace-and-team-collaboration architecture (personal workspace per user, team workspaces with owner/admin/editor/viewer roles, optimistic locking against concurrent overwrites) is designed but not yet shipped; today's model remains single-owner (sys_user_id) with per-user file isolation.
Privacy
- Files are stored securely at rest; access is restricted to the authenticated owner's session
- File contents are never analyzed or processed server-side beyond storage, manifest/preview extraction, and SHA-256 fingerprinting
Last updated: 2026-10-12