Local development
AWS Blocks is local-first. When you run npm run dev, your entire application runs on your local machine: backend, frontend, database, authentication, and background jobs. You don’t need an AWS account, AWS credentials, an internet connection, or a running container daemon. Every Block ships with a local implementation that exposes the same typed API as its AWS counterpart, so no emulator or additional tooling is required. The only prerequisite is Node.js version 22 or later.
This page explains how local development works, what each Block maps to locally, and how to use local implementations to test your application.
How local development works
Conditional exports
Every Block is an npm package with several implementations behind a single import. AWS Blocks uses Node.js conditional exportsimport statement to the right implementation for the execution context:
{ "exports": { ".": { "browser": "./dist/index.browser.js", "cdk": "./dist/index.cdk.js", "aws-runtime": "./dist/index.aws.js", "default": "./dist/index.local.js" } } }
| Context | Export condition | What resolves |
|---|---|---|
|
Local development |
|
The local implementation. Your code runs in-process on your machine, backed by the filesystem, in-memory state, or an embedded engine. |
|
Deployment (CDK synthesis) |
|
CDK constructs that define the Block’s AWS infrastructure. |
|
Production runtime (Lambda) |
|
The AWS SDK implementation that calls real AWS services. |
|
Browser |
|
The client-side code bundled into your frontend. |
The local implementation is the default resolution, so any Node.js process that imports your backend gets the local implementations automatically. This includes the dev server, your test runner, and any script you run. You don’t turn on a local mode. The local implementation is used whenever no deployment context applies. AWS Blocks applies the aws-runtime condition only when it bundles your backend for Lambda, so the AWS SDK is never loaded during local development.
You never configure this yourself. AWS Blocks sets up the build system to select the correct implementation for each context automatically.
The dev server
npm run dev starts a local dev server in watch mode:
-
Your backend runs in-process. There is no separate compute to provision, and code changes reload automatically.
-
Your frontend is served at
http://localhost:3000. -
ApiNamespacecalls route through the local HTTP server with the same request and response semantics as Amazon API Gateway in production, includingBlocksContext, cookies, and headers. -
Realtimechannels connect over a WebSocket served by the dev server. -
CronJobschedules fire while the dev server is running.
Where local state lives
Blocks that persist data write to a .bb-data/ directory at your project root. Each Block instance gets its own subdirectory, named after the Block’s scope ID.
This has three practical benefits:
-
Data persists across restarts. Stop and restart the dev server, and your users, files, and table rows are still there.
-
You can inspect the state directly. The data is stored as plain files, so you can open your
EmailClientoutbox or a `KVStore’s contents in your editor. -
You can reset at any time. Delete
.bb-data/, or a single Block’s subdirectory, to start from a clean state.
Keep .bb-data/ out of version control. Scaffolded AWS Blocks applications add it to .gitignore for you.
What each Block maps to locally
Every Block exposes the same typed API locally and on AWS. The following table shows what backs that API in each environment.
| Block | Locally | On AWS |
|---|---|---|
|
|
Signed JWT sessions in HTTP-only cookies, with user records in a local store |
Same JWT sessions, with user records in a DynamoDB table |
|
|
An in-process stub identity provider, so you can complete full sign-in flows without registering an external provider |
Real OIDC redirect flow with your configured provider (Google, GitHub, Okta, or any compliant provider) |
|
|
An in-process simulation of the Cognito user pool. Sign-up, sign-in, MFA, and passkey flows work without any AWS calls |
An Amazon Cognito user pool with your configured options |
|
|
File-backed store in |
DynamoDB table |
|
|
File-backed store in |
DynamoDB table with Global Secondary Indexes |
|
|
PGlite, an embedded WebAssembly PostgreSQL engine, running in-process with data in |
Aurora Serverless v2 (PostgreSQL) |
|
|
PGlite with a validation layer that enforces Amazon Aurora DSQL compatibility, so unsupported SQL fails locally instead of at deploy time |
Aurora DSQL |
|
|
A directory in |
S3 bucket |
|
|
Typed pub/sub over a WebSocket served by the local dev server |
API Gateway WebSocket API with DynamoDB connection management |
|
|
In-process queue. Handlers execute inside the dev server, and job status transitions ( |
SQS queue with a Lambda consumer |
|
|
In-process scheduler. |
EventBridge rule triggering a Lambda function |
|
|
A canned, keyword-based provider that returns predictable responses without a real model, API key, or cloud cost. You can alternatively point an |
Amazon Bedrock |
|
|
A local text index built over your documents in |
Amazon Bedrock Knowledge Bases with automatic ingestion, chunking, and embedding |
|
|
Emails are captured to a local outbox ( |
Amazon SES |
|
|
File-backed values in |
SSM Parameter Store (SecureString for secrets) |
|
|
Structured logs to your terminal, using the same code path as production |
Same output, captured by CloudWatch Logs with request correlation |
|
|
EMF-formatted JSON to your terminal, using the same code path as production |
Same output, captured as CloudWatch metrics |
|
|
Traces recorded to files in |
AWS X-Ray distributed tracing |
|
|
Not available locally. The dashboard route responds with a message directing you to deploy, because there is no CloudWatch dashboard to render |
CloudWatch dashboard with widgets for your metrics |
Note two things about this table:
-
LoggerandMetricsrun the same code in both environments. Only the destination of the output differs: what you see in your terminal locally is what CloudWatch captures in production. -
The data Blocks use real database engines locally.
Databaseruns actual PostgreSQL through PGlite, andDistributedDatabaseadditionally validates your SQL against Aurora DSQL restrictions, so incompatible SQL fails on your machine instead of during a deployment.
Testing with local implementations
Local implementations also form the basis for testing. A typical AWS Blocks test setup has three levels: unit tests for your business logic, integration tests against local implementations, and end-to-end tests against a sandbox. The first two levels run entirely on your machine.
Unit test business logic with Blocks as parameters
Extract business logic from your API handlers into pure functions that accept Block instances as parameters. The functions depend only on the Block’s typed interface, so you can test them with a standard test runner such as Vitest, without starting the dev server:
// orders.ts - testable without the full framework export async function createOrder(store: KVStore, userId: string, input: OrderInput) { if (!input.title) throw new Error('Title required'); const order = { id: crypto.randomUUID(), ...input, userId }; await store.put(`${userId}:${order.id}`, order); return order; }
// orders.test.ts import { it, expect, vi } from 'vitest'; import { createOrder } from './orders.js'; it('creates an order', async () => { const testStore = { put: vi.fn(), get: vi.fn() }; const result = await createOrder(testStore, 'user-1', { title: 'Test' }); expect(result.title).toBe('Test'); expect(testStore.put).toHaveBeenCalled(); });
For more information about structuring your application this way, see Testing in Best practices.
Integration test against local implementations
For tests that cover Block behavior itself, run your application with npm run dev and test against it. Blocks resolve to their local implementations automatically.
-
Tests run in milliseconds because everything is in-process.
-
Stateful Blocks behave realistically: conditional writes on
KVStorefail when they should,Databaseenforces your migrations and Row Level Security, andAsyncJobmoves jobs through its status transitions. -
Tests run anywhere Node.js runs, including CI, without AWS credentials.
Control and inspect state through .bb-data/
Because local state is plain files, your tests can seed and assert on it directly:
-
Reset between tests or runs by deleting
.bb-data/or one Block’s subdirectory. -
Assert on side effects that are hard to observe in the cloud. For example,
EmailClientwrites every email to a local outbox file. A test can trigger a password reset and then read the outbox to verify the message content.
Test against a sandbox
Local implementations match the behavior of their AWS counterparts at the API level, but some things can only be verified against real services: IAM permission boundaries, service quotas and throttling, DynamoDB query performance at scale, or real model output from Amazon Bedrock.
For those cases, deploy a sandbox with npm run sandbox and run the same tests against it. A sandbox is a fast, ephemeral deployment to AWS that each developer gets in isolation. A useful pattern is to parameterize your end-to-end tests by target environment, so the same test suite runs against localhost during development and against a sandbox URL before you ship:
| Run against | Use for |
|---|---|
|
Local dev server |
Day-to-day development, fast feedback, CI on every commit |
|
Sandbox deployment |
IAM and permission verification, performance characteristics, service-specific behavior, real AI model responses |
|
Production deployment |
Smoke tests after release |
Do most of your development and testing against local implementations, and use a sandbox when you need to verify the behavior of the AWS services themselves.
For more information about how Blocks are structured, see Conditional exports. For deploying to AWS, see Deploy to AWS.