> ## Documentation Index
> Fetch the complete documentation index at: https://docs.raykoi.com/llms.txt
> Use this file to discover all available pages before exploring further.

# File Uploads

> Files upload directly to storage — your server never proxies the bytes.

## How it works

A file field doesn't submit its bytes as part of the JSON payload. Instead, the SDK:

1. Requests a short-lived signed upload URL for that specific field.
2. Uploads the file **directly to storage**, bypassing your server entirely.
3. Includes the resulting file reference in the submission payload in place of the raw file.

```javascript theme={null}
// This happens automatically inside form.submit() —
// shown here for what's actually going on under the hood.
const form = client.form('YOUR_PUBLIC_ID');

const fileRef = await form.upload('resume', file);
// fileRef: { path, originalFilename, mimeType, sizeBytes }

await form.submit({
  email: 'user@example.com',
  resume: fileRef,
});
```

If you're using a framework adapter (`useRaykoiForm`) or `client.form(id).submit()` directly, you don't write any of this — pass a `File`/`FileList` value under a field's name and the SDK detects it, uploads it, and substitutes the reference automatically, with progress events available via `onProgress`.

## Validation

Two layers, both enforced server-side (client-declared values are never trusted on their own):

* **Per-field**, from the field's own configuration in the form builder — `maxSizeMB`, `allowedTypes` (supports wildcards like `image/*`).
* **Per-workspace**, from your plan — a per-file size ceiling and a total storage quota.

The stricter of the two always applies. After the file lands in storage, its *actual* reported size and MIME type are re-checked against both — a client that lies about `Content-Type` at upload time doesn't get through on a technicality.

<Warning>
  File uploads are not supported on the [No-JS HTML](/integrations/no-javascript) path. The signed-URL flow is inherently a multi-step, JavaScript-orchestrated exchange — a browser navigating a plain `<form>` can only make one request, with no way to fetch a signed URL and PUT to it first.
</Warning>

## Downloading a file

Files aren't publicly accessible by path. Your backend requests a short-lived signed download URL through the [Server Data API](/api-reference/server-data-api) using your secret API key:

```javascript theme={null}
import { createAdminClient } from '@raykoi/sdk/admin';

const admin = createAdminClient({ apiKey: process.env.RAYKOI_SECRET_KEY });
const url = await admin.files.getDownloadUrl('YOUR_PUBLIC_ID', filePath);
```
