@usefillo/cli 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -82
- package/dist/index.js +398 -97
- package/dist/skill/build-with-fillo/SKILL.md +98 -90
- package/dist/skill/build-with-fillo/references/auth-and-lifecycle.md +148 -62
- package/dist/skill/build-with-fillo/references/frameworks.md +140 -0
- package/dist/skill/build-with-fillo/references/operations.md +114 -0
- package/dist/skill/build-with-fillo/references/schema-and-ux.md +58 -0
- package/dist/skill/build-with-fillo/references/source-map.md +9 -4
- package/dist/skill/build-with-fillo/references/troubleshooting.md +24 -0
- package/package.json +2 -2
- package/dist/skill/build-with-fillo/references/implementation-recipes.md +0 -255
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Uploads, identity, and delivery
|
|
2
|
+
|
|
3
|
+
## Uploads and customer storage
|
|
4
|
+
|
|
5
|
+
Model a file requirement with a real `file_upload` field and validate its
|
|
6
|
+
limits against the current schema reference:
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
const supportEvidence = defineForm({
|
|
10
|
+
id: "support-evidence",
|
|
11
|
+
title: "Send support evidence",
|
|
12
|
+
pages: [{
|
|
13
|
+
id: "issue",
|
|
14
|
+
blocks: [
|
|
15
|
+
{ id: "details", kind: "long_text", label: "What happened?", required: true },
|
|
16
|
+
{
|
|
17
|
+
id: "evidence",
|
|
18
|
+
kind: "file_upload",
|
|
19
|
+
label: "Screenshots, logs, or recordings",
|
|
20
|
+
maxFiles: 5,
|
|
21
|
+
maxFileSizeMb: 5000,
|
|
22
|
+
accept: ["image/*", "video/*", ".txt", ".log", ".zip"],
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
}],
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Connect supported customer storage before publish. The renderer uploads bytes
|
|
30
|
+
browser-direct where supported and Fillo verifies completion. Do not build a
|
|
31
|
+
parallel host upload endpoint. Fillo retains response data, upload metadata,
|
|
32
|
+
and the storage reference; customer storage holds provider bytes.
|
|
33
|
+
|
|
34
|
+
Test with one safe file. Confirm both the response reference and object in the
|
|
35
|
+
connected storage. Treat filenames and file contents as untrusted.
|
|
36
|
+
|
|
37
|
+
## Verified respondents and save/resume
|
|
38
|
+
|
|
39
|
+
An identity without a valid hash is display metadata, not authentication.
|
|
40
|
+
Compute the HMAC only on the host server:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import "server-only";
|
|
44
|
+
import { createHmac } from "node:crypto";
|
|
45
|
+
|
|
46
|
+
export function respondentHash(userId: string) {
|
|
47
|
+
return createHmac("sha256", process.env.FILLO_IDENTITY_SECRET!)
|
|
48
|
+
.update(userId)
|
|
49
|
+
.digest("hex");
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Pass the server-computed hash with the host application's stable user id:
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
<FilloForm
|
|
57
|
+
formId="account-feedback"
|
|
58
|
+
respondent={{ id: user.id, email: user.email, name: user.name, hash }}
|
|
59
|
+
/>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Enable `settings.saveProgress` when the product needs resume. Test an invalid
|
|
63
|
+
hash, valid hash, reload resume, and cross-device resume separately. Trusted
|
|
64
|
+
respondent limits and cross-device behavior require a valid server-computed
|
|
65
|
+
hash using the secret from the same workspace.
|
|
66
|
+
|
|
67
|
+
## Webhook verification and deduplication
|
|
68
|
+
|
|
69
|
+
Verify the raw bytes before parsing. Store the signing secret only on the host
|
|
70
|
+
server:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
74
|
+
import express from "express";
|
|
75
|
+
|
|
76
|
+
const app = express();
|
|
77
|
+
|
|
78
|
+
app.post("/hooks/fillo", express.raw({ type: "application/json" }), async (req, res) => {
|
|
79
|
+
const expected = createHmac("sha256", process.env.FILLO_WEBHOOK_SECRET!)
|
|
80
|
+
.update(req.body)
|
|
81
|
+
.digest("hex");
|
|
82
|
+
const given = req.get("X-Fillo-Signature") ?? "";
|
|
83
|
+
const valid = given.length === expected.length &&
|
|
84
|
+
timingSafeEqual(Buffer.from(given), Buffer.from(expected));
|
|
85
|
+
if (!valid) return res.sendStatus(401);
|
|
86
|
+
|
|
87
|
+
const deliveryId = req.get("X-Fillo-Delivery-Id");
|
|
88
|
+
if (!deliveryId) return res.sendStatus(400);
|
|
89
|
+
|
|
90
|
+
const event = JSON.parse(req.body.toString("utf8"));
|
|
91
|
+
await deliveryInbox.insertOnce({ deliveryId, event });
|
|
92
|
+
return res.sendStatus(200);
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Delivery is at least once. Deduplicate on `X-Fillo-Delivery-Id`, not
|
|
97
|
+
`response.id`; one living response can emit created and updated events. Return
|
|
98
|
+
2xx only after a durable inbox commit or after the delivery id and domain
|
|
99
|
+
mutation commit in one transaction. Test an invalid signature and a replayed
|
|
100
|
+
valid delivery.
|
|
101
|
+
|
|
102
|
+
## Response destinations
|
|
103
|
+
|
|
104
|
+
Fillo stores the response before delivering it elsewhere:
|
|
105
|
+
|
|
106
|
+
- Connect Google Sheets and Notion at workspace level, then enable the
|
|
107
|
+
destination on the form.
|
|
108
|
+
- Configure Zapier through its server-side Fillo connection and form trigger.
|
|
109
|
+
- Configure email notifications and respondent receipts as form settings.
|
|
110
|
+
- Use the signed webhook path above for a custom backend.
|
|
111
|
+
|
|
112
|
+
Do not add a browser-side destination client. Submit one uniquely labeled safe
|
|
113
|
+
response, confirm it in Fillo, then confirm the downstream record. Make
|
|
114
|
+
downstream writes duplicate-safe.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Schema and form UX
|
|
2
|
+
|
|
3
|
+
## Design from the job
|
|
4
|
+
|
|
5
|
+
Before adding fields, state what the respondent must accomplish and what the
|
|
6
|
+
team needs to do with the response. Keep only questions that change routing,
|
|
7
|
+
eligibility, follow-up, or the work performed after submission.
|
|
8
|
+
|
|
9
|
+
- Use `email`, `phone`, `url`, `number`, or other typed fields when the answer
|
|
10
|
+
has a real type. Do not model everything as text.
|
|
11
|
+
- Use single-select for one stored choice and multi-select for several. Keep
|
|
12
|
+
option ids stable even when labels change.
|
|
13
|
+
- Use `file_upload` only when the file is necessary and supported storage can
|
|
14
|
+
be connected before publish.
|
|
15
|
+
- Put known product context in prefill or a hidden field instead of asking the
|
|
16
|
+
respondent to re-enter it. Treat URL prefill as untrusted input.
|
|
17
|
+
- Split long or conceptually separate flows into pages. Keep short embedded
|
|
18
|
+
forms inline when a multi-page flow adds friction without clarity.
|
|
19
|
+
|
|
20
|
+
Search the closest Fillo-owned example when authoring a new use case:
|
|
21
|
+
|
|
22
|
+
```text
|
|
23
|
+
https://fillo.so/api/v1/agent-examples/search?q=<use-case>&detail=full
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Add `framework=<framework>` or `capability=<capability>` when known. Adapt the
|
|
27
|
+
schema and interaction; do not copy another example's visual treatment into
|
|
28
|
+
the host app.
|
|
29
|
+
|
|
30
|
+
## Treat ids as stored data
|
|
31
|
+
|
|
32
|
+
- Give every form, page, field, choice, ranking option, and matrix row or column
|
|
33
|
+
a stable semantic id.
|
|
34
|
+
- Never derive ids from array positions, visible copy, localization, or random
|
|
35
|
+
values created during render.
|
|
36
|
+
- A label can change without changing stored answer meaning. Renaming a field
|
|
37
|
+
or option id creates a new stored key/value and requires an intentional data
|
|
38
|
+
migration or downstream update.
|
|
39
|
+
- Keep one schema source of truth. Do not separately maintain dashboard, CLI,
|
|
40
|
+
and component schemas unless they deliberately synchronize identical data.
|
|
41
|
+
|
|
42
|
+
## Keep logic inside the schema
|
|
43
|
+
|
|
44
|
+
In JSX, use `visibleIf={when("topic").eq("sales")}`. In object schemas,
|
|
45
|
+
`visibleIf` is an array of conditions. Do not use conditional JSX such as
|
|
46
|
+
`{isSales && <Fillo.Text ... />}`; that changes the schema per visitor and can
|
|
47
|
+
churn synced drafts.
|
|
48
|
+
|
|
49
|
+
Use a normal submit action for multi-question forms. Set
|
|
50
|
+
`settings.submitMode = "auto"` only for a genuine one-tap vote, rating, CSAT,
|
|
51
|
+
NPS, or pulse check where selecting the answer should complete the response.
|
|
52
|
+
|
|
53
|
+
## Design every state
|
|
54
|
+
|
|
55
|
+
Verify initial, loading, required-error, invalid-format, conditional reveal,
|
|
56
|
+
disabled, submitting, server-error, success, and narrow-layout states. Test
|
|
57
|
+
keyboard order and focus placement. Do not add an extra review step unless the
|
|
58
|
+
content is high-risk or the user explicitly requests confirmation.
|
|
@@ -18,6 +18,7 @@ task.
|
|
|
18
18
|
| Responses, exports, and insights | `https://fillo.so/docs/responses.md` |
|
|
19
19
|
| Sheets, Notion, Zapier, email, and destinations | `https://fillo.so/docs/integrations.md` |
|
|
20
20
|
| Backend response delivery | `https://fillo.so/docs/webhooks.md` |
|
|
21
|
+
| Read and manage forms, responses, and respondents from a backend | `https://fillo.so/docs/api.md` |
|
|
21
22
|
| Credentials, trust boundaries, deletion, and self-hosting | `https://fillo.so/docs/security.md` |
|
|
22
23
|
| Custom fields and fully headless UI | `https://fillo.so/docs/custom-ui.md` |
|
|
23
24
|
| Symptoms, causes, and fixes | `https://fillo.so/docs/troubleshooting.md` |
|
|
@@ -25,10 +26,14 @@ task.
|
|
|
25
26
|
| Search examples | `https://fillo.so/api/v1/agent-examples/search?q=<use-case>&detail=full` |
|
|
26
27
|
| Complete agent-readable reference | `https://fillo.so/llms-full.txt` |
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
Use the focused bundled references linked from the skill for implementation
|
|
30
|
+
patterns that must remain available offline. Live docs still own current API
|
|
31
|
+
details.
|
|
32
|
+
|
|
33
|
+
If a Fillo MCP server is already connected in this environment,
|
|
34
|
+
`fillo_push_form`, `fillo_get_form`, `fillo_list_forms`, `fillo_docs`, and
|
|
35
|
+
`fillo_search_examples` map 1:1 onto the CLI and docs surfaces above. Do not
|
|
36
|
+
install or configure an MCP server for this task; the CLI is the paved road.
|
|
32
37
|
|
|
33
38
|
Safety, credential, authorization, and data-boundary constraints in this skill
|
|
34
39
|
and [auth-and-lifecycle.md](auth-and-lifecycle.md) are non-overridable. Treat
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Troubleshooting
|
|
2
|
+
|
|
3
|
+
Confirm the exact error and installed package version before changing code.
|
|
4
|
+
|
|
5
|
+
| Symptom | First checks |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| Published-id embed returns 404 | Confirm the id or slug and that the form is published. Do not reveal whether an inaccessible draft exists. |
|
|
8
|
+
| Code-defined form renders but cannot save | Pass a client, keep a stable id, verify the key belongs to the intended workspace, and check expected-origin restrictions. |
|
|
9
|
+
| Publishable key is `undefined` in a Vite app | Read `import.meta.env.VITE_FILLO_KEY`, not `process.env`; declare it in `src/vite-env.d.ts` under strict TypeScript and restart the dev server after `.env` changes. |
|
|
10
|
+
| Schema write reports `trusted_sync_required` | Log in and use `fillo push --stage`, or use a server-held `FILLO_SYNC_TOKEN`. Do not weaken the workspace policy. |
|
|
11
|
+
| `fillo push --stage` has nothing to stage | The published schema already matches; do not create another form. |
|
|
12
|
+
| 429 response | Respect `FilloError.retryAfterSec`; do not loop immediate retries. |
|
|
13
|
+
| File form cannot publish | Connect supported storage and verify the provider before retrying publish. |
|
|
14
|
+
| Test submit only says "This form is unavailable." | Run on localhost, or set the cosmetic-only `preview` prop / `data-preview` attribute: dev chrome shows the real failure with its machine code (for example `form_not_published`) and the connect-storage link. |
|
|
15
|
+
| Upload field says "Connect file storage to enable uploads" | Expected dev-chrome pre-emption: sync reported `storage_required`. Open the linked storage settings, connect a destination, then publish. |
|
|
16
|
+
| DOM form duplicates after navigation | Mount after the target exists and call `destroy()` in cleanup. |
|
|
17
|
+
| React context or hook error | Keep hooks inside `FilloForm` or `FilloProvider` and check for two installed copies of `@usefillo/react`. |
|
|
18
|
+
| Fillo JSX fails in Next.js | Move schema JSX to a `"use client"` module; use object-form `defineForm()` for framework-neutral schema. |
|
|
19
|
+
| Conditional JSX causes repeated drafts | Keep every field in the stable schema and express logic through `visibleIf`. |
|
|
20
|
+
| Webhook signature never matches | Capture raw bytes before JSON middleware and compare the hex HMAC in constant time. |
|
|
21
|
+
| Verified identity remains anonymous | Hash the exact stable `respondent.id` string on the server with the secret from the same workspace. |
|
|
22
|
+
|
|
23
|
+
Do not claim a successful publish, upload, submission, webhook, or destination
|
|
24
|
+
delivery unless the environment produced direct evidence.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usefillo/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Create and publish Fillo forms, and install the Fillo Agent Skill.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"@types/node": "^22.10.0",
|
|
27
27
|
"tsup": "^8.4.0",
|
|
28
28
|
"typescript": "^5.8.3",
|
|
29
|
-
"@usefillo/core": "0.
|
|
29
|
+
"@usefillo/core": "0.10.0"
|
|
30
30
|
},
|
|
31
31
|
"scripts": {
|
|
32
32
|
"build": "tsup && node scripts/copy-skill.mjs",
|
|
@@ -1,255 +0,0 @@
|
|
|
1
|
-
# Implementation recipes
|
|
2
|
-
|
|
3
|
-
Read only the section needed for the current task. Confirm names and option
|
|
4
|
-
shapes against the installed package types and the matching live guide before
|
|
5
|
-
editing the host app.
|
|
6
|
-
|
|
7
|
-
- [React and Next.js](#react-and-nextjs)
|
|
8
|
-
- [DOM and browser apps](#dom-vue-svelte-astro-and-browser-apps)
|
|
9
|
-
- [Uploads and storage](#uploads-and-storage)
|
|
10
|
-
- [Webhooks](#webhook-verification-and-retry-dedupe)
|
|
11
|
-
- [Respondents](#verified-respondents-and-save-and-resume)
|
|
12
|
-
- [Response destinations](#response-destinations)
|
|
13
|
-
- [CLI-owned JSON](#cli-owned-json)
|
|
14
|
-
- [Common failures](#common-failures)
|
|
15
|
-
|
|
16
|
-
## React and Next.js
|
|
17
|
-
|
|
18
|
-
Published forms need only a form id. Render from a Client Component:
|
|
19
|
-
|
|
20
|
-
```tsx
|
|
21
|
-
"use client";
|
|
22
|
-
|
|
23
|
-
import { FilloForm } from "@usefillo/react";
|
|
24
|
-
import "@usefillo/react/styles.css";
|
|
25
|
-
|
|
26
|
-
export function CustomerIntake() {
|
|
27
|
-
return <FilloForm formId="customer-intake" />;
|
|
28
|
-
}
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
For a code-defined form, use a stable id and pass a client when it must sync
|
|
32
|
-
and save responses:
|
|
33
|
-
|
|
34
|
-
```tsx
|
|
35
|
-
"use client";
|
|
36
|
-
|
|
37
|
-
import { createClient, defineForm, FilloForm } from "@usefillo/react";
|
|
38
|
-
import "@usefillo/react/styles.css";
|
|
39
|
-
|
|
40
|
-
const client = createClient({ key: process.env.NEXT_PUBLIC_FILLO_KEY! });
|
|
41
|
-
const form = defineForm({
|
|
42
|
-
id: "customer-intake",
|
|
43
|
-
title: "Customer intake",
|
|
44
|
-
pages: [{ id: "details", blocks: [
|
|
45
|
-
{ id: "email", kind: "email", label: "Work email", required: true },
|
|
46
|
-
{ id: "goal", kind: "long_text", label: "What should we know?" },
|
|
47
|
-
] }],
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
export function CustomerIntake() {
|
|
51
|
-
return <FilloForm form={form} client={client} />;
|
|
52
|
-
}
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
Do not move Fillo JSX schema authoring into a Server Component. Keep host-side
|
|
56
|
-
navigation or analytics in `onSubmitted`; the response is already stored when
|
|
57
|
-
that callback runs.
|
|
58
|
-
|
|
59
|
-
## DOM, Vue, Svelte, Astro, and browser apps
|
|
60
|
-
|
|
61
|
-
Use the framework lifecycle to mount after the target exists and destroy on
|
|
62
|
-
unmount:
|
|
63
|
-
|
|
64
|
-
```ts
|
|
65
|
-
import { renderForm } from "@usefillo/dom";
|
|
66
|
-
import "@usefillo/dom/styles.css";
|
|
67
|
-
|
|
68
|
-
const instance = renderForm("#customer-intake", {
|
|
69
|
-
formId: "customer-intake",
|
|
70
|
-
onSubmitted: (responseId) => console.log("response", responseId),
|
|
71
|
-
onError: (error) => console.error(error.status, error.message),
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
// Call from onBeforeUnmount, onDestroy, or the host cleanup callback.
|
|
75
|
-
instance.destroy();
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
In a Svelte component's scoped `<style>`, wrap renderer selectors with
|
|
79
|
-
`:global(...)` (for example, `.settings-card :global(.fillo-control)`) so the
|
|
80
|
-
compiler does not scope them away from the imperatively inserted DOM.
|
|
81
|
-
|
|
82
|
-
For a custom element, register it once in browser code:
|
|
83
|
-
|
|
84
|
-
```ts
|
|
85
|
-
import { registerFilloElement } from "@usefillo/dom";
|
|
86
|
-
import "@usefillo/dom/styles.css";
|
|
87
|
-
|
|
88
|
-
registerFilloElement();
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
```html
|
|
92
|
-
<fillo-form form-id="customer-intake"></fillo-form>
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
Listen for `fillo-change`, `fillo-submit`, and `fillo-error` when the host needs
|
|
96
|
-
custom event handling. Use `createFormController()` only when the host will
|
|
97
|
-
render every field, validation message, page action, loading state, and success
|
|
98
|
-
state itself.
|
|
99
|
-
|
|
100
|
-
## Uploads and storage
|
|
101
|
-
|
|
102
|
-
Model the requirement with a real `file_upload` field:
|
|
103
|
-
|
|
104
|
-
```ts
|
|
105
|
-
const supportEvidence = defineForm({
|
|
106
|
-
id: "support-evidence",
|
|
107
|
-
title: "Send support evidence",
|
|
108
|
-
pages: [{ id: "issue", blocks: [
|
|
109
|
-
{ id: "details", kind: "long_text", label: "What happened?", required: true },
|
|
110
|
-
{
|
|
111
|
-
id: "evidence",
|
|
112
|
-
kind: "file_upload",
|
|
113
|
-
label: "Screenshots, logs, or recordings",
|
|
114
|
-
maxFiles: 5,
|
|
115
|
-
maxFileSizeMb: 5000,
|
|
116
|
-
accept: ["image/*", "video/*", ".txt", ".log", ".zip"],
|
|
117
|
-
},
|
|
118
|
-
] }],
|
|
119
|
-
});
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
Before publish, connect Drive, Box, or supported S3-compatible storage in the
|
|
123
|
-
workspace. The renderer uploads provider bytes browser-direct and the server
|
|
124
|
-
verifies completion. Do not build another upload endpoint. Fillo keeps response
|
|
125
|
-
data, upload metadata, and the storage reference; customer storage holds the
|
|
126
|
-
provider bytes. Treat filenames and contents as untrusted, and do not turn an
|
|
127
|
-
authenticated file URL into a public link.
|
|
128
|
-
|
|
129
|
-
Verify with one safe file. Confirm both the response reference and the object in
|
|
130
|
-
the connected storage. Exercise resume only when the provider supports it.
|
|
131
|
-
|
|
132
|
-
## Webhook verification and retry dedupe
|
|
133
|
-
|
|
134
|
-
Configure the webhook on the form, store its signing secret on the host server,
|
|
135
|
-
and verify the raw bytes before parsing:
|
|
136
|
-
|
|
137
|
-
```ts
|
|
138
|
-
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
139
|
-
import express from "express";
|
|
140
|
-
|
|
141
|
-
const app = express();
|
|
142
|
-
|
|
143
|
-
app.post("/hooks/fillo", express.raw({ type: "application/json" }), async (req, res) => {
|
|
144
|
-
const expected = createHmac("sha256", process.env.FILLO_WEBHOOK_SECRET!)
|
|
145
|
-
.update(req.body)
|
|
146
|
-
.digest("hex");
|
|
147
|
-
const given = req.get("X-Fillo-Signature") ?? "";
|
|
148
|
-
const valid = given.length === expected.length &&
|
|
149
|
-
timingSafeEqual(Buffer.from(given), Buffer.from(expected));
|
|
150
|
-
if (!valid) return res.sendStatus(401);
|
|
151
|
-
|
|
152
|
-
const deliveryId = req.get("X-Fillo-Delivery-Id");
|
|
153
|
-
if (!deliveryId) return res.sendStatus(400);
|
|
154
|
-
|
|
155
|
-
const event = JSON.parse(req.body.toString("utf8"));
|
|
156
|
-
// insertOnce commits the payload to a durable inbox. Duplicate ids are
|
|
157
|
-
// no-ops; storage errors throw so Fillo retries. A worker drains the inbox.
|
|
158
|
-
await deliveryInbox.insertOnce({ deliveryId, event });
|
|
159
|
-
return res.sendStatus(200);
|
|
160
|
-
});
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
Delivery is at least once. Dedupe on `X-Fillo-Delivery-Id`, not `response.id`:
|
|
164
|
-
one living response can emit both `response.created` and `response.updated`.
|
|
165
|
-
Acknowledge only after the inbox commit. Do not claim an id and then start
|
|
166
|
-
uncommitted work: a crash between those steps loses the retry. A direct
|
|
167
|
-
database-only handler can instead record the delivery id and its domain mutation
|
|
168
|
-
in one transaction. Test one invalid signature and one replayed valid delivery
|
|
169
|
-
before handoff.
|
|
170
|
-
|
|
171
|
-
## Verified respondents and save and resume
|
|
172
|
-
|
|
173
|
-
Pass the host application's stable user id with the renderer. An identity
|
|
174
|
-
without a valid hash is display metadata, not authentication.
|
|
175
|
-
|
|
176
|
-
Compute the hash only on the host server:
|
|
177
|
-
|
|
178
|
-
```ts
|
|
179
|
-
import "server-only";
|
|
180
|
-
import { createHmac } from "node:crypto";
|
|
181
|
-
|
|
182
|
-
export function respondentHash(userId: string) {
|
|
183
|
-
return createHmac("sha256", process.env.FILLO_IDENTITY_SECRET!)
|
|
184
|
-
.update(userId)
|
|
185
|
-
.digest("hex");
|
|
186
|
-
}
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
Then pass the server-computed value to the client renderer:
|
|
190
|
-
|
|
191
|
-
```tsx
|
|
192
|
-
<FilloForm
|
|
193
|
-
formId="account-feedback"
|
|
194
|
-
respondent={{ id: user.id, email: user.email, name: user.name, hash }}
|
|
195
|
-
/>
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
Enable `settings.saveProgress` for the form. The default renderers autosave and
|
|
199
|
-
restore progress. Headless UI can inspect `resumedDraft` and call
|
|
200
|
-
`flushDraft()` or `resetDraft()`. Cross-device resume and trusted
|
|
201
|
-
respondent-keyed update behavior require a valid server-computed hash. Test a
|
|
202
|
-
valid hash, an invalid hash, reload resume, and cross-device resume separately.
|
|
203
|
-
|
|
204
|
-
## Response destinations
|
|
205
|
-
|
|
206
|
-
The embed collects and stores the response first. Keep destination credentials
|
|
207
|
-
and connections out of client code:
|
|
208
|
-
|
|
209
|
-
- Google Sheets and Notion are connected at workspace level, then enabled in
|
|
210
|
-
the form's response settings.
|
|
211
|
-
- Zapier uses its server-side Fillo connection and form trigger.
|
|
212
|
-
- Email notifications and respondent receipts are form settings.
|
|
213
|
-
- A custom backend uses the signed webhook recipe above.
|
|
214
|
-
|
|
215
|
-
Submit one uniquely labeled response, confirm it in Fillo, then confirm a
|
|
216
|
-
downstream record with the expected raw and formatted values. Exercise or
|
|
217
|
-
inspect the destination's duplicate behavior for retried delivery. Do not write
|
|
218
|
-
a parallel browser-side destination client.
|
|
219
|
-
|
|
220
|
-
## CLI-owned JSON
|
|
221
|
-
|
|
222
|
-
Follow an existing handoff before starting a new setup flow. For an intentional
|
|
223
|
-
new preview workspace, use only an email the user explicitly supplied:
|
|
224
|
-
|
|
225
|
-
```bash
|
|
226
|
-
npx @usefillo/cli@latest init --email user@example.com
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
For an existing account, use `npx @usefillo/cli@latest login`. Review JSON
|
|
230
|
-
before pushing it with a stable handle:
|
|
231
|
-
|
|
232
|
-
```bash
|
|
233
|
-
npx @usefillo/cli@latest push form.json --handle customer-intake
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
Authenticated `push` publishes by default. `--draft` changes the form to draft
|
|
237
|
-
and can take a live form offline; it is not a staged version beside production.
|
|
238
|
-
Never read or report `~/.fillo/config.json`.
|
|
239
|
-
|
|
240
|
-
## Common failures
|
|
241
|
-
|
|
242
|
-
| Symptom | First checks |
|
|
243
|
-
| --- | --- |
|
|
244
|
-
| Published-id embed returns 404 | Confirm the id or slug and that the form is published. Do not reveal whether an inaccessible draft exists. |
|
|
245
|
-
| Code-defined form renders but cannot save | Pass a client, keep a stable id, confirm the publishable key belongs to the intended workspace, and check its expected-origin restriction. |
|
|
246
|
-
| 429 response | Respect `FilloError.retryAfterSec`; do not loop immediate retries. |
|
|
247
|
-
| File form cannot publish | Connect supported storage and verify the provider before retrying publish. |
|
|
248
|
-
| DOM form duplicates after navigation | Mount after the target exists and call `destroy()` in cleanup. |
|
|
249
|
-
| React context or hook error | Keep hooks inside `FilloForm` or `FilloProvider` and check for two installed copies of `@usefillo/react`. |
|
|
250
|
-
| Fillo JSX fails in Next.js | Move schema JSX to a `"use client"` module; use object-form `defineForm()` for framework-agnostic schema. |
|
|
251
|
-
| Webhook signature never matches | Capture raw bytes before JSON middleware and compare the hex HMAC in constant time. |
|
|
252
|
-
| Verified identity is anonymous | Hash the exact stable `respondent.id` string on the server and use the secret from the same workspace. |
|
|
253
|
-
|
|
254
|
-
Do not claim a successful publish, upload, submission, webhook, or destination
|
|
255
|
-
delivery unless the environment produced direct evidence.
|