From an empty server to a received answer — create an org, store a definition, render it on your own page, and watch the submission land.
Five steps, about ten minutes. Everything below assumes the server is running at
http://localhost:8123 with QUICKFORM_ADMIN_TOKEN unset (open dev mode). Add
-H "Authorization: Bearer $TOKEN" to every admin call once you set one.
The org is the ownership container — the same idea as a GitHub organisation.
curl -X POST http://localhost:8123/api/orgs \
-H 'content-type: application/json' \
-d '{"slug":"acme","name":"Acme ApS"}'
Name it, and list the origins allowed to fetch its definition and post to it. An empty list means any origin, which is the honest default for a form embedded on a site we do not host — but name your domains as soon as you know them.
curl -X PUT http://localhost:8123/api/orgs/acme/forms/contact \
-H 'content-type: application/json' \
-d '{
"name": "Contact",
"allowedOrigins": ["https://acme.example", "http://localhost:3000"],
"webhooks": [{"url": "https://agent.acme.example/hooks/quickform", "secret": "whsec_…"}]
}'
The definition is plain QuickForm JSON. Store it verbatim — the server versions it, and never re-shapes it.
curl -X PUT http://localhost:8123/api/orgs/acme/forms/contact/definition \
-H 'content-type: application/json' \
-d '{
"intro": { "text": "Write to us", "submitText": "Start" },
"questions": {
"name": { "text": "What is your name?", "inputType": "text" },
"message": { "text": "What is it about?", "inputType": "text" }
},
"ending": { "text": "Thanks — we will get back to you." }
}'
You do not write a submit block. That is the server's job.
Install the runtime in your app — this is the only code you write:
npm install @eavfw/quickform-core@vnext
import { QuickFormProvider, QuickForm } from "@eavfw/quickform-core";
const definition = await (
await fetch("http://localhost:8123/api/orgs/acme/forms/contact/definition", {
credentials: "include",
})
).json();
export default function Contact() {
return (
<QuickFormProvider definition={definition} payload={{}} asContainer>
<QuickForm />
</QuickFormProvider>
);
}
Two details that are load-bearing:
credentials: "include" — the runtime's own submit uses credentialed
fetch, so the server echoes your exact Origin and sets
Access-Control-Allow-Credentials: true. A wildcard * would be rejected by
the browser, which is why the origin allow-list is not optional decoration.onSubmitAsync — supplying a submit handler bypasses submitUrl, and
the answers never reach the server.Fill the form in the browser, then:
curl http://localhost:8123/api/orgs/acme/forms/contact/submissions | jq
curl http://localhost:8123/api/orgs/acme/submissions?limit=20 | jq
The webhook fired at the same time. See Webhooks for the payload and how to verify the signature.
Under USER_DATA_DIR, as files you can read without us:
user-data/orgs/acme/org.json
user-data/orgs/acme/forms/contact/form.json
user-data/orgs/acme/forms/contact/definition.json
user-data/orgs/acme/forms/contact/versions/0001.json
user-data/orgs/acme/forms/contact/submissions/<timestamp>-<id>.json
definition.json is the current one; versions/NNNN.json is every definition
that was ever served, so a form that changed under a live embed can be explained
afterwards.
Back that directory up and you have backed up the product.