This guide is for developers who want to write code directly in the Vibe Coding Block, rather than prompting the AI. Use it when you need full control over your block’s markup, logic, and data.
The Basics
Block’s source code is a TypeScript file with a default-exported React component:
This only runs in the browser - you can fetch and mutate data from connected data sources, but you cannot run server-side code or use Node.js APIs.
Vibe coding block is configured to use Tailwind for styling and shadcn/ui for components, but you are free to import any public npm package as needed. In fact, any npm package import you add will automatically install it for you.
shadcn/ui
shadcn/ui components are already pre-configured and follow your app’s theme out of the box. They’re available under the @/components/ui path, so you can import them like this:
Currently the following components are available:
accordion, alert, alert-dialog, aspect-ratio, avatar, badge, button, calendar, card, carousel, chart, checkbox, collapsible, command, context-menu, dialog, drawer, dropdown-menu, empty, hover-card, input, input-group, input-otp, item, kbd, label, menubar, native-select, navigation-menu, pagination, popover, progress, radio-group, resizable, scroll-area, select, separator, sheet, skeleton, slider, sonner, spinner, switch, table, tabs, textarea, toggle, toggle-group, tooltip
Check out the shadcn/ui docs for usage details and examples for each component.
Icons
Preferred icon pack is Lucide Icons, but you can opt for a different one.
Styling
We follow the default shadcn/ui naming convention for background/foreground color pairs (e.g. bg-primary / text-primary-foreground) which maps to your app’s theme colors.
By default, block occupies full width of the page but special classes - container and content are available to constrain the width of content to match app’s max width settings to ensure visual consistency with other blocks:
Data from Your Datasource
Import data hooks from @/lib/datasource. Use these when you want to display records from your connected data source. All data fetching hooks follow a query builder pattern so you can be quite expressive with your queries.
One or many datasources
A block can connect to multiple datasources. When it does, every fetch or mutation needs to say which datasource it targets. The datasource.define utility gives you readable aliases for that. Declare it once at the top of the file, then pass the alias as from on each hook:
With a single datasource you can skip datasource.define and omit from. The hooks then default to that one datasource. As soon as a block has more than one, leaving out from throws an error, since the hook can’t tell which datasource the call belongs to.
Every record hook takes from the same way: useRecords, useRecord, useLinkedRecords, useFieldOptions, useMetric, useChartData, useRecordCreate, useRecordUpdate, and useRecordDelete. useUpload and useCurrentRecordId work at the app level and thus from is not applicable to them.
Defining a select query
Field mappings have to be static, we also use static analysis to determine which fields your block actually uses so we don’t overfetch and don’t accidentally expose potentially sensitive data.
So something like this is not allowed as it breaks static analysis:
useRecords — fetch a list of records
useRecord — fetch a single record by ID
Use with useCurrentRecordId() to display details of the record currently shown in a list/detail context:
Filtering records
Use the q query builder for filters. Filters support up to 2 levels of nesting.
Available filter methods:
Sorting records
useLinkedRecords — fetch options from a linked table
Use for dropdowns, comboboxes, or tag pickers where you need to show values from a related table:
Each item only carries its id and title (the linked table’s primary field). To read other fields off those records, connect the linked table as its own datasource and query it with useRecords. See One or many datasources.
Mutating Records
All mutation hooks expose an enabled boolean — always check it before rendering the mutation UI or calling the function. It reflects whether the current user has sufficient permissions. If called without checking enabled, the mutation will throw an error.
useRecordCreate
useRecordUpdate
useRecordDelete
Uploading Files
Use useUpload from @/lib/datasource to upload files and get back a URL to store in a record.
For multiple files:
Current User
Get info about the logged-in user with useCurrentUser from @/lib/user. Returns null if no user is logged in.
Available fields:
id: string | null (only present when user sync is enabled)
fullName: string | null
firstName: string | null
lastName: string | null
email: string | null
avatar: string | null
Custom user properties
Beyond the reserved fields above, any custom fields that exist on your user record are available under user.properties. Pass a properties map to alias each field to a readable name, the same way a select query works.
Metrics & Charts
useMetric — single aggregated value
Useful for KPI cards (total sales, average rating, etc.):
Aggregations: metric.sum(field), metric.avg(field), metric.max(field), metric.min(field), metric.distinct(field), metric.count()
useChartData — grouped data for charts
Grouping buckets:
Editable Settings
Editable settings let builders modify block content through the editor UI (Content → Settings tab) without touching code. Always use them for any text, images, icons, or lists that might change between block instances.
Import from @/lib/editable-settings.
useTextSetting
Returns a string. Use for titles, descriptions, button labels, URLs, etc.
useImageSetting
Returns { src: string; alt: string }.
useVideoSetting
Returns { src: string }.
useVibeCodingBlockIconSetting
Returns { icon: string } where icon is a lucide-react icon name. Render it with the DynamicIcon component.
useNavigationSetting
Returns { action: "OPEN_URL" | "OPEN_PAGE"; destination: string; openIn: "SELF" | "TAB" } | { action: "OPEN_CHAT" } | { action: "TRIGGER_CUSTOM_WORKFLOW" }.
The NavigationAction component also accepts an optional recordId?: string prop. When rendering record-specific links in a list/loop, pass the current record ID so it can be dynamically added as a URL parameter to the final URL ?recordId=<id>.
useArraySetting
Returns an array of items with a consistent shape. Use for feature lists, team members, FAQs, testimonials, etc.
useBooleanSetting
Returns a boolean. Use for toggles, switches, show/hide elements, etc.
Schema field types: "text", "image", "video", "vibeCodingBlockIcon"
Constraints:
- Schema cannot contain nested arrays — for list-like text, use a
"text" field with a separator (e.g. comma) and split it in code
- Do not put a
vibeCodingBlockIcon field as the first field in the schema
- Calling two settings hooks with the same
name is not allowed
Complete Example — Feature Showcase
A full-featured block combining editable settings, datasource records, and shadcn/ui:
Fetching field options
Use useFieldOptions to fetch available options for SELECT or multi-select fields. This is useful for building filters, dropdowns, badges, or any UI that needs to display the available choices from the datasource without hardcoding them. Like the other data hooks, it accepts a from alias (useFieldOptions({ from: ds.orders, select, field })), required when the block has multiple datasources.
Example to build a filter UI using useFieldOptions:
Fetching from a REST API
When using a REST API as a datasource, call it with useProxyFetch from @/lib/datasource. The proxy attaches authentication for you, so don’t send tokens, API keys, or auth headers yourself.
useProxyFetch returns a function with the same signature as fetch. TanStack Query (or a similar data-fetching library) pairs nicely with it for caching and request state, and we strongly recommend it over fetching inside a useEffect.
Caveats: the proxy currently only supports text payloads. Streams, FormData, and file uploads won’t work.
Reading data
Mutating data
Wrap writes in useMutation and invalidate the affected queries on success so the UI refetches:
Multiple datasources
When a block has more than one datasource, useProxyFetch needs to know which one to route the request to, so pass the datasource alias as its argument. With a single datasource you can call useProxyFetch() with no argument; once there’s more than one, leaving it out throws an error. Define the aliases with datasource.define, the same pattern the record hooks use: