HANDOFF SESSION 109 PULSE R2 SETUP
Documentation for Developers
GoGrab Pulse - Complete Architecture & Operations Guide
Last Updated: February 12, 2026 (Sessions 108-109) Status: Production - Live at pulse.gograb.space
What Is GoGrab Pulse?
GoGrab Pulse is a space-themed web music player built as a static PWA deployed on Cloudflare Pages, with audio files stored on Cloudflare R2 object storage and song metadata managed through both a static JSON file and a NestJS backend module connected to MongoDB.
Live URLs:
- Landing page: https://pulse.gograb.space
- Player: https://pulse.gograb.space/player
Architecture Diagram
SONG SOURCES STORAGE SERVING
----------- ------- -------
Apple iCloud ──┐
├─ sync CLI ──> Cloudflare R2 ────────> Browser (audio streaming)
Local folder ──┘ | (pulse-music bucket) Free CDN egress
|
└──> songs.json ──> Cloudflare Pages ──> Browser (player UI)
| (pulse.gograb.space)
└──> MongoDB ──────> Backend API ──────> (future API access)
(pulse_songs) (/api/pulse/*)
Why Cloudflare R2?
We evaluated four storage options for the MP3 files:
| Option | Pros | Cons | Verdict |
|---|---|---|---|
| Cloudflare R2 | Free egress, CDN-edge delivery, S3-compatible API, scales to thousands of files | Needs initial setup, no CORS on r2.dev URLs | CHOSEN |
| Cloudflare Pages | Simple (deploy with site), already working | 92MB deploys take 80s, 25MB/file limit, doesn't scale past ~100 tracks | Used initially, replaced |
| MongoDB GridFS | Already have MongoDB, good for small files | Not designed for streaming large audio, adds backend dependency for playback | Not suitable for audio |
| Old FTP server | Was working before migration | Broke when DNS moved to Cloudflare, no CDN, manual uploads | Deprecated |
Key R2 benefits:
- Deploys dropped from 92MB / 80 seconds to 184KB / 1 second
- Zero egress costs (Cloudflare's R2 has no bandwidth charges)
- Files are served from Cloudflare's global CDN edge
- Adding new songs doesn't require redeploying the site
How Songs Are Stored
Cloudflare R2 Bucket: pulse-music
All 22 MP3 files live in the R2 bucket under the songs/ prefix:
R2 bucket: pulse-music
songs/A darle, mijo.mp3 (4.1 MB)
songs/Camine.mp3 (3.8 MB)
songs/Dreams.mp3 (4.2 MB)
songs/higher-ground.mp3 (5.0 MB)
... (22 files, ~93 MB total)
Public URL pattern:
https://pub-5d1dc33f9faa4d5f8bf8aebe972f62de.r2.dev/songs/{filename}.mp3
songs.json (Playlist Manifest)
Lives at deploy/gograb-pulse/music/songs.json and is deployed with the site. Contains the R2 base URL, song metadata, language tags, and genre tags:
{
"r2BaseUrl": "https://pub-5d1dc33f9faa4d5f8bf8aebe972f62de.r2.dev/songs",
"songs": [
{
"title": "Higher Ground",
"artist": "GoGrab Space",
"file": "higher-ground.mp3",
"lang": "en",
"genre": "inspirational"
}
]
}
The player reads r2BaseUrl from this file and constructs audio URLs as:
{r2BaseUrl}/{encodeURIComponent(file)}
If r2BaseUrl is missing, it falls back to /music/{file} (same-origin Pages).
MongoDB (Backend)
The NestJS Pulse module stores song metadata in two collections:
pulse_songs- title, artist, filename, r2Key, r2Url, duration, genre, lang, fileSize, playCount, isActivepulse_playlists- name, description, song references, isDefault
This is not yet the primary source for the player (the static songs.json is), but provides the API for future dynamic playlist management.
Are We Still Using FTP?
No. The old FTP-based hosting at gograb.space/pulse is fully deprecated. It broke when DNS moved to Cloudflare nameservers. All songs are now served from Cloudflare R2, and the player UI is deployed via Cloudflare Pages.
How the Player Works
The player is a single HTML file (deploy/gograb-pulse/player/index.html) with inline CSS and JavaScript. No build step, no framework.
Key features:
- Media Session API (car Bluetooth, lock screen controls - shows title + artist)
- PWA installable (manifest.json + service worker)
- Shuffle, repeat (off/all/one), playback speed (0.5x-2x)
- Sleep timer (15/30/45/60/90 min, end of track)
- Language filter tabs (All, English, Espanol, Liked, Worship, Inspirational, Latin)
- Like/heart toggle per track (localStorage)
- Search filter
- gSpace tide animation that dances to simulated beat
- Persistent state (track, position, volume, shuffle, repeat, speed)
- Keyboard shortcuts (Space, arrows, M, S, R, F, L, 0-9)
- Swipe gestures on album art (mobile)
Why no Web Audio API visualizer?
createMediaElementSource() on cross-origin audio (R2) causes the browser to mute all output as a CORS security measure. Instead, we use the gSpace tide SVG animation driven by a JS beat simulator that varies speed and scale with a sinusoidal pattern.
How to Deploy the Player
Quick Deploy (just the UI)
# From the gspace1 workspace root:
yarn deploy:pulse
This deploys deploy/gograb-pulse/ (HTML, CSS, JS, JSON, SVGs) to Cloudflare Pages. Takes about 1-2 seconds since MP3s are not included.
Sync Songs + Deploy
# Sync from iCloud and deploy in one command:
yarn sync:pulse:deploy
Manual Steps
# 1. Copy any new MP3s from iCloud to the deploy music folder
cp ~/Library/Mobile\ Documents/com~apple~CloudDocs/Music/Pulse\ Player\ Songs/*.mp3 \
deploy/gograb-pulse/music/
# 2. Upload new MP3s to R2 (only new/changed files)
export CLOUDFLARE_ACCOUNT_ID=$(grep CLOUDFLARE_ACCOUNT_ID .env.local | cut -d= -f2)
export CLOUDFLARE_API_TOKEN=$(grep CLOUDFLARE_API_TOKEN .env.local | cut -d= -f2)
npx wrangler r2 object put "pulse-music/songs/My New Song.mp3" \
--remote --file="deploy/gograb-pulse/music/My New Song.mp3" \
--content-type="audio/mpeg"
# 3. Add the song to songs.json
# Edit deploy/gograb-pulse/music/songs.json and add:
# { "title": "My New Song", "artist": "Artist Name", "file": "My New Song.mp3", "lang": "en", "genre": "inspirational" }
# 4. Deploy the updated site
yarn deploy:pulse
How to Add New Songs
Step 1: Place the MP3
Drop the MP3 file into your Apple iCloud folder:
~/Library/Mobile Documents/com~apple~CloudDocs/Music/Pulse Player Songs/
Or any local folder.
Step 2: Upload to R2
cd ~/developer/repos-gspace/gspace1
export CLOUDFLARE_ACCOUNT_ID=$(grep CLOUDFLARE_ACCOUNT_ID .env.local | cut -d= -f2)
export CLOUDFLARE_API_TOKEN=$(grep CLOUDFLARE_API_TOKEN .env.local | cut -d= -f2)
npx wrangler r2 object put "pulse-music/songs/Song Title.mp3" \
--remote \
--file="/path/to/Song Title.mp3" \
--content-type="audio/mpeg"
Step 3: Update songs.json
Edit deploy/gograb-pulse/music/songs.json and add to the songs array:
{
"title": "Song Title",
"artist": "Artist Name",
"file": "Song Title.mp3",
"lang": "en",
"genre": "worship"
}
Valid lang values: en (English), es (Spanish)
Valid genre values: worship, inspirational, latin
Step 4: Deploy
yarn deploy:pulse
Using the Sync Script (All-in-One)
# Sync everything from iCloud, upload to R2, and deploy:
./tools/scripts/sync-pulse-music.sh --source icloud --r2 --deploy
# Dry run first to see what would happen:
./tools/scripts/sync-pulse-music.sh --source icloud --r2 --deploy --dry-run
Key Files Reference
| File | Purpose |
|---|---|
deploy/gograb-pulse/index.html |
Landing page |
deploy/gograb-pulse/player/index.html |
Player (all features in one file) |
deploy/gograb-pulse/music/songs.json |
Playlist manifest with R2 URL, language, genre |
deploy/gograb-pulse/manifest.json |
PWA manifest |
deploy/gograb-pulse/sw.js |
Service worker (network-first, v4) |
deploy/gograb-pulse/gSpace-symbol-white.svg |
White logo (header, album art) |
deploy/gograb-pulse/GoGrab_Space_symbol.svg |
Full color logo (favicon, PWA icon) |
deploy/gograb-pulse/wrangler.toml |
Cloudflare Pages config |
tools/scripts/sync-pulse-music.sh |
CLI: sync songs from iCloud/local to R2 |
tools/scripts/deploy-pulse.sh |
CLI: deploy site to Cloudflare Pages |
apis/backend-api/src/app/pulse/ |
NestJS backend module (MongoDB) |
.gitignore |
MP3s are gitignored: deploy/gograb-pulse/music/*.mp3 |
Credentials & Config
All credentials live in .env.local (gitignored):
| Variable | Purpose |
|---|---|
CLOUDFLARE_ACCOUNT_ID |
Cloudflare account for R2 + Pages |
CLOUDFLARE_API_TOKEN |
Token with Pages + R2 + DNS permissions |
R2 bucket: pulse-music
R2 public URL: https://pub-5d1dc33f9faa4d5f8bf8aebe972f62de.r2.dev/songs/
Custom domain (pending SSL): r2.pulse.gograb.space
Backend API Endpoints (When Running)
Start: yarn serve:backend-api
| Endpoint | Method | Description |
|---|---|---|
/api/pulse/songs |
GET | List all active songs |
/api/pulse/songs.json |
GET | Backward-compatible playlist format |
/api/pulse/songs |
POST | Register a new song |
/api/pulse/songs/bulk |
POST | Bulk upsert songs |
/api/pulse/songs/:id |
PUT | Update metadata |
/api/pulse/songs/:id |
DELETE | Soft-delete |
/api/pulse/songs/:id/play |
POST | Increment play count |
/api/pulse/playlists |
GET/POST | List/create playlists |
/api/pulse/stats |
GET | Playback statistics |
Troubleshooting
| Problem | Solution |
|---|---|
| Songs don't play | Check browser console. If "outputs zeroes due to CORS", ensure the audio element does NOT have crossorigin="anonymous" and that initAudioContext is NOT called for cross-origin audio |
| Old version cached | The service worker (v4) is network-first. Hard refresh (Cmd+Shift+R) or clear site data in DevTools > Application > Storage |
| New song not appearing | Did you add it to songs.json? Did you redeploy (yarn deploy:pulse)? |
| R2 upload fails | Ensure --remote flag is used with wrangler (npx wrangler r2 object put ... --remote). Without it, files go to local miniflare |
| Deploy is slow (>10s) | MP3s might be in the deploy dir. Check that deploy/gograb-pulse/music/*.mp3 files are gitignored and excluded |