Kapable starter template: Survey / Form Builder (auth, data dynamic tables)
  • TypeScript 88.4%
  • CSS 8.7%
  • JavaScript 2.9%
Find a file
hgeldenhuys c70735c1c5 fix(forms): drop client-set updated_at — it's system-managed by kapable-data
Chrome-verify on the live origin surfaced a 400 on Publish: 'column updated_at
is system-managed (stamped from request context) and cannot be set by clients;
remove it from the payload'. updateFormMeta and publishForm were sending
updated_at in their patchRow payloads. kapable-data stamps updated_at itself.
Removed it from both patches (the service bumps it). App-local fix.

Re-verified the full publish path against live kapable-data: 'Form published.'
with table forms_responses_<formId>, columnCount=2 (DYNAMIC, derived).
2026-06-21 17:02:35 -04:00
app fix(forms): drop client-set updated_at — it's system-managed by kapable-data 2026-06-21 17:02:35 -04:00
public feat(forms): Survey / Form Builder starter app 2026-06-21 16:42:59 -04:00
.gitignore Add .gitignore 2026-06-21 20:41:56 +00:00
.npmrc feat(forms): Survey / Form Builder starter app 2026-06-21 16:42:59 -04:00
bun.lock feat(forms): Survey / Form Builder starter app 2026-06-21 16:42:59 -04:00
DESIGN.md feat(forms): Survey / Form Builder starter app 2026-06-21 16:42:59 -04:00
package.json feat(forms): Survey / Form Builder starter app 2026-06-21 16:42:59 -04:00
react-router.config.ts feat(forms): Survey / Form Builder starter app 2026-06-21 16:42:59 -04:00
README.md feat(forms): Survey / Form Builder starter app 2026-06-21 16:42:59 -04:00
seed.ts feat(forms): Survey / Form Builder starter app 2026-06-21 16:42:59 -04:00
tsconfig.json feat(forms): Survey / Form Builder starter app 2026-06-21 16:42:59 -04:00
vite.config.ts feat(forms): Survey / Form Builder starter app 2026-06-21 16:42:59 -04:00

Forms — Survey / Form Builder

A Kapable v2 starter app (#13 in the Top-20). Build a form from a field palette, watch a live preview take shape, publish it to get a shareable public link, collect responses, and read them as per-field counts plus a raw responses table.

The differentiated capability it demos: on publish, the app dynamically creates a typed table in kapable-data (forms_responses_<formId>) whose columns are derived from the fields the user built — short-text/long-text/email → text, number → float, single-select → text, multi-select → json. The schema isn't known until the form is composed. See DESIGN.md.

  • Routes: / (forms list) · /forms/:id/edit (builder + live preview) · /f/:id (public, unauthenticated fill view) · /forms/:id/results (dashboard).
  • Data: forms_forms, forms_fields (static) + one dynamic forms_responses_<formId> per published form. All via the server-only org key KAPABLE_ORG_KEY (an sk_live_ key for org kapable-starters) — never in the client bundle. The public fill route is anonymous to end-users but writes server-side through that key.
  • Gotcha encoded: kapable-data columns are NOT NULL by default — any column that can be blank is declared nullable: true (see app/lib/forms.server.ts).

Built on the Kapable starter kit (skeleton docs below).


Kapable Starter Kit

The on-brand fork-point for Kapable v2 apps. Every starter/template app forks this repo, then builds its own routes on top.

What you get out of the box:

  • React Router 7 SSR + TypeScript, with the /health resource route the deploy pipeline gates on (keep it).
  • Kapable theme system — the canonical --kp-* token set from kapable-theme.css, [data-theme="dark|light"] on <html>, FOUC-free first paint (the in-<head> script + kapable-theme.js), and a theme toggle wired to window.KapableTheme.cycle(). Tailwind v4 is bridged to the same tokens (bg-surface, text-fg, border-line, text-accent, …).
  • @kapable/sdk wired server-side in app/lib/kapable.server.ts with explicit per-service base URLs (never the bare api.kapable.ai default for everything). Session (kses_) read from the request; tokens never reach the client bundle.
  • Error → toast on every fetch (sonner), a skip-to-content link, a global :focus-visible ring, 44px touch targets, and a branded error boundary.

Develop

bun install
bun run dev

Build + run (what the platform does)

bun run build
bun run start   # react-router-serve on :3000

Calling the SDK

All SDK calls are server-side (loaders/actions). Always pin the client to the service you're calling — each has its own base URL:

import { kapable } from "~/lib/kapable.server";

export async function loader({ request }) {
  const data = kapable("data", { request });        // data.kapable.ai
  const { data: rows } = await data.data.listRows("my_table");
  return { rows };
}
Service Base URL Notes
auth, ai, data, comms, board, billing, store, harbor dedicated subdomain own Caddy host block
wiki, knowledge, secrets, warrant, projects, herald https://api.kapable.ai path-routed gateway (no dedicated subdomain — the subdomain falls through to launchpad)

api.kapable.ai reaches every service via /v1/* and is the safe universal choice. Prefer the caller's kses_ session over a bare sk_live_ for writes.

Add a route

  1. Create app/routes/<name>.tsx.
  2. Register it in app/routes.ts — RR7 404s unregistered routes silently.

Theme tokens

Defined in public/kapable-theme.css. Use var(--kp-bg), --kp-surface, --kp-text, --kp-accent, --kp-border, --kp-danger/success/warn/info, or the Tailwind aliases. A single data-theme flip recolors everything.