A blazing-fast, real-time synchronized bookmark manager built on Google Apps Script
Features β’ Architecture β’ Installation β’ API Reference β’ Performance
SROBY is an ultra-fast, multi-user bookmark manager designed for teams and power users who need to organize thousands of bookmarks with real-time synchronization across devices. Built entirely on Google Apps Script, it requires zero server infrastructure while supporting 25+ concurrent users with instant sync.
- π Optimized for scale: Handles 2,000+ bookmarks without breaking a sweat
- π₯ Multi-user ready: 25 concurrent users with zero conflicts
- β‘ Real-time sync: Changes propagate to all users within 1-2 seconds
- π Zero cost: Runs entirely on Google's free infrastructure
- π Secure: Google account authentication built-in
| Feature | Description |
|---|---|
| Categories | Organize bookmarks into collapsible categories |
| Tags | Add multiple tags per bookmark for flexible organization |
| Search | Multi-word search across names, URLs, and tags |
| Info/Notes | Add descriptions and notes to any bookmark |
| Favicons | Automatic favicon fetching for visual identification |
| Feature | Description |
|---|---|
| Drag & Drop | Reorder bookmarks within categories via drag and drop |
| Cross-Category Move | Drag bookmarks between different categories |
| Open All | One-click to open all bookmarks in a category |
| Inline Edit | Edit bookmarks without leaving the main view |
| Smart URL | Auto-adds https:// and .com when needed |
| Feature | Description |
|---|---|
| Instant Sync | Changes sync across all users within 1-2 seconds |
| User Attribution | See who made the last change |
| Conflict Prevention | LockService prevents simultaneous write conflicts |
| Active Users | See how many users are currently active |
| Feature | Description |
|---|---|
| Optimistic UI | Instant visual feedback before server confirmation |
| Auto-Rollback | Automatic restoration on save failure |
| Offline Detection | Graceful degradation with cached data |
| Connection Recovery | Auto-reconnects and syncs when connection returns |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β FRONTEND (index.html) β
β βββββββββββββββ βββββββββββββββ βββββββββββββββββββββββββββ β
β β Dark UI β β Search β β Drag & Drop Manager β β
β β (CSS Vars) β β Engine β β (Reorder/Move) β β
β βββββββββββββββ βββββββββββββββ βββββββββββββββββββββββββββ β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β Request Queue (Priority-based) ββ
β β β’ Deduplication β’ Retry Logic β’ Throttling ββ
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β Event Bus (Pub/Sub Pattern) ββ
β β β’ Persistence β’ Cross-component Communication ββ
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
google.script.run
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β BACKEND (Code.gs) β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β CACHING LAYERS ββ
β β βββββββββββββ βββββββββββββ βββββββββββββββββββββ ββ
β β β L1: Redis β => β L2: Scriptβ => β L3: Properties β ββ
β β β -like β β Cache β β Service β ββ
β β β (6h TTL) β β (6h TTL) β β (Persistent) β ββ
β β βββββββββββββ βββββββββββββ βββββββββββββββββββββ ββ
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββββββββββ β
β β LockService β β Chunking β β Change Notifications β β
β β (Conflicts) β β (>9KB data) β β (Real-time sync) β β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Manages all backend communication with intelligent prioritization:
Priority Levels:
βββ 10 (Critical) β Save operations, deletions
βββ 8 (High) β User-initiated updates
βββ 5 (Normal) β Standard CRUD operations
βββ 1 (Low) β Background sync, pollingFeatures:
- Deduplication: Prevents duplicate requests
- Retry Logic: Auto-retries with exponential backoff
- Throttling: Max 3 concurrent requests
- Queue Limit: 20 items max, drops lowest priority when full
Pub/sub system for component communication:
Events:
βββ dataLoaded β Initial data fetch complete
βββ bookmarkAdded β New bookmark created
βββ bookmarkUpdated β Bookmark modified
βββ bookmarkDeleted β Bookmark removed
βββ categoryAdded β New category created
βββ syncComplete β Real-time sync finished
βββ connectionError β Backend communication failedReal-time visual feedback system:
| Status | Color | Meaning |
|---|---|---|
| Ready | π’ Green | Connected, all synced |
| Syncing | π΅ Blue | Fetching/sending data |
| Saving | π£ Purple | Write operation in progress |
| Warning | π‘ Yellow | Slow connection detected |
| Error | π΄ Red | Operation failed |
| Offline | β« Gray | No connection |
Request Flow:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
L1 Cache Hit?
β
βββββββ΄ββββββ
βYes βNo
βΌ βΌ
Return L2 Cache Hit?
β β
β βββββββ΄ββββββ
β βYes βNo
β βΌ βΌ
β Return Read from
β β Properties
β β β
β β Populate L1+L2
β β β
βββββββ΄ββββββββββββ
Cache Configuration:
CACHE_DURATION: 6 hours (21,600 seconds) - Google's maximumSHORT_CACHE: 5 minutes (300 seconds) - for frequently changing dataMAX_CHUNK_SIZE: 8,000 bytes - safe limit under 9KB quota
Handles large datasets that exceed Google's 9KB property limit:
// Automatic chunking for large datasets
if (jsonData.length > 8000) {
// Split into multiple chunks
for (i = 0; i < jsonData.length; i += 8000) {
chunks.push(jsonData.substring(i, i + 8000));
}
}
// Store: SRO_CHUNK_0, SRO_CHUNK_1, SRO_CHUNK_2...Prevents concurrent write conflicts:
var lock = LockService.getScriptLock();
lock.tryLock(3000); // 3 second timeout
// If lock fails β throw "Busy" error
// Frontend auto-retries with exponential backoffEnables real-time cross-user synchronization:
// On every write operation:
updateChangeNotification(userId, action);
// Stored as:
{
timestamp: 1706961234567,
userId: "john.doe",
action: "addBookmark"
}
// Frontend polls every 1 second:
if (notification.timestamp > lastKnownChangeTime) {
// Trigger full sync
loadBookmarks();
}{
"categories": [
{
"id": "cat1706961234567_a1b2c3d4e",
"name": "Development Tools",
"bookmarks": [
{
"id": "b1706961234567_x9y8z7w6v",
"title": "GitHub",
"url": "https://github.com",
"tags": ["code", "git", "repos"],
"info": "Main code repository"
},
{
"id": "b1706961234568_m5n4o3p2q",
"title": "Stack Overflow",
"url": "https://stackoverflow.com",
"tags": ["help", "community"],
"info": ""
}
]
}
],
"lastModified": 1706961234567
}Collision-free IDs using timestamp + random suffix:
// Category ID
"cat" + Date.now() + "_" + Math.random().toString(36).substr(2, 9)
// Example: cat1706961234567_a1b2c3d4e
// Bookmark ID
"b" + Date.now() + "_" + Math.random().toString(36).substr(2, 9)
// Example: b1706961234567_x9y8z7w6v| Key | Purpose |
|---|---|
SRO_META |
Metadata (chunk count, lastModified, size) |
SRO_CHUNK_0..N |
Data chunks (for large datasets) |
SRO_CHANGE_NOTIFICATION |
Cross-user sync flag |
- Google Account
- Access to Google Apps Script
1. Go to https://script.google.com
2. Click "New Project"
3. Name it "SROBY"
1. In Code.gs (default file), paste contents of Code.gs
2. Save (Ctrl+S)
1. Click + β HTML
2. Name it "index" (creates index.html)
3. Paste contents of index.html
4. Save (Ctrl+S)
1. Click "Deploy" β "New deployment"
2. Select type: "Web app"
3. Configure:
- Description: "SROBY Bookmark Manager"
- Execute as: "Me"
- Who has access: "Anyone with Google Account"
(or "Anyone" for public access)
4. Click "Deploy"
5. Copy the Web App URL
1. Open the Web App URL
2. Click "Review Permissions"
3. Select your Google Account
4. Click "Advanced" β "Go to SROBY (unsafe)"
5. Click "Allow"
When you make changes:
1. Click "Deploy" β "Manage deployments"
2. Click βοΈ (edit) on your deployment
3. Version: "New version"
4. Click "Deploy"
| Function | Parameters | Returns | Description |
|---|---|---|---|
getBookmarks() |
- | {categories, lastModified} |
Fetch all data |
saveBookmarks(data) |
{categories, lastModified} |
{success, lastModified} |
Save all data |
getLastModified() |
- | number |
Get last update timestamp |
| Function | Parameters | Returns | Description |
|---|---|---|---|
addBookmark(categoryId, bookmark) |
string, {title, url, tags?, info?} |
{success, lastModified} |
Add new bookmark |
updateBookmark(categoryId, bookmarkId, updates) |
string, string, {title?, url?, tags?, info?, newCategoryId?} |
{success, lastModified} |
Update bookmark |
deleteBookmark(categoryId, bookmarkId) |
string, string |
{success, lastModified} |
Delete bookmark |
reorderBookmarks(categoryId, orderedIds) |
string, string[] |
{success, lastModified} |
Reorder bookmarks |
| Function | Parameters | Returns | Description |
|---|---|---|---|
addCategory(name) |
string |
{success, lastModified} |
Create category |
updateCategory(categoryId, newName) |
string, string |
{success, lastModified} |
Rename category |
deleteCategory(categoryId) |
string |
{success, lastModified} |
Delete category with all bookmarks |
getCategoryIndex() |
- | {categories: [{id, name, count}], lastModified} |
Lightweight category list |
| Function | Parameters | Returns | Description |
|---|---|---|---|
getChangeNotification() |
- | {timestamp, userId, action} |
Get last change info |
getUserEmail() |
- | string |
Get current user's email |
getDataSize() |
- | {size, chunks, bookmarks, categories} |
Storage statistics |
getCacheStats() |
- | {redis, legacy, storage} |
Cache hit/miss stats |
clearCache() |
- | {success, message} |
Invalidate all caches |
| Metric | Value |
|---|---|
| Initial load (cold cache) | ~800-1200ms |
| Initial load (warm cache) | ~150-300ms |
| Add bookmark | ~400-600ms |
| Delete bookmark | ~300-500ms |
| Sync detection latency | ~1000-2000ms |
| Max bookmarks tested | 2,000+ |
| Concurrent users tested | 25 |
- Three-tier caching - L1 (Redis-like) β L2 (Script Cache) β L3 (Properties)
- Cache warming - Pre-populate cache after writes
- Optimistic UI - Instant visual feedback before server confirmation
- Request deduplication - Prevent redundant backend calls
- Priority queue - Critical operations execute first
- Chunked storage - Handle datasets > 9KB limit
- Lightweight polling - 1-second notification checks (not full data)
- Background sync - 60-second idle refresh cycle
| Resource | Limit | SROBY Usage |
|---|---|---|
| Script Properties | 500KB total | β Chunking handles large data |
| Single Property | 9KB | β 8KB chunks (safe margin) |
| Cache | 100KB per key | β Well under limit |
| Cache TTL | 6 hours max | β Using maximum |
| Execution time | 6 minutes | β Operations < 1 second |
| Daily triggers | 90 min/day | β No time-based triggers |
Color Palette (Dark Theme):
--bg: #0a0e17 /* Main background */
--bg-secondary: #111827 /* Cards, header */
--bg-hover: #1f2937 /* Hover states */
--text: #f3f4f6 /* Primary text */
--text-dim: #9ca3af /* Secondary text */
--accent: #3b82f6 /* Primary accent (blue) */
--accent-hover: #60a5fa /* Accent hover */
--border: #374151 /* Borders */
--success: #10b981 /* Success states */
--error: #ef4444 /* Error states */| Key | Action |
|---|---|
/ |
Focus search box |
Escape |
Close modals, clear search |
@media (max-width: 768px) β Tablet layout
@media (max-width: 480px) β Mobile layout| Issue | Solution |
|---|---|
| "Busy" error | Another user is saving. Auto-retries in 2-5 seconds. |
| Changes not syncing | Check connection. Try manual refresh (F5). |
| Slow performance | Clear cache via clearCache() in Script Editor. |
| "Category not found" | Data may be stale. Refresh the page. |
| Favicon not loading | Some sites block favicon access. Normal behavior. |
Open browser console (F12) to see detailed logs:
π REAL-TIME SYNC: Change from "john" (action: addBookmark)
β±οΈ Latency: 847ms | Active users detected
π Sync notification received
MIT License - feel free to use, modify, and distribute.
Contributions welcome! Please:
- Fork the repository
- Create a feature branch
- Submit a pull request
Built with β€οΈ using Google Apps Script