Kibble quickstart
Kibble puts a form on a site that a coding agent builds, and handles everything after submit. This page is the whole happy path: about two minutes, and no human is needed until the claim at the end. There is no push step to remember: the framework plugin registers forms while you develop and when the site builds.
Started in the Kibble dashboard? If you created the form under New form in Kibble, open the form's page and click Copy prompt. Give it to your coding agent: it starts with
npx kibbleforms init --link <code>, which connects this repo to that site (already claimed, no sandbox) and lists the form files to write. Then continue at step 2.
1. Set up the project
npx kibbleforms init --agent <your name>
npm install kibbleforms
Agents: name yourselves with --agent (for example claude-code, cursor, codex), so the site's owner can see which agent set it up.
In a monorepo, run init in the app's folder (the one with next.config, vite.config or astro.config); kibble.json lives there and every kibble command runs from there. Run it at a monorepo root and it stops, names the app folder, and leaves a link code unused.
init creates a sandbox project and sets up the repo:
kibble.json(public project id), the secret key in.kibble/secret(gitignored),kibble/client.ts,kibble/forms/.- The framework plugin: wraps
next.config.*inwithKibble(...), or addskibble()tovite.config.*/astro.config.*. It works alongside other config wrappers (withSentryConfig,withMDXand the like) in any order. If the config has a shape it can't edit safely, it prints the two lines to add. - The CSS: next to
@import "tailwindcss"in your global stylesheet it addskibbleforms/theme.cssand the@sourceline, pluskibbleforms/themes/shadcn.csson shadcn/ui sites that use shadcn's color variables (it checkscomponents.jsonand that your CSS defines them; a site with its own tokens keeps Kibble's defaults). Without Tailwind it tells you to importkibbleforms/styles.css. - A short "Kibble forms" section in
AGENTS.mdorCLAUDE.md.
It prints a claim link. Keep it; the human needs it in step 5. Sandbox projects accept submissions from localhost only.
2. Write the schema
kibble/forms/waitlist.ts:
import { defineForm } from 'kibbleforms/schema';
export const waitlist = defineForm({
slug: 'waitlist',
name: 'Waitlist',
pages: [
{
fields: [
{ key: 'email', type: 'email', label: 'Work email', autocomplete: 'email', required: true },
{ key: 'name', type: 'short_text', label: 'Name', autocomplete: 'name' },
{ key: 'role', type: 'dropdown', label: 'Role', options: ['Founder', 'Engineer', 'Designer', 'Other'] },
{ key: 'consent', type: 'consent', label: 'Email me when there is news', required: true },
],
},
],
settings: {
success: { type: 'message', title: 'You are on the list', message: 'We will email you when your spot opens.' },
},
});
See Form schema for every field type and constraint. Notifications, allowed domains and triage are set in Kibble, not here (npx kibble destinations add email [email protected] --all-forms, kibble domains, kibble review).
3. Render it
// components/waitlist-form.tsx
'use client';
import { Form } from 'kibbleforms';
import { kibble } from '@/kibble/client';
import { waitlist } from '@/kibble/forms/waitlist';
export function WaitlistForm() {
return <Form schema={waitlist} client={kibble} />;
}
Then use <WaitlistForm /> in any page. In Next.js (App Router) the form has to live in a client component like this one ('use client' at the top): kibble, the client, can't be passed from a server component. Pass it as client, or wrap the app once in KibbleProvider; without either, <Form> throws and says so.
<Form> with no children renders every field, validation, the submit button and the success state; multi-page schemas render one step at a time. It inherits your site's font and follows its dark mode. Adjust it with theme, size, layout and classNames (Theming) or compose it yourself (Components).
Optional, so ask the site owner first: <KibbleAttribution /> (from kibbleforms), added once to the root layout, lets each response say which page on the site led to the form and where the visit started. It keeps a small record of the visit in the visitor's browser tab (sessionStorage). It takes no props, so a server layout can render it. See Attribution and hidden fields.
If the site sets a Content Security Policy, add the Kibble API to connect-src (https://api.kibbleforms.com, and http://localhost:4000 against a local API).
4. Run the dev server and test
npm run dev
npx kibble test waitlist
While the dev server runs, saving a file in kibble/forms/ registers that form (one line in the terminal: [kibble] waitlist: v2 registered (development), or the validation issues). kibble test sends a sample submission end to end and reads it back. Submit the form on localhost yourself too; npx kibble submissions waitlist shows what landed.
No dev server (a script, CI)? npx kibble push does the same registration by hand.
5. Claim, then give the host the key
Tell the human the form works locally and share the claim link (npx kibble claim prints it again). They sign in with Google, GitHub or a code sent to their email, accept the terms and name the production domains. Then, once:
npx kibble env vercel # or: npx kibble env netlify, npx kibble env print
This puts KIBBLE_SECRET_KEY into the host's build environment (on stdin, never echoed). From then on every deploy registers its forms during the build, tagged production or preview from the host's environment; production builds set the questions the dashboard shows.
Without the key a build still checks: it fails only if visitors would be rejected (the project is unclaimed, or a form was never registered) and warns when questions changed since the last registration (those responses are accepted and flagged). npx kibble sync runs the same step in any other CI.
What happens after submit
- The server validates every submission against the registered schema and normalizes values (trimmed, lowercased emails, E.164 phone numbers).
- Spam is scored (honeypot, time to submit, disposable emails, link stuffing, optional Turnstile) and goes to a spam view. Nothing is discarded.
- Submissions are stored by stable field id, so renaming a field with
renamedFromkeeps its data together. - Completed submissions go to verified email addresses, Slack (connect a channel with "Add to Slack", or
kibble destinations add slack --form <slug>for a link a person approves), your database and signed webhooks. - Multi-step progress is saved at each step as a partial submission. Partials never trigger notifications.