FILES FOR THE APP YOU ARE BUILDING

Make files a feature your app already has.

Store the original. Keep your customers’ existing IDs. Get a useful preview. Add your own data and UI. Folio supplies managed storage and the file contract, so you can build the actual product.

One package. Your choice of experience.

  • /server for your API, jobs and authorized agent tools.
  • /client for React providers and hooks in your app.
  • Optional /ui for a working uploader, library and previews—or copy the shadcn components and make them yours.

The selected Solid v1 API has passed production and fresh-app verification.

Install and connect the SDK

Give every customer a file library.

Create one Folio workspace for your application. Keep its secret on your server. Authorize the signed-in user, select their existing team/customer UUID and upload into that tenant. Your app owns membership and file permissions; a tenant header is a storage selector.

Your server · keep the IDs you already use
import { createFolio } from '@ionite/folio-storage/server';

const folio = createFolio({ apiKey: process.env.FOLIO_STORAGE_KEY! });
const tenant = folio.forTenant(authorizedCustomerId);
const file = await tenant.files.upload({
  file: invoiceBlob, filename: 'invoice.pdf', purpose: 'attachment',
  metadata: { orderId: 'your-existing-order-id' },
}, { idempotencyKey: 'order-123-invoice-upload' });
// Save file.id in your app. The original is stored; its PDF preview may process.

For users, connect the server adapter and FolioProvider once. Set allowList only for users who may browse the entire tenant; supply your authorized listFiles index for record-level visibility. Change the provider’s contextKey when user, workspace or permission state changes.

An avatar, a card image, a useful thumbnail.

Upload a raster image once. Folio generates a thumbnail and optimized image automatically. Request another named recipe when a card or avatar needs its own dimensions; keep the original file ID and rendition name in your app.

Choose an image output
const avatar = await tenant.files.renditions.request(imageId, {
  name: 'avatar',
  recipe: { width: 256, height: 256, fit: 'cover', format: 'webp' },
});
const operation = await tenant.files.renditions.get(imageId, avatar.name);
if (operation.status === 'ready') {
  const response = await tenant.files.renditions.download(imageId, 'avatar');
  // Serve response.body through your authorized app route.
}

Use inside to fit, cover to fill and explicit WebP/JPEG/PNG output. Dimensions are bounded to 4096 with no upscaling. Same name and recipe reuse the operation; a changed recipe needs a new name. A failure leaves the original available and offers an explicit retry.

Choose delivery deliberately: authenticated bytes through your existing app domain, or a five-minute link for built-in original/thumbnail/preview/optimized variants. Named outputs use authenticated bytes in this release. A temporary link is a bearer capability, not a permanently public asset.

Let your AI understand a file. Keep the result beside it.

Your application downloads the authorized original and calls its own AI provider. Save extracted fields, alt text or a summary as opaque Folio metadata, alongside your model, schema version and review state. Folio receives the result; it does not receive your AI key or run a hosted AI job.

App-side integration pattern · your model function belongs to your app
import { extractWithYourModel } from '@/lib/your-ai';

const response = await tenant.files.download(fileId);
const result = await extractWithYourModel({
  bytes: await response.arrayBuffer(), apiKey: process.env.AI_API_KEY!,
});
const current = await tenant.files.get(fileId);
if (current.version === undefined) throw new Error('Upgrade before editing.');
await tenant.files.updateMetadata(fileId, {
  expectedVersion: current.version,
  metadata: { ai: {
    result, provider: 'your-provider', model: 'your-model',
    schemaVersion: 'invoice-v1', extractedAt: new Date().toISOString(),
    reviewed: false,
  } },
});

The candidate also includes a runnable examples/byo-ai.mjs. Its optional HTTP adapter calls an app-owned extraction endpoint that accepts base64 file input and returns { metadata: { ... } }; provider-specific code stays behind that endpoint. Configure FOLIO_STORAGE_KEY, AI_API_KEY and AI_EXTRACTION_URL on your server, then run:

Included example · downloaded package
node node_modules/@ionite/folio-storage/examples/byo-ai.mjs customer_123 fil_0123456789abcdef01234567

The example bounds AI input to 4 MiB; Folio’s storage upload cap remains 25 MiB. Final metadata must fit 16 KiB. Store large generated reports as a separate file and keep their ID in metadata. A version conflict requires reconciling the latest file; do not silently overwrite another writer’s data.

Group files with a tag. Search the whole tenant.

Tag invoices, project attachments or review queues. A collection is simply a tag shared by those files. There is no second folder/collection model to maintain.

Indexed tags and flexible metadata are separate
const current = await tenant.files.get(fileId);
await tenant.files.setTags(fileId, {
  expectedVersion: current.version!, tags: ['invoice', 'needs-review'],
});
const page = await tenant.files.list({ tag: 'invoice', limit: 25 });
const counts = await tenant.tags.list({ limit: 25 });
// In React: useFolioFiles({ tag: 'invoice' }) or <FolioLibrary tag="invoice" />.

Indexed tags support 32 entries, trimming and Unicode NFC, with a 64-byte UTF-8 limit and case-sensitive equality. They are indexed per workspace and tenant. A tags array inside metadata stays opaque and does not affect search. Use the returned cursor unchanged with the same tag to continue across the whole library.

The ready-made library can expose metadata and tag editing with editableMetadata and editableTags. Both are off by default. Your server must authorize the corresponding writes even when an editor is hidden.

Save text, attachments and voice notes.

A pasted note can be a file. So can an audio recording, PDF or application export. Folio stores arbitrary nonempty originals up to 25 MiB; unsupported preview formats remain downloadable originals.

A text note is still a file
const note = await tenant.files.upload({
  file: new Blob([pastedText], { type: 'text/plain' }),
  filename: 'note.txt', purpose: 'note', metadata: { source: 'pasted-text' },
}, { idempotencyKey: 'note-upload-123' });

Byte detection determines delivery type; a declared MIME type cannot make active content render inline. Voice transcription is not automatic. Your own transcription job can later save a bounded transcript/result in metadata, or a separate text file for a long transcript.

Give your own agent tools the same file data.

Metadata and tags are serializable JSON through the same server SDK. Your MCP server can expose them after checking the customer identity and file permission. Hosted customer-scoped Folio MCP is a backlog item; the inherited console shell’s MCP capability is not the public storage API.

Inside your MCP tool handler · authorization is your app’s policy
async function readFileMetadata(fileId, identity) {
  const access = await authorizeFileRead(identity, fileId);
  const file = await folio.forTenant(access.tenantId).files.get(fileId);
  return { fileId: file.id, metadata: file.metadata,
    tags: file.tags ?? [], version: file.version };
}
// For writes, authorize the write action and call updateMetadata/setTags
// with the customer's expectedVersion. Do not accept an arbitrary tenant ID.

The next experiences to choose together.

These are documented backlog use cases, not configuration available in this candidate:

  • Live changes: an opt-in tenant-authorized SSE/socket feed with reconnects and an off switch. Current hooks poll processing within a bounded budget and invalidate their own edits.
  • Many uploads: a bounded queue with a proposed default of five concurrent transfers and an app-controlled limit; every file keeps its own outcome.
  • Large uploads: resumable transfers, so interrupted uploads continue without duplicate quota or originals.
  • A job panel: separate upload progress from each file’s rendition processing, with per-file retry after partial failures.
  • Rotating QA: bounded Orca agents test different integration angles with disposable fixtures and reproducible evidence.

Managed R2 storage stays the product boundary. Customer buckets, hosted extraction/transcription, custom domains, pricing and npm publication are deferred. Pick the next use case by its tracking ID.

Choose the next tracked feature
Get an AI summary of Folio