---
title: "The request lifecycle of a native product form"
description: "Follow one Fillo form from a versioned schema through native rendering, server validation, an accepted response, and a downstream handoff."
order: 29
type: "guide"
topic: "Frameworks"
tags:
  - "architecture"
  - "SDK"
  - "API"
  - "validation"
  - "responses"
updated: "2026-08-07"
---

A form inside a product crosses several trust boundaries. The host app knows the signed-in account. The browser owns interaction state. The form service owns the published schema and server checks. A storage provider may receive files, and a webhook consumer may own the next operational step.

Treating that as one `POST` hides the decisions that usually break later. This is the complete lifecycle Fillo uses, from schema to accepted response.

## 1. One schema names the contract

The form starts as an editor-managed definition or a code-defined schema. Stable field IDs matter more than labels: a label can change from “Work email” to “Company email,” while the response, export column, webhook mapping, and historical version still refer to `email`.

```ts
import { defineForm } from "@usefillo/react";

export const feedback = defineForm({
  id: "settings-feedback",
  title: "Product feedback",
  pages: [
    {
      id: "feedback",
      blocks: [
        {
          id: "score",
          kind: "rating",
          label: "How is settings working for you?",
          max: 5,
          required: true,
        },
        {
          id: "note",
          kind: "long_text",
          label: "What should we fix?",
          visibleIf: [{ fieldId: "score", op: "lt", value: 4 }],
        },
      ],
    },
  ],
});
```

Code-defined changes can be staged for review before publication. Editor-managed forms keep the same draft-versus-published separation. Respondents resolve the published version, not whichever draft somebody is editing.

## 2. The browser gets a public capability

The React or DOM renderer uses a `pk_` publishable key. That key is allowed to resolve and submit a published form from an approved browser origin. It is not a workspace management secret.

An `fsk_` key belongs only on a trusted server. It can carry scopes for reading forms and responses through the [form builder API](/form-builder-api). The management routes are not CORS-open, which makes accidentally pasting a secret into browser code fail instead of silently granting authority.

```tsx
import { createClient, FilloForm } from "@usefillo/react";
import "@usefillo/react/styles.css";

const fillo = createClient({ key: import.meta.env.VITE_FILLO_KEY });

export function SettingsFeedback() {
  return <FilloForm client={fillo} form={feedback} />;
}
```

The first public resolve is recorded once for the form. That is a more useful activation signal than “opened the editor”: it proves the published contract reached an actual render path.

## 3. Native rendering keeps the product context

The SDK renders real labels, inputs, buttons, errors, and focus targets in the host document. The route, layout, design system, signed-in session, and after-submit behavior remain the application's responsibility.

That also means the host must do ordinary product work: supply a visible heading when hiding the form title, test focus and error states, and avoid treating signed respondent traits as an authorization mechanism. Native rendering removes the frame boundary; it does not remove application security or accessibility responsibilities.

When a form belongs mid-workflow—such as [in-app feedback](/in-app-feedback)—the host can pass coarse route context or server-signed respondent identity instead of asking the person to repeat information the product already knows.

## 4. Client validation is guidance; server validation is authority

The renderer uses the schema to give immediate errors and evaluate visibility. The submission endpoint validates the same field IDs and rules again. A hidden required field is not required, a visible required field is, and unknown or malformed values do not become valid because a custom browser skipped a check.

Do not put authorization decisions in conditional visibility. Hiding a field changes the form flow; it does not grant access to an account, file, or management action.

## 5. Files take a separate browser-direct path

An upload is not embedded as a large form-body field. Fillo opens a scoped upload session, the browser sends chunks directly to the active storage provider, and the server verifies completion before accepting the file reference on the response.

For durable workflows, connect Google Drive, Box, Amazon S3, or Cloudflare R2. Eligible new workspaces can begin with the documented capped transit lane. In both cases the response keeps file metadata and a reference, so reviewers do not have to reconcile an inbox attachment with a separate answer row.

See [collect file uploads in your own storage](/guides/collect-file-uploads) for the provider and failure-state checklist.

## 6. Acceptance creates the durable response

After validation, challenge checks, upload verification, duplicate handling, and response-limit checks pass, the server commits the response. A quarantined or duplicate submission does not count as the workspace's first accepted response.

The commit preserves the form version used to interpret the answers. That is why a later label or option change does not rewrite the meaning of an earlier response.

Fillo records three distinct milestones instead of flattening them into “converted”:

1. `sync.first_resolve`: the form definition reached a public render path.
2. `submit.first_success`: this form accepted its first response.
3. `workspace_activated`: this workspace accepted its first response across all forms.

Those events let the team see whether acquisition stalls before render, between render and submit, or after a form succeeds but before the workspace becomes habitual.

## 7. Handoffs happen after the source record exists

The accepted response is the source record. CSV export, response review, notifications, integrations, and signed webhook delivery build on it. Delivery can retry without asking the respondent to submit again, and the response workspace can show the handoff state beside the answer.

If a downstream consumer needs server-side access, use the scoped management API. If it should react to each accepted response, use a signed webhook and make the receiver idempotent because delivery is at least once.

## Verify the whole path

Before sending traffic, test the real published route:

- Load it at desktop and mobile widths with keyboard-only navigation.
- Trigger a client error, then try the equivalent malformed server request.
- Exercise every conditional branch.
- Interrupt and retry an upload when the form includes files.
- Submit one accepted response and find it in the response workspace.
- Confirm the intended webhook or integration delivery state.
- Change a label in a draft, publish it, and verify the old response remains readable.

## Start from a working repository

- [Next.js product-feedback starter](https://github.com/jacobfunch/fillo-nextjs-feedback-starter): A native feedback card inside an account-settings route.
- [Vite React client-intake starter](https://github.com/jacobfunch/fillo-vite-client-intake-starter): A typed intake schema with optional document upload.

Both repositories use only a browser-safe publishable key, include environment examples, and have been built from a clean install. Keep a workspace secret key on the server if you add response-management API calls.

The [custom layout example](/examples#custom-layout) shows the renderer boundary in a working form. The [headless form builder overview](/headless-form-builder) compares the available rendering paths, and the [React guide](/guides/react-form) is the shortest implementation starting point.

## Related

- [Build a native React form](/guides/react-form)
- [Compare headless form rendering paths](/headless-form-builder)
- [Design an in-app feedback flow](/in-app-feedback)
- [Collect file uploads in connected storage](/guides/collect-file-uploads)
