BETA
Skip to content

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

  1. Your HTML form uses enctype="multipart/form-data".
  2. Users select files via <input type="file"> fields.
  3. On submission, files are extracted and uploaded to S3-compatible storage.
  4. File metadata is stored in the submission's files field.

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.pdf

File 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

FieldTypeDescription
filenamestringOriginal filename as uploaded by the user
content_typestringMIME type of the file
statusstringUpload status (pending, uploaded, failed, rejected)
reasonstringPresent only when status is rejected. One of mime_not_allowed, too_large, too_many_files, read_failed.

Status semantics and retry behaviour

ValueMeaningRetried?
pendingInitial value before the S3 upload step runs. Terminal only if S3 isn't configured at submit time.No. No worker re-scans pending entries.
uploadedThe file was streamed to object storage successfully and s3_key + url are present on the metadata.n/a
failedThe S3 upload call returned an error. The submission row is kept; the file is not.No. No worker re-scans failed entries either.
rejectedThe 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:

VariableDescription
AWS_ACCESS_KEY_IDS3 access key
AWS_SECRET_ACCESS_KEYS3 secret key
S3_BUCKETBucket name for file storage
S3_ENDPOINTCustom endpoint for S3-compatible providers (e.g., MinIO)
S3_REGIONAWS 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):

LimitValue
Max files per form submission10
Max bytes per file10 MiB (10 * 1024 * 1024 bytes)
Allowed MIME typesimage/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:

ConditionSubmission rowfiles field on the submission
More than 10 filesCreated (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 capCreated (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 typeCreated (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 fileCreated (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.

Built by Krafter Studio