Quickstart

preview

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.

Author: Poul Kjeldager
Platform: linuxmacoswindowsdocker
Category: agents

Quickstart

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.

1. Create an org

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"}'

2. Create a form

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_…"}]
  }'

3. Store the definition

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.

4. Render it on your own page

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.
  • No onSubmitAsync — supplying a submit handler bypasses submitUrl, and the answers never reach the server.

5. Watch it land

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.

Where does it live on disk?

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.