API · Backend + Storage
The api-backend base plus file storage wired — @voltro/plugin-storage with public (CDN-direct) and private (policy + grants) objects, with upload examples.
The same minimal notes base as api-backend, plus file storage wired out of the box via @voltro/plugin-storage. It ships two upload actions demonstrating the public and private object classes. Template id: api-backend-storage.
Scaffold
voltro create-project acme --api=api-backend-storageWhat ships
apps/acme/api/ # dir named by the app, not the template
├── app.config.ts # api + storagePlugin({ provider: 'memory' })
├── package.json
├── tsconfig.json
├── README.md
├── database/
│ └── schema.ts # actors + tenants + notes (same as api-backend)
├── queries/
│ └── notes.query.ts + .query.server.ts
├── mutations/
│ └── notes.create.mutation.ts + .mutation.server.ts
└── actions/
├── uploadAvatar.action.ts + .action.server.ts # storage.uploadAvatar — PUBLIC
└── uploadDocument.action.ts + .action.server.ts # storage.uploadDocument — PRIVATEThe wiring
app.config.ts adds the storage plugin with the zero-config memory provider:
import { storagePlugin } from '@voltro/plugin-storage'
export default {
type: 'api' as const,
name: '{{projectNamePascal}}{{appNamePascal}}',
store: 'memory' as const,
plugins: [
storagePlugin({ provider: 'memory' }),
],
}The memory provider keeps blobs in-process (dev only). Swap to 's3' / 'minio' (any S3-compatible bucket via endpoint — AWS, R2, GCS, B2, Wasabi), 'azure', 'filesystem', or 'database' for real object storage. Set cdnBaseUrl on the plugin for production public delivery.
Two object classes
- PUBLIC (
visibility: 'public') — served direct from the bucket/CDN; the returnedurlis what you put in an<img src>. The app's serve route is only a dev fallback. - PRIVATE (default) — gated by an access policy (any-of rules over
owner/roles/groups/scopes/tenant/apiKey/password/ a customguard) PLUS per-object grants (share a file with a specific user / group / api-key, optionally expiring).
actions/uploadAvatar.action.server.ts — a PUBLIC object
import { Effect } from 'effect'
import type { AppContext } from '@voltro/runtime'
import { StorageService } from '@voltro/plugin-storage'
const execute = (input: { bytesBase64: string; contentType: string }, ctx: AppContext) =>
Effect.gen(function* () {
const storage = yield* StorageService
const bytes = new Uint8Array(Buffer.from(input.bytesBase64, 'base64'))
const ref = yield* storage.put({
bytes, contentType: input.contentType,
visibility: 'public',
ownerId: ctx.request.subject.id, tenantId: ctx.request.subject.tenantId,
key: `avatars/${ctx.request.subject.id ?? 'anon'}/avatar`,
})
const url = yield* storage.getUrl(ref.id)
return { id: ref.id, url }
})
export default executeThe shipped uploadDocument action mirrors this with visibility: 'private' so the file is access-checked on read. File upload is external I/O → an action, not a mutation. For large files prefer a presigned direct-to-bucket upload via the plugin's built-in storage.mintUploadUrl route.
Built-in routes + serve endpoint
The plugin ships typed client routes — storage.upload, storage.mintUrl, storage.mintUploadUrl, storage.share, storage.revoke, storage.listGrants — plus the serve endpoint GET /_voltro/storage/:id (public → 302 to CDN; private → access-checked). Browse and share stored objects from the dashboard's Storage tab. _voltro_storage_refs + _voltro_storage_grants are auto-migrated on a SQL store.
Pairs well with
- Any web template — wire the upload actions behind a file picker.
See also
@voltro/plugin-storage— providers, access policies, grants, upload constraints, virus scanning, image transforms.api-backend— the base this builds on.