---
title: "RingFileBase (object storage API)"
description: "Self-hosted upload API for Ring clones — file() → RingBaseAdapter → MinIO/RGW, {fileId}_v_* derivatives, push-only geo"
locale: "en"
---
# RingFileBase (object storage API)

> **Info**
> Use **Founder** / **Developer** tabs in the docs sidebar to filter this page. Pair with [Ring CDN](/docs/integrations/ring-cdn.md) for public delivery of uploaded objects. [Ring File Cabinet](/docs/features/file-cabinet.md) is a first-class `file()` consumer (`getCabinetStorageConfig()`, gallery `derivativesProfile`).

**RingFileBase** is Ring’s self-hosted object-storage plane: an Express API (`ring-filebase-api`) that accepts authenticated multipart uploads and writes to **S3-compatible** storage (MinIO or Ceph RGW). Ring apps never talk to MinIO from the browser — they call `file().upload(...)`, which selects `RingBaseAdapter` when the storage provider is `ring_filebase`.

| Concern | RingFileBase (this page) | Ring CDN |
|---------|--------------------------|----------|
| Role | Authenticated **write** API | Public **read** edge |
| Prod URL | In-cluster `http://ring-filebase-api.ring-filebase.svc.cluster.local` | `https://cdn.` |
| Dev / CI URL | Public `https://filebase-api.ring-platform.org` (k3s-or primary only) | Same CDN (multi-A edges; master **push** to slave ingest) |
| Client | Server-side `file()` / Server Actions | Browsers, ``, OG tags |
| Auth | Bearer token (`RINGBASE_API_TOKEN`) | None (GET `/files/...`) |

**Authority:** writes always go to **k3s-or** (Oregon). The API then **pushes** objects to slave MinIO ingest endpoints (Finland / Ukraine). Slaves never pull from master; pull CronJobs are disabled.

## Provider SSOT (shared)

Resolution order in `lib/storage/storage-config.ts` (mirrors `db()`):

1. `NEXT_PUBLIC_STORAGE_PROVIDER` or `STORAGE_PROVIDER`
2. `ring-config.json` → `storage.provider`
3. Default: `local_storage` (development) / `vercel_blob` (production)

| Provider value | Adapter | When to use |
|----------------|---------|-------------|
| `local_storage` | `LocalStorageAdapter` | Local `npm run dev`, PVC-backed uploads |
| `vercel_blob` | `VercelAdapter` | Vercel-hosted clones with `BLOB_READ_WRITE_TOKEN` |
| `ring_filebase` | `RingBaseAdapter` | Self-hosted / Ringdom k8s with RingFileBase API |
| `firebase_storage` | Falls back to local in selector | Not a first-class upload path today |

### For founders

## Why this matters for your clone

Product images, KYC scans, chat attachments, and ImageConductor outputs all need a durable home. **Vercel Blob** is fine for small OSS demos; a marketplace clone that owns its data usually wants **RingFileBase** so media stays on your cluster, behind your CDN hostname, without per-GB Blob invoices.

### Typical scenarios

  
- **[Local development](/docs/deployment/environment.md)** — Keep `storage.provider` / env on `local_storage` — files land under `public/uploads` (or your PVC mount).

  
- **[Production marketplace](/docs/features/store.md)** — Set `ring_filebase`, point `RINGBASE_API_URL` at the in-cluster API, publish URLs via Ring CDN (see Related below).

  
- **[File Cabinet](/docs/features/file-cabinet.md)** — Member uploads call `file()` with cabinet MIME/25MB limits; public gallery items reuse CDN `/files/{uuid}`.

  
- **[Managed Ringdom hosting](/docs/development/oss-vs-enterprise.md)** — Operators deploy `ring-filebase` + `ring-filebase-minio` namespaces; your clone only needs ConfigMap/Secret wiring.

  
- **[Backup pairing](/docs/deployment/backup.md)** — Postgres dumps do not include MinIO objects — schedule object-store backups separately from `pg_dump`.

  `api.` hostnames often route to the Ring app ingress. Uploads must hit **ring-filebase-api** (in-cluster) or **https://filebase-api.ring-platform.org** (public write on primary) — never the Next.js deployment. Wrong URL surfaces as HTML or HTTP 500 instead of JSON success/fileId/url.

### For developers

## Architecture

```mermaid
flowchart LR
  App["Ring app\nfile().upload"] --> Adapter["RingBaseAdapter"]
  Adapter -->|"POST /api/v1/upload\nBearer token"| API["ring-filebase-api"]
  API --> Primary["Primary MinIO"]
  API --> Outbox["Durable outbox"]
  Outbox -->|"PutObject / DeleteObject"| SlaveFI["Slave ingest FI"]
  Outbox -->|"PutObject / DeleteObject"| SlaveUA["Slave ingest UA"]
  CDN["cdn.host/files/..."] --> Primary
  CDN --> SlaveFI
  CDN --> SlaveUA
```

### Verified modules

| Path | Role |
|------|------|
| `lib/storage/storage-config.ts` | Provider SSOT + `getStorageConfig()` |
| `lib/file/FileService.ts` | `file()` / `fileService.upload` |
| `lib/file/FileSelector.ts` | Backend map; constructs `RingBaseAdapter` from env |
| `lib/file/adapters/RingBaseAdapter.ts` | Multipart upload, delete, metadata, `deriveDerivatives` |
| `lib/images/derive-webp.ts` | Optional WebP sibling; `storage.webpDerivative.provider=ringbase` |
| `lib/uploads/server/upload-core.ts` | Unified upload used by `POST /api/uploads` |
| `app/api/uploads/route.ts` | Browser/form upload entry |
| `infrastructure/ring-file-base/services/ring-filebase-api/` | API source (derivatives + propagate) |
| `infrastructure/ring-file-base/k8s/ring-filebase/10-api-deployment.yaml` | Deploy template |
| `infrastructure/k3s-or/ring-filebase/60-propagate-slaves-configmap.yaml` | Slave ingest registry |

### Environment variables

| Variable | Required for `ring_filebase` | Notes |
|----------|------------------------------|-------|
| `NEXT_PUBLIC_STORAGE_PROVIDER` | Yes | Set to `ring_filebase` (aliases: `ringbase`, `filebase`) |
| `RINGBASE_API_URL` | Yes | Host or host + `/api/v1`; adapter normalizes to `…/api/v1` |
| `RINGBASE_API_TOKEN` | Yes | Bearer for API auth middleware |
| `RINGBASE_PUBLIC_URL` | Recommended | Public base used in returned URLs (usually CDN origin) |
| `NEXT_PUBLIC_RINGBASE_API_URL` | Optional | Fallback if `RINGBASE_API_URL` unset |
| `BLOB_READ_WRITE_TOKEN` | No | Only for `vercel_blob` |
| `NEXT_PUBLIC_LOCAL_STORAGE_URL` | No | Only for `local_storage` |

`ring-config.json` example:

{`{
  "storage": {
    "provider": "ring_filebase",
    "maxFileSize": "10MB"
  }
}`}

k8s ConfigMap / Secret pattern (see `k8s/secrets.example.yaml`):

{`# Prod ConfigMap on k3s-or
RINGBASE_API_URL=http://ring-filebase-api.ring-filebase.svc.cluster.local
RINGBASE_PUBLIC_URL=https://cdn.ring-platform.org
NEXT_PUBLIC_STORAGE_PROVIDER=ring_filebase
# RINGBASE_API_TOKEN from Secret — never commit production tokens

# Local next / CI (.env.local)
RINGBASE_API_URL=https://filebase-api.ring-platform.org
RINGBASE_PUBLIC_URL=https://cdn.ring-platform.org
NEXT_PUBLIC_STORAGE_PROVIDER=ring_filebase`}

### Upload type mapping (`resolveRingBaseUploadType`)

The API validates MIME types per `type` field. The adapter maps:

| Content / access | `type` sent to API |
|------------------|--------------------|
| `access: 'private'` | `document` |
| `image/*` | `image` (first-class; `product` is a deprecated alias) |
| `video/*` or `audio/*` | `media` |
| Everything else | `other` |

Optional overrides: `ringbaseType`, `derivativesProfile` (`none` \| `thumb` \| `gallery` \| `product` \| `news`).

### Derivative key scheme (SSOT)

| Pattern | Status |
|---------|--------|
| `{fileId}_v_{variant}` e.g. `_v_thumb.webp`, `_v_blur.webp`, `_v_sync_thumb.jpg` | **Current** — only scheme the API writes |
| `{fileId}_v_video_frame_N.jpg` / `_v_video_frame_N_480.webp` | **Current** — async HEIC/video frames (`_480.webp` under `_v_` is intentional) |
| `{fileId}_thumb`, `{fileId}_thumb_v{N}`, `{fileId}_thumb_v{N}_480w.webp` | **Banned** — never write; purge orphans with `infrastructure/ring-file-base/scripts/purge-legacy-derivative-keys.sh` |

Prefer upload `type=image` for rasters (`product` is a deprecated alias → `image`). Thumbnail status poll: `GET /api/v1/files/{fileId}/thumbnail?type=image`.

**App wiring (2026-07-15):** `upload-core` and `/api/uploads` request ladders when `storage.provider` is `ring_filebase` (purpose→profile: news→`news`, product media→`product`, nft/chat images→`gallery`, avatar/logos→`thumb`, docs→`none`). Responses include `fileId` + `derivatives`. Persist as `MediaImageAsset` on news (`featuredImageAsset` / `gallery`) and `GalleryItem.derivatives` on store/NFT. Display via `pickImageSrc` / `pickGalleryDisplayUrl`. Dead Vikka dual-compat (`vikka-field-normalize` / `NEXT_PUBLIC_NEWS_VIKKA_COMPAT`) was **deleted** — camelCase JSONB only.

Sending images as `media` fails validation (A/V + HEIC only in current API rules).

  Dry-run on primary, then FI/UA slaves: `./scripts/purge-legacy-derivative-keys.sh --endpoint https://… --bucket ring-filebase`. Add `--delete` only after reviewing the sample list. Does not match intentional `_v_video_frame_*_480.webp` or `_v_thumb.webp`.

### Wire-up steps

### Choose provider

Set env and/or `ring-config.json` `storage.provider` to `ring_filebase`. Restart Next.js after env changes.

### Deploy API + MinIO (cluster)

Apply RingFileBase manifests under `infrastructure/ring-file-base/k8s/ring-filebase/` (API Deployment/Service, MinIO namespace, CDN stack). Ensure the API can reach `minio-service.ring-filebase-minio.svc.cluster.local:9000` and the target bucket exists.

### Issue a Bearer token

API auth (`services/ring-filebase-api/src/middleware/auth.ts`) expects a JWT whose **decoded** claims include:

- `iss` = `kubernetes/serviceaccount`
- `kubernetes.io/serviceaccount/namespace`
- `kubernetes.io/serviceaccount/service-account.name`

Today the middleware **decodes** claims and does **not** call TokenReview or enforce `exp`. Prefer a long-lived claim-shaped token in Secrets — do **not** paste a one-hour projected ServiceAccount JWT into `RINGBASE_API_TOKEN` and forget it.

### Smoke test from the app pod

{`curl -sS http://ring-filebase-api.ring-filebase.svc.cluster.local/api/v1/health

curl -sS -X POST http://ring-filebase-api.ring-filebase.svc.cluster.local/api/v1/upload \
  -H "Authorization: Bearer $RINGBASE_API_TOKEN" \
  -F "file=@./sample.png;type=image/png" \
  -F "type=image" \
  -F "derivatives=gallery"`}

Expect JSON with `success: true`, `fileId`, `url`, and optional `derivatives` under your CDN host. Within seconds, slave ingest HEAD for the same key should be 200 (master push).

### App-level upload

{`import { file } from '@/lib/file/FileService'

const result = await file().upload('avatar.png', buffer, {
  contentType: 'image/png',
  access: 'public',
})
if (!result.success) throw new Error(result.error)
// result.url → https://cdn./files/...`}

Browser forms should use `POST /api/uploads` (unified upload core), not call RingFileBase directly.

### Common failures

| Symptom | Likely cause |
|---------|----------------|
| HTML / 500 from upload URL | `RINGBASE_API_URL` points at Next.js (`api.` ingress) |
| `401 Invalid ServiceAccount token` | Token missing legacy SA claim shape |
| `Invalid file type … got: image/png` | `type=media` instead of `image` / `other` |
| Upload OK, CDN 404 on slave briefly | Rare race before push; edges must keep `proxy_cache_valid 404 0` |
| Upload OK, CDN 403 | MinIO bucket lacks anonymous `GetObject` (or CDN Host header misconfigured) |
| Node `stream.isDisturbed` on Blob | Prefer `local_storage` or `ring_filebase`; Vercel adapter uploads `Uint8Array` |
| Propagate stuck `failed` | `POST /api/v1/replication/redrive` on primary (max 3 retries then stop) |

## Related documentation

- [integrations/ring-cdn](/docs/integrations/ring-cdn.md) — Next-step: public /files/... edge, host→bucket map, and geo delivery for uploaded objects.

- [features/file-cabinet](/docs/features/file-cabinet.md) — Same-workflow: member /file-cabinet uploads via file() + getCabinetStorageConfig; gallery uses derivativesProfile gallery; subscriber+ /profile/shared is ACL-only (no own uploads).

- [deployment/environment](/docs/deployment/environment.md) — Depends-on: storage env block (RINGBASE_*) and generative conductor prerequisites.

- [deployment/backup](/docs/deployment/backup.md) — See-also: object store is a separate backup asset from Postgres.

- [integrations](/docs/integrations.md) — See-also: full integration catalog hub.
