August 19, 2026 · v1.4.20
v1.4.20 grows the data plane and tightens the edges. RDS gains opt-in Aurora PostgreSQL reader instances backed by real streaming replication and a working FailoverDBCluster; AWS IoT Jobs lands with its iot-jobs-data device plane; and CloudFormation learns API Gateway keys and usage plans. Around that is a broad S3 correctness pass — server-side encryption is now real contract state, CRC64NVME checksums compute instead of erroring, and versioning, canned ACLs and conditional multipart all move closer to AWS — plus parity fixes in DynamoDB, CloudWatch, EC2, IAM, Step Functions and Aurora DSQL.
MINISTACK_RDS_PG_CLUSTER_REPLICATION=1, the second and later members of an Aurora PostgreSQL cluster launch their own PostgreSQL containers, cloned from the writer with pg_basebackup and streaming WAL as hot standbys. A reader is genuinely read-only (pg_is_in_recovery() is true, writes fail with 25006), sees the writer's data, and the cluster ReaderEndpoint resolves to an available reader — so read/write routing, replica lag and reader-endpoint pooling can be rehearsed locally. Off by default; Aurora MySQL and the no-flag path keep aliasing the writer's shared container. Contributed by @kiran01bm.FailoverDBCluster. Forcing an Aurora failover was InvalidAction; it now promotes a reader to writer. An explicit TargetDBInstanceIdentifier must be an available reader in the cluster, or the lowest-PromotionTier reader is chosen; the response reports the transitional failing-over status and a follow-up DescribeDBClusters shows available with flipped IsClusterWriter flags, mirroring FailoverGlobalCluster. Contributed by @kiran01bm.CreateJob used to fall through to Unsupported IoT path and the iot-jobs-data endpoint did not exist, so device OTA loops could not be rehearsed. The iot service now serves the nine job operations and a new iot-jobs-data service the device ones (including the $next sentinel), sharing one store and the AWS execution state machine. Contributed by @iot-rocket.AWS::ApiGateway::ApiKey, AWS::ApiGateway::UsagePlan and AWS::ApiGateway::UsagePlanKey failed with Unsupported resource type; they now provision through the same stores the runtime APIs use, with Ref and Fn::GetAtt wired, unblocking CDK RestApi / ApiKey and Terraform aws_api_gateway_api_key through CloudFormation. Contributed by @ryan-bennett.InvalidRequest, a wrong-key read a 403 AccessDenied, following versions, copies and multipart completes. Contributed by @gaul.CRC64NVME checksums are computed instead of refused. CRC-64/NVME is what current SDKs and the CLI checksum with by default, and MiniStack answered InvalidRequest, so a stock aws s3 cp failed before it began. It is now computed from a stdlib table (no new dependency): uploads validate a client-supplied value with BadDigest on mismatch, and GET/HEAD return it. CRC32C still requires its native library. Contributed by @gaul.null id, a pre-versioning object stays addressable as VersionId=null, DeleteObjects mints and reports markers, a marker-hidden 404 carries x-amz-delete-marker: true, and UploadPartCopy honors the source ?versionId=. Contributed by @gaul.PutBucketAcl --acl public-read arrives as an x-amz-acl header with an empty body and was dropped, so buckets read back owner-only; the canned value is now validated and stored (bucket and CreateBucket), and object ACLs are per-version like tags. Contributed by @gaul.CompleteMultipartUpload honors If-Match / If-None-Match. Conditional writes were evaluated on PutObject but ignored on the multipart path, so a create-once or compare-and-swap upload could silently overwrite; the complete now evaluates the same preconditions (412 on violation, 404 NoSuchKey for an If-Match on a missing object). Contributed by @gaul.PutItem, Query and UpdateTable. A key declared S accepted an N value on write and in a key condition, and an attribute-definitions-only UpdateTable changed a key's type in place; all three now return the AWS ValidationException so no API changes a key's type. Reported by @iot-rocket.Average. GetMetricData and alarm evaluation returned Average for any pNN statistic; percentiles are now interpolated from the period's samples on both paths, and a percentile alarm's StateReason reports the actual statistic (e.g. p95). Contributed by @MGSousa.DescribeInstances / RunInstances emitted the address under publicIpAddress / publicDnsName rather than the wire tags ipAddress / dnsName, so every SDK dropped both; they now ride the real tags, and generated addresses are completed to four octets. Contributed by @iot-rocket.arn:aws:states:::events:putEvents actually publishes. The optimized EventBridge integration fell through to the task passthrough, so the state reported SUCCEEDED while nothing reached any rule target; it now calls EventBridge PutEvents and returns its response. Reported by @iwasakar.Description charset is validated. CreateRole, UpdateRole and UpdateRoleDescription now reject a description outside IAM's allowed character set or longer than 1000 characters with a 400 ValidationError. Reported by @iot-rocket.SELECT ... FOR UPDATE is no longer refused for its predicate. Strict mode enforced a rule DSQL does not: measured against a live cluster, DSQL restricts the lock strength, not the predicate. FOR UPDATE now locks whatever the query selects (no WHERE, non-key columns, joins, keyless tables all succeed), while FOR NO KEY UPDATE / FOR SHARE / FOR KEY SHARE are refused — so the emulator stops failing correct ORM code.DROP COLUMN "ID" and mixed-case or schema-qualified table names were mis-resolved; identifiers are now folded as PostgreSQL folds them (bare to lower case, quoted verbatim) and the relation is requoted part by part before lookup.AWS::IoT::Policy updates apply instead of rolling back. The type had no update handler, so an edit fell through to create, hit ResourceAlreadyExistsException and rolled back; a changed PolicyDocument is now a no-interruption update stored as a new default version (pruning the oldest to stay under IoT's five-version cap), and a changed PolicyName is a replacement.AWS::SSM::Parameter goes through the SSM API. Provisioning now writes through the API, so a create over an existing name fails (ParameterAlreadyExists), updates increment Version and record history, a Name change replaces, a SecureString type is rejected, and Fn::GetAtt exposes Arn / Type / Value; a Parameter::Value<...> reference is re-resolved on UpdateStack. Reported by @iot-rocket.docker pull ministackorg/ministack:1.4.20 docker run -d -p 4566:4566 ministackorg/ministack:1.4.20
Or pin in compose.yaml:
services:
ministack:
image: ministackorg/ministack:1.4.20
ports:
- "4566:4566"
Issues and PRs welcome on GitHub. Discussion on r/ministack.