Let visitors save changes
Goal
Let somebody on your portal raise a request, change one, or delete one — not just read.
Before you start
- A portal where Who can open it is set to People who sign in. Writing is refused on a portal anyone can open: there would be nobody to attribute the change to. See Ask visitors to sign in.
- A Custom API for each thing they may do. One that reads is not one that writes, and the platform will not treat it as both.
Steps
- Write the Custom API. For creating, make its verb
POST; for changing,PATCH; for deleting,DELETE. Write a request schema for it: it is the first thing standing between a visitor and a column they should not be setting. Without one, a visitor's body may carry only the fields your script reads withgetInput, and anything else is refused. - Open your solution, choose Portals, and open the portal.
- Under What it can read, press Add something to read and choose:
- the table your screen names in
createRecord/updateRecord/deleteRecord, - the operation — Create a record, Change a record or Delete a record,
- the API that answers it.
- the table your screen names in
- Press Save.
Each of the five things a screen can do needs its own row. A portal that can list requests cannot create one until you add the Create row as well — the two are separate contracts, and the page can only name the table.
What your screen does
Nothing new. The same actions you would draw for an app inside the platform:
createRecord("par_request", {
par_name: getState("subject"),
par_detail: getState("detail")
}, "saved");
On a portal that becomes a call to the API you allow-listed for par_request / Create. The
drawing is unchanged — that is the whole point of publishing one app to two places.
What the API has to do
Decide who is allowed to do this, and fill in what the visitor must not.
var me = portalEmail();
if (me == '') {
throwValidationError('Sign in before raising a request.');
} else {
var subject = getInput('par_name');
if (subject == '') {
throwValidationError('Please give your request a subject.');
} else {
// **The visitor does not get to say who raised it.** Whatever the page sent for this column
// is ignored; the address comes off their signed-in session.
var id = createRecord('par_request', {
par_name: subject,
par_detail: getInput('par_detail'),
par_raised_by_email: me,
par_status: 'New'
});
setOutput('record', getRecord('par_request', id));
}
}
Keeping a visitor out of the columns they should not set
The portal forwards the body exactly as the page sent it, and it cannot do otherwise: which fields a visitor may set is a question about your table, and only your contract knows the answer.
Two things answer it, and you want both.
Your request schema, which refuses a field you never promised. Set
"additionalProperties": false and list only what a visitor may send:
{
"type": "object",
"additionalProperties": false,
"required": ["par_name"],
"properties": {
"par_name": { "type": "string", "minLength": 1, "maxLength": 200 }
}
}
A body carrying par_raised_by_email is now refused before your script runs.
Reading each field by name, as the script above does. getInput('par_name') takes what you
asked for and nothing else.
A script that hands the whole body to createRecord lets a visitor set any column they can guess
the name of — including the one that says whose record it is. The schema makes that hard; reading by
name makes it impossible.
What the visitor sees when it fails
- A refusal you wrote —
throwValidationError('Please give your request a subject.')— appears beside the button they pressed. - Anything else shows a general "that could not be saved", and the real reason goes to the portal host's log.
That split is deliberate: your sentence is about them and they can act on it, while an internal error is neither.
Because the first kind is shown, write those sentences for the public. Do not name a column, a table, or an internal rule — "Please give your request a subject" rather than "par_name failed constraint NOT NULL".
What the platform does not check
Whether a visitor can read back what they just wrote. If your Create API stores a record that your Query API filters out, the record exists and the list does not show it. Nothing warns you: the two scripts are separate contracts, and the platform will not second-guess a pair you wrote on purpose.
If a screen saves something and then shows an empty list, that is usually this — compare the filter in your read API with what your write API stored.
Turning it off
Remove the row from What it can read, or set Who can open it back to Anyone. Either one stops the portal writing immediately. The second also stops it the moment you save, even though the rows are still listed — an open portal serves its reads and none of its writes.