To integrate Amazon DynamoDB with Bolt.new, install @aws-sdk/client-dynamodb and @aws-sdk/lib-dynamodb — they are pure JavaScript and communicate via HTTPS, so they work in Bolt's WebContainer unlike MongoDB or PostgreSQL. Set AWS credentials in .env, use the DynamoDB Document Client for clean CRUD operations with PutCommand, GetCommand, and QueryCommand, and keep all AWS calls in Next.js API routes to protect your credentials.
DynamoDB: The NoSQL Database That Actually Works in Bolt's WebContainer
Bolt.new's WebContainer runtime has a fundamental constraint: it cannot open raw TCP sockets. This blocks the standard database drivers that connect to PostgreSQL (the `pg` package), MongoDB (`mongoose`), and MySQL (`mysql2`) — all of which establish persistent TCP connections. DynamoDB is architecturally different: it has no TCP connection layer at all. Every DynamoDB operation is an HTTPS API call to a regional endpoint like dynamodb.us-east-1.amazonaws.com. The @aws-sdk packages are pure JavaScript, make HTTPS requests using the Fetch API internally, and have zero native C++ dependencies. This is why DynamoDB works during Bolt development while other databases fail.
The practical advantage for Bolt builders is that you can develop and test your full data layer without deploying first. Create a table, run PutItem and GetItem operations, build your React UI, and verify everything works — all inside the Bolt preview. Unlike Supabase (which Bolt recommends and works well with), DynamoDB requires no external platform setup beyond an AWS account. It's also serverless with per-request billing, so there is no database server to manage, no connection pooling to configure, and no idle cost.
The Document Client (@aws-sdk/lib-dynamodb) is the developer-friendly layer that handles DynamoDB's type system automatically. Raw DynamoDB stores values with explicit type annotations: { 'S': 'hello' } for strings, { 'N': '42' } for numbers. The Document Client lets you write and read plain JavaScript objects instead, marshaling and unmarshaling types transparently. For a Bolt.new app, always use the Document Client — it makes your code cleaner and matches how other ORMs feel. The raw DynamoDBClient is reserved for advanced operations like managing tables or working with DynamoDB Streams.
Integration method
The @aws-sdk/client-dynamodb and @aws-sdk/lib-dynamodb packages are pure JavaScript and communicate with DynamoDB exclusively via HTTPS API calls, bypassing the TCP socket limitation that prevents MongoDB, PostgreSQL, and MySQL from working in Bolt's WebContainer. You use the DynamoDB Document Client (which handles type marshaling automatically) in Next.js API routes that keep AWS credentials server-side. This makes DynamoDB genuinely usable during Bolt development without needing deployment first.
Prerequisites
- An AWS account — free tier includes 25GB DynamoDB storage and 200M requests/month permanently
- An IAM user with AmazonDynamoDBFullAccess permissions (or a scoped policy for specific tables)
- Your AWS Access Key ID and Secret Access Key stored in .env
- A Bolt.new project using Next.js (for server-side API routes to keep AWS credentials secure)
- Familiarity with key-value data modeling — DynamoDB requires thinking about access patterns before designing your table
Step-by-step guide
Create a DynamoDB Table and Configure AWS Credentials
Create a DynamoDB Table and Configure AWS Credentials
Log in to the AWS console at console.aws.amazon.com and navigate to DynamoDB. Click 'Create table'. Give your table a name (e.g., 'Items'). Set the partition key — this is the primary attribute used to look up items. For most CRUD apps, the partition key is an ID like 'itemId' (String type). If you need time-ordered queries within a partition (like messages per conversation, or events per user), add a sort key too (e.g., 'createdAt' as String storing ISO timestamps). For table settings, choose 'On-demand' capacity mode for development and small apps — you pay per request with no provisioning required and no idle cost. Click 'Create table'. DynamoDB tables are created in seconds. Now create AWS credentials for your Bolt app. In the AWS console, go to IAM → Users → Create user. Give it a name like 'bolt-dynamodb-app'. Attach the 'AmazonDynamoDBFullAccess' managed policy for development. After creating the user, go to Security credentials → Create access key, select 'Application running outside AWS', and download the credentials. Create a .env.local file in your Bolt project with AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION (e.g., us-east-1), and DYNAMODB_TABLE matching your table name. DynamoDB is region-specific. Always use the same region for your table and your SDK client — mixing regions causes 'ResourceNotFoundException' even if the table name is correct.
Set up the project for Amazon DynamoDB integration. Install @aws-sdk/client-dynamodb and @aws-sdk/lib-dynamodb via npm. Create a .env.local file with AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION=us-east-1, and DYNAMODB_TABLE=Items as placeholder values. Create a src/lib/dynamodb.ts helper that initializes a DynamoDBClient with credentials from environment variables and exports a DynamoDBDocumentClient wrapping it. Export the document client as 'ddbClient' and export a 'TABLE' constant from the env var.
Paste this in Bolt.new chat
1# .env.local2AWS_ACCESS_KEY_ID=your_access_key_id3AWS_SECRET_ACCESS_KEY=your_secret_access_key4AWS_REGION=us-east-15DYNAMODB_TABLE=ItemsPro tip: DynamoDB is free tier eligible permanently: 25GB storage and 200M requests per month at no cost. For a typical Bolt.new app, you will likely stay within the free tier indefinitely. On-demand pricing beyond free tier is $1.25 per million write requests and $0.25 per million read requests.
Expected result: A DynamoDB table exists in your AWS account in the correct region. Your .env file has the four required environment variables. The @aws-sdk packages are installed in your Bolt project.
Initialize the DynamoDB Document Client
Initialize the DynamoDB Document Client
Create a shared DynamoDB client module that your API routes will import. The Document Client is the key abstraction: it wraps the raw DynamoDBClient and automatically converts between JavaScript types and DynamoDB's native attribute format. Without the Document Client, you would need to write { 'S': 'hello' } for every string value. With it, you write plain JavaScript objects and the SDK handles the type encoding. The DynamoDBDocumentClient.from() factory method wraps the base client. You configure translation options: marshallOptions controls how JavaScript values are encoded (removing undefined values is recommended), and unmarshallOptions controls decoding. The client is stateless and thread-safe — create one instance and export it for reuse across all API routes. The @aws-sdk packages are pure JavaScript and HTTPS-based, so they initialize and operate correctly inside Bolt's WebContainer. You can import and use ddbClient in API routes immediately during development. There is no TCP connection to establish, no connection pool to manage, and no keepalive to configure — each DynamoDB operation is an independent HTTPS request to AWS.
Create src/lib/dynamodb.ts with the DynamoDB Document Client setup. Import DynamoDBClient from @aws-sdk/client-dynamodb and DynamoDBDocumentClient from @aws-sdk/lib-dynamodb. Initialize DynamoDBClient with region and credentials from environment variables. Wrap it with DynamoDBDocumentClient.from() using marshallOptions { removeUndefinedValues: true } and unmarshallOptions { wrapNumbers: false }. Export the document client as ddbClient and export a TABLE constant from process.env.DYNAMODB_TABLE. Use TypeScript.
Paste this in Bolt.new chat
1// src/lib/dynamodb.ts2import { DynamoDBClient } from '@aws-sdk/client-dynamodb';3import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';45const client = new DynamoDBClient({6 region: process.env.AWS_REGION ?? 'us-east-1',7 credentials: {8 accessKeyId: process.env.AWS_ACCESS_KEY_ID!,9 secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,10 },11});1213export const ddbClient = DynamoDBDocumentClient.from(client, {14 marshallOptions: {15 // Remove undefined values instead of encoding as DynamoDB NULL16 removeUndefinedValues: true,17 // Convert JavaScript Date objects to ISO strings18 convertClassInstanceToMap: false,19 },20 unmarshallOptions: {21 // Return numbers as regular JS numbers, not BigInt22 wrapNumbers: false,23 },24});2526export const TABLE = process.env.DYNAMODB_TABLE ?? 'Items';Pro tip: Always set removeUndefinedValues: true in marshallOptions. Without it, passing an object with undefined fields throws a TypeError because DynamoDB's native format does not support undefined — only NULL, which is a different type.
Expected result: A reusable ddbClient export is available in src/lib/dynamodb.ts. All API routes can import it without repeating configuration. The Document Client handles JavaScript-to-DynamoDB type conversion automatically.
Implement CRUD API Routes with the Document Client
Implement CRUD API Routes with the Document Client
With the Document Client configured, build Next.js API routes for Create, Read, Update, and Delete operations. The Document Client provides command classes that map directly to DynamoDB operations: PutCommand (create or replace an item), GetCommand (read a single item by key), QueryCommand (read multiple items matching a partition key), UpdateCommand (modify specific attributes), DeleteCommand (remove an item), and ScanCommand (read all items — use sparingly in production). Every DynamoDB item must include the table's partition key attribute. If your table has a sort key, items must include that too. All other attributes are optional and schemaless — you can store different attributes on different items in the same table. This flexibility is a core DynamoDB feature but also the source of confusion for developers coming from relational databases. Error handling in DynamoDB SDK follows a try-catch pattern. Common exceptions include ResourceNotFoundException (table not found — usually a region mismatch), ValidationException (invalid attribute types or missing required keys), and ConditionalCheckFailedException (a conditional write failed). Wrap each API route in try-catch and return meaningful error messages that help debug issues during development. All five DynamoDB operations below are synchronous-looking JavaScript (using async/await) but are actually independent HTTPS requests under the hood. There is no connection to maintain, no session to expire, and no connection pool to size.
Create a CRUD API route file at app/api/items/route.ts that handles all four HTTP methods. GET: if itemId query param provided, use GetCommand to fetch a single item; otherwise use ScanCommand to list all items. POST: accept a JSON body, add a generated itemId (crypto.randomUUID()) and createdAt (new Date().toISOString()), then use PutCommand. PUT: accept itemId and updated fields in body, use UpdateCommand to modify only the provided fields. DELETE: accept itemId as query param, use DeleteCommand. Import ddbClient and TABLE from src/lib/dynamodb. Return appropriate HTTP status codes. Use TypeScript.
Paste this in Bolt.new chat
1// app/api/items/route.ts2import { NextRequest, NextResponse } from 'next/server';3import {4 GetCommand,5 PutCommand,6 DeleteCommand,7 ScanCommand,8 UpdateCommand,9} from '@aws-sdk/lib-dynamodb';10import { ddbClient, TABLE } from '@/lib/dynamodb';1112// GET /api/items or GET /api/items?itemId=xxx13export async function GET(request: NextRequest) {14 try {15 const itemId = request.nextUrl.searchParams.get('itemId');1617 if (itemId) {18 const result = await ddbClient.send(new GetCommand({19 TableName: TABLE,20 Key: { itemId },21 }));22 if (!result.Item) return NextResponse.json({ error: 'Item not found' }, { status: 404 });23 return NextResponse.json({ item: result.Item });24 }2526 const result = await ddbClient.send(new ScanCommand({ TableName: TABLE }));27 return NextResponse.json({ items: result.Items ?? [] });28 } catch (err: any) {29 return NextResponse.json({ error: err.message }, { status: 500 });30 }31}3233// POST /api/items — create new item34export async function POST(request: NextRequest) {35 try {36 const body = await request.json();37 const item = {38 itemId: crypto.randomUUID(),39 createdAt: new Date().toISOString(),40 ...body,41 };4243 await ddbClient.send(new PutCommand({ TableName: TABLE, Item: item }));44 return NextResponse.json({ item }, { status: 201 });45 } catch (err: any) {46 return NextResponse.json({ error: err.message }, { status: 500 });47 }48}4950// PUT /api/items — update item fields51export async function PUT(request: NextRequest) {52 try {53 const { itemId, ...updates } = await request.json();54 if (!itemId) return NextResponse.json({ error: 'itemId required' }, { status: 400 });5556 const updateExpression = 'SET ' + Object.keys(updates).map((k, i) => `#f${i} = :v${i}`).join(', ');57 const expressionNames = Object.fromEntries(Object.keys(updates).map((k, i) => [`#f${i}`, k]));58 const expressionValues = Object.fromEntries(Object.keys(updates).map((k, i) => [`:v${i}`, updates[k]]));5960 await ddbClient.send(new UpdateCommand({61 TableName: TABLE,62 Key: { itemId },63 UpdateExpression: updateExpression,64 ExpressionAttributeNames: expressionNames,65 ExpressionAttributeValues: expressionValues,66 }));6768 return NextResponse.json({ ok: true });69 } catch (err: any) {70 return NextResponse.json({ error: err.message }, { status: 500 });71 }72}7374// DELETE /api/items?itemId=xxx75export async function DELETE(request: NextRequest) {76 try {77 const itemId = request.nextUrl.searchParams.get('itemId');78 if (!itemId) return NextResponse.json({ error: 'itemId required' }, { status: 400 });7980 await ddbClient.send(new DeleteCommand({ TableName: TABLE, Key: { itemId } }));81 return NextResponse.json({ ok: true });82 } catch (err: any) {83 return NextResponse.json({ error: err.message }, { status: 500 });84 }85}Pro tip: Use ScanCommand only for small tables (under a few thousand items) or admin operations. ScanCommand reads every item in the table, consuming read capacity proportional to table size. For production apps, design your access patterns around QueryCommand which reads only items matching a specific partition key.
Expected result: A complete CRUD API at /api/items supports GET (list all or fetch one), POST (create), PUT (update fields), and DELETE. All operations use the DynamoDB Document Client and work in Bolt's preview without deployment.
Build the Data Table UI in React
Build the Data Table UI in React
With working API routes, build the React data table component that displays DynamoDB items and provides a form to create new ones. This is a standard CRUD UI pattern: fetch items on mount, display them in a table, provide an 'Add Item' form, and include edit and delete buttons per row. For state management, use React's useState and useEffect hooks for simple CRUD tables — no external state management library is needed. Fetch all items on mount with useEffect, handle optimistic updates by immediately adding new items to local state after a successful POST, and remove deleted items from local state after a successful DELETE without re-fetching the full list. Handle the loading and error states explicitly. DynamoDB responses are fast (single-digit millisecond latency for GetCommand and QueryCommand), but network latency from the Bolt WebContainer to AWS can add 50-200ms on the first call. A loading spinner prevents UI flash during initial load. For a more structured form, use React Hook Form (which Bolt's Bolt installs by default with shadcn/ui projects) to manage form state and validation. The form fields should map to the attributes you want to store in DynamoDB — add or remove fields to match your use case.
Build a data table UI at app/page.tsx that manages DynamoDB items. On mount, fetch all items from GET /api/items and display them in a table with columns for name, description, createdAt, and action buttons (Edit, Delete). Add an 'Add Item' button that opens a modal form with name and description fields. When the form is submitted, POST to /api/items and add the returned item to the table without reloading. Delete buttons call DELETE /api/items?itemId=xxx and remove the row. Show a loading spinner during initial fetch. Use TypeScript and Tailwind CSS.
Paste this in Bolt.new chat
1// app/page.tsx2'use client';3import { useEffect, useState } from 'react';45interface Item {6 itemId: string;7 name: string;8 description?: string;9 createdAt: string;10}1112export default function Home() {13 const [items, setItems] = useState<Item[]>([]);14 const [loading, setLoading] = useState(true);15 const [showForm, setShowForm] = useState(false);16 const [formData, setFormData] = useState({ name: '', description: '' });17 const [submitting, setSubmitting] = useState(false);1819 useEffect(() => {20 fetch('/api/items')21 .then(r => r.json())22 .then(data => {23 setItems(data.items ?? []);24 setLoading(false);25 })26 .catch(() => setLoading(false));27 }, []);2829 const createItem = async (e: React.FormEvent) => {30 e.preventDefault();31 setSubmitting(true);32 const res = await fetch('/api/items', {33 method: 'POST',34 headers: { 'Content-Type': 'application/json' },35 body: JSON.stringify(formData),36 });37 const data = await res.json();38 setItems(prev => [data.item, ...prev]);39 setFormData({ name: '', description: '' });40 setShowForm(false);41 setSubmitting(false);42 };4344 const deleteItem = async (itemId: string) => {45 await fetch(`/api/items?itemId=${itemId}`, { method: 'DELETE' });46 setItems(prev => prev.filter(item => item.itemId !== itemId));47 };4849 return (50 <div className="max-w-4xl mx-auto p-6">51 <div className="flex justify-between items-center mb-6">52 <h1 className="text-2xl font-bold">DynamoDB Items</h1>53 <button onClick={() => setShowForm(!showForm)} className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700">54 Add Item55 </button>56 </div>5758 {showForm && (59 <form onSubmit={createItem} className="border rounded-lg p-4 mb-6 bg-gray-50">60 <div className="grid grid-cols-2 gap-4">61 <input value={formData.name} onChange={e => setFormData(p => ({ ...p, name: e.target.value }))}62 placeholder="Name" required className="border rounded px-3 py-2" />63 <input value={formData.description} onChange={e => setFormData(p => ({ ...p, description: e.target.value }))}64 placeholder="Description" className="border rounded px-3 py-2" />65 </div>66 <button type="submit" disabled={submitting} className="mt-3 bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700 disabled:opacity-50">67 {submitting ? 'Creating...' : 'Create Item'}68 </button>69 </form>70 )}7172 {loading ? (73 <p className="text-gray-500">Loading items...</p>74 ) : (75 <table className="w-full border rounded-lg overflow-hidden">76 <thead className="bg-gray-100">77 <tr>78 <th className="text-left p-3 font-semibold">Name</th>79 <th className="text-left p-3 font-semibold">Description</th>80 <th className="text-left p-3 font-semibold">Created</th>81 <th className="p-3"></th>82 </tr>83 </thead>84 <tbody>85 {items.map(item => (86 <tr key={item.itemId} className="border-t hover:bg-gray-50">87 <td className="p-3 font-medium">{item.name}</td>88 <td className="p-3 text-gray-600">{item.description ?? '--'}</td>89 <td className="p-3 text-sm text-gray-400">{new Date(item.createdAt).toLocaleDateString()}</td>90 <td className="p-3">91 <button onClick={() => deleteItem(item.itemId)} className="text-red-500 hover:text-red-700 text-sm">Delete</button>92 </td>93 </tr>94 ))}95 </tbody>96 </table>97 )}98 </div>99 );100}Pro tip: DynamoDB ScanCommand does not guarantee a consistent sort order — items return in partition key hash order, which is not alphabetical or chronological. If you need sorted results, either sort client-side with Array.sort() or redesign your table to use a sort key with QueryCommand.
Expected result: A working data table UI displays items from DynamoDB, with an Add Item form that creates new entries and Delete buttons that remove them. All operations update the UI immediately without a full page reload.
Add QueryCommand for Filtered Data and Deploy
Add QueryCommand for Filtered Data and Deploy
ScanCommand reads every item in the table — fine for small datasets but expensive and slow at scale. QueryCommand is DynamoDB's efficient retrieval operation: it reads only items matching a specific partition key value, optionally filtered by sort key conditions. QueryCommand is up to 100x more efficient than ScanCommand for large tables. To use QueryCommand, your table needs a partition key that groups related items (like userId, conversationId, or categoryId). Items belonging to the same logical group share the same partition key value and are retrieved together in a single QueryCommand call. This is called 'single-table design' in DynamoDB best practices. For deploying your app, add your environment variables to Netlify (Site configuration → Environment variables) or Bolt Cloud settings: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, and DYNAMODB_TABLE. Trigger a new deployment after adding them. Since DynamoDB uses HTTPS (not TCP), it works identically in the deployed server environment as it does in the WebContainer preview — no integration changes are needed for production. For production IAM policies, scope the IAM user's permissions down to specific table ARNs rather than using AmazonDynamoDBFullAccess. A minimal policy allows dynamodb:GetItem, dynamodb:PutItem, dynamodb:UpdateItem, dynamodb:DeleteItem, dynamodb:Query, and dynamodb:Scan only on your specific table ARN.
Add a filtered search feature to the app. Create a /api/items/by-user/route.ts that accepts a userId query parameter and uses QueryCommand to fetch all items where the userId attribute matches (assuming the Items table has a Global Secondary Index on userId). Also add a search bar to the UI that filters the displayed items client-side by name using JavaScript Array.filter(). Show the item count next to the table heading. Add pagination support: only show 10 items at a time with Next/Previous buttons.
Paste this in Bolt.new chat
1// app/api/items/by-user/route.ts2import { NextRequest, NextResponse } from 'next/server';3import { QueryCommand } from '@aws-sdk/lib-dynamodb';4import { ddbClient, TABLE } from '@/lib/dynamodb';56export async function GET(request: NextRequest) {7 try {8 const userId = request.nextUrl.searchParams.get('userId');9 if (!userId) return NextResponse.json({ error: 'userId required' }, { status: 400 });1011 // Requires a GSI with userId as partition key12 // GSI name: UserIdIndex, partition key: userId13 const result = await ddbClient.send(new QueryCommand({14 TableName: TABLE,15 IndexName: 'UserIdIndex',16 KeyConditionExpression: 'userId = :userId',17 ExpressionAttributeValues: { ':userId': userId },18 ScanIndexForward: false, // Most recent first (requires sort key)19 Limit: 50,20 }));2122 return NextResponse.json({23 items: result.Items ?? [],24 nextCursor: result.LastEvaluatedKey25 ? JSON.stringify(result.LastEvaluatedKey)26 : null,27 });28 } catch (err: any) {29 return NextResponse.json({ error: err.message }, { status: 500 });30 }31}Pro tip: Global Secondary Indexes (GSIs) let you query DynamoDB by attributes other than the primary partition key. Create a GSI in the AWS console on your table for any attribute you want to filter by. GSIs are provisioned separately and have their own read capacity costs.
Expected result: Your app supports both full-table ScanCommand for general listings and efficient QueryCommand for filtered results. The app deploys to Netlify or Bolt Cloud with environment variables set, and DynamoDB operations work identically in production.
Common use cases
User Profiles and Settings Store
Store per-user preferences, settings, and profile data in DynamoDB with the user ID as the partition key. Fast single-item lookups by user ID are one of DynamoDB's core strengths. Settings update and read in milliseconds with single-digit latency at any scale.
Create a user settings feature using DynamoDB. Set up a Next.js API route at /api/settings that reads and writes to a DynamoDB table called 'UserSettings' with userId as the partition key. The settings object should include: theme (light/dark), notificationsEnabled (boolean), and preferredLanguage (string). Build a settings page with a form that loads the current settings via GET /api/settings?userId=xxx and saves updates via POST /api/settings. Use the DynamoDB Document Client with GetCommand and PutCommand. Store AWS credentials in .env.
Copy this prompt to try it in Bolt.new
Real-Time Activity Feed
Build a time-ordered activity feed where each event is stored in DynamoDB with a composite key: the user ID as partition key and a timestamp as sort key. QueryCommand retrieves recent events in sorted order. This pattern supports paginated feeds, filtered views, and fan-out notifications.
Build a user activity feed using DynamoDB. Create a table called 'ActivityFeed' with userId as the partition key and timestamp (ISO string) as the sort key. Create a /api/activity/log route that writes activity events (PutCommand) with fields: userId, timestamp, action, and metadata. Create /api/activity/feed that queries the last 20 events for a user using QueryCommand with ScanIndexForward: false for reverse chronological order. Display the feed as a timeline list component with infinite scroll that fetches the next page using the LastEvaluatedKey cursor.
Copy this prompt to try it in Bolt.new
Product Catalog with Category Filtering
Store product data in DynamoDB with a product ID as partition key and use a Global Secondary Index (GSI) on the category attribute to enable fast filtering by category. This demonstrates DynamoDB's access pattern design — modeling data around how you query it.
Create a product catalog using DynamoDB. Set up a 'Products' table with productId as partition key. Create a /api/products/list route that uses ScanCommand to list all products and a /api/products/get?id=xxx route that uses GetCommand for single product lookup. Add a /api/products/create route that uses PutCommand. Build a product grid UI that shows all products with filter buttons by category. Store products with fields: productId, name, description, price, category, imageUrl, createdAt. Add a simple admin form to create new products.
Copy this prompt to try it in Bolt.new
Troubleshooting
ResourceNotFoundException: Requested resource not found when calling PutItem or GetItem
Cause: The DynamoDB table does not exist in the region configured in your AWS_REGION environment variable. Tables are region-specific — a table in us-east-1 is not accessible from a client configured for us-west-2.
Solution: Verify that AWS_REGION in your .env matches the region where you created the DynamoDB table in the AWS console. Open the AWS console, switch to the region where your table should exist, and confirm the table name matches DYNAMODB_TABLE exactly (case-sensitive).
ValidationException: The provided key element does not match the schema
Cause: The Key object passed to GetCommand, UpdateCommand, or DeleteCommand does not include the correct partition key attribute name or type. The partition key attribute name is case-sensitive and must match exactly what was defined when creating the table.
Solution: Open the DynamoDB console, click your table, and check the 'Overview' tab under 'Table details' for the exact partition key name and type. Your GetCommand Key object must use that exact attribute name. If your table has both a partition key and sort key, both must be included in the Key object.
1// Example: if your table has partitionKey='pk' and sortKey='sk'2const result = await ddbClient.send(new GetCommand({3 TableName: TABLE,4 Key: {5 pk: 'user#abc123', // must match partition key attribute name exactly6 sk: '2024-01-15T10:30:00.000Z', // must match sort key attribute name exactly7 },8}));TypeError: Cannot read properties of undefined (reading 'Item') after GetCommand
Cause: GetCommand returns { Item: undefined } when the requested key does not exist in the table — it does not throw an error. Accessing result.Item.name directly crashes when the item is not found.
Solution: Always check if result.Item is defined before accessing its properties. Return a 404 response when the item is not found rather than crashing.
1const result = await ddbClient.send(new GetCommand({ TableName: TABLE, Key: { itemId } }));2if (!result.Item) {3 return NextResponse.json({ error: 'Item not found' }, { status: 404 });4}5// Safe to access result.Item.name etc. below hereBest practices
- Use the DynamoDB Document Client (@aws-sdk/lib-dynamodb) for all data operations — it handles JavaScript type marshaling automatically and makes your code significantly cleaner than using the raw DynamoDBClient.
- Always set removeUndefinedValues: true in marshallOptions when creating the Document Client — undefined fields in JavaScript cause validation errors in DynamoDB's native type system.
- Design your table around access patterns before creating it: decide what queries you need to run, then choose partition keys and sort keys that make those queries efficient with QueryCommand rather than ScanCommand.
- Keep AWS credentials exclusively in server-side API routes — never import ddbClient in client components, as this would bundle your AWS credentials into the browser JavaScript.
- Use DynamoDB's on-demand capacity mode during development and early production — it requires no capacity planning and costs nothing when idle.
- For production IAM policies, scope permissions to specific table ARNs rather than using AmazonDynamoDBFullAccess — grant only the specific operations your app needs (GetItem, PutItem, etc.).
- Handle the case where GetCommand returns undefined for missing items — DynamoDB returns { Item: undefined } rather than throwing an error when an item is not found.
Alternatives
MongoDB Atlas uses a REST-based Data API that also works in WebContainers, but DynamoDB is natively serverless with per-request billing and no cluster management.
Firestore uses HTTP/WebSocket protocols and works in WebContainers like DynamoDB, but is schema-based with a document model and integrates natively with Firebase Auth.
Redis is a faster in-memory store for caching and session data, while DynamoDB is a durable persistent store suited for primary application data at any scale.
AWS S3 stores binary files and large objects, while DynamoDB stores structured key-value and document data — they complement each other in AWS-native architectures.
Frequently asked questions
Why does DynamoDB work in Bolt.new when MongoDB and PostgreSQL don't?
MongoDB (via mongoose) and PostgreSQL (via the pg package) establish persistent TCP socket connections to their servers. Bolt's WebContainer runtime cannot open raw TCP sockets — it can only make outbound HTTP and WebSocket connections. DynamoDB has no TCP layer at all; every operation is an HTTPS API call to AWS's endpoint. The @aws-sdk packages use JavaScript's Fetch API internally, which is fully supported in WebContainers.
Do I need to deploy my app to use DynamoDB during development?
No — this is one of DynamoDB's key advantages for Bolt.new development. Since DynamoDB communicates over HTTPS, all CRUD operations work in the Bolt preview during development. You can create items, query data, and build your entire data layer without deploying first. There is no webhook requirement for basic database operations, so the full integration works end-to-end in the WebContainer.
Is DynamoDB suitable for relational data with complex joins?
DynamoDB is a key-value and document store — it does not support SQL JOINs. For relational data with complex query requirements, consider Supabase (PostgreSQL via HTTP) or Firebase (Firestore document model). DynamoDB excels at high-performance, high-scale applications with well-defined access patterns: user profiles, activity feeds, session data, product catalogs with known filter dimensions.
How much does DynamoDB cost for a typical Bolt.new app?
DynamoDB's free tier is permanently available: 25GB storage and 200 million requests per month at no cost. For a typical early-stage Bolt.new app with hundreds of users and thousands of daily operations, you will almost certainly stay within the free tier. Beyond free tier, on-demand pricing is $1.25 per million write requests and $0.25 per million read requests — significantly cheaper than a managed database server.
Can I use DynamoDB with both Vite and Next.js projects in Bolt?
DynamoDB API calls must happen server-side to keep your AWS credentials secure. In a Next.js project, use Next.js API routes (/app/api/). In a Vite project, you need a separate server component — Vite's dev server can proxy requests, but there is no built-in server-side execution. The recommended pattern for Vite projects is pairing with Supabase Edge Functions for server-side logic. For DynamoDB specifically, a Next.js Bolt project is the cleanest fit.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation