Docs

Docs

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.

code
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#

typescript
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#

typescript
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.

typescript
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#

typescript
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#

typescript
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#

typescript
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#

typescript
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.

typescript
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#

Register your service to get an app_id, then exchange credentials for a Bearer token:

bash
curl -X POST https://api.cyberpod.app/api/v1/apps/register \
  -H "Authorization: Bearer $CPOD_ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "my-service", "description": "My service"}'
# → { "app_id": "app_a1b2c3d4" }

See Apps & MCP for the full registration and credential flow.


EDM Metadata Entities#

The primitives above (files, db, kv) handle raw storage operations. Two EDM entities represent the metadata layer on top — queryable through the standard EDM API.

StorageObject#

A StorageObject is the EDM metadata record for every file written via sdk.storage.files. The bytes live in MinIO; the metadata document lives in MongoDB and is queryable by tier, path, content type, size, and checksum.

ID prefix: sobj-

FieldTypeRequiredDescription
idstringautosobj-{uuid}
tenantIdstringautoStamped from JWT
appIdstringautoStamped from OAuth client_id
userIdstringnoSet only for tier=user
tierenumyesprivate | user | shared
pathstringyesRelative path supplied by the SDK consumer
minioKeystringautoFull MinIO object key — resolved server-side, never trusted from client
sizeintegeryesObject size in bytes
contentTypestringyesMIME content type
checksumstringnoSHA-256 hex digest of the object bytes
createdAtstringautoISO 8601 UTC
updatedAtstringautoISO 8601 UTC
deletedAtstring|nullautoISO 8601 UTC when soft-deleted
typescript
import { CpodClient } from '@cpod/sdk'
const sdk = CpodClient.fromEnv()
 
// List StorageObject records for private-tier files
const result = await sdk.storage.objects.list({ tier: 'private', pageSize: 20 })
 
for (const obj of result.data) {
  console.log(obj.id, obj.path, obj.size, obj.contentType)
}
 
// Get a single StorageObject by ID
const obj = await sdk.storage.objects.get('sobj-abc123')
 
// Create a StorageObject metadata record
const created = await sdk.storage.objects.create({
  tier: 'private',
  path: 'reports/q1.pdf',
  size: 204800,
  contentType: 'application/pdf',
  checksum: 'e3b0c44298fc1c149afb...',
})
 
// Soft-delete
await sdk.storage.objects.delete('sobj-abc123')

StorageRecord#

A StorageRecord is the EDM metadata record for every document written via sdk.storage.db. The underlying data is stored in MongoDB, namespaced by tenantId + appId + collection. StorageRecord adds EDM-level queryability: filter by tier, collection, or full-text search across value.

ID prefix: srec-

FieldTypeRequiredDescription
idstringautosrec-{uuid}
tenantIdstringautoStamped from JWT
appIdstringautoStamped from OAuth client_id
userIdstringnoSet only for tier=user
tierenumyesprivate | user | shared
collectionstringyesCollection name as supplied by the SDK consumer
keystringyesDocument key within the collection
valueobjectyesArbitrary JSON document
ttlinteger|nullnoTTL in seconds — document auto-expires via MongoDB TTL index
versionintegerautoOptimistic concurrency counter, incremented on every update
createdAtstringautoISO 8601 UTC
updatedAtstringautoISO 8601 UTC
deletedAtstring|nullautoISO 8601 UTC when soft-deleted
typescript
// List StorageRecord records in a specific collection
const result = await sdk.storage.records.list({
  tier: 'private',
  collection: 'preferences',
  pageSize: 50,
})
 
for (const record of result.data) {
  console.log(record.id, record.key, record.version)
}
 
// Create a StorageRecord with a TTL
const created = await sdk.storage.records.create({
  tier: 'user',
  collection: 'sessions',
  key: 'session:abc123',
  value: { userId: 'usr-001', plan: 'pro' },
  ttl: 3600,
})
 
// Get a single StorageRecord by ID
const record = await sdk.storage.records.get('srec-xyz789')
 
// Soft-delete
await sdk.storage.records.delete('srec-xyz789')