DEV Community

What to build a file portal on — Nx Plugin for AWS 1.0, AWS Blocks and Amplify Gen 2, same four features (File Portal Part 7)

Introduction

Nx Plugin for AWS reached 1.0 and AWS Blocks appeared in preview, so starting an application on AWS now has three front doors. I took all three to the same scope, counted the resources the same way, deployed them for real, implemented four features on each, and deleted everything at the end.

This article stands on its own. It continues a series, but the only thing it assumes is that a file portal is running on an FSx for ONTAP S3 Access Point.

What prompted it was reading a Zenn article by an AWS SA that compared all three. Someone else's comparison is useful, but as a basis for a decision in my own repository it is thin: you have to deploy and delete before you can say you compared anything.

Part 1 was about the application layer — what to build a file portal with. This one is about the tooling underneath it.

What the verification covers:

  • The resource count of each generated output, and the fixed cost its defaults imply
  • Deploying each one for real and exercising it
  • The differences that appear once the same four features are implemented on all three
  • What survives a stack deletion, and the order in which to clean it up

Out of scope: which tool is better, Nx or monorepos as concepts, and keeping generated code current over time.

The conclusions first:

  • Choosing a tool is a decision you can take back. The data path is common to all three, and switching tools does not mean rebuilding the FSx for ONTAP side.
  • The difference is not "can it be built" but what gets fixed in place once you have chosen. All three deployed without trouble.
  • The generated defaults mean fixed monthly cost at zero requests.
  • What a stack deletion leaves behind comes in two kinds. Retain and deletion protection are the generator's defaults and can be released on the generating side. Log groups that never appear in the template belong to CDK's internals, and there is no route to release those. The KMS waiting period is the only thing that cannot be avoided.
  • For some resources, notably KMS keys, DELETE_COMPLETE means "scheduled for deletion after a 30-day wait" rather than "gone".

Switching tools does not touch the data foundation underneath

The components side by side, matched to one scope: web, auth, API and a table. The data path below the S3 Access Point is this portal's real configuration and is identical in all three.

Components of the file portal composed on Amplify Gen 2. From a web browser through AWS Amplify, AWS AppSync and AWS Lambda to an Amazon S3 Access Point, Amazon FSx for NetApp ONTAP and NFS / SMB clients. Amazon Cognito beside AWS Amplify, Amazon DynamoDB beside AWS Lambda. Zero AWS WAF web ACLs and zero customer managed KMS keys are created by default
Figure 1: components of the file portal composed on Amplify Gen 2

Amplify Gen 2 is what this portal actually runs on. No perimeter resources — AWS WAF, customer managed KMS keys — are created by default. Authentication is Amazon Cognito, on the default ESSENTIALS tier.

Components of the file portal composed on the AWS Blocks production preset. From a web browser through Amazon CloudFront, Amazon API Gateway and 13 AWS Lambda functions to an Amazon S3 Access Point, Amazon FSx for NetApp ONTAP and NFS / SMB clients. There is no Amazon Cognito; two of the four Amazon DynamoDB tables carry authentication. One customer managed KMS key
Figure 2: components of the file portal composed on the AWS Blocks production preset

What to read in the AWS Blocks figure is the absence of Amazon Cognito. Authentication is AuthBasic (DynamoDB plus JWTs), so it moves out of a managed service and into the tables. Two of the four tables are for auth, and all four carry deletion protection.

Components of the file portal composed on Nx Plugin for AWS. From a web browser through Amazon CloudFront, Amazon API Gateway (tRPC), AWS Lambda and Amazon DynamoDB to an Amazon S3 Access Point, Amazon FSx for NetApp ONTAP and NFS / SMB clients. Two AWS WAF web ACLs sit on the path (the CLOUDFRONT-scoped one in us-east-1, plus a REGIONAL one), Amazon Cognito is on PLUS with MFA required, and there are four AWS Key Management Service keys
Figure 3: components of the file portal composed on Nx Plugin for AWS

For Nx Plugin for AWS the difference is more perimeter in the figure: two AWS WAF web ACLs on the path (a third sits in a separate us-east-1 stack), Amazon Cognito on PLUS with MFA required, and four KMS keys. Those three are the fixed-cost difference described later.

That the three figures match below the access point matters: switching tools does not mean rebuilding the FSx for ONTAP side. Nothing was created or deleted on the FSx for ONTAP side during this verification.

Reaching it was also identical on all three. An Internet-origin access point is reached by passing the alias as a bucket name to the standard S3 SDK — no endpoint override, no VPC attachment. A file on the NAS can be found, opened and written from a browser with no VPN and no SMB mount, and through phase 3 the functions do not even need to sit in a VPC.


The same four features on all three tools (2026-09-14)

Everything above compares the outputs as generated. What someone choosing a tool actually wants to know is the difference once their own application sits on it. So a few days later I implemented the same four features — sign in, list, read, upload (listFiles / readFile / uploadFile) — on each and ran them against the same FSx for ONTAP S3 Access Point.

The target was fixed too: the same prefix (13 objects), the same file (1,615 bytes, text/markdown; charset=utf-8), the same 24-byte write. All three returned the same values.

The listing

The file listing on the AWS Blocks build. A prefix box holds reports/2026/05/10/, the count reads 13 objects, and thirteen compliance-report-<uuid>.md rows are listed with sizes and modification times
The listing on the AWS Blocks build: thirteen files.

The file listing on the Nx Plugin for AWS build. The generated shadcn sidebar is on the left, and the body shows the same prefix, the same 13 objects, and the same thirteen rows
The listing on the Nx Plugin for AWS build, with the generated shadcn sidebar.

The file listing on the Amplify Gen 2 portal. A breadcrumb reads / reports / 2026 / 05 / 10, and the same thirteen files appear alongside a .. entry for the parent, each row carrying a star and a document icon
The listing on the Amplify Gen 2 portal. Being a portal in actual use, it carries extra features such as stars and download icons.

Reading

The read screen on the AWS Blocks build. Below a line reading 1615 bytes, text/markdown; charset=utf-8, the Japanese compliance report is rendered in the page

The read screen on the Nx build. The header reads portal-web; under compliance-report-<uuid>.md sit 1615 bytes and text/markdown; charset=utf-8, the same report is rendered in the page, and an Upload section follows below it

The two above render the report in the page. The Amplify Gen 2 portal below does not: it hands .md to an AI Assistant panel instead, because that is how I wrote it.

The same file opened on the Amplify Gen 2 portal. The contents are not rendered; an AI Assistant panel has opened on the right, naming the selected file and offering Analyze document

An aside: a by-product of the capture pass was one accessibility defect in my own portal. The document icon on a file row announces itself as aria-label="Download <filename>", yet what it actually does is select the file for AI processing. The title attribute mentions both, but a screen reader reads out only Download, so someone using assistive technology is told the button downloads a file when it starts AI processing instead. It is unfixed as of writing, recorded as something for the portal side to correct. Without lining up three builds in the same order, I probably would not have noticed.

Uploading

The upload screen on the AWS Blocks build. The heading reads File Portal — AWS Blocks; the prefix is portal-parity/blocks/ with 1 object, and the note-2026-09-14.txt that was just written is listed at 35 B. Below it are a filename field, a body field and an Upload button, with a note that writes cannot leave portal-parity/blocks/

The upload screen on the Nx build. The heading reads File Portal on Nx Plugin for AWS, subtitled Amazon FSx for NetApp ONTAP S3 Access Point, via tRPC procedures. The prefix is portal-parity/nx/ with 1 object, and the note-2026-09-14.txt that was just written is listed at 24 bytes

The two above are listings right after a successful write. The Amplify screen below is a refusal: the same file name was sent twice and the server side declined to overwrite. That is a difference in what I implemented, not in the tools.

The upload screen on the Amplify Gen 2 portal. The left sidebar lists All Files, Favorites, Recent, Upload, AI Processing, Job History, Snapshots, Lock, ARP/AI and Version Diff. In the upload queue, note-2026-09-14.txt is 24 B with a status of Overwrite prevented and 0% progress, and a warning below reads Overwrite prevented for all files.

How the IAM permission differs by tool

Reaching the S3 Access Point is the same on all three, but the IAM needs care: all three require the access-point ARN form.

arn:aws:s3:<region>:<account-id>:accesspoint/<name>          # ListBucket
arn:aws:s3:<region>:<account-id>:accesspoint/<name>/object/* # GetObject, PutObject
Enter fullscreen mode Exit fullscreen mode

Written in the bucket form (arn:aws:s3:::<alias>), the deploy succeeds and list, get and put all return AccessDenied at run time — a failure that is hard to read.

AWS Blocks in particular has FileBucket.fromExisting(alias), which internally calls s3.Bucket.fromBucketName(alias).grantReadWrite() and therefore generates the bucket form. Trust the block's grant and add nothing, and it passes locally and fails once deployed.

Which layer refuses a malformed input

Trying an input that walks out of the prefix (../escape.txt) showed that the refusing layer differs by tool.

Input Nx Plugin for AWS AWS Blocks
../escape.txt HTTP 403 {"message":"Forbidden"} (blocked by WAF) The application's own domain error
An empty file name HTTP 400 plus the application's error The application's own domain error

On the Nx build, AWS WAF stopped the request before the Lambda. Defence in depth on by default is a good thing, but the refusal arrives as a generic HTTP 403, so the starting point of an investigation moves from your own code to the WAF metrics — worth knowing before you are debugging at speed.

What this round measured (deploy time and resource count)

Deploy time Resources Application Lambdas
AWS Blocks (production preset) 318 s 82 1 (the whole API)
Nx Plugin for AWS (sandbox) 307 s 100 5 (one per procedure)

Amplify Gen 2 is excluded from this round's figures (deploy time, resource count) because the comparison used the portal already in production rather than a fresh build.

Worth noting: a green local e2e run on AWS Blocks says nothing about whether the access point is reachable, because the mocked storage closes the loop inside the mock.


Deploying the starters as generated (2026-09-13)

Stopping at synth leaves out everything you learn while deleting resources. So all three were deployed to a real AWS account in their generated state, before any features were added.

Output Resources Deploy Exercised
AWS Blocks (sandbox preset) 83 signUp and friends returned 200 over JSON-RPC
AWS Blocks (production preset) 117 1,228 s all five operations 200, CloudFront delivery included
Nx Plugin for AWS 86 317 s TOTP enrolled, then 200 from the API via the identity pool

All three deployed without trouble. The Blocks production preset takes longer because DynamoDB builds its GSIs one at a time.


Fixed cost that accrues at zero requests

A generator's defaults reach the monthly bill even when the template says nothing about them.
The figure is settled at the moment the tooling is chosen, which makes it a number to read
before starting rather than after.

With not a single request served, the floor for each tool looks like this (FSx for ONTAP's own cost is not included).

Output WAF KMS CMK Monthly floor
Nx Plugin for AWS 3 ACLs + 6 rules = $21 4 keys, $4 from $25
AWS Blocks (production preset) none 1 key, $1 $1
Amplify Gen 2 (starter) none none $0

Nx starts higher because what production needs is present from the start. It does bite when something is stood up for a test and left running.


What a production portal still needs

The feature set beyond these four, arranged in phases. Which phase your requirements stop at also changes how you choose a tool.

Phase Feature VPC attachment Newly required
1 Paging, search, sorting not needed nothing
2 Downloads and share links not needed presigned URLs and an expiry policy
3 Large file uploads not needed multipart, or similar
4 Showing ONTAP information needed reaching the management LIF, splitting the functions
5 Operating ONTAP needed audit logs, approval for irreversible operations
6 AI processing depends confidence thresholds, data classification

From phase 4 the ONTAP management LIF has to be reachable, which is where the decision to put a Lambda in a VPC arrives, and with it fixed cost such as a NAT gateway. If phase 5 or later is a possibility, splitting the functions into "touches the S3 Access Point" and "touches ONTAP" from the start is the design that pays off.


Tearing down the evaluation — what a stack deletion leaves

This is about putting the evaluation environment back, not about running the portal. A portal that
gets built runs for years; the deletion happens while you are still trying things out. Even so, how
much is left behind is worth knowing before you start.

Listing what remained after each stack deletion turned up resources that static analysis of the template (synth) could not show.

Output Left after the stack deletion
Blocks (sandbox preset) 5 log groups
Blocks (production preset) 4 DynamoDB tables (deletion protection), 8 log groups, 1 KMS key (waiting)
Nx Plugin for AWS 1 Cognito user pool, 1 DynamoDB table, 4 KMS keys, 2 IAM roles, 7 log groups

Those eight and nine are the generator's defaults, not an AWS constraint. The generated code
is what applies Retain and deletion protection, so anything that may go can be released on the
generating side (RemovalPolicy.DESTROY in CDK; the deletion-protection properties on DynamoDB
and Cognito set to false). Defaults leaning towards keeping data is a production-shaped choice;
it conflicts with intent only when the deployment is a test.

Log groups are the awkward ones. The Lambda functions behind CDK and tooling custom resources create them on first execution with the default retention (never expire), which puts them outside CloudFormation's control and leaves them behind. One sweep at the end of the teardown catches them:

aws logs describe-log-groups --log-group-name-prefix "/aws/lambda/<stack-name>" \
  --query "logGroups[].[logGroupName,retentionInDays]" --output text
Enter fullscreen mode Exit fullscreen mode

The removal order matters too. Nx's user pool cannot be deleted until deletion protection is off, and the SMS configuration needed to switch it off references one of the two IAM roles listed above as survivors. Delete those IAM roles first, while clearing up what the stack left behind, and the route to switching off deletion protection is gone. Delete the user pool first, then the roles.

Your own functions are a different case from CDK's. A Lambda you write can declare its log
group in the template, so it does not survive — measured, the declared ones went away on all
three. What survives belongs to CDK's internal custom resources, and there is no route to declare
those from your own code: the CustomResourceProvider base class declares an IAM role and a
Lambda function and no log group (custom-resource-provider-base.ts,
read in full on main, 2026-09-16).

No released version fixes this (checked 2026-09-16). Two feature requests are open upstream
(aws-cdk #26553,
aws-cdk #23909). A third,
aws-cdk #24815, is closed as completed — but
its closing comment, from an AWS maintainer, says the fixing PR was reverted, that the problem
is general to all custom resources, and that tracking moved to #23909. Closed is not shipped.

AWS Support has not been asked. Lambda documents the implicit creation and the never-expire
default, the CDK source shows what the provider declares, and CDK's primary channel is its
GitHub repository rather than AWS Support.
What would be worth filing is the per-output residue
count measured here, which appears in neither issue.


Two behaviours that are easy to misread in operation

Both are documented, and both are still easy to read the wrong way.

Trap 1: a deployment mode cannot be dropped later

Nx generates deploy-sandbox as cdk deploy ... --express under the hood. If a deploy breaks, "drop --express and run it again" does not work: a stack last updated in express mode requires its later operations to be express too. The remedy is to pair --express --rollback from the start.

Trap 2: DELETE_COMPLETE does not always mean gone

DELETE_COMPLETE in CloudFormation does not always mean the resource is gone. For a KMS key it means deletion was scheduled — a 30-day wait by default — and that wait cannot be shortened afterwards. Confirm a teardown from the listed state rather than from the status.


Choosing between them

The three are not mutually exclusive; they suit different purposes.

  • Nx Plugin for AWS: production-leaning defaults and dependency management that holds up as projects multiply. Fixed cost is the highest of the three.
  • AWS Blocks: running the whole application locally with no AWS account (it is in preview, so treat a Block ID as immutable after deployment).
  • Amplify Gen 2: treating the backend definition, the sandbox and hosting as one thing.

Part 1's conclusion stands unchanged — Nextcloud when file sharing is the centre, Amplify Gen 2 when AI processing and ONTAP operations are. What this part settled is which tool the "build" side sits on.


Test environment

Item Value
Measured 2026-09-13 (starters, live), 2026-09-14 (live, with the four features implemented)
Region ap-northeast-1 (only Nx's CloudFront web ACL in us-east-1)
Outputs Nx Plugin for AWS 1.0 (@aws/nx-plugin@1.0.0), AWS Blocks (preview), Amplify Gen 2 (npm create amplify)
Local Node.js v26.4.0, npm 11.17.0
Pricing AWS Price List API, ap-northeast-1, 2026-09-07
Teardown everything deleted; only 5 KMS keys remain in their waiting period (PendingDeletion is not billed)

The record in the repository

The numbers, the sources and the teardown procedure live in the repository, in more detail than this article.


Closing

Everything in this part is about defaults. Choosing a tool fixes your monthly floor and your teardown procedure, and neither is written in the template.

What worked as a method was writing the same four features three times. The difference shows up not as how much code you write but as where you write it, and which layer refuses a bad request. Choosing a tool is, I think, choosing where you look first when something breaks.

I hope this record helps anyone weighing the same choice.


Everything in the test environment has been deleted (only KMS keys remain in their waiting period). The figures come from one specific environment and configuration and will differ with workload and setup. Prices are the published ap-northeast-1 rates as of 2026-09-07.

Top comments (0)