BoilerPrompt

Bolt prompts

Describe an app, get a running full-stack project in your browser tab.

20 prompts, every one free to copy. Jump to: App, Backend, Features, Frontend, Refactor, Testing

What Bolt is good at

Bolt (bolt.new) is StackBlitz's AI app builder. Your whole project runs inside a WebContainer, a Node environment in the browser tab, so the dev server, npm installs, and terminal all execute right there with no local setup. You describe an app, Bolt scaffolds the files, and a live preview sits next to an editable file tree, so you can fix code by hand mid-conversation. It fits developers who want a working full-stack starting point in minutes and are comfortable reading generated source. Against its neighbors: v0 focuses on React UI components you export into an existing project, and Lovable leans hard on Supabase-backed apps with less file-level exposure. Bolt sits between them, producing the entire project, frontend, server routes, and config, while keeping every file open for direct edits. Deploys go out through its Netlify integration, and persistent data usually means connecting Supabase, since the in-browser runtime resets between sessions.

How to prompt Bolt

Put the whole spec in your first message. Bolt meters usage by tokens and big course corrections trigger wholesale rewrites, so a complete opening brief (pages, data model, behaviors, states, styling) is cheaper than five rounds of "actually, also add...".

Name the stack explicitly. Left to defaults you will usually get a Vite and React project; if you want Next.js, Vue, or plain HTML, say so in the first line.

Decide persistence up front. The WebContainer cannot run Postgres or other native binaries, so state whether data lives in memory, in localStorage, or in Supabase through Bolt's integration. Skip this and you may get a mock layer you did not want.

Ask for seed data. The live preview is the whole point of Bolt; populated lists and charts let you judge output at a glance, while empty screens hide layout bugs.

Scope follow-ups tightly. Say which file or feature to change and add "do not modify other files." When the preview breaks, paste the exact terminal or console error rather than describing the symptom; Bolt fixes precise errors far more reliably than vague ones.

End prompts with a verification step: what to click in the preview and what should happen when you do.

All 20 Bolt prompts

App 5 prompts

Build a SaaS starter app for a product that helps small teams manage {{resource}}. Stack: React with TypeScript, Supabase for auth and application data, Stripe stubbed behind a feature flag rather than integrated.

Pages:
- Public landing: hero, three feature sections, pricing table with Free, Pro, and Team tiers, and a footer
- Sign up and log in with email plus password, and a magic-link option
- Protected app shell: dashboard home, a {{resource}} section with create, edit, and delete, a team page with invite-by-email, and settings with profile and workspace tabs
- Billing page with the current plan and an upgrade button calling a clearly marked placeholder checkout
- A dedicated {{addon}} page in the protected shell with its own sidebar entry. Give it a real empty state explaining the feature before any data exists and gate its actions by role like the rest of the app; if it touches payments or billing, keep it behind the same flag as the Stripe stub. Done means the sidebar link, the route, and the empty state all render in a fresh workspace.

Data model: workspaces, users, and memberships with roles (owner, admin, member), plus {{resource}} records scoped to a workspace. Every query filters by workspace id, enforced in one data-access module rather than scattered through components.

Behaviors:
- Unauthenticated visitors hitting app routes get redirected to login, then back to their intended destination
- Only owners and admins see invite and billing actions
- Optimistic UI on {{resource}} create and delete, rolling back on failure

Empty and error states: a fresh workspace shows an onboarding checklist; failed logins distinguish a wrong password from an unconfirmed email.

Styling: marketing polish on the public pages, a denser utilitarian layout inside the app, one primary color shared by both.

Do not implement real payment processing. Seed one demo workspace with sample records so the app is browsable right after the first sign-up.
Build a to-do app as a Vite + React project with Tailwind in this Bolt workspace. Single screen, three zones: an input row pinned to the top, a filter bar with All, Active, and Done, and the task list.

Data model: task with id, title, due_date, done, created_at. Persist to localStorage under the key bolt_todos so the list survives a preview refresh. No backend for v1.

Behaviors:
- Enter in the input adds a task, an empty input does nothing and shows no alert.
- Checkbox toggles done, completed rows get strikethrough and sink below open ones.
- Double click a title to edit inline, Escape cancels, blur saves.
- Each row has a small calendar button that reveals a native date input, picking a date sets due_date, clearing it removes the date, and the chosen date renders next to the title.
- A live footer count in the form 3 of 8 open.
- Clear completed asks for confirmation before deleting.

Edge cases: duplicate titles allowed, titles trimmed of surrounding whitespace, a due_date earlier than today renders its date text in red on open tasks only, never on completed ones. If localStorage throws, fall back to in-memory state and show a small banner explaining that tasks will not persist.

Empty state: the message Nothing here, add your first task, with focus already in the input.

Styling: neutral grays, one accent color shared by the add button and the active filter, system font stack, content column capped at 640px and centered.

File layout: App.tsx for composition, TaskItem.tsx for a row, useTasks.ts for all state and persistence, nothing else, so the whole app stays readable in Bolt's editor in one sitting.

Acceptance: add three tasks, give one a due date of yesterday and confirm its date shows in red, complete one, refresh the preview, and confirm the due date, the filter counts, and the completed ordering all survive the reload.
Build a blog with a small self-hosted CMS in one Bolt project: public site plus an /admin area, Vite + React, connected to Supabase for storage and auth.

Data model, created as a Supabase migration:
- posts: id, title, slug, excerpt, body_md, cover_url, status (draft or published), published_at, author_id.
- tags: id, name, slug, plus a post_tags join table.
Enable row level security: anyone can select published posts, only authenticated authors can insert or update their own rows.

Public screens:
- / : published posts newest first, excerpt and tag chips, paginated 10 per page.
- /post/:slug : renders body_md as sanitized HTML, code blocks in monospace, and a 404 view for unknown or draft slugs.
- /tag/:slug : the same list filtered by tag.

Admin screens behind Supabase email auth:
- /admin : a table of all posts with status badges and edit links.
- /admin/edit/:id : title, slug auto-generated from the title but editable, a markdown textarea with side-by-side preview, and separate Save draft and Publish buttons. Publishing sets published_at once and never overwrites it on later edits.

Edge cases: slug collisions get a numeric suffix, deleting a tag detaches it from posts rather than deleting them, and an empty admin list shows a Create your first post button.

Styling: a reading measure around 65 characters, generous line height, no sidebar on the public site.

Acceptance: sign in through the preview, publish a post, open it by slug while logged out, and confirm drafts stay hidden from anonymous visitors.
Build a small e-commerce store for selling {{resource}} in this Bolt project: Vite + React + Tailwind, Supabase for the catalog, cart kept client-side.

Data, as Supabase migrations with seed data:
- products: id, name, slug, description, price_cents, currency, image_url, stock, active.
- orders: id, email, total_cents, status, created_at, plus order_items with product_id, qty, unit_price_cents.
Seed 8 products so the preview is browsable immediately.

Screens:
- / : product grid with image, name, price, and a sold-out overlay when stock is 0.
- /product/:slug : gallery placeholder, quantity stepper capped at available stock, Add to cart.
- /cart : line items with qty editing and removal, subtotal recomputed from freshly fetched product rows, never from values stored in the cart itself.
- /checkout : email and shipping form with inline validation, and a Place order button that writes the order and its items to Supabase in one RPC call, then shows a confirmation with the order id. Payment is a stub marked TODO, do not fake a card form.

Cart rules: persists in localStorage under cart_v1, merges duplicate lines, and drops items whose product went inactive while telling the user which ones were removed.

Empty and error states: an empty grid says the store has no products yet, the empty cart links back to the grid, and a failed order write keeps the form filled and shows a retry banner.

Money: always integer cents, formatted at render, no floating point arithmetic anywhere.

Acceptance: add two items, refresh the preview and confirm the cart survives, place an order, then open Supabase's table view and find the order row with its items.
Build a booking app in this Bolt project: a public page where clients pick a slot, and an owner view for availability. Vite + React, Supabase for data and auth, every timestamp stored UTC.

Data model as Supabase migrations:
- services: id, name, duration_min, buffer_min, active.
- availability_rules: id, weekday, start_time, end_time.
- bookings: id, service_id, starts_at timestamptz, client_name, client_email, status, plus a unique index on (service_id, starts_at) so double booking fails at the database, not just in the UI.

Public flow at /book:
1. Pick a service from a card list showing duration.
2. A week view of open slots computed server-side from availability rules minus existing bookings minus buffer time, rendered in the visitor's local timezone with the timezone name printed under the grid.
3. Confirm with name and email, inline validation, then a confirmation screen with a cancel link carrying a signed token.

Owner view at /admin behind Supabase auth: a weekly calendar of bookings with click-to-cancel, and an availability editor of weekday rows with add and remove time ranges. Overlapping ranges merge on save with a notice.

Edge cases: past slots never render, a slot taken between page load and confirm returns a slot_taken error and refreshes the grid, cancelled bookings free their slot immediately, and slot generation gets checked across a daylight saving boundary week.

Empty states: an owner with no availability sees a setup prompt, and no active services hides the public flow behind a Coming soon card.

Acceptance: book a slot in the preview, open a second tab, try booking the same slot, and confirm the unique index rejects the duplicate.

Backend 5 prompts

Build a REST API in Node with TypeScript, using Express, for managing {{resource}} records, backed by {{database}}.

The in-browser container runs Node only, never a database server. If {{database}} is Postgres, MySQL, MongoDB, or Redis, read a connection string from an env var and point the data layer at a hosted instance. Without one, fall back to a JSON file store behind the same interface so the preview still works.

Endpoints:
- GET /api/{{resource}} with pagination (page and limit query params, default limit 20) and a total count in the response body
- GET /api/{{resource}}/:id, returning 404 with a JSON error body when the id does not exist
- POST /api/{{resource}} with request body validation; reject unknown fields and return 422 with per-field messages
- PATCH /api/{{resource}}/:id for partial updates, validating only the fields present
- DELETE /api/{{resource}}/:id, returning 204 on success and 404 for a missing id
- GET /api/health returning uptime, which store is active, and, when hosted, whether the database connection is alive

Data model: each record has an id, name, description, status (active or archived), createdAt, and updatedAt, plus two or three extra fields that fit a {{resource}}.

Behaviors:
- One JSON error envelope for every non-2xx response: { error: { code, message, details } }
- Request logging middleware printing method, path, status, and duration to the terminal
- CORS enabled so the preview page can call the API
- Seed 15 realistic records on startup so list responses are never empty

Also serve a plain HTML page at the root route documenting every endpoint with an example request, plus a form to POST a new record and buttons that hit each endpoint and print the raw JSON.

Structure the code as a server entry, routes, validation, and data access modules. After scaffolding, start the server and confirm /api/health and the list endpoint both return 200.
Create a Node project running GraphQL Yoga so the server starts inside Bolt's WebContainer and the preview pane serves the GraphiQL playground at /graphql.

Schema, SDL-first in schema.graphql: a {{resource}} type with id, name, status, and createdAt. Queries: a paginated list taking limit, offset, and an optional status filter, plus a single fetch by id. Mutations: create, update, and delete for {{resource}}, each returning the affected record, delete included, not a boolean.

Resolvers live in src/resolvers.ts, one file, no codegen for v1. Back them with an in-memory array seeded with 12 records so the playground is useful immediately, but isolate every read and write in src/store.ts so I can swap in {{database}} later without touching a resolver.

Rules:
- Reject an empty name with a GraphQLError carrying extensions.code BAD_USER_INPUT.
- Clamp limit to 50 silently.
- Unknown id: null from the single query, a NOT_FOUND error from mutations.

Add GET /healthz returning { ok: true } outside GraphQL, and print the playground URL to the terminal on boot.

Dependencies: graphql and graphql-yoga only, no Apollo, no subscriptions, nothing with native bindings since WebContainers cannot compile them.

Acceptance: from GraphiQL in the preview I can create a record, page through the list with offset, filter by status, update a status, delete the record, and get NOT_FOUND when deleting it a second time.
Add JWT auth to the Express API in this Bolt project. Use jsonwebtoken and bcryptjs, not bcrypt, because native bindings will not compile inside the WebContainer.

Endpoints in server/auth.ts:
- POST /api/auth/register: email plus password, hash with bcryptjs at 10 rounds, reject passwords under 8 characters with 422 and a field-level error body.
- POST /api/auth/login: verify credentials, return a 15 minute access token in the JSON body and a 7 day refresh token in an httpOnly cookie.
- POST /api/auth/refresh: read the cookie, rotate the refresh token, return a new access token. A reused old refresh token invalidates the whole session family.
- POST /api/auth/logout: clear the cookie and revoke the refresh token server-side.

Middleware requireAuth in server/middleware.ts: read the Authorization Bearer header, verify signature and expiry, attach req.user, and return 401 with { error: "token_expired" } distinct from { error: "invalid_token" } so the client knows when to refresh.

Secrets: JWT_SECRET comes from .env. Generate a placeholder value and remind me to change it, never hardcode it.

Apply the middleware to the existing /api/{{resource}} routes, leaving GET public. Persist users and refresh tokens in whatever store the project already uses, adding a users table if none exists.

Acceptance: write scripts/auth-check.sh with curl calls that register, log in, hit a protected route, tamper with one token character and get 401, refresh successfully, then log out and prove the refresh path is dead.
Design and apply a Postgres schema through Bolt's Supabase connection. Deliver it as ordered SQL files in supabase/migrations, one concern per file, so I can read each in the editor before it runs.

Migration 1, core tables for a {{resource}} tracker:
- profiles: id uuid referencing auth.users, display_name, created_at.
- {{resource}}s: id uuid default gen_random_uuid(), owner_id referencing profiles, title text not null with a length check between 1 and 200, status as a proper enum type (draft, active, archived), created_at and updated_at timestamptz defaulting to now().
- comments: id, {{resource}}_id with on delete cascade, author_id, body.

Migration 2, integrity and speed:
- A trigger function touch_updated_at applied to every table carrying updated_at.
- Indexes on owner_id, on comments by parent and created_at descending, and a partial index on status where status is active.
- A unique constraint on (owner_id, title), with a SQL comment explaining the choice.

Migration 3, row level security:
- Enable RLS on all three tables.
- Owners get full access to their rows, authenticated users can select active {{resource}}s, and comments are readable wherever the parent is readable. Each policy carries a one-line comment stating what it permits.

Rules: no serial columns, uuid keys everywhere, no nullable booleans, every timestamp timestamptz, and never store money or quantities as float.

Finish with a smoke check I can paste into the Supabase SQL editor: insert a profile and a record, update it to verify updated_at moves, then attempt a cross-owner update and confirm RLS rejects it.
Build a file upload endpoint in this Bolt project's Express server, storing files in Supabase Storage, because the WebContainer filesystem is wiped between sessions and must never be the system of record.

Route: POST /api/uploads in server/uploads.ts, multipart/form-data with field name file, parsed by multer using memory storage so nothing lingers on disk.

Validation, in this order, each with its own error code:
- 413 file_too_large past 10 MB, enforced through multer limits so oversized bodies abort early.
- 415 unsupported_type unless the magic bytes say png, jpeg, webp, or pdf. Detect with the file-type package, never trusting the client mimetype or the extension.
- 400 missing_file when the field is absent.

Naming and storage: build the object key as uploads/{year}/{month}/{uuid} with the extension taken from the detected type, and keep the original filename only as metadata after stripping path separators and control characters. Upload to a private bucket, then return 201 with id, key, size, contentType, and a short-lived signed URL.

Companion routes: GET /api/uploads/:id returns metadata plus a fresh signed URL, and DELETE removes both the object and the metadata row. Metadata lives in an uploads table with uploader_id taken from the auth middleware.

Failure behavior: if the Storage call dies mid-request, return 502 storage_unavailable and log the attempted key. Insert the metadata row only after the upload succeeds so no half-written records exist.

Acceptance: from Bolt's terminal, curl a small png and get 201, rename an .exe to .png and get 415, send an 11 MB file and get 413, then open the signed URL in the preview and see the image.

Features 4 prompts

Add a search feature to this app so users can find {{resource}} records by name and description.

Behavior:
- A search input in the header, focusable with the / key, with a clear button
- Debounce input by roughly 250ms so matching does not run on every keystroke
- Case-insensitive matching on name and description; treat multiple words as AND terms
- Highlight the matched substring in each result using a mark element, not custom spans with inline styles
- Show a result count, and when nothing matches, an empty state suggesting a spelling check or clearing filters
- Sync the query to the URL as ?q= so results survive a reload and can be shared; restore the input from the URL on load
- Keyboard support: arrow keys move through results, Enter opens the selected record, Escape clears and closes

Implementation:
- If the data is already client-side, filter in a memoized selector and do not mutate the underlying list
- If records come from an API, add a q parameter to the list endpoint and match server-side, keeping only the debounce in the browser
- Keep the logic in one module (src/lib/search.ts or equivalent) as a pure function I can unit test: given records and a query, return matches with their positions

Edge cases: an empty query shows the normal list, whitespace-only queries count as empty, queries containing regex characters like ( or * must not crash the matcher, and clearing the search restores the prior scroll position.

Finish by demonstrating three searches in the preview: a term with hits, a term with none, and a term containing a special character. Then list the files you changed.
Add a chat assistant to the existing app in this Bolt project without restructuring it.

Server side: server/chat.ts, an Express route POST /api/chat that proxies to the LLM provider. Read the key from LLM_API_KEY in .env, keep .env out of version control, and never let the key reach the client bundle. Stream the reply back as server-sent events so tokens render as they arrive. A missing key returns 503 with { error: "missing_key" } instead of crashing the WebContainer process. Disable compression middleware on this route so SSE chunks flush.

Client side: a ChatPanel component docked to the right edge, collapsible. Parts: message list, a textarea growing to four lines, a send button disabled while a reply streams. Send the last 20 messages as context and drop older ones.

States:
- First open shows a short hint describing what the assistant can help with in this app.
- Streaming shows a cursor at the end of the incoming message plus a Stop button that aborts the fetch and keeps the partial text.
- Provider errors and rate limits render inline in the transcript with a Retry link, and the user's draft stays in the box.
- Offline disables send with a tooltip saying why.

Persist the transcript to sessionStorage so a preview reload mid-test does not wipe the conversation.

Log one line per request on the server with model, token counts if the provider returns them, and latency.

Acceptance: with a key in .env, ask a question and watch tokens stream in the preview, hit Stop halfway and confirm the partial reply stays, then remove the key and confirm the 503 path renders as an inline error.
Add transactional email notifications to this Bolt project. Use {{addon}} as the provider, called through its HTTP API from the server only, because outbound SMTP from a WebContainer is unreliable and the key must never reach the client bundle.

Structure:
- server/email/client.ts: one send(to, template, data) function, key read from EMAIL_API_KEY in .env, throwing a typed EmailError on any non-2xx response.
- server/email/templates/: welcome.ts, password-reset.ts, weekly-digest.ts, each exporting a subject and an HTML body built from plain template strings, no templating library. Inline all CSS, use tables for layout, and include a plain-text alternative per template.

Triggers: hook welcome into the existing signup flow, password-reset into the reset request, and expose POST /api/notifications/test that sends any template to my own address so I can proof designs from the preview.

Reliability:
- Sends are fire-and-forget from the request path, pushed onto an in-process queue with three retries and exponential backoff, so a provider outage never blocks signup.
- Log every attempt with status sent, retried, or failed.
- If EMAIL_API_KEY is unset, print the fully rendered email to the terminal tagged SIMULATED instead of failing, so dev flows still complete.

Edge cases: strip newlines from user-supplied names to block header injection, lowercase recipient addresses, and refuse obviously invalid addresses before calling the API.

Acceptance: hit the test endpoint with no key and read the SIMULATED render in Bolt's terminal, then add a real key and receive the welcome email in an actual inbox.
Add CSV import and export for {{resource}} records to this Bolt project.

Import is a three-step modal launched from an Import button on the {{resource}} list:
1. Upload: drag and drop or file picker, .csv only, files over 5 MB rejected with a clear message before parsing. Parse in the browser with PapaParse in streaming mode so the preview pane never freezes on big files.
2. Map columns: show the first five rows in a table, with a dropdown per CSV column mapping it to a {{resource}} field or Skip. Auto-match by header name, case-insensitive. If a required field is unmapped, disable Next and say which one.
3. Validate and commit: run every row through the same validation the normal create form uses. Show counts of valid and invalid rows, offer a downloadable errors.csv containing only the failed rows plus a reason column, then import valid rows in batches of 100 with a progress bar. Cancel between batches keeps rows already written.

Import rules: trim cells, treat empty strings as null, accept dates as either YYYY-MM-DD or DD/MM/YYYY and report which format was detected, and resolve duplicates by natural key with one Skip or Overwrite choice applied to the whole file.

Export: a button honoring the list's current filters, served as GET /api/{{resource}}/export.csv from the Node server with a Content-Disposition header, values escaped per RFC 4180, and any cell starting with =, +, -, or @ prefixed with a single quote to block spreadsheet formula injection.

Acceptance: round-trip it, export the seed data, re-import that same file, and finish with zero invalid rows and zero duplicates created.

Frontend 4 prompts

Build a responsive admin dashboard UI for monitoring {{resource}} activity. Use React with TypeScript and Tailwind.

Layout: a collapsible sidebar with navigation groups (Overview, {{resource}}, Reports, Settings) and active-route highlighting, plus a top bar with search, a date-range selector, and a user menu.

Screens:
1. Overview: four stat cards (total records, active this period, change versus the previous period computed from the mock data, and open alerts), a line chart of the last 30 days, a bar chart grouped by category, and a recent activity feed.
2. {{resource}} table: sortable columns, a text filter, a status dropdown, pagination, and a detail drawer that opens on row click.
3. Settings: a simple preferences form persisted to localStorage.

Data: create src/data/mock.ts, a module that generates records with a seeded deterministic generator so charts and tables look identical on every reload. No backend calls; components read through a thin data-access layer I can later point at a real API.

States: skeleton loaders on first paint, an empty state with a clear-filters action when nothing matches, and an error boundary around each chart so one failure cannot blank the whole page.

Styling: neutral palette with a single accent color, an 8px spacing scale, cards with subtle borders instead of heavy shadows. Everything must work down to 375px wide, with the sidebar collapsing to a hamburger menu on mobile.

When you are done, check the preview at desktop and mobile widths and fix any horizontal overflow before declaring the task finished.
Build a landing page for a developer tool as a Vite + React + Tailwind project, ready to deploy to Netlify straight from Bolt.

Sections, top to bottom, each its own component in src/sections/:
1. Hero.tsx: one-line value proposition, a subline, primary CTA Start free, secondary Read docs, and a terminal-style block showing an install command with a copy button.
2. Features.tsx: three cards, each an icon, a bold claim, two sentences. No carousel.
3. HowItWorks.tsx: a numbered three-step row that stacks vertically under 768px.
4. SocialProof.tsx: a grayscale logo strip using placeholder SVGs I can swap, and no invented testimonial quotes.
5. FAQ.tsx: five items using native details and summary elements, no JavaScript accordion.
6. Footer.tsx: links, a license note, a mailto.

Behaviors: a fixed header fades in past 400px of scroll, anchor links smooth-scroll to sections, the copy button flashes Copied for two seconds, and the CTA scrolls to a signup form that validates email format inline rather than with a browser alert. Wire the form to a console.log stub marked TODO for the real endpoint.

Constraints: system font stack only, no external font or image requests, every asset inline SVG or local so the deployed page makes zero third-party calls. One h1 on the page, alt text on every image, real button elements for every action. No animation library, CSS transitions only.

Acceptance: list the files created, then confirm in Bolt's preview that the page holds together at 1280px, 768px, and 360px without horizontal scrolling.
Build a pricing page as src/pages/Pricing.tsx in this Bolt project, Tailwind for styling, no new dependencies.

Layout: three tier cards, Free, Pro, Team, in a single row that stacks vertically under 700px. Pro gets a highlighted border and a Most popular tag, and all cards stay equal height regardless of feature list length.

Above the cards, a monthly and annual toggle implemented as a real radio group, not two styled divs. Annual view shows per-month pricing with a small billed yearly note and a savings badge computed from the two numbers in config, never hardcoded as a percentage string. If the computed savings is zero or negative, hide the badge.

All plan data lives in src/pricing.config.ts: name, monthly, annual, tagline, features, CTA label, CTA target. The page renders entirely from that file so prices change without touching JSX.

Feature lists: a checkmark for included items, excluded items in muted gray reading Not included with no icon. Any feature needing context gets an info icon whose tooltip opens on focus as well as hover.

Below the cards, a comparison table built from the same config, sticky first column, horizontal scroll contained inside its own wrapper on small screens. Then three billing questions in native details elements.

Edge cases: a zero price renders Free instead of $0, a tier missing an annual price hides its toggle-dependent block, and currency goes through Intl.NumberFormat.

Acceptance: flip the toggle and confirm every price, badge, and CTA updates from config alone, then narrow Bolt's preview to phone width and verify the page never pans horizontally.
Build a reusable data table in this Bolt project: src/components/DataTable.tsx plus a useSort.ts hook, TypeScript, Tailwind, no table library.

API: the component takes rows and a columns array, each column being { key, header, sortable, type, align, render }. Everything else stays internal.

Sorting behavior:
- Clicking a sortable header cycles ascending, descending, then unsorted, and the third click restores the original row order exactly.
- The active header shows a direction arrow and sets aria-sort on the th so screen readers announce it.
- String columns compare with localeCompare, numeric columns numerically, date columns by timestamp, chosen by the column's type field, never by sniffing cell values.
- Null and undefined cells sink to the bottom in both directions.
- Sorting is stable, so equal values keep their relative order.

Mechanics: a sticky header row inside a scrollable container with a fixed max height, zebra striping, hover highlight, and the container owning overflow-x so wide tables scroll inside themselves instead of stretching the page in Bolt's preview.

States: a loading prop renders skeleton rows matching column widths, an empty prop shows a message centered across all columns, and a cell whose render function throws shows an error glyph instead of unmounting the table.

Demo: wire it to a demo page with 200 generated rows including accented names, negative numbers, mixed-case strings, and some null dates, because those inputs expose most comparator bugs.

Acceptance: in the preview, sort each column both directions, confirm accented names order sensibly, nulls stay last either way, and the third click returns the seed order untouched.

Refactor 1 prompt

Audit and fix accessibility in this Bolt project without changing visual design or breaking existing behavior. Work in passes and show me a diff summary per pass instead of one giant rewrite.

Pass 1, semantics: replace clickable divs with button or anchor elements, ensure exactly one h1 per route, heading levels never skip, lists use ul and li, and nav, main, and footer landmarks exist.

Pass 2, keyboard: everything interactive reachable with Tab in a sensible order, a visible focus ring using outline-offset so layout never shifts, Escape closes any modal or dropdown, and focus returns to the trigger element on close. Add a skip-to-content link as the first focusable element.

Pass 3, forms and labels: every input gets a label element or aria-label, errors are announced by wiring aria-describedby to the error text, required fields carry aria-required, and no placeholder-as-label pattern survives.

Pass 4, contrast and motion: flag any text below 4.5:1 against its background and propose the nearest passing shade from the existing palette, then wrap animations in a prefers-reduced-motion media query.

Rules: no accessibility overlay library, no aria roles on elements that already carry them implicitly, and never remove an existing test id.

Acceptance: I will tab through the app in Bolt's preview pane with the mouse untouched, so when you finish, state the expected focus order per screen and list every file changed, grouped by pass.

Testing 1 prompt

Go through this project and add consistent error handling in three layers, without changing any feature behavior.

1. UI layer: wrap each route-level component in an error boundary that renders an inline fallback with a retry button, never a blank screen. Form submissions must show field-level validation messages plus a summary at the top of the form when a request fails. Every async button gets a loading state and is disabled while a request is in flight, to prevent double submits.

2. Data layer: centralize fetch calls in one module if they are not already. Normalize failures into a typed AppError with a kind (network, validation, auth, not_found, server), a message, and the original cause. Map HTTP statuses to those kinds. Retry idempotent GET requests once on network failure; never retry mutations automatically.

3. Reporting: add a small logError helper that logs structured errors to the console in development, with a clearly marked hook where a real error-tracking service can plug in later. Do not add a third-party dependency for this.

Specific cases to cover: offline (failed fetch), a 401 that redirects to login exactly once instead of looping, a 404 on a detail page rendering a not-found view with a link back to the list, and a JSON parse failure from a malformed response.

Before finishing, prove each path in the preview: add a dev-only panel with buttons that force every error kind through the real code paths, then tell me which files you changed. No empty catch blocks anywhere; if a catch exists only to ignore an error, add a comment explaining why that is safe.

Prompting patterns that work in Bolt

Whole spec, first message

Write the complete brief before you hit enter: screens, data model, behaviors, empty states, styling direction. Bolt regenerates aggressively when scope shifts, so late additions cost more than early ones. Treat the opening prompt like a ticket you would hand a contractor.

Persistence declaration

State where data lives in the first prompt: in memory, in localStorage, or in Supabase via the integration. The browser runtime resets between sessions, so undeclared persistence becomes whatever Bolt guesses, usually a mock you will rip out later.

Seeded preview

Ask for realistic seed records and deterministic mock generators so every list, chart, and detail view renders populated in the preview. You cannot judge a dashboard from an empty table, and layout bugs hide behind blank states.

Error-paste debugging

When the preview goes blank or the terminal prints a stack trace, copy the exact text into chat instead of describing it. Bolt resolves a pasted TypeError in one turn; "the page is broken" leads to guessing and unrelated rewrites.

Common mistakes

One-line prompts, then steering

Starting with "build me a CRM" and refining over ten messages burns tokens and accumulates half-rewritten files. The fix is boring: write the full spec first, then keep each follow-up to a single feature.

Expecting a real database in the container

Bolt cannot run Postgres, Redis, or other native services inside the browser runtime. Prompts that assume them either stall or get silently faked. Specify Supabase through the integration, or accept in-memory data that resets.

Letting small asks trigger big rewrites

A request to change one button can come back with three rewritten files. Constrain scope explicitly, name the file, and lock anything you have hand-edited so the agent cannot clobber your changes.

Prompts for other tools