softr-vibe-coding 1.11.2 → 2.1.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.
@@ -15,21 +15,29 @@ Run through this catalog before delivering any block. Every row is a violation o
15
15
  | `q.select()` for REST API fields | Access raw API response directly |
16
16
  | Hardcoding API keys for connected API | Use `useProxyFetch` -- key stays server-side |
17
17
  | Using `q.select({})` to dump all fields on Softr Database | Returns record IDs with empty `fields: {}`. Look up field IDs in Studio's Data tab, or use the Softr DB REST API with `fieldNames=true` |
18
+ | Building an invisible helper block + `window` globals just to read a second table | A block can connect to **multiple data sources**. Declare them with `datasource.define({ alias: "uuid" })` and pass `from: ds.alias` on every hook. One block instead of two, no page-order dependency, no mount-timing race. See [datasources/multi-datasource.md](../datasources/multi-datasource.md). Helper blocks remain correct for genuinely cross-*block* jobs (triggering another block, sharing computed state) — just not for plain multi-table reads |
19
+ | Omitting `from:` on a hook when the block has more than one datasource | Throws at runtime. `from:` is optional ONLY when exactly one source is connected — then hooks default to it. Applies to `useRecords`, `useRecord`, `useLinkedRecords`, `useFieldOptions`, `useMetric`, `useChartData`, `useRecordCreate`, `useRecordUpdate`, `useRecordDelete`. NOT to `useUpload` / `useCurrentRecordId`, which are app-level. `useProxyFetch` has the same multi-datasource requirement but takes the alias as its **argument** — `useProxyFetch(ds.store)` — not as `from:` |
20
+ | Hoisting datasource ids into constants: `datasource.define({ people: PEOPLE_DS_ID })` | Fails to compile — *"datasource.define() object values must be string literals."* Softr statically analyses the call, same as `q.select()`. Keep the UUIDs **inline**: `datasource.define({ people: "74d2cbfd-…" })`. Fails fast with an explicit message, but hoisting magic strings is a strong reflex — resist it here |
21
+ | Asking Studio's AI chat "what are the datasource IDs?" and pasting the answer | **It fabricates them.** Verified July 2026: asked three times for the same three connected tables, it gave three different UUID sets, once reusing a previously-mentioned table's uuid for a different table — all confidently worded, none hedged. Ask it to **write code** instead (*"write a datasource.define call covering every connected source, plus one useRecords per source, code only"*) — scaffolding is bound to the real connections. Then RUN it: real rows under each heading proves each alias maps where you think. A wrong uuid fails safe (matches nothing → error); a *swapped pair* of valid uuids does not |
18
22
 
19
23
  ## Mutations
20
24
 
21
25
  | Anti-Pattern | Correct Approach |
22
26
  |---|---|
23
27
  | `.mutate({ id: ... })` | `.mutate({ recordId: ... })` -- `id` causes 404 |
24
- | `updateRecord.mutate({ recordId, status: "..." })` — flat payload | `updateRecord.mutate({ recordId, fields: { status: "..." } })` — fields **must** be nested. The flat form can run at runtime but Softr's Action parser doesn't see field references inside it, so the derived Update Action never gets created. The hook's `enabled` stays `false`, the Save button never lights up, the Actions tab in Studio shows "No actions used in this block yet" — all with no error, no warning. The symptom is a button that does nothing and a console log showing `enabled: false, error: null, status: "idle"`. Use the nested form for EVERY mutate call, even single-field updates. See [datasources/writing.md](../datasources/writing.md#critical-two-parser-requirements-for-userecordupdate) |
25
- | `updateRecord.mutateAsync(payload).then(...).catch(...)` | `updateRecord.mutate(payload, { onSuccess, onError })` Softr's Action parser scans for the **literal `.mutate(` token** to detect mutation call sites. `.mutateAsync()` runs fine at runtime (it's just a Promise wrapper) but the parser ignores it no Action gets derived, `enabled` stays `false`, the Actions tab shows "No actions used in this block yet". This is the same silent-failure mode as the flat-payload anti-pattern, and the two often appear together because devs reach for `mutateAsync` to chain `.then()/.catch()`. The fix is to use `.mutate(payload, { onSuccess, onError })` per-call handlers go in the second argument (react-query convention). Verified by direct experiment, May 2026. See [datasources/writing.md](../datasources/writing.md#critical-two-parser-requirements-for-userecordupdate) |
28
+ | `updateRecord.mutate({ recordId, status: "..." })` — flat payload | `updateRecord.mutate({ recordId, fields: { status: "..." } })` — fields **must** be nested. The flat form can run at runtime but Softr's Action parser doesn't see field references inside it, so the derived Update Action never gets created. The hook's `enabled` stays `false`, the Save button never lights up, the Actions tab in Studio shows "No actions used in this block yet" — all with no error, no warning. The symptom is a button that does nothing and a console log showing `enabled: false, error: null, status: "idle"`. Use the nested form for EVERY update call, even single-field updates (create payloads are flat — see the dedicated row below). See [datasources/writing.md](../datasources/writing.md#critical-the-userecordupdate-payload-shape-and-the-retired-mutate-only-rule) |
29
+ | Sequencing multi-row saves with nested `.then()/.catch()` chains, or firing the rows in parallel | `await hook.mutateAsync(row)` per row, in order header first, then lines; stop on the first failure with renderable retry state; never re-issue completed writes. `mutateAsync` is **fully supported** on the current platform (verified live 2026-08-25 this supersedes the May 2026 finding that the Action parser only recognized the literal `.mutate(` token; that limitation is gone, and is worth checking only when maintaining an old app whose Action refuses to derive). Full queue pattern: [datasources/writing.md](../datasources/writing.md#sequential-multi-row-writes-mutateasync) |
26
30
  | Assuming `mutation.enabled === false` always means a code bug | `enabled` is BOTH a parser signal AND a permissions signal. Per the official Softr docs, "`enabled` reflects user permissions." When code looks correct and the Actions tab shows the action listed, the cause is almost always permissions. Test by switching "Preview as" in Studio to an Owner / admin; if it then works, the issue is permissions. Three places to check, in priority order: (1) the block's **Visibility** tab (right panel), (2) **Studio → Users → Data Restrictions → Global data restrictions** — an app-wide layer that easily gets overlooked because it's hidden under Users (not on the block); it overlays every block in the app, and a single restriction on the target table will silently disable every mutation against that table for the affected user group, (3) the data-source PAT scope — if granted read-only, every write fails regardless of UI permissions. See [datasources/writing.md](../datasources/writing.md#how-actions-work-studios-actions-tab) |
27
31
  | `deleteRecord.mutate({ id: r.id })` | `deleteRecord.mutate(r.id)` -- just the string |
28
- | `var { mutateAsync } = useRecordUpdate({...})` | `var updateRecord = useRecordUpdate({...})` -- keep full object for `.enabled`, `.status`, `.reset()` |
32
+ | `var { mutateAsync } = useRecordUpdate({...})` -- destructuring the mutate function off the hook | `var updateRecord = useRecordUpdate({...})` -- keep the full object so `.enabled`, `.status`, `.reset()` stay reachable (using `.mutateAsync` itself is fine) |
29
33
  | Not calling `refetch()` after mutations | Always `refetch()` in `onSuccess` |
30
34
  | Including read-only fields (formula / rollup / aiText / lookup / createdTime / lastModifiedTime / autoNumber) in the `fields` q.select passed to `useRecordCreate` or `useRecordUpdate` | Softr's Action parser silently rejects the **entire** create/update Action — not just the bad alias. Same all-or-nothing failure mode as a renamed/missing column: Studio's Actions tab shows "No actions used in this block yet", `createRecord.enabled` / `updateRecord.enabled` stays `false`, `.mutate()` calls dispatch but resolve to "not yet ready", every OTHER writable field in the same q.select is also lost. Reads handle these field types fine — only the write q.select chokes. Fix: split into separate q.selects per the Three Mappings Pattern (`useRecord` / `useRecords` gets the full select with read-only fields; `useRecordCreate` / `useRecordUpdate` gets a writable-only subset). Diagnostic when the symptom shows up: same bisection procedure as the renamed-column case — strip the write q.select to a known-writable minimum, then add fields back in halves until the Action drops out. Verified 2026-05-22: `blocks/wig-details/wig-details-page.jsx` shared one `wigSelect` between `useRecord` and `useRecordUpdate`; the select included `Wig Tag ID` (formula), `Total client price` (rollup), `Total worker pay` (rollup), `Instrucciones` (aiText) — Update Action stayed disabled until those four read-only fields were lifted out into a separate write-only select. See [datasources/airtable.md](../datasources/airtable.md#maintainability-gotcha) and [datasources/writing.md](../datasources/writing.md). |
31
- | Linked record as plain string | Must be `[{ id: "..." }]` array |
32
- | Writing dropdown values as `{ id, label }` objects (the read shape) | Write the option UUID as a plain string -- e.g. `status: "822b8d69-..."`, not `status: { id: "...", label: "..." }` |
35
+ | Linked record as bare string, or `[{ id }]` objects on Softr Database | Array of record-id **strings**: `familyLink: [familyId]` — verified live 2026-08-25 on Softr DB. (The `[{ id }]` object shape was the May 2026 verified form on Airtable-backed blocks; try it if a string-array write fails there.) See [datasources/writing.md](../datasources/writing.md#linked-record-format-for-mutations) |
36
+ | Writing dropdown values as `{ id, label }` objects (the read shape) or hunting for option UUIDs | Write the option **LABEL string**, exactly matching a defined choice e.g. `status: "Active"` (verified live 2026-08-25; supersedes the April 2026 UUID rule). Keep vocabularies as greppable constants or fetch live via `useFieldOptions` and write `option.label` |
37
+ | Writing a formatted phone value (`(212) 555-0100`, `212-555-0100`) to a PHONE field | Sanitize to unformatted international format — `+` followed by digits only (`+12125550100`) — before `mutate()`; some datasources (Monday.com, per the official guide) reject formatted values outright. Official sanitizer: `const sanitizePhone = (raw) => raw.replace(/[^\d+]/g, "").replace(/(?!^)\+/g, "");` See [datasources/writing.md](../datasources/writing.md#phone) |
38
+ | Wrapping a `useRecordCreate` payload in `{ fields: {...} }` (copied from an update call) | Create payloads are **FLAT** — `createRecord.mutate({ name: "Jane" })`. Only update payloads nest: `{ recordId, fields: {...} }`. The asymmetry is by design (verified 2026-08-25) |
39
+ | Passing a data hook's options through a variable or wrapper function: `useRecords(buildOpts())` | **Fails to compile** — the options object must be an inline literal at the call site (verified live 2026-08-25; hit in production, fixed by making the wrapper take the hook's *result* instead). Share `q.select` mappings between hooks, never whole options objects |
40
+ | Tightening Actions-tab permissions before the block's final redeploy | Every code recompile **resets the auto-registered Actions to default permissions** (verified live 2026-08-25). Tighten permissions after the LAST redeploy, and re-check after any future one |
33
41
  | Treating Studio's Actions tab as a separately-managed configuration to keep in sync with code | Actions auto-derive from your `useRecordCreate`/`useRecordUpdate`/`useRecordDelete` + `q.select` on every save. The Actions tab is a read-only inspector; there is no manual delete control. To change an Action, change the code |
34
42
  | One alias in a write-side `q.select` referencing a renamed / non-existent Airtable column | Softr's Action parser silently rejects the **entire** create/update Action — not just the bad alias. Symptoms: Studio's Actions tab shows "No actions used in this block yet", `createRecord.enabled` / `updateRecord.enabled` stays `false`, `.mutate()` calls dispatch but resolve immediately to "not yet ready". Every OTHER field in the same `q.select()` is also lost, even the ones that map cleanly. Diagnostic: bisect the `q.select` — strip down to a known-good minimal set, confirm the Action appears in Studio, then add fields back in halves until it drops out. The culprit is in the last half added. Once narrowed to a single field, grep its name against the freshest Airtable schema export to catch the rename / trailing-space / case-mismatch. Verified 2026-05-21: a `"Photos"` column on Wigs was renamed to `"Before Photos"`, the helper that wrote `photos: "Photos"` had its entire Action disabled even though 11 other fields in the same `q.select` were fine. See [datasources/airtable.md](../datasources/airtable.md#maintainability-gotcha) |
35
43
 
@@ -47,7 +55,7 @@ Run through this catalog before delivering any block. Every row is a violation o
47
55
  | `import React from 'react'` | Named imports only |
48
56
  | Named export | `export default function Block()` |
49
57
  | Hook declared after conditional `return` | All hooks at top before any conditional `return` -- React error #310 |
50
- | `fetchNextPage()` in render body | Inside `useEffect` only -- in render = infinite loop |
58
+ | `fetchNextPage()` in render body | Never in the render body (render data update → re-render → infinite loop). Call from an event handler — the official Load More pattern: `<button onClick={() => fetchNextPage()} disabled={isFetching}>` — or a guarded `useEffect` for auto-load-all |
51
59
  | `useRef` for IDs used in `useMemo` | `useState` -- ref mutations don't trigger recomputation |
52
60
  | Defining a sub-component INSIDE the `Block()` function body | Define ALL sub-components at MODULE scope (above `export default function Block()`). Sub-components defined inside `Block()` get a brand-new function reference on every render, which makes React unmount/remount their entire DOM subtree every time `Block` re-renders. The user-visible symptom: **inputs lose focus after typing one character** (because each keystroke triggers a `setState` -> re-render -> the `<input>` is destroyed and recreated). Move `function FieldLabel`, `function TextInput`, `function ChipButton`, `function SectionCard`, etc. above `export default function Block()` so React sees stable component identity across renders. Closure-captured `Block`-internal state must be passed as props, not closed over. |
53
61
 
@@ -60,7 +68,9 @@ Run through this catalog before delivering any block. Every row is a violation o
60
68
  | Emojis in UI | lucide-react icons only |
61
69
  | `[&_svg]:opacity-0` on SelectTrigger | `<style>` + `data-fix-chevron` attribute (Softr bundler limitation) |
62
70
  | Relying on `custom-code-header.html` (Softr → Settings → Custom Code → Code inside header) to apply brand fonts/colors INSIDE a Vibe Coding block | Vibe Coding blocks render inside a shadow DOM. CSS custom properties (`--brand-*`) pierce that boundary, but `html, body { font-family: ... !important }` rules **do not** — `<html>` and `<body>` don't exist inside the shadow root. Apply brand fonts/colors at the block's **own outermost wrapper** via inline style: `style={{ fontFamily: "'Manrope', system-ui, sans-serif", color: BRAND_INK }}` on the outer `<div>` so every descendant inherits brand defaults. Override per-element with explicit inline `fontFamily` (e.g., `"'Fraunces', Georgia, serif"` on h1/h2). Google `<link>` tags in the page head DO load `@font-face` globally — the fonts are available inside shadow DOM, they just need to be applied. |
63
- | Painting `backgroundColor: BRAND_CANVAS` on a Vibe Coding block's outer wrapper when `custom-code-header.html` already sets `body { background-color: var(--brand-canvas) !important }` | Don't double-paint. If the body bg is already the brand canvas, the block leaves its own backgroundColor unset and the page bg shows through. Painting the same color twice produces a visible seam — Softr's content wrapper sits between `<body>` and the Vibe Coding block, and the two backgrounds composite slightly differently due to sub-pixel rendering, transparency stacking, or wrapper paddings. Set fontFamily and color on the block's wrapper (those don't inherit cleanly through shadow DOM), but **leave backgroundColor unset** — let the page bg flow through. The exception: if the block needs a brand-tinted *section* (e.g., a card-style admin shell that's different from the page bg), paint that bg explicitly on its specific container, not on the outer wrapper. |
71
+ | Painting `backgroundColor: BRAND_CANVAS` on a Vibe Coding block's outer wrapper when `custom-code-header.html` already sets `body { background-color: var(--brand-canvas) !important }` | Don't double-paint. If the body bg is already the brand canvas, the block leaves its own backgroundColor unset and the page bg shows through. Painting the same color twice produces a visible seam — Softr's content wrapper sits between `<body>` and the Vibe Coding block, and the two backgrounds composite slightly differently due to sub-pixel rendering, transparency stacking, or wrapper paddings. Set fontFamily and color on the block's wrapper (those don't inherit cleanly through shadow DOM), but **leave backgroundColor unset** — let the page bg flow through. The exception: if the block needs a brand-tinted *section* (e.g., a card-style admin shell that's different from the page bg), paint that bg explicitly on its specific container, not on the outer wrapper. **Second exception — DARK brands: see the next row.** |
72
+ | Relying on the page background on a **dark** brand, and shipping a block whose own canvas is unpainted | The row above assumes a light app whose page bg is already correct. On a dark brand it inverts: `custom-code-header.html`'s `body { background-color: #000 !important }` does NOT cross the shadow-DOM boundary, and a default Softr page is white — so a block with white text, a white-only logo, or a white primary button renders **invisibly on white**. Verified July 2026: a black-canvas feedback form shipped with its wordmark and its Submit button both white-on-white; the button was there and clickable, just unseeable. On a dark brand, paint `backgroundColor` on the block's own outer wrapper AND set the Softr page background to the same value — two identical pure blacks composite with no seam, so the double-paint concern above doesn't bite. Painting it in the block also keeps it correct if the custom-code snippet is ever removed. |
73
+ | Setting only `html, body { background }` in `custom-code-header.html` and expecting the app to change colour | Softr paints the same page fill on **four stacked layers**: `html`, `body`, `#page-content`, and a **class-less wrapper div** nested inside `#page-content`. Styling one gets covered by the ones above it, so `body` alone appears to do nothing. Paint the backdrop on `html`, then clear the duplicates: `body, #page-content { background: transparent }` plus `#page-content div:not(.softr-topbar):not(.softr-topbar *)`. The `:not()` exclusion is required — the nav renders inside `#page-content` and that id's specificity out-ranks `.softr-topbar` rules, so a blanket clear silently flattens the dropdown panel. Full recipe in [native-chrome-styling.md](native-chrome-styling.md). |
64
74
  | `document.getElementById(...)` / `document.querySelector(...)` to find an element inside the block — for example, a hidden `<input type="file">` triggered by a visible "Upload" button via `getElementById('myInput').click()` | Vibe Coding blocks render inside a shadow DOM. The global `document` traversal stops at the shadow boundary, so id/selector lookups for elements inside the block return `null`. The user-visible symptom is a control that does nothing — no error, no file picker, no focus, no scroll — because the chained `.click()` / `.focus()` / `.scrollIntoView()` was called on `null`. Use a **React `useRef`** instead: `var inputRef = useRef(null)`, then `<input ref={inputRef} />` and `<button onClick={function() { if (inputRef.current) inputRef.current.click(); }}>`. Refs hold direct node references and don't depend on DOM traversal, so they work regardless of which DOM tree the node lives in. This applies to every "trigger a hidden element" pattern: hidden file inputs, programmatic focus, scroll-into-view, `.click()` on a non-visible button. |
65
75
  | Using `window.addEventListener("beforeunload", ...)` as the only unsaved-changes guard in a form block | Softr is a SPA. Internal nav (Softr's nav bar, sidebar links, `<NavigationAction>`) changes the route via the client-side router — `beforeunload` only fires on full page unload (tab close, refresh, external link), so the warning silently misses every in-app navigation. Use `useNavigationBlocker(isDirty)` from `@/lib/use-navigation-blocker` instead; it covers SPA nav AND browser unload with one API. Softr's Vibe Coding bundler often wires this automatically when a form is detected as dirty — you only need to add it manually for advanced cases (multi-step forms, custom dirty tracking, blocking on non-form state). See [common-patterns.md](common-patterns.md#navigation-blocker-for-unsaved-changes). |
66
76
  | Targeting Softr's hashed build classes (e.g. `.f8f11e5_m9ntthp`) when restyling the native header/nav from `custom-code-header.html` | Softr regenerates the hash on every deploy, so the rule silently dies. Target stable hooks: `.softr-topbar`, `.softr-nav-link`, `.softr-nav-button`, `.softr-nav-logo`, `#topbar-root`; for dropdown menus (no `softr-*` class) use the Radix/ARIA attrs `[role="menu"]` / `[role="menuitem"]` / `[role="group"]` / `[aria-expanded="true"]`, scoped under `.softr-topbar`. The native header is Softr chrome (main document), not a block — it can't be built as a Vibe Coding block. See [native-chrome-styling.md](native-chrome-styling.md). |
@@ -88,4 +98,4 @@ Run through this catalog before delivering any block. Every row is a violation o
88
98
  | Helper publishes only raw records | Also publish computed `filterOptions` as separate globals |
89
99
  | Refactoring helper shape without updating consumers | Version namespace OR update all consumers in same commit |
90
100
  | Helper B placed above A when B depends on A | A must be above B -- Softr renders top-to-bottom |
91
- | Using `useLinkedRecords` for rich foreign data | It only returns `{id, title}` -- use a helper block instead |
101
+ | Using `useLinkedRecords` for rich foreign data | It only returns `{id, title}` and silently ignores extra `select` fields. Connect the foreign table as a **second datasource** and read it with its own `useRecords({ from: ds.x })` — see [multi-datasource.md](../datasources/multi-datasource.md). (A helper block also works and is what older blocks do, but it's now the heavier option.) |
@@ -1,6 +1,6 @@
1
1
  # Common Patterns
2
2
 
3
- Small reusable patterns that come up across Vibe Coding blocks but don't warrant their own reference file. Each is a copy-pasteable snippet using the skill's preferred style (`var`, `function() {}`).
3
+ Small reusable patterns that come up across Vibe Coding blocks but don't warrant their own reference file. Each is a copy-pasteable snippet. The snippets below use legacy var-style (`var`, `function() {}`), which remains valid — write new blocks in modern TS (see SKILL.md Style Conventions).
4
4
 
5
5
  ## Table of Contents
6
6
 
@@ -5,6 +5,8 @@ Cross-block communication via `window` globals, the invisible helper block patte
5
5
  ## Table of Contents
6
6
 
7
7
  - [When You Need Helper Blocks](#when-you-need-helper-blocks)
8
+ - [When you still need a helper block](#when-you-still-need-a-helper-block)
9
+ - [The original rationale (historic)](#the-original-rationale-historic)
8
10
  - [Companion Field Helpers](#companion-field-helpers)
9
11
  - [Breadcrumb / Back Navigation](#breadcrumb--back-navigation)
10
12
  - [Multi-Table Access via Invisible Helper Blocks](#multi-table-access-via-invisible-helper-blocks)
@@ -17,16 +19,44 @@ Cross-block communication via `window` globals, the invisible helper block patte
17
19
 
18
20
  ## When You Need Helper Blocks
19
21
 
20
- A Vibe Coding block can only connect to **one source table**. When your main block needs data from a second table (e.g., a task detail block that also needs the Users table for a team picker), you cannot add another `useRecords` for a different table.
22
+ > ⚠️ **Read this first the main reason for helper blocks is gone.** A Vibe Coding block can now
23
+ > connect to **multiple data sources** and read each one with its own `useRecords({ from: ds.x })`.
24
+ > See [../datasources/multi-datasource.md](../datasources/multi-datasource.md). For a plain
25
+ > "my block also needs data from a second table", **use a second datasource, not a helper block** —
26
+ > it's one block instead of two, no `window` globals, no page-order dependency, no mount-timing races.
27
+ >
28
+ > Everything below still works, and the cross-block sections are still the right tool for the jobs
29
+ > listed under "When you still need a helper block". Historic blocks built on this pattern don't
30
+ > need rewriting.
21
31
 
22
- **Solution:** Drop an invisible helper block on the same Softr page, connected to the second table. It fetches records, publishes them to a `window` global, and dispatches a custom event. The main block reads the global and listens for updates. The helper returns `null` so it renders nothing in the published app.
32
+ ## When you still need a helper block
23
33
 
24
- `useLinkedRecords` always returns `{id, title}` only and silently ignores extra fields in `select`. This is the core reason the helper block pattern exists. If your task involves showing status, due dates, or any non-title field from a linked table, skip `useLinkedRecords` entirely and build a helper -- there is no other way to get those fields.
34
+ Reach for one only when the job is genuinely cross-block, not merely cross-table:
35
+
36
+ - **Cross-block communication** — one block triggering behaviour in another on the same page
37
+ (see [Triggering Actions in Other Blocks](#triggering-actions-in-other-blocks-bi-directional-events)).
38
+ - **Publishing computed state** — an expensive derivation several consumer blocks share, computed once.
39
+ - **Rich filter options** shared across multiple blocks on a page.
40
+
41
+ `useLinkedRecords` still returns `{id, title}` only and silently ignores extra fields in `select`.
42
+ That limitation is unchanged — but the fix is now a second datasource with its own `from:`, not a
43
+ helper block.
44
+
45
+ ## The original rationale (historic)
46
+
47
+ A Vibe Coding block used to connect to **one source table** only. When a block needed data from a
48
+ second table (e.g., a task detail block that also needs the Users table for a team picker), you
49
+ could not add another `useRecords` for a different table.
50
+
51
+ **The workaround:** drop an invisible helper block on the same Softr page, connected to the second
52
+ table. It fetches records, publishes them to a `window` global, and dispatches a custom event. The
53
+ main block reads the global and listens for updates. The helper returns `null` so it renders nothing
54
+ in the published app.
25
55
 
26
56
  Key rules:
27
57
  - The helper block must be on the **same Softr page** as the consumer -- `window` is page-scoped, globals don't cross pages.
28
58
  - **One helper block per foreign table.** Multiple helpers with distinct namespaces coexist fine on the same page.
29
- - **Read-only pattern.** Helpers expose foreign table data for lookups, pickers, and display only. Writes still happen from the main block via its own `useRecordUpdate` / `useRecordCreate`. If the main block needs to write to the helper's table, use a webhook or the Softr Database REST API (see [writing.md Cross-Table Operations](../datasources/writing.md#cross-table-operations)), not the helper.
59
+ - **Read-only pattern.** Helpers expose foreign table data for lookups, pickers, and display only. Writes still happen from the main block via its own `useRecordUpdate` / `useRecordCreate`. **Modern path for writing to another table:** connect it as a second datasource and write with `useRecordCreate({ from: ds.x })` — see [writing.md Cross-Table Operations](../datasources/writing.md#cross-table-operations); a webhook or the Softr Database REST API remain fallbacks for writes the hooks can't express. Never write through the helper.
30
60
 
31
61
  ## Companion Field Helpers
32
62
 
@@ -114,7 +144,9 @@ Key points:
114
144
  import { useRecords, q } from "@/lib/datasource";
115
145
  import { useEffect, useMemo, useRef } from "react";
116
146
 
117
- var select = q.select({ fullName: "fldXXX", isActive: "fldYYY" });
147
+ // q.select values: field NAMES for Airtable/Notion/Google Sheets, field IDs for Softr DB/Supabase.
148
+ // (An Airtable "fldXXX" ID here compiles and saves, then silently returns empty data.)
149
+ var select = q.select({ fullName: "Full Name", isActive: "Active" });
118
150
 
119
151
  function toOption(record) {
120
152
  return { id: record.id, title: record.fields.fullName || "" };
@@ -367,4 +399,4 @@ For per-user saved filter views on a list page:
367
399
  | Refactoring helper shape without updating consumers | Version namespace OR update all consumers in same commit |
368
400
  | Helper B placed above A when B depends on A | A must be above B -- Softr renders top-to-bottom |
369
401
  | `useRef` for IDs consumed by `useMemo` | Use `useState` -- ref mutations don't trigger recomputation |
370
- | Using `useLinkedRecords` for rich foreign data | It only returns `{id, title}` -- use a helper block instead |
402
+ | Using `useLinkedRecords` for rich foreign data | It only returns `{id, title}` and silently ignores extra `select` fields. Connect the foreign table as a **second datasource** and read it with its own `useRecords({ from: ds.x })` — see [multi-datasource.md](../datasources/multi-datasource.md). (A helper block also works and is what older blocks do, but it's now the heavier option.) |
@@ -8,7 +8,7 @@ Softr's native **List / Grid** blocks support category chips and *static* filter
8
8
 
9
9
  > **Sibling to [native-chrome-styling.md](native-chrome-styling.md).** That doc restyles Softr's *shell* (header/footer/nav) with global CSS. This one *drives and augments native blocks* with custom-code JS. Both reach the **main document**, never a block's shadow DOM — so this is a **Custom Code Static block**, not a Vibe Coding (JSX) block.
10
10
 
11
- House code style still applies in the `<script>`: `var`, `function(){}`, **no** optional chaining (`?.`) or nullish coalescing (`??`).
11
+ Modern JS is fine in the `<script>` — Custom Code blocks run unbundled in the browser, and the old `var`/`function(){}`/no-`?.` house style is retired everywhere (the Vibe Coding compiler accepts modern syntax too, verified 2026-08-25).
12
12
 
13
13
  ---
14
14
 
@@ -2,12 +2,14 @@
2
2
 
3
3
  Fast lookup for imports, hook signatures, field mapping syntax, and common patterns. Use when you already know what you need and just want the shape.
4
4
 
5
+ The current platform compiles TypeScript with modern syntax (`?.`, `??`, arrows, `const`, generics) — verified live 2026-08-25. Snippets below in `var` style predate that and remain valid; both styles compile.
6
+
5
7
  ## Imports
6
8
 
7
9
  ```jsx
8
- // DATASOURCE
9
- import { useRecords, useRecord, useRecordCreate, useRecordUpdate, useRecordDelete,
10
- useCurrentRecordId, useLinkedRecords, useUpload, useMetric, useChartData,
10
+ // DATASOURCE (add `datasource` when the block connects to more than one source)
11
+ import { datasource, useRecords, useRecord, useRecordCreate, useRecordUpdate, useRecordDelete,
12
+ useCurrentRecordId, useLinkedRecords, useFieldOptions, useUpload, useMetric, useChartData,
11
13
  q, metric } from "@/lib/datasource";
12
14
 
13
15
  // USER
@@ -41,13 +43,32 @@ var updateFields = q.select({ alias: "FIELD_ID" }); // writable only
41
43
  var createFields = q.select({ alias: "FIELD_ID" }); // writable only
42
44
  ```
43
45
 
46
+ ## Multiple datasources (static, outside component)
47
+
48
+ ```jsx
49
+ var ds = datasource.define({ // values MUST be inline string literals
50
+ people: "74d2cbfd-f2cb-4f5c-82d9-0d3a0651e531",
51
+ shifts: "ec7a6311-f6c3-4c99-881d-aae308148716",
52
+ });
53
+
54
+ useRecords({ from: ds.people, select: select }); // `from:` required once >1 source
55
+ var proxyFetch = useProxyFetch(ds.people); // REST API source: alias is the ARGUMENT, not a from: option
56
+ ```
57
+
58
+ Ids are plain UUIDs. Get them by asking Studio's AI chat to **write code**, never to recite a
59
+ value — it fabricates them in prose. Full detail: [../datasources/multi-datasource.md](../datasources/multi-datasource.md).
60
+
44
61
  ## Read
45
62
 
46
63
  ```jsx
47
64
  var result = useRecords({ select: select, count: 100 });
48
- var records = (result.data && result.data.pages) ? result.data.pages.flatMap(function(p) { return p.items; }) : [];
65
+ var records = result.data?.pages.flatMap(p => p.items) ?? [];
49
66
  ```
50
67
 
68
+ ⚠ The options object MUST be an inline literal — `useRecords(opts)` with `opts` built in a
69
+ variable or returned by a wrapper function **fails to compile** (verified live 2026-08-25).
70
+ Share `q.select` mappings, not options objects.
71
+
51
72
  ## Filter + Sort
52
73
 
53
74
  ```jsx
@@ -70,8 +91,9 @@ var result = useRecord({ recordId: recordId, select: select });
70
91
  ## Current User
71
92
 
72
93
  ```jsx
73
- var currentUser = useCurrentUser(); // { id, fullName, email, avatar }
74
- var softrUser = window.__softr_current_user; // full object with userGroups
94
+ var currentUser = useCurrentUser(); // { id, fullName, firstName, lastName, email, avatar } | null; id only with user sync
95
+ var withProps = useCurrentUser({ properties: { plan: "FIELD_ID" } }); // custom user fields → withProps.properties.plan
96
+ var softrUser = window.__softr_current_user; // userGroups/role ONLY — not exposed by the hook
75
97
  ```
76
98
 
77
99
  ## Create
@@ -82,7 +104,7 @@ var createRecord = useRecordCreate({
82
104
  onSuccess: function(newRecord) { refetch(); },
83
105
  onError: function(err) { toast.error(err.message); },
84
106
  });
85
- createRecord.mutate({ name: "Jane", email: "jane@example.com" });
107
+ createRecord.mutate({ name: "Jane", email: "jane@example.com" }); // FLAT — no { fields } wrapper
86
108
  ```
87
109
 
88
110
  ## Update (THE CORRECT PATTERN)
@@ -94,7 +116,7 @@ var updateRecord = useRecordUpdate({
94
116
  onError: function(err) { toast.error(err.message); },
95
117
  });
96
118
 
97
- // Call .mutate() — NOT .mutateAsync() — and use the nested {recordId, fields:{}} shape.
119
+ // Payload is the nested {recordId, fields:{}} shape (create is flat — the asymmetry is by design).
98
120
  // Per-call onSuccess/onError go in the second argument.
99
121
  updateRecord.mutate(
100
122
  { recordId: record.id, fields: { name: "New" } },
@@ -105,11 +127,26 @@ updateRecord.mutate(
105
127
  );
106
128
  ```
107
129
 
108
- **Two parser requirements both must hold or `enabled` stays `false`:**
109
- 1. `.mutate(...)` — NOT `.mutateAsync(...).then(...)` (parser ignores `mutateAsync`)
110
- 2. Payload is `{ recordId, fields: {...} }` NOT flat `{ recordId, status: "..." }`
130
+ **Payload shapes or `enabled` stays `false`:**
131
+ 1. Update payload is `{ recordId, fields: {...} }` — NOT flat `{ recordId, status: "..." }`
132
+ 2. Create payload is FLAT — `{ name: "..." }`, no `fields` wrapper
133
+
134
+ See [datasources/writing.md](../datasources/writing.md#critical-the-userecordupdate-payload-shape-and-the-retired-mutate-only-rule) for the full debugging path.
135
+
136
+ ## Sequential Multi-Row Saves (mutateAsync)
137
+
138
+ `mutateAsync` is fully supported (verified live 2026-08-25 — the old ".mutate() only" parser
139
+ rule is retired). It's the tool whenever writes must happen in order:
111
140
 
112
- See [datasources/writing.md](../datasources/writing.md#critical-two-parser-requirements-for-userecordupdate) for the full debugging path.
141
+ ```tsx
142
+ const header = await createHeader.mutateAsync({ name, date }); // header first
143
+ for (const line of lines) {
144
+ await createLine.mutateAsync({ header: [header.id], ...line }); // then lines, in order
145
+ }
146
+ ```
147
+
148
+ Stop on the first failure, render it with a Retry, never re-issue completed writes. Full queue
149
+ pattern: [datasources/writing.md](../datasources/writing.md#sequential-multi-row-writes-mutateasync).
113
150
 
114
151
  ## Delete
115
152
 
@@ -124,7 +161,10 @@ deleteRecord.mutate(record.id); // Just the ID string
124
161
 
125
162
  `.enabled`, `.status`, `.error`, `.mutate()`, `.mutateAsync()`, `.reset()`
126
163
 
127
- `.mutateAsync()` exists at runtime but is **invisible to Softr's Action parser** — using it on `useRecordUpdate` leaves `enabled` permanently `false`. Always call `.mutate(payload, { onSuccess, onError })` for updates.
164
+ Both `.mutate(payload, { onSuccess, onError })` and `await .mutateAsync(payload)` derive
165
+ Actions correctly on the current platform (verified 2026-08-25). Use `.mutate` for
166
+ fire-and-forget single writes, `mutateAsync` for sequenced flows. (Pre-2026-08 platforms only
167
+ recognized the literal `.mutate(` token — relevant only when maintaining old apps.)
128
168
 
129
169
  ## Linked Records Picker
130
170
 
@@ -138,9 +178,12 @@ var options = (result.data && result.data.pages) ? result.data.pages.flatMap(fun
138
178
  ## Linked Records in Mutations
139
179
 
140
180
  ```jsx
141
- teamMembers: [{ id: "MEMBER_ID" }]
181
+ teamMembers: ["MEMBER_ID_1", "MEMBER_ID_2"] // array of record-id STRINGS (verified Softr DB, 2026-08-25)
142
182
  ```
143
183
 
184
+ Legacy/Airtable: the `[{ id: "..." }]` object shape was the verified form on Airtable-backed
185
+ blocks (May 2026) — try it if a string-array write fails there.
186
+
144
187
  ## Formula Booleans
145
188
 
146
189
  ```jsx
@@ -0,0 +1,140 @@
1
+ # Softr MCP Server
2
+
3
+ The official Softr MCP server (`https://mcp.softr.io/mcp`) gives an AI assistant (Claude Code, Claude Desktop, claude.ai, Cursor, ChatGPT, Mistral) direct access to a Softr **workspace**: databases, applications, vibe coding blocks, integrations (external data sources), and workflows. Everything the assistant does happens as the connected user, with their permissions, and shows up in Studio like any other change.
4
+
5
+ **This file is a sibling concern to the [../datasources/](../datasources/) guides, which cover in-block data fetching (`useRecords` + `q.select()`).** The MCP runs at chat-build time, not inside the block. For Vibe Coding work it matters twice: it answers "what fields does this table have?" without any paste-ins, and it can create and deploy the block itself — no copy-paste into Studio.
6
+
7
+ > Historical note: this file was previously named `softr-database-mcp.md` and described a databases-only server with granular scopes. That server has since grown into the workspace-wide MCP documented here; the old "does NOT cover external sources" limitation is gone (see [Integrations](#browsing-integrations-external-data-sources)).
8
+
9
+ ## Contents
10
+
11
+ - [What it covers](#what-it-covers)
12
+ - [Connection and auth](#connection-and-auth)
13
+ - [Permissions model](#permissions-model)
14
+ - [Vibe coding block tools](#vibe-coding-block-tools)
15
+ - [Vibe coding gotchas (official)](#vibe-coding-gotchas-official)
16
+ - [Browsing integrations (external data sources)](#browsing-integrations-external-data-sources)
17
+ - [Softr Database tools](#softr-database-tools)
18
+ - [Two delivery paths for this skill](#two-delivery-paths-for-this-skill)
19
+ - [When the MCP is not installed](#when-the-mcp-is-not-installed)
20
+
21
+ ## What it covers
22
+
23
+ | Area | What the assistant can do | Official docs |
24
+ |---|---|---|
25
+ | Databases | Query, filter, aggregate; create/update records; build tables and fields | https://docs.softr.io/mcp/databases |
26
+ | Applications | Read apps, pages, blocks, permissions; preview; publish | https://docs.softr.io/mcp/apps |
27
+ | Vibe coding blocks | Create and edit blocks, manage settings, visibility, versions, data source connections | https://docs.softr.io/mcp/vibe-coding |
28
+ | Integrations | Browse external data sources connected to the workspace, down to field level | https://docs.softr.io/mcp/integrations |
29
+ | Workflows | Build, test, and publish workflows | https://docs.softr.io/mcp/workflows |
30
+
31
+ `list_workspaces` is often the first call — it turns "my Sales workspace" into the workspace ID every other tool needs.
32
+
33
+ ## Connection and auth
34
+
35
+ - **Server URL:** `https://mcp.softr.io/mcp` (streamable HTTP)
36
+ - **Official docs:** https://docs.softr.io/mcp/overview
37
+
38
+ Install in Claude Code:
39
+
40
+ ```bash
41
+ claude mcp add --transport http softr https://mcp.softr.io/mcp
42
+ ```
43
+
44
+ Then start a new session and run `/mcp` to complete OAuth in the browser.
45
+
46
+ Two auth methods:
47
+
48
+ 1. **OAuth (recommended)** — pre-built clients exist for Claude (claude.ai), Cursor, ChatGPT, and Mistral. If a Client ID is requested, use the value from the [overview docs](https://docs.softr.io/mcp/overview); leave Client Secret blank (Softr's OAuth clients are public). The assistant cannot request permissions — the user always picks them on Softr's authorization screen.
49
+ 2. **Personal access token** — for custom clients. Created in Softr under **Settings → API tokens** (name, expiry, workspace + permission scoping), then used as a Bearer token.
50
+
51
+ Revoke or edit access anytime in **Settings → API tokens** (Authorized apps section for OAuth, token list for PATs).
52
+
53
+ ## Permissions model
54
+
55
+ Permissions are chosen per workspace across **three areas with bundled levels** — not granular per-tool scopes. Each level includes everything below it (no "write without read").
56
+
57
+ | Area | Levels | Highest level adds |
58
+ |---|---|---|
59
+ | Applications & Forms | Full access · Read only · None | Creating/editing vibe coding blocks, previewing, publishing |
60
+ | Databases | Full access · Edit data · View only · None | Schema changes (tables/fields); Edit data adds record writes |
61
+ | Workflows | Full access · Read only · None | Building, testing, publishing workflows |
62
+
63
+ For block-building work you need **Applications & Forms: Full access** (to create/edit blocks) plus at least **Databases: View only** (schema discovery). Integrations browsing rides on Applications & Forms read access.
64
+
65
+ ## Vibe coding block tools
66
+
67
+ Before writing any block code through the MCP, call `get_vibe_coding_docs` — it returns the current version of the [Vibe Coding Developer Guide](https://docs.softr.io/vibe-coding-developer-guide), which is the authority on hook signatures if it and this skill ever disagree.
68
+
69
+ | Group | Tools |
70
+ |---|---|
71
+ | Create / read | `get_vibe_coding_docs`, `create_vibe_coding_block`, `get_vibe_coding_block_code`, `get_vibe_coding_block_settings` |
72
+ | Edit code | `update_vibe_coding_block_code` (full replace), `update_vibe_coding_block_code_search_replace` (targeted edit) |
73
+ | Settings / visibility | `update_vibe_coding_block_settings`, `set_vibe_coding_block_visibility`, `set_vibe_coding_block_action_visibility` |
74
+ | Versions | `list_vibe_coding_block_versions`, `restore_vibe_coding_block_version`, `duplicate_vibe_coding_block_from_version` |
75
+ | Data sources | `connect_vibe_coding_block_data_source`, `disconnect_vibe_coding_block_data_source`, `set_vibe_coding_block_data_source_sort`, `set_vibe_coding_block_data_source_record_filters` |
76
+
77
+ Editable settings via MCP are the same fields as the block's **Content → Settings** panel; sort and record filters are the same as the **Source** tab. Duplicating from a version is the safe way to try an alternative — the original keeps working while you experiment on the copy.
78
+
79
+ ## Vibe coding gotchas (official)
80
+
81
+ From the official MCP docs — these hold for MCP-driven and Studio-driven edits alike:
82
+
83
+ - **A broken block can't be saved.** Code is validated before storage; on failure the block keeps its last working state and nothing is lost.
84
+ - **A version is a snapshot of the whole block** — code, settings, visibility, AND data source connections. Setting-only changes don't create a version.
85
+ - **Rolling back reverts more than the code.** Restoring a version also restores settings, visibility, and data source connections as they were at that point.
86
+ - **Changing the code resets action permissions.** Any code change rebuilds the block's record actions at default visibility — restrictions to user groups must be re-applied. (This is Hard Constraint 21 in SKILL.md, now officially documented: tighten Action permissions only after the LAST redeploy.)
87
+ - **A block with an unconnected data source saves without complaint**, then errors when the page loads. If a freshly created block looks broken but the code seems right, check its data source connection first.
88
+
89
+ ## Browsing integrations (external data sources)
90
+
91
+ An integration is an external data source connected once per workspace (the builder says "integrations", the tools say "data sources" — same thing). Five read-only tools drill down from workspace to fields; each level needs an ID from the level above:
92
+
93
+ ```
94
+ list_data_sources workspace's integrations
95
+ └── list_data_source_databases a base, spreadsheet, or database
96
+ └── list_data_source_schemas SQL schemas — Supabase only (usually just `postgres`)
97
+ └── list_data_source_tables tables or sheets
98
+ └── list_data_source_table_fields fields, types, options, primary field
99
+ ```
100
+
101
+ **Only five integration types are browsable/connectable through MCP today:** Softr Databases, Airtable, Google Sheets, Notion, and Supabase. Anything else still appears in `list_data_sources` but must be connected through the block's **Source** tab in Studio, with schema discovery via the manual workflows in [../datasources/fields.md](../datasources/fields.md#field-inspector-block).
102
+
103
+ `list_data_source_table_fields` also tells you **how fields must be referenced in `q.select()`**:
104
+
105
+ | Integration | Reference fields by |
106
+ |---|---|
107
+ | Airtable, Google Sheets, Notion | Name |
108
+ | Softr Databases, Supabase | ID (for Supabase, the SQL column name) |
109
+
110
+ Getting this wrong **fails silently** — the code compiles, saves, and looks right in the builder, then returns nothing at page load. If a block renders but its data is empty, check this first.
111
+
112
+ ## Softr Database tools
113
+
114
+ For Softr's native databases the MCP goes far beyond browsing: `get_schema` (authoritative field-type + filter-operator reference — call it before building tables or filters), database/table/field CRUD, `list_views`, record reads (`list_records`, `search_records`, `get_record`), record writes (`create_record`, `create_records` batch, `update_record`), and `aggregate_data` for grouped summaries.
115
+
116
+ Known limits and behaviors (per official docs):
117
+
118
+ - Record field keys are **field IDs**, not labels — `list_fields` maps between them.
119
+ - Computed fields (formula, lookup, rollup, count) and system fields (created/updated time and by, autonumber, record ID) are read-only; a field's type cannot be changed after creation.
120
+ - **Nothing can be deleted through the MCP yet** — no record/table/field/database delete tools (docs say deletion is coming). Deletions happen in the builder.
121
+ - Limits: 100 records per `create_records` call, 200 records per read (silently capped, not an error), 2 group-by fields in `aggregate_data`. For big tables prefer a filter or aggregate over paging.
122
+
123
+ Typical Vibe Coding uses: "list every field on `Wigs` with id, name, type, and dropdown options", "what's the option id for `Payment status` = 'Partially paid'?", "show 3 sample records so we know value shapes", "verify the field id in my `q.select()` exists". This eliminates the field-id-typo / wrong-option-uuid class of bugs entirely.
124
+
125
+ ## Two delivery paths for this skill
126
+
127
+ When generating a block, pick the delivery path by what's connected:
128
+
129
+ 1. **MCP connected with Applications & Forms full access** — write the `.tsx` file locally first (it remains the source of truth and the reviewable artifact), then offer to deploy it directly: `create_vibe_coding_block` (or `update_vibe_coding_block_code` for edits), then `connect_vibe_coding_block_data_source` to wire up the data. Remember the action-permissions reset gotcha after every code push.
130
+ 2. **No MCP (or read-only access)** — classic path: write the `.tsx` file and have the user paste it into Studio's Vibe Coding editor, then connect the data source in the **Source** tab themselves.
131
+
132
+ Either way, never deliver code inline in chat (JSX character corruption — see SKILL.md workflow step 5).
133
+
134
+ ## When the MCP is not installed
135
+
136
+ If the user hasn't installed the MCP (and doesn't want to right now), fall back to the schema-discovery methods documented per source:
137
+
138
+ - **Softr Database:** bundled CLI script — see [../datasources/softr-database.md](../datasources/softr-database.md#bundled-cli-script-get-softr-database); or the `tablespace-with-tables` network paste — see [../datasources/fields.md](../datasources/fields.md#field-inspector-block).
139
+ - **Airtable:** bundled `get-airtable-base` script — see [../datasources/airtable.md](../datasources/airtable.md#bundled-cli-script-get-airtable-base).
140
+ - **Other sources:** Field Inspector block and vendor workflows in [../datasources/fields.md](../datasources/fields.md#field-inspector-block).
@@ -1,59 +0,0 @@
1
- # Softr Database MCP Server
2
-
3
- Out-of-band integration for AI-assisted Vibe Coding workflows. The MCP server lets the AI assistant (Claude Code, Claude Desktop, Cursor, ChatGPT, Mistral) read schema, list field IDs, query records, and write data directly into Softr Databases — eliminating the manual "paste `tablespace-with-tables` JSON" step and removing transcription errors on field IDs and dropdown option UUIDs.
4
-
5
- **This file is a sibling concern to [../datasources/softr-database.md](../datasources/softr-database.md), which covers in-block data fetching (`useRecords` + `q.select()`).** The MCP runs at chat-build time, not inside the block. Same parallel as [airtable-automations.md](airtable-automations.md) sits next to [../datasources/airtable.md](../datasources/airtable.md).
6
-
7
- ## Connection
8
-
9
- - **Server URL:** `https://mcp.softr.io/mcp`
10
- - **Transport:** streamable HTTP
11
- - **Auth:** OAuth (pre-configured for Claude / Cursor / ChatGPT / Mistral) or Personal API Token (`Settings → API Tokens` in Softr)
12
- - **Official docs:** https://docs.softr.io/mcp-server
13
-
14
- ## Install (Claude Code)
15
-
16
- ```bash
17
- claude mcp add --transport http softr https://mcp.softr.io/mcp
18
- ```
19
-
20
- Then start a new Claude Code session and run `/mcp` to complete the OAuth authorization in the browser. See [the Softr docs](https://docs.softr.io/mcp-server) for Cursor / ChatGPT / Mistral / custom client setup.
21
-
22
- ## Permissions (three granular scopes)
23
-
24
- Grant only what you need:
25
-
26
- | Scope | Use case for Vibe Coding |
27
- |-----------------------------|-----------------------------------------------------------------------|
28
- | `databases.records:read` | AI discovers field IDs, dropdown UUIDs, verifies value shapes |
29
- | `databases.records:write` | AI mutates live data (e.g. seeding test records, bulk updates) |
30
- | `databases.schema:write` | AI provisions databases / tables / fields for you |
31
-
32
- For block-writing workflows, **read scopes are the most valuable** — they cover the AI's schema-discovery needs (the bottleneck the MCP solves) without any blast-radius into live data. Add write scopes only when you want the AI to mutate records; schema-write only when you want it to provision tables.
33
-
34
- ## Tools available (20 total)
35
-
36
- - **Databases (4):** list / get / create / update
37
- - **Tables (8):** list tables, list fields, list views, get table schema, create / update tables, create / update fields
38
- - **Records (8):** list / get / create (batch ≤ 100) / update / delete + filter-based and view-filtered search
39
-
40
- ## Why this changes Vibe Coding workflows
41
-
42
- Without the MCP, the AI needs schema shared manually — either paste `tablespace-with-tables` network JSON or describe fields by name. Both are slow, and verbal-name approaches lose dropdown option UUIDs entirely without a second copy step.
43
-
44
- With the MCP installed, you can ask things like:
45
-
46
- - "List every field on the `Wigs` table with id, name, type, and dropdown options."
47
- - "What's the option id for `Wigs.Payment status` = 'Partially paid'?"
48
- - "Show me 3 sample records from `Wig Services` so we know the value shapes."
49
- - "Verify the field id I used for `q.select({ status: 'sel...' })` exists on this table."
50
-
51
- The AI then writes `q.select()` keys and write payloads against the live schema, eliminating an entire class of "field id typo" / "wrong option uuid" bugs.
52
-
53
- ## Scope: Softr Database only
54
-
55
- The MCP server exposes Softr's **native** databases only. It does NOT proxy external sources (Airtable, Google Sheets, HubSpot, Notion, Xano, etc.) — those still need the schema-discovery workflows documented in [../datasources/fields.md](../datasources/fields.md#field-inspector-block) (Field Inspector block, vendor APIs, network inspector paste, etc.). If your Softr app blends Softr DB with external sources, the MCP helps only with the Softr DB tables.
56
-
57
- ## When the MCP is not installed
58
-
59
- If the user hasn't installed the MCP (and doesn't want to right now), fall back to the schema-sharing methods documented in [../datasources/fields.md](../datasources/fields.md#field-inspector-block) — primarily the `tablespace-with-tables` network paste, which gives the AI everything it needs in one shot.