Let visitors attach a file
Goal
Let somebody on your portal attach a document — a receipt, a photograph, a signed form — to the record they are creating.
Before you start
- A portal where Who can open it is set to People who sign in, because attaching a file is part of a write and a write needs somebody to attribute it to. See Ask visitors to sign in.
- A table with a File or Image column on it.
- The Create row already in the portal's allow-list, from Let visitors save changes. There is nothing to add for the file — it travels in the body of the write.
Steps
-
Draw a file picker on your screen and bind its value to a screen state field —
state.document. -
Send it with the record. The picker hands your screen the file, not its name, so it goes in the same object as everything else:
createRecord("par_claim", {par_name: getState("subject"),par_document: getState("document")}, "saved"); -
Describe it in the API's request schema, as an object of four properties:
"par_document": {"type": "object","additionalProperties": false,"required": ["fileName", "contentType", "contentBase64"],"properties": {"fileName": { "type": "string", "maxLength": 260 },"contentType": { "type": "string", "maxLength": 200 },"sizeBytes": { "type": "integer" },"contentBase64": { "type": "string", "maxLength": 5600000 }}} -
Store the record first, then the file, because a file belongs to a record and there is nothing to attach one to until the row exists:
var file = getInput('par_document');var id = createRecord('par_claim', {par_name: getInput('par_name'),par_raised_by_email: portalEmail(),par_document_name: isEmpty(file) ? '' : file.fileName});if (!isEmpty(file)) {putRecordFile('par_claim', id, 'par_document',file.fileName, file.contentType, file.contentBase64);} -
Clear it after a successful send, so the picker empties with the rest of the form:
setState("document", null);
What a picked file actually is
Four fields, and they are the same four a script gets back from getRecordFile — so a file going
out and a file coming back have one shape:
fileName | what it is called on the visitor's own machine |
contentType | its media type, as their browser reported it |
sizeBytes | how big it is — what a caption says |
contentBase64 | the bytes, as text |
A caption bound to the whole thing reads as the file's name, so
text(state.document) says contract.pdf rather than something unreadable. To show a picked
image, bind an img's source to fileUrl(state.document).
Why there is no "attach a file" row in the allow-list
A portal's allow-list names table operations — read a list, read one, create, change, delete — and a file is not one of them. It rides in the body of the write, so a portal that may create a claim may put a document on the claim it is creating, and a portal that may not, cannot. There is nothing extra to grant, and nothing extra to forget to take away.
This is also why no file endpoint is on the public internet. The bytes go to your Custom API, your
script decides which record they land on, and putRecordFile stores them under the record's own
permissions — it needs Update on that row, exactly as a form inside the platform does.
Attaching to a record that already exists
The same, through a change. Send the file in updateRecord's values, and have the Custom API
behind the Change a record row read the record, update it, and then call putRecordFile. On a
table with a rule on whose records a visitor sees, the
file obeys the rule too: a visitor can attach a file only to a record the rule gives them.
How big a file can be
4 MB, and the picker says so before anything is uploaded.
That is smaller than what the platform will store — a deployment with object storage configured takes far larger files through a record's own file endpoint — and the difference is deliberate. Here the file is encoded as text inside a JSON body and held in the visitor's browser while they finish filling the form in. A very large attachment on that road is a very large string in a stranger's browser and a body no contract should be asked to parse.
The same screen inside the platform
Nothing changes. Open the same app in Parsware and the same handler writes the table directly, with the file going to the record's File column through the platform's own endpoint. You draw one screen, not two.
Three things that catch people out
- Write the record before the file.
putRecordFileneeds a record id. A script that tries to attach first has nothing to attach to. - Keep the file's name on a text column beside it. A list that wants to say "📎 invoice.pdf" should read a plain text column, not fetch every file to find out its name.
- A refusal is read by strangers. If you refuse an attachment — the wrong sort of file, say — write the sentence for the public: it appears beside the button they pressed.
Confirm it worked
Attach a file on the portal, press the button, then open the record in the platform. The document is on its File field, with the name you stored beside it — and the portal never saw a record endpoint.