# {{projectName}} / {{appName}}

Voltro backend scaffold (template: **api-backend-storage**).

## Boot

```bash
pnpm install                # at the repo root
pnpm --filter @{{projectName}}/{{appName}} dev
# → http://localhost:4000
# → ws://localhost:4000/ws
```

## What's in here

| File | Role |
|---|---|
| `app.config.ts`                            | App declaration + `storagePlugin` wired (`memory` provider). |
| `database/schema.ts`                       | One example table (`notes`) with the `tenant()` mixin. |
| `queries/` + `mutations/`                  | Streaming subscription + tenant-guarded mutation examples. |
| `actions/uploadAvatar.{rpc,handler}.ts`    | `storage.uploadAvatar` — stores a PUBLIC avatar, returns its URL. |
| `actions/uploadDocument.{rpc,handler}.ts`  | `storage.uploadDocument` — stores a PRIVATE doc (owner + admin + tenant policy). |

## Storage

File storage is wired out of the box via `@voltro/plugin-storage`:

- **Public objects** (`visibility: 'public'`) are served direct from the
  bucket/CDN — set `cdnBaseUrl` on the plugin for production. The app's
  `/_voltro/storage/:id` route is only a fallback for the `memory` /
  `filesystem` providers in dev.
- **Private objects** (default) are gated by an **access policy** — any-of
  rules over `owner` / `roles` / `groups` / `scopes` / `tenant` / `apiKey`
  / `password` / a custom `guard` — PLUS **per-object grants** (share a
  file with a specific user / group / api-key, optionally expiring).
- **Browse + share** stored objects from the dashboard's **Storage** tab
  (`/_voltro/inspect/plugins/storage/*`).

### Delivering a private file

```ts
// In an action/handler — access-checked, returns a presigned URL (s3) or
// an app URL carrying a short-lived signed grant token (fs/memory):
import { StorageService } from '@voltro/plugin-storage'
const storage = yield* StorageService
const url = yield* storage.mintUrl(fileId, ctx.request.subject)
```

Or use the plugin's built-in typed RPC routes from the client:
`storage.upload`, `storage.mintUrl`, `storage.mintUploadUrl` (presigned
PUT), `storage.share`, `storage.revoke`, `storage.listGrants`.

### Switching providers

```ts
storagePlugin({ provider: 's3', bucket: 'my-bucket', region: 'us-east-1',
                cdnBaseUrl: 'https://cdn.example.com' })   // + S3_* env
storagePlugin({ provider: 'minio', bucket: 'my-bucket', endpoint: 'http://localhost:9000' })
storagePlugin({ provider: 'azure', bucket: 'my-container', accountName: 'acct' }) // + AZURE_STORAGE_KEY
storagePlugin({ provider: 'filesystem', root: '.voltro-storage' })
storagePlugin({ provider: 'database' })   // bytes in your DB — zero extra infra
```

Any S3-compatible bucket works via `endpoint` (AWS, Cloudflare R2, GCS,
Backblaze B2, MinIO, Wasabi). `azure` covers Azure Blob; `database` keeps
small blobs in the app's DB. `_voltro_storage_refs` +
`_voltro_storage_grants` (+ `_voltro_storage_blobs` for the `database`
provider) are auto-migrated on `store: 'postgres'`.

### Constraints, virus scanning & image transforms

```ts
import { storagePlugin, clamavScanner } from '@voltro/plugin-storage'

storagePlugin({
  provider: 'memory',
  limits: { maxBytes: 10_000_000, allowedContentTypes: ['image/*', 'application/pdf'], sniff: true },
  scan: clamavScanner({ host: '127.0.0.1', port: 3310 }),  // optional — needs a clamd
})
```

- **Constraints** reject oversized / wrong-type uploads (`sniff` magic-byte-checks
  the bytes) with a typed `StorageRejected`.
- **Virus scan** runs on every `put`; a detection fails with `StorageScanRejected`.
- **Image transforms** are on-the-fly via the serve URL — `?w=400&format=webp&q=80`
  (needs the optional `sharp` dependency).
