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

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.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.
s3_keystringPresent when status is uploaded. The object's key in storage — the stable identifier for the file
urlstringPresent 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 itLink lifetime
Dashboard15 minutes
GET /api/v1/forms/:id/submissions/:sub_id15 minutes
Webhook payload24 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 ​

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",
      "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:

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):

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