Appearance
File Uploads
Krafter Forms supports file uploads via multipart/form-data. Uploaded files are stored in S3-compatible object storage and linked to their submission.
How It Works
- Your HTML form uses
enctype="multipart/form-data". - Users select files via
<input type="file">fields. - On submission, files are extracted and uploaded to S3-compatible storage.
- File metadata is stored in the submission's
filesfield.
HTML Example
Allow-list
Only the MIME types listed in Limits are accepted. Files with other types (e.g. DOCX, ODT, ZIP) land in submission.files with status: "rejected", reason: "mime_not_allowed". Constrain the accept attribute on your <input type="file"> so the file picker only offers users formats the platform actually stores.
html
<form action="https://app.krafter.dev/f/job-application" method="POST"
enctype="multipart/form-data">
<label for="name">Name</label>
<input type="text" id="name" name="name" required>
<label for="email">Email</label>
<input type="email" id="email" name="email" required>
<label for="resume">Resume (PDF only)</label>
<input type="file" id="resume" name="resume" accept="application/pdf">
<label for="screenshot">Screenshot (image, optional)</label>
<input type="file" id="screenshot" name="screenshot" accept="image/*">
<button type="submit">Submit Application</button>
</form>WARNING
You must set enctype="multipart/form-data" on your form element. Without it, the browser will not send files.
Storage
Uploaded files are stored in S3-compatible object storage (AWS S3, MinIO, or any S3-compatible provider). Files are organized using the following key format:
forms/{team_id}/{form_id}/{submission_id}/{filename}For example:
forms/tm_abc123/frm_def456/sub_789ghi/resume.pdfFile Metadata
File metadata is stored in the submission's files field as a JSON object. Each key is the form field name, and the value contains the file details:
json
{
"files": {
"resume": {
"filename": "jane-doe-resume.pdf",
"content_type": "application/pdf",
"status": "pending"
},
"screenshot": {
"filename": "screenshot.png",
"content_type": "image/png",
"status": "pending"
}
}
}Metadata Fields
| Field | Type | Description |
|---|---|---|
filename | string | Original filename as uploaded by the user |
content_type | string | MIME type of the file |
status | string | Upload status (pending, uploaded, failed, rejected) |
reason | string | Present only when status is rejected. One of mime_not_allowed, too_large, too_many_files, read_failed. |
Status semantics and retry behaviour
| Value | Meaning | Retried? |
|---|---|---|
pending | Initial value before the S3 upload step runs. Terminal only if S3 isn't configured at submit time. | No. No worker re-scans pending entries. |
uploaded | The file was streamed to object storage successfully and s3_key + url are present on the metadata. | n/a |
failed | The S3 upload call returned an error. The submission row is kept; the file is not. | No. No worker re-scans failed entries either. |
rejected | The file was rejected at the upload boundary before any S3 attempt. The accompanying reason describes which check failed. | No. Submitters must re-upload a file that satisfies the limits. |
There is no retry worker for failed, pending, or rejected files today. Once a submission is created with one of those states, the file content is gone (the temp file uploaded by the browser is cleaned up when the request ends) — the only way to recover is to ask the submitter to resubmit. If you depend on file uploads being delivered, validate file size and type client-side before submitting.
Retrieving File Metadata
File metadata is included when you fetch a submission via the API:
bash
curl https://app.krafter.dev/api/v1/forms/FORM_ID/submissions/SUBMISSION_ID \
-H "Authorization: Bearer kr_live_abc123def456"json
{
"id": "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"data": {
"name": "Jane Doe",
"email": "jane@example.com"
},
"files": {
"resume": {
"filename": "jane-doe-resume.pdf",
"content_type": "application/pdf",
"status": "uploaded"
}
},
"is_spam": false,
"is_read": false,
"created_at": "2025-01-15T10:30:00Z"
}curl Example
Upload a file along with form data using curl:
bash
curl -X POST https://app.krafter.dev/f/job-application \
-F "name=Jane Doe" \
-F "email=jane@example.com" \
-F "resume=@/path/to/resume.pdf" \
-F "screenshot=@/path/to/screenshot.png"json
// 201 Created
{
"ok": true,
"id": "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d"
}TIP
The -F flag in curl automatically sets Content-Type: multipart/form-data. Use @ before the file path to upload a file.
Configuration
File upload support requires S3-compatible storage configured on the server side. The following environment variables control storage behavior:
| Variable | Description |
|---|---|
AWS_ACCESS_KEY_ID | S3 access key |
AWS_SECRET_ACCESS_KEY | S3 secret key |
S3_BUCKET | Bucket name for file storage |
S3_ENDPOINT | Custom endpoint for S3-compatible providers (e.g., MinIO) |
S3_REGION | AWS region (default: us-east-1) |
INFO
If S3 storage is not configured, file fields in multipart/form-data submissions are silently skipped. Text fields are still processed and stored normally.
Limits
The following limits are enforced by KrafterWeb.FormSubmissionController on every public submission (form_submission_controller.ex:127-129):
| Limit | Value |
|---|---|
| Max files per form submission | 10 |
| Max bytes per file | 10 MiB (10 * 1024 * 1024 bytes) |
| Allowed MIME types | image/jpeg, image/png, image/gif, image/webp, application/pdf, text/plain |
These limits are platform-wide and not configurable per form today.
Rejection behaviour
The submission still succeeds when these limits are exceeded — the controller never returns an error to the submitter. Each rejected file lands in submission.files with status: "rejected" and a reason so the dashboard and your downstream processors can see exactly which files were dropped:
| Condition | Submission row | files field on the submission |
|---|---|---|
| More than 10 files | Created (201) | Every entry persists with status: "rejected" and reason: "too_many_files". No S3 upload happens for any file in that submission. |
| File exceeds the 10 MiB size cap | Created (201) | That entry persists with status: "rejected" and reason: "too_large". Other valid files in the same submission still upload normally. |
| File uses a disallowed MIME type | Created (201) | That entry persists with status: "rejected" and reason: "mime_not_allowed". Other valid files in the same submission still upload normally. |
| Server cannot read the temp file | Created (201) | That entry persists with status: "rejected" and reason: "read_failed". |
There is still no error response to the submitter and no automatic resubmission. If you depend on file uploads being delivered, validate file size and type client-side before submitting. Server-side enforcement of "reject the whole submission" is on the roadmap.