Storage

sdk.storage gives every app three isolated storage primitives — blobs, structured documents, and ephemeral key-value — without holding any database credentials. All operations flow through cpod-backend, which enforces isolation from the JWT claims and routes to the correct backing store.

Your App  →  sdk.storage.*  →  REST  →  cpod-backend

                                    extracts from JWT:
                                      tenantId  (hard partition)
                                      appId     (stamped from client_id)
                                      userId    (for user-tier ops)

                              ┌───────────────┼───────────────┐
                              ▼               ▼               ▼
                            MinIO          MongoDB          Redis
                          (files)           (db)             (kv)

No DB credentials ever reach the SDK. The tenantId, appId, and userId that determine where data lives are extracted server-side from the Bearer token — the client cannot spoof them.


Storage Tiers

Every operation targets one of three tiers. The tier determines who can read the data — not which physical store it uses.

TierScopeRequired scope
private (default)This app only — other apps cannot read itstorage.private.read / storage.private.write
userThe authenticated user, visible to all apps they usestorage.user.read / storage.user.write
sharedAll apps within the same tenantstorage.shared.read / storage.shared.write

Declare the scopes your app needs at registration time. Tokens cannot grant scopes not declared.


sdk.storage.files — Blob Storage

Backed by MinIO/S3. Use this for PDFs, images, exports, raw data files — anything binary or large.

Write

import { CpodClient } from '@cpod/sdk'
const sdk = CpodClient.fromEnv()
 
const data = Buffer.from('{"hello":"world"}')
 
// Private (default) — only this app can read it
const result = await sdk.storage.files.write('config/settings.json', data, {
  contentType: 'application/json',
})
// result.key  → "config/settings.json"
// result.tier → "private"
 
// User-scoped — any app this user uses can read it
await sdk.storage.files.write('avatar.png', imageBuffer, {
  contentType: 'image/png',
  tier: 'user',
})
 
// Tenant shared — all apps in this tenant can read it
await sdk.storage.files.write('brand/logo.svg', svgBuffer, {
  contentType: 'image/svg+xml',
  tier: 'shared',
})

Read

const stream = await sdk.storage.files.read('config/settings.json')
const text = await new Response(stream).text()

Presigned URL

Generate a time-limited public URL for downloads — share with end users or pass to a browser without exposing credentials.

const url = await sdk.storage.files.url('exports/report.pdf', {
  expiresIn: 3600,  // seconds, default 3600
  tier: 'private',
})
// Send url to the user's browser — no auth header needed to download

List & Delete

const listing = await sdk.storage.files.list('exports/', { limit: 50 })
for (const entry of listing.items) {
  console.log(entry.key, entry.size, entry.lastModified)
}
 
await sdk.storage.files.delete('exports/old-report.pdf')

sdk.storage.db — Structured Document Storage

Backed by MongoDB. Use this for structured app state, user preferences, cached records, per-user configuration — anything that needs filtering or batch operations.

You work with named collections and string keys. No MongoDB query language is exposed — the interface is intentionally constrained so the backing store can be swapped.

Set & Get

const prefs = sdk.storage.db.collection('preferences')
 
// Write
await prefs.set('theme', { mode: 'dark', accent: 'purple', fontSize: 14 })
 
// Read
const theme = await prefs.get('theme')
// → { mode: 'dark', accent: 'purple', fontSize: 14 }
 
// Not found → null
const missing = await prefs.get('does-not-exist')
// → null
 
// Per-user preferences (user tier)
const userPrefs = sdk.storage.db.collection('preferences', { tier: 'user' })
await userPrefs.set('layout', { sidebar: 'collapsed' })

Query

const sessions = sdk.storage.db.collection('sessions')
 
const active = await sessions.query({
  filter: { status: 'active', plan: 'pro' },
  sort: { createdAt: -1 },
  limit: 20,
})
 
for (const item of active.items) {
  console.log(item.key, item.value)
}
 
// Paginate
if (active.nextCursor) {
  const page2 = await sessions.query({
    filter: { status: 'active' },
    cursor: active.nextCursor,
    limit: 20,
  })
}

Batch

const cache = sdk.storage.db.collection('cache')
 
const result = await cache.batch([
  { op: 'set', key: 'user:123', value: { name: 'Alice', plan: 'pro' } },
  { op: 'set', key: 'user:456', value: { name: 'Bob', plan: 'free' } },
  { op: 'delete', key: 'user:999' },
])
// result.succeeded → 3
// result.failed    → 0

sdk.storage.kv — Ephemeral Key-Value

Backed by Redis. Use this for sessions, locks, rate limit counters, temporary tokens — anything with a TTL that doesn’t need querying.

const kv = sdk.storage.kv
 
// Set with TTL (seconds)
await kv.set('session:abc123', { userId: 'usr-xxx', plan: 'pro' }, { ttl: 3600 })
 
// Get
const session = await kv.get('session:abc123')
// → '{"userId":"usr-xxx","plan":"pro"}'  (always a string — parse if needed)
 
// Exists check
const alive = await kv.exists('session:abc123')  // → true
 
// Atomic counter
await kv.set('counter:api-calls', '0')
const count = await kv.incr('counter:api-calls')      // → 1
const count2 = await kv.incr('counter:api-calls', 5)  // → 6
 
// TTL check
const remaining = await kv.ttl('session:abc123')  // → seconds left
 
// Delete
await kv.del('session:abc123')

Path Isolation Contract

cpod-backend resolves the actual storage path from the JWT — never from the client. The SDK consumer always writes short relative paths like config/settings.json. The backend maps this to:

TierActual MinIO / MongoDB path
private/{tenantId}/apps/{appId}/{path}
user/{tenantId}/users/{userId}/apps/{appId}/{path}
shared/{tenantId}/shared/{path}

Two different apps writing config/settings.json at tier private will never collide — the appId segment keeps them separated. An app can only read its own private data; it cannot request another app’s appId.


Register Your App With Storage Scopes

Declare storage scopes when registering your OAuth app:

curl -X POST https://api.yourdomain.com/v1/oauth/apps \
  -H "Authorization: Bearer $CPOD_ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "app-myservice",
    "name": "My Service",
    "declared_scopes": [
      "storage.private.read",
      "storage.private.write",
      "storage.user.read",
      "storage.user.write"
    ],
    "app_type": "service"
  }'
⚠️

Tokens cannot grant scopes not declared at registration time. If your app tries to write to shared without storage.shared.write declared, the request returns 403 invalid_scope.