Guide version migrations for MBC CQRS Serverless framework. Use this when upgrading framework versions, migrating from deprecated APIs, or understanding breaking changes between versions.
Before executing this skill, check for updates:
mbc install-skills --check to check if a newer version is availablembc install-skills --force to updateNote: Skip this check if the user explicitly says to skip updates or if you've already checked in this session.
This skill helps migrate MBC CQRS Serverless projects between versions.
| From Version | To Version | Migration Complexity | Key Changes |
|---|---|---|---|
| v1.0.16 | v1.0.17 | Low | MasterDataService.search() fix |
| v1.0.17 | v1.0.18 | Low | ImportStatusHandler.sendTaskFailure() |
| v1.0.18 | v1.0.19 | Low | ImportQueueEventHandler error handling |
| v1.0.19 | v1.0.20 | Low | CsvImportSfnEventHandler status fix |
| v1.0.20 | v1.0.21 | Medium | ZIP Finalization Hooks |
| v1.0.21 | v1.0.22 | Low | DynamoDB export filter fix |
| v1.0.22 | v1.0.23 | Low | sendInlineTemplateEmail() |
| v1.0.x | v1.1.0 | High | TENANT_COMMON → lowercase; publish() removed |
| v1.1.x | v1.1.4 | Low | publishSync audit trail (transparent) |
| v1.1.x | v1.1.5 | Medium | CSV import v2 batch architecture |
| v1.1.x | v1.2.0 | High | publishSync null return; genNewSequence() removed |
| v1.2.0 | v1.2.1 | Low | SqsService added (new feature) |
| v1.2.1 | v1.2.2 | Low | CsvBatchProcessor Poison Pill fix (transparent) |
| v1.2.x | v1.2.4 | Medium | TaskModule.register() must be in AppModule |
| v1.2.4 | v1.2.5 | Low | ZIP import refactor (transparent); MCP AP016–AP020 added |
| v1.2.5 | v1.2.6 | Low | Repository RYW improvements (transparent); getVersion API |
| v1.2.7 | v1.3.0 | Low | AppSync Events API support (opt-in, no breaking changes) |
| v1.3.0 | v1.3.1 | Low | Group-based roles (@GroupRoleResolver, custom:groups); UserContext.tenantRoles/tenantGroupIds added (opt-in, no breaking changes) |
| v1.3.2 | v1.3.3 | Low | ATTRIBUTE_LIMIT_SIZE default lowered 389120 → 102400 for Step Functions payload limit (transparent unless overridden) |
| v1.3.5 | v1.4.0 | Low | Configurable DynamoDB table names (opt-in) + registerAsync on domain modules; prismaService now always required; MasterModule+SettingModule shared-table alias warns |
| v1.4.0 | v1.5.0 | Low | Local dev only: LocalStack → Floci as the local S3 emulator; bucket CORS must be applied locally for presigned URLs (no application code changes) |
Breaking Change: MasterDataService.search() settingCode behavior
Before (v1.0.16):
// settingCode was incorrectly using partial match
const results = await masterDataService.search({
settingCode: 'CONFIG', // Matched 'CONFIG', 'CONFIG_A', 'CONFIG_B'
});
After (v1.0.17):
// settingCode now uses exact match
const results = await masterDataService.search({
settingCode: 'CONFIG', // Only matches 'CONFIG' exactly
});
Migration Steps:
MasterDataService.search() calls// If you need multiple settings
const configs = await Promise.all([
masterDataService.search({ settingCode: 'CONFIG_A' }),
masterDataService.search({ settingCode: 'CONFIG_B' }),
]);
New Feature: ImportStatusHandler.sendTaskFailure()
Addition:
// New method available for explicit failure signaling
await importStatusHandler.sendTaskFailure({
taskToken: event.taskToken,
error: 'ValidationError',
cause: 'Invalid CSV format',
});
Migration Steps:
sendTaskFailure() for better Step Functions integrationBug Fix: ImportQueueEventHandler error handling
Before (v1.0.18):
After (v1.0.19):
Migration Steps:
Bug Fix: CsvImportSfnEventHandler status determination
Before (v1.0.19):
After (v1.0.20):
FAILURE status when processedRows === 0Migration Steps:
New Feature: ZIP Finalization Hooks
Addition:
// New interface for ZIP import finalization
interface IZipFinalizationHook {
afterFinalize(context: ZipFinalizationContext): Promise<void>;
}
// Module registration
@Module({
imports: [
ImportModule.register({
zipFinalizationHooks: [MyCustomFinalizationHook],
}),
],
})
export class AppModule {}
Migration Steps:
Example Implementation:
@Injectable()
export class NotificationFinalizationHook implements IZipFinalizationHook {
constructor(private readonly notificationService: NotificationService) {}
async afterFinalize(context: ZipFinalizationContext): Promise<void> {
const { results, status } = context;
if (status === ImportStatusEnum.SUCCESS) {
await this.notificationService.sendSuccess({
message: `Import completed: ${results.processedRows} rows processed`,
});
} else {
await this.notificationService.sendFailure({
message: `Import failed: ${results.failedRows} rows failed`,
});
}
}
}
Bug Fix: DynamoDB export S3 filter
Before (v1.0.21):
After (v1.0.22):
Migration Steps:
New Feature: sendInlineTemplateEmail()
Addition:
// New method for inline email templates
await notificationService.sendInlineTemplateEmail({
to: ['user@example.com'],
subject: 'Order Confirmation - {{orderId}}',
htmlBody: '<h1>Thank you for your order!</h1><p>Order ID: {{orderId}}</p>',
textBody: 'Thank you for your order! Order ID: {{orderId}}',
templateData: {
orderId: 'ORD-12345',
},
});
Migration Steps:
1. TENANT_COMMON renamed to lowercase
// Before (v1.0.x)
const pk = `MASTER_SETTING#COMMON#${settingCode}`
// After (v1.1.0+)
const pk = `MASTER_SETTING#common#${settingCode}`
#COMMON must be migrated to #commonnormalizeTenantCode(), isCommonTenant()2. publish() and publishPartialUpdate() removed
// Removed — compilation error in v1.1.0+
this.commandService.publish(...)
this.commandService.publishPartialUpdate(...)
// Use instead
this.commandService.publishAsync(...)
this.commandService.publishPartialUpdateAsync(...)
publishSync now writes a full audit trail to Command and History tables (parity with async pipeline). No migration required; behavior is transparent.
Step Functions state machine changes required:
finalize_parent_job state is now requiredimport_tmp table is removedprocessedRows, succeededRows, failedRows) are aggregated at completion1. publishSync / publishPartialUpdateSync return type change
// Before (v1.1.x) — always returned CommandModel
const result = await commandService.publishSync(entity, options)
console.log(result.pk) // safe
// After (v1.2.0+) — returns null when command is not dirty (no-op)
const result = await commandService.publishSync(entity, options)
if (!result) return // no-op
console.log(result.pk) // safe after null check
2. SequenceService.genNewSequence() removed
// Removed — compilation error in v1.2.0+
await sequenceService.genNewSequence(...)
// Use instead
await sequenceService.generateSequenceItem(...)
// or
await sequenceService.generateSequenceItemWithProvideSetting(...)
3. Read-Your-Writes (RYW) consistency (new optional feature)
import { DetailKey, Repository } from '@mbc-cqrs-serverless/core'
// Enable: set RYW_SESSION_TTL_MINUTES env var (e.g. "5")
// Session table {NODE_ENV}-{APP_NAME}-session must be created
// No effect if unset — zero impact on existing projects
singleImportProcessor.process() now receives event.importEvent instead of event.payloadBoth fixes are transparent — no migration steps needed.
Breaking for apps using @mbc-cqrs-serverless/master
TaskModule.register() is now global (global: true) and must be called exactly once in the host AppModule. MasterModule no longer registers TaskModule internally.
// Before (v1.2.3 and earlier) — worked automatically
@Module({
imports: [MasterModule.register({ enableController: true, prismaService: PrismaService })],
})
export class AppModule {}
// After (v1.2.4+) — must register explicitly
import { TaskModule, TaskQueueEventFactory } from '@mbc-cqrs-serverless/master'
@Module({
imports: [
TaskModule.register({ taskQueueEventFactory: MyTaskQueueEventFactory }),
MasterModule.register({ enableController: true, prismaService: PrismaService }),
],
})
export class AppModule {}
Migration steps:
TaskQueueEventFactory from @mbc-cqrs-serverless/masterTaskModule.register() once in AppModuleTaskModule.register() from all feature modulesSymptom if skipped: App crashes at startup with Nest can't resolve dependencies of MyTaskService (?).
Framework changes (transparent):
ZipImportQueueEventHandler removed; ZIP import jobs are now processed directly inside ImportService.ImportEventHandler no longer publishes SQS messages for ZIP_MASTER_JOB events.CreateZipImportDto and improved error handling/logging.No application code changes are required. If you previously imported ZipImportQueueEventHandler directly (uncommon), remove the import.
MCP server changes:
mbc_check_anti_patterns tool expanded from 15 to 20 detectors (AP016–AP020 added):@ApiTags (Low)getCommandSource for Tracing (Low)@modelcontextprotocol/sdk updated 1.26.0 → 1.29.0.The Repository class now actively purges stale RYW sessions when the data table catches up to or surpasses the session version. This eliminates the "stale override" issue where a user could see their own older write even after an external update was already absorbed into the data table.
Behavior changes (transparent — no code changes required):
getItem: when existing.version >= session.version, the session is purged in the background and the persisted data is returned directly (skipping the unnecessary command-table read).listItemsByPk: synchronized sessions are cleaned up in-place during the merge loop.listItems (RDS path): per-session checks are now parallelized via Promise.all, eliminating sequential N+1 latency.New optional optimization for listItems:
// If your RDS rows already carry `version`, supply `getVersion` to skip the
// extra DynamoDB GetItem entirely when the existing RDS row proves caught-up.
await repository.listItems(
() => rdsQuery(),
{
latestFlg: true,
transformCommand: (cmd) => ({
id: cmd.id,
version: cmd.version,
// ...map other CommandModel fields to your RDS row shape
}),
getVersion: (item) => item.version, // ← new in v1.2.6
},
options,
)
Note: getVersion only short-circuits the update path (when the session's itemId matches an existing RDS row). Create-new items (session present but not yet reflected in RDS) still fetch the command as before — there is no existing row to derive a version from.
All changes are backward-compatible: getVersion is optional, and the cleanup behavior is automatic when RYW_SESSION_TTL_MINUTES is enabled. Projects with RYW disabled (env var unset) are unaffected.
Deprecated in: v1.0.0 Removed in: TBD
Before:
await this.commandService.publish(command, options);
After:
await this.commandService.publishAsync(command, options);
Migration Script:
# Find all occurrences
grep -r "\.publish(" --include="*.ts" src/
# Replace (use with caution)
find src/ -name "*.ts" -exec sed -i '' 's/\.publish(/\.publishAsync(/g' {} \;
Deprecated in: v1.0.0 Removed in: TBD
Before:
await this.commandService.publishPartialUpdate(command, options);
After:
await this.commandService.publishPartialUpdateAsync(command, options);
Status: Not deprecated, but use sparingly
Recommendation:
// Only use publishSync when immediate consistency is required
// Example: User registration where we need the user ID immediately
const result = await this.commandService.publishSync(command, options);
// For most cases, prefer publishAsync
await this.commandService.publishAsync(command, options);
npm installWhen using this skill, Claude will:
Analyze Current Version:
npm list @mbc-cqrs-serverless/core
Check for Deprecated Usage:
# Search for deprecated patterns
grep -r "\.publish(" --include="*.ts" src/
grep -r "\.publishPartialUpdate(" --include="*.ts" src/
grep -r "publishSync" --include="*.ts" src/
Identify Breaking Changes:
Generate Migration Plan:
Cause: Version field handling changed Solution:
// Always fetch current version before update
const existing = await this.dataService.getItem(key);
await this.commandService.publishPartialUpdateAsync({
...updateData,
version: existing.version,
}, options);
Cause: Type mismatch in decorator Solution:
// Ensure type matches entity's type field
@DataSyncHandler({ type: 'ORDER' }) // Must match command.type
export class OrderDataSyncRdsHandler implements IDataSyncHandler {}
Cause: Missing error handling in v1.0.18 Solution: Upgrade to v1.0.19+ for proper error propagation
Change: AppSync Events API support added (opt-in, no breaking changes)
No code changes are required. AppSyncEventsService is registered in NotificationModule automatically but does nothing unless NOTIFICATION_TRANSPORTS includes appsync-event.
To adopt the new Events API transport:
Step 1 — Provision infrastructure (CDK)
Add appsyncEvents and notificationTransports to your Config:
// infra/config/config.ts
export const config: Config = {
appsyncEvents: {
enabled: true,
namespace: 'default', // must match the ChannelNamespace created in AppSync
apiKeyExpireDays: 365,
},
notificationTransports: 'appsync-graphql,appsync-event', // dual-publish during migration
}
Run cdk deploy to create the EventApi and ChannelNamespace. The stack outputs AppSyncEventsHttpEndpoint and AppSyncEventsNamespace.
Step 2 — Enable dual-publish (migration phase)
When appsyncEvents.enabled: true, the CDK stack automatically injects these env vars into Lambda/ECS:
NOTIFICATION_TRANSPORTS=appsync-graphql,appsync-event # dual-publish
APPSYNC_EVENTS_ENDPOINT=https://xxxx.appsync-api.ap-northeast-1.amazonaws.com/event
APPSYNC_EVENTS_NAMESPACE=default
APPSYNC_ENDPOINT=https://xxxx.appsync-api.ap-northeast-1.amazonaws.com/graphql # keep existing
Step 3 — Switch to Events API only
Once all clients have migrated to the Events API, update notificationTransports in CDK Config:
notificationTransports: 'appsync-event', // Events API only
Then redeploy. APPSYNC_ENDPOINT can be removed from the environment.
Channel subscription patterns (for client-side code):
// All events for a tenant
appsyncClient.subscribe(`/${namespace}/${tenantCode}/*`)
// Filtered by action
appsyncClient.subscribe(`/${namespace}/${tenantCode}/${action}/*`)
// Track one specific command result
appsyncClient.subscribe(`/${namespace}/${tenantCode}/${action}/${sanitizedId}`)
Non-alphanumeric characters in id (e.g. #, @ from DynamoDB pk/sk) are sanitized to - automatically on the server side. Client-side code using EventsSubscriptionClientImpl from the example app applies the same sanitization.
Migration Steps:
appsyncEvents and notificationTransports to CDK Config and deploynotificationTransports: 'appsync-event' and redeployChange: Group-based roles added (opt-in, no breaking changes).
RolesGuard now checks direct roles from custom:roles first, then roles derived from the user's groups in custom:groups. UserContext gains two fields — tenantRoles (direct roles array) and tenantGroupIds (group IDs for the active tenant) — while tenantRole (singular) is kept for backward compatibility. No code changes are required to upgrade; existing role checks keep working.
To adopt group-based roles:
Step 1 — Add custom:groups to the JWT (tenant-scoped; group → role mappings are NOT in the token):
{
"custom:roles": "[{\"tenant\":\"tenant-a\",\"role\":\"admin\"}]",
"custom:groups": "[{\"tenant\":\"tenant-a\",\"groups\":[\"sales-team\"]}]"
}
Step 2 — Implement exactly one resolver that maps group IDs to roles (loaded from DynamoDB/RDS/config):
import {
GroupRoleResolver,
IGroupRoleResolver,
} from '@mbc-cqrs-serverless/core';
// Do NOT add @Injectable() — @GroupRoleResolver() already registers the provider
// as a singleton. A second @Injectable() can override the scope and break bootstrap.
@GroupRoleResolver()
export class AppGroupRoleResolver implements IGroupRoleResolver {
async resolveRoles({ tenantCode, groupIds, claims }) {
// Map the user's group IDs to roles for this tenant
return ['viewer', 'reporter'];
}
}
Register the class in your module providers. AuthModule is imported automatically via AppModule.forRoot().
Notes / gotchas:
REQUEST/TRANSIENT scope.custom:groups is tolerated (fail-closed → no group roles), so a bad token degrades to direct-role-only checks rather than crashing.getUserRole() on RolesGuard is deprecated and no longer called by the default guard. For custom authorization, override verifyRole, resolveGroupRoles, or canOverrideTenant instead.custom:roles consistent with your @Roles(...) values.Migration Steps:
@Roles() checks keep working.custom:groups in the JWT and implement one @GroupRoleResolver().providers.Change: The framework's default ATTRIBUTE_LIMIT_SIZE in generated CDK templates (infra/libs/infra-stack.ts) and env examples (.env.local, .env.example) is lowered from 389120 (380 KB) to 102400 (100 KB).
Why: The old default was sized for DynamoDB's 400 KB item limit, but AWS Step Functions enforces a 256 KB payload limit per state. The command state machine passes the DynamoDB stream event twice per state (input.$ + context.$), so inline attributes above ~110 KB could trigger States.DataLimitExceeded in production. See Debug Guide — States.DataLimitExceeded for the full symptom/cause/fix.
Action required:
ATTRIBUTE_LIMIT_SIZE and redeploy your CDK stack, the new lower default takes effect automatically — no code changes needed.ATTRIBUTE_LIMIT_SIZE=389120 (or another value above ~110 KB) in your own .env or CDK stack, lower it to 102400 or less to avoid States.DataLimitExceeded. Attributes above the limit are automatically offloaded to S3, so lowering the value does not lose data.v1.4.0 makes the DynamoDB table names of the domain modules (master, directory, survey-template, ui-setting) configurable and adds registerAsync support. All new options default to the current names, so existing apps upgrade with no code changes.
New capabilities (opt-in):
tableName? on MasterModule / DirectoryStorageModule / SurveyTemplateModule / SettingModule (defaults: master / directory / survey / master).DirectoryStorageModule also accepts pkPrefix? (default DIRECTORY) and prismaModelName? (default directory).registerAsync(...) on all four modules.Behavior changes to be aware of (not breaking for the common setup):
prismaService is now always required when registering MasterModule / DirectoryStorageModule / SurveyTemplateModule (previously it was only validated when enableController: true). The domain services inject PRISMA_SERVICE unconditionally, so a missing prismaService now fails fast at startup with a clear message instead of a cryptic UnknownDependenciesException. Action: if you registered one of these modules with enableController: false and omitted prismaService (relying on a globally-provided token), pass prismaService explicitly.MasterModule + SettingModule on the same (default master) table, the framework now logs a warning at startup when both own the master_CommandEventHandler alias, and which module's data-sync handlers run is import-order dependent. Set registerEventHandlerAlias: false on SettingModule so MasterModule owns the alias (deterministic). This was a silent ambiguity before; nothing breaks, but the warning is new.Changing a table name is opt-in and backward compatible. Example: run the directory module as document (this is how the v1.3.x directory → document rename is now expressed — as configuration, not a forced default):
// Synchronous
DirectoryStorageModule.register({
enableController: true,
prismaService: PrismaService,
tableName: 'document', // physical tables: <env>-<app>-document-command/-data/-history
pkPrefix: 'DOCUMENT', // partition key: DOCUMENT#<tenantCode>
prismaModelName: 'document', // RDS reads via prismaService.document
})
// Async — the factory must resolve and return the PrismaService INSTANCE
DirectoryStorageModule.registerAsync({
tableName: 'document',
pkPrefix: 'DOCUMENT',
prismaModelName: 'document',
imports: [PrismaModule],
inject: [PrismaService],
useFactory: (prisma) => ({ prismaService: prisma }),
})
For MasterModule / SurveyTemplateModule / SettingModule, only tableName applies:
MasterModule.register({ enableController: true, prismaService: PrismaService, tableName: 'catalog' })
Set all three (tableName + pkPrefix + prismaModelName) together for directory — changing only tableName leaves the PK as DIRECTORY# and reads prismaService.directory, which is inconsistent.
Required provisioning and data migration (application responsibility):
"document") — not the -command/-data/-history variants — to prisma/dynamodbs/cqrs.json, then run npm run migrate:ddb (prisma/ddb.ts). This creates <name>-command/-data/-history. Only @mbc-cqrs-serverless/master's postinstall auto-adds its default master; every other module and every custom name must be added to cqrs.json by hand. Mirror the three physical tables in your IaC.model Document { ... } for prismaModelName: 'document') to schema.prisma, then run npm run migrate:rds (prisma migrate deploy).migrate only creates the (empty) tables. Copying existing rows from directory-* / DIRECTORY# / the directory model to the new document-* / DOCUMENT# / document targets is your responsibility (DynamoDB scan-and-rewrite + an RDS data migration). See Data Migration Patterns.Caveat — do NOT rename the
mastertable. Themastertable is a framework-wide central config store:TtlService(TTL config) and@mbc-cqrs-serverless/sequence(numbering formats) read a fixedmaster-dataname. OverridingMasterModule'stableNamemakes those readers miss their config (TTLs not applied, sequence formats fall back silently);MasterModule.register/registerAsynclogs a warning in that case. Renamingdirectory/survey-template/ui-settinghas no such caveat.
Action required: none if you keep the defaults. To adopt a custom name, follow the steps above.
v1.5.0 replaces LocalStack with Floci (floci/floci:1.6.0) as the local S3 emulator. LocalStack Community Edition reached end of life in March 2026 (it now requires an auth token and no longer receives security updates), and the template pinned no image version. Floci serves S3 on the same port (4566) with the same path-style addressing, so application code and .env values (S3_ENDPOINT, LOCAL_S3_PORT, S3_REGION, S3_BUCKET_NAME) do not change. Deployed environments are not affected.
Newly scaffolded projects (mbc new) already use Floci. Existing projects keep working on LocalStack, but that image no longer receives security patches, so migrate when convenient.
Step 1 — Replace the compose service. In infra-local/docker-compose.yml, replace the localstack service with:
floci:
image: floci/floci:1.6.0
ports:
- '${LOCAL_S3_PORT:-4566}:4566'
environment:
- FLOCI_DEFAULT_REGION=ap-northeast-1
- FLOCI_STORAGE_MODE=persistent
volumes:
- floci-data:/app/data
and declare the named volume at the top level of the same file:
volumes:
floci-data:
FLOCI_STORAGE_MODE=persistent is required: the default is memory, which silently discards every bucket on docker compose down. The volume is project-scoped through COMPOSE_PROJECT_NAME in .env, so projects on the same machine do not share S3 data.
Step 2 — Remove the plugin. Remove serverless-localstack from package.json (and any commented localStackConfig block in infra-local/serverless.yml), then run npm install.
Step 3 — Add the bucket CORS rule to your resource scripts (required for presigned URLs). LocalStack allowed every origin through EXTRA_CORS_ALLOWED_ORIGINS=*. Floci 1.6.0 does not honor that variable (its own global switch is FLOCI_SECURITY_EXTRA_CORS_ALLOWED_ORIGINS), so without a CORS rule it answers the browser preflight with 403 and presigned upload/view URLs from DirectoryFileService fail in the browser. The template applies a bucket CORS rule instead, mirroring how production configures CORS on the bucket in the CDK stack. Copy the configure S3 bucket CORS block from the latest template's infra-local/scripts/resources.sh / resources.ps1 into your own scripts, so the rule is re-applied every time the scripts run (npm run offline:sls runs them for you).
On Windows, the template passes the JSON as file:// and writes the file without a BOM: Windows PowerShell 5.1 adds one for Set-Content -Encoding utf8, and the AWS CLI rejects it with Error parsing parameter '--cors-configuration'. Keep the template's [System.IO.File]::WriteAllText($path, $json, (New-Object System.Text.UTF8Encoding $false)) and $LASTEXITCODE check when you copy the block.
Step 4 — Start Floci and recreate the bucket. Objects under docker-data/localstack are not migrated. npm run offline:docker runs docker compose up in the foreground and does not return, so use two terminals from the project root:
# Terminal 1 — keeps running
npm run offline:docker
# Terminal 2 — once Floci is up: creates the bucket and applies the CORS rule
bash infra-local/scripts/resources.sh # Windows: npm run resources:win32
npm run offline:sls also runs these scripts, but on macOS/Linux it starts them in the background alongside Serverless Offline (not before it, and without stopping on failure); only on Windows does it wait for them. Run resources.sh by hand once as above and confirm it succeeded before you test presigned URLs.
If you did not update the scripts in Step 3, apply the rule once by hand after the bucket exists:
set -a; . ./.env; set +a # load S3_ENDPOINT, S3_BUCKET_NAME and the local credentials
aws --endpoint-url="$S3_ENDPOINT" s3api put-bucket-cors \
--bucket "$S3_BUCKET_NAME" \
--cors-configuration '{"CORSRules":[{"AllowedOrigins":["*"],"AllowedMethods":["GET","PUT","POST","DELETE","HEAD"],"AllowedHeaders":["*"],"ExposeHeaders":["ETag"]}]}'
Verify (from the project root, with .env loaded as above):
aws --endpoint-url="$S3_ENDPOINT" s3 ls
aws --endpoint-url="$S3_ENDPOINT" s3api get-bucket-cors --bucket "$S3_BUCKET_NAME"
Action required: Steps 1–4 for existing projects that want to leave LocalStack. None for application code.
| Core Version | CLI Version | NestJS | Node.js | TypeScript |
|---|---|---|---|---|
| v1.5.0 | v1.5.0 | 10.x | 18+ | 5.x |
| v1.4.0 | v1.4.0 | 10.x | 18+ | 5.x |
| v1.3.0 | v1.3.0 | 10.x | 18+ | 5.x |
| v1.0.23 | v1.0.23 | 10.x | 18+ | 5.x |
| v1.0.22 | v1.0.22 | 10.x | 18+ | 5.x |
| v1.0.21 | v1.0.21 | 10.x | 18+ | 5.x |
| v1.0.20 | v1.0.20 | 10.x | 18+ | 5.x |
If you encounter migration issues: