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).
Attachment links expire — but the object is not secret
There is no permanent public address for an attachment any more: links are signed and expire, and are issued when you read the submission rather than stored alongside it. Nothing hands out a durable URL.
Attachments are stored in a bucket that is not served over the CDN host, so an attachment cannot be fetched without a signed link — not even by someone who knows its exact key.
Attachments uploaded before this change were in the CDN-served bucket, and any URL issued for them keeps working until they are migrated. Treat those older links as public until Krafter confirms the migration has run.
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. |
s3_key | string | Present when status is uploaded. The object's key in storage — the stable identifier for the file |
url | string | Present when status is uploaded, generated at read time and expiring. Never stored; see below |
About url
url is not a property of the file. It is a signed link minted when you read the submission, and it stops working when it expires:
| Where you read it | Link lifetime |
|---|---|
| Dashboard | 15 minutes |
GET /api/v1/forms/:id/submissions/:sub_id | 15 minutes |
| Webhook payload | 24 hours — long enough to survive delivery retries and your own queueing |
Do not store a url and expect it to keep resolving. Store the s3_key if you need a stable reference, and fetch the submission again for a fresh link.
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",
"s3_key": "forms/tm_abc123/frm_def456/sub_789ghi/jane-doe-resume.pdf",
"url": "https://media.krafter.dev/forms/tm_abc123/...?X-Amz-Signature=..."
}
},
"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):
| 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.