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.
| Tier | Scope | Required scope |
|---|---|---|
private (default) | This app only — other apps cannot read it | storage.private.read / storage.private.write |
user | The authenticated user, visible to all apps they use | storage.user.read / storage.user.write |
shared | All apps within the same tenant | storage.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 downloadList & 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 → 0sdk.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:
| Tier | Actual 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.