File Uploads
Grove provides a built-in file upload system with presigned URLs, content-type validation, size limits, and transparent encryption for PII-classified fields. Files are stored in S3 (production) or the local filesystem (development).
Prerequisites: API Routes, Field Classifications. What you'll learn: How to declare File fields, upload and download files via the web layer, handle PII-encrypted files, and understand the storage backend.
The File Type
Grove has a first-class File type for record fields. Declare it with optional content_types and max_size constraints:
record {
kind "invc"
title: String
invoice: File {
content_types: ["application/pdf", "image/*"]
max_size: 25mb
}
medical_scan: File {
content_types: ["image/dicom"]
max_size: 500mb
} @pii
}
| Constraint | Description |
|---|---|
content_types | Array of allowed MIME types. Supports wildcards (image/*). |
max_size | Maximum file size. Accepts units like kb, mb, gb. |
@pii | Marks the field as PII -- uploads and downloads are encrypted with a per-subject data encryption key. |
When a client attempts to upload a file that violates these constraints, the server rejects the request before any bytes reach storage.
Upload Flow (Presigned URL)
The standard upload is a two-step process: obtain a presigned PUT URL, then upload directly to S3.
Step 1 -- Get a Presigned URL
POST /_file/presign-put
Content-Type: application/json
{
"module": "invoices",
"record_id": "inv_abc123",
"field_name": "invoice",
"filename": "receipt.pdf",
"content_type": "application/pdf"
}
Response:
{
"url": "https://s3.amazonaws.com/bucket/...",
"key": "proj_1/invoices/a3/inv_abc123/invoice/b7e2...a3.pdf",
"encrypted": false,
"expires_at": "2026-04-08T12:05:00Z"
}
The url is a time-limited presigned PUT URL. The key is the object key that identifies this file in storage. The encrypted flag indicates whether the file requires server-side encryption (always false for non-PII fields).
Step 2 -- Upload Directly to S3
PUT <presigned url>
Content-Type: application/pdf
<file bytes>
The client uploads directly to S3 using the presigned URL. No file data passes through the Grove server.
Multipart Upload
For cases where a direct S3 upload is not practical (e.g., server-side processing, or you need a BLAKE3 hash), use the multipart form upload endpoint. The file passes through the Grove server, which computes a BLAKE3 integrity hash.
POST /_file/upload
Content-Type: multipart/form-data
module=invoices
record_id=inv_abc123
field_name=invoice
file=@receipt.pdf
Response:
{
"key": "proj_1/invoices/a3/inv_abc123/invoice/b7e2...a3.pdf",
"hash": "b7e2...a3",
"size": 204800,
"content_type": "application/pdf"
}
The hash field is a BLAKE3 digest that can be used for deduplication or integrity verification.
Download Flow
To download a file, obtain a presigned GET URL and redirect or fetch from it.
POST /_file/presign-get
Content-Type: application/json
{
"key": "proj_1/invoices/a3/inv_abc123/invoice/b7e2...a3.pdf"
}
Response:
{
"url": "https://s3.amazonaws.com/bucket/...",
"encrypted": false
}
Redirect the user's browser to the url, or fetch it server-side. The presigned GET URL is time-limited.
PII-Classified Files
File fields annotated with @pii are encrypted with a per-subject data encryption key (DEK). Because the server must encrypt and decrypt the file contents, presigned URLs cannot be used. Instead, uploads and downloads go through dedicated server-side endpoints.
Encrypted Upload
PUT /_file/enc-upload/proj_1/scans/c4/rec_xyz/medical_scan/d91f...c4.dcm
<file bytes>
The server encrypts the file with the subject's DEK before writing it to storage.
Encrypted Download
GET /_file/enc-download/proj_1/scans/c4/rec_xyz/medical_scan/d91f...c4.dcm
The server retrieves the encrypted file from storage, decrypts it with the subject's DEK, and streams the plaintext back to the client.
How to Tell Which Flow to Use
The encrypted flag in the presign responses tells you:
encrypted | Upload endpoint | Download endpoint |
|---|---|---|
false | Presigned PUT URL (direct to S3) | Presigned GET URL (direct from S3) |
true | PUT /_file/enc-upload/{key} | GET /_file/enc-download/{key} |
When encrypted is true, the presigned URL fields are empty -- use the enc-upload/enc-download paths instead.
Storage Backends
Production -- S3
In production, files are stored in S3. Presigned URLs allow clients to upload and download without file data passing through the application server (except for PII-encrypted fields).
Development -- Local Filesystem
When running manzano dev, files are stored on the local filesystem at .dev/blobs/. Grove proxies local file operations through two development-only endpoints:
PUT /_file/local/{*key}
GET /_file/local/{*key}
These endpoints are only available in development mode. In production, they return 404.
Object Key Layout
Files are stored using a deterministic key structure:
{project_id}/{module}/{hash[-2:]}/{record_id}/{field_name}/{hash}.{ext}
| Segment | Description |
|---|---|
project_id | The Grove project identifier |
module | The module that owns the record |
hash[-2:] | Last two characters of the BLAKE3 hash (for sharding) |
record_id | The record the file belongs to |
field_name | The field name on the record |
hash.ext | Full BLAKE3 hash with the original file extension |
Example:
proj_1/invoices/a3/inv_abc123/invoice/b7e2f9d1...a3.pdf
The two-character hash prefix (a3) distributes objects across key prefixes to avoid S3 hot-partition issues.
Endpoint Reference
| Endpoint | Method | Purpose |
|---|---|---|
/_file/presign-put | POST | Get a presigned PUT URL for direct S3 upload |
/_file/presign-get | POST | Get a presigned GET URL for download |
/_file/upload | POST | Multipart form upload through the server (computes BLAKE3 hash) |
/_file/enc-upload/{*key} | PUT | Encrypted upload for @pii fields |
/_file/enc-download/{*key} | GET | Encrypted download for @pii fields |
/_file/local/{*key} | PUT/GET | Local filesystem proxy (development only) |
See Also
- Cookbook: File Uploads -- Step-by-step recipe for adding file uploads to an app
- Field Classifications -- How
@piiand other classifications work - Credential Store -- Where S3 credentials and encryption keys are managed