AWS Amplify Is A Grift

Yes, this is a punchy headline, but if you'll join me on this journey, you'll see how. 👀

So, here's the context. For the past year or so, I was at a renewable energy startup. It was a great experience, but I recently resigned: I'm having another child, and I just don't need to work full-time—so I'm not going to. To be clear, this post is entirely my opinion: as of writing, I'm only employed by myself.

The startup heavily used Amplify—I inherited that decision—but one of my major initatives was removing as much of it as possible. This took hundreds of engineering hours away from actual work.

So, let me be as clear as possible, and you can quote me personally on this: "AWS Amplify is actively harmful", primarily because of its database choice.

Consider using just a normal database—personally for me that would be Firestore (Google-hosted) or MongoDB (basically self-hotsed), because I strongly prefer scalable NoSQL databases. But for most people, it's probably going to literally just be Postgres. You don't need more than Postgres.

How can a framework be actively harmful?

A core idea of cloud providers is that you can build your infra and not have to worry about scale. AWS Amplify makes promises that it will be:

Easy to start, easy to scale

But herein lies the grift: it's useless for anything except toy applications with trivial amounts of data. And these toy apps are conveniently easy to demo.

And data is the key here; there's a lot of AWS Amplify which is just fine and boring, like its user authentication libraries. The issue lies with GraphQL and the way it stores data.

The challenge, of course, is that you don't know that it's terrible going in: that's why I'm writing this post. The issue is that it it's built by the world's biggest cloud provider, who is completely happy to pay US$200k/yrs to DevRels to push this unusable bit of software—so you might think, sure, it's fine.

But it's not. 💥

The Boring Stuff

Let's get the boring stuff out of the way. By that, I mean the parts of AWS Amplify that are innocuous, indifferent, and are plainly things that are hard to mess up or be particularly amazing at.

Again, this is fine. Any provider pitched at the same level—Firebase, Supabase, Azure probably has one—has this kind of "app-builder" stack.

Data, It's What Apps Crave

AWS Amplify allows you to specify GraphQL models that turn into database rows. To be fair, this is an annoying problem: let's say you're DIYing it, and writing a GraphQL server and database integration yourself.

However, the fundamental mistake that's made here is that AWS Amplify puts your data into DynamoDB, which is not a general-purpose database.

Amplify's approach in using DynamoDB is literally called out as something you should avoid by AWS, because it uses a single table per resource. Additionally, you cannot filter or sort by arbitrary columns—DynamoDB is a high-performance, low-level database, that doesn't let you do arbitrary queries. That's the point.

Yes, Amplify awkwardly describes the way you can create secondary indexes, but it's often not what you need (everything needs primary AND secondary keys, etc), and you can't index on any sort of ACLs.

So using it is literally an anti-pattern. And part of the problem is, once you're in production using a database—whatever database, not just DynamoDB—it's hard to get out.

But DynamoDB Scales?

But Sam, you say—DynamoDB is a fast, scalable database. Surely these tradeoffs are worth it?

Yes, in some cases. But you should only use it when a traditional database won't cut it (and if you think you know what that threshold is, go and double the number and come back to me), and you can confidently design your queries up-front.

Amplify, which is literally pitched at startups, isn't suitable because you're going to pivot. Your CEO is going to ask you for some weird data 'shape' that just isn't possible because Dynamo plainly cannot answer your arbitrary queries. Yes, no data is ever in a perfect table, but this makes it worse.

There is a flipside, which is that Dynamo is fairly fast at a "scan", which is a fancy term for "load every row and filter it at runtime". And yes, if you don't have much data—again, think of a number and add an order of magnitude to it—you can just do that, but then why are you using DynamoDB? Dynamo scales by sharding—your primary key space is divided ito partitions. But a partition is literally 10gb of data.

So if your company doesn't have that much data (even 100gb might be a good target), but chooses to use Dynamo, your effective option to do arbitrary queries is just to… scan all the data.

So why are you using Dynamo, a hyper-performant and extremely limited database, again? 🤔

But DynamoDB Is Fast?

Ah, well, here's the real killer.

If you put access controls on your data, and say, a user wants to retrieve their "Foos"… AWS Amplify literally has to fetch matching data and then filter it down.

Let me say that again. If you have 1,000 users, and each privately owns one row of data, Amplify's default pagination of 100 items per fetch will take—in the worst case—O(users/100) pages to retrieve the user's item. For 1,000, that's 10 pages. For 100,000, that's 100 pages—100 round trips from your user's web browser back to the database.

The VTL, a language used by AppSync, which is Amplify's underlying provider, looks like the following (a tiny bit snipped for brevity). And note that the 'data source' items are in $ctx.result.items—that's the page we have to scan to see if a user can see their own data.

#set( $items = [] )
#foreach( $item in $ctx.result.items )
  ## [Start] Dynamic Group Authorization Checks **
  #set( $isLocalDynamicGroupAuthorized = false )
  ## Authorization rule: { allow: groups, groupsField: "groupsWithRead", groupClaim: "cognito:groups" } **
  #set( $allowedGroups = $util.defaultIfNull($item.groupsWithRead, []) )
  #set( $userGroups = $util.defaultIfNull($ctx.identity.claims.get("cognito:groups"), []) )
  #foreach( $userGroup in $userGroups )
    #if( $allowedGroups.contains($userGroup) )
      #set( $isLocalDynamicGroupAuthorized = true )
  ## [End] Dynamic Group Authorization Checks **

  #if( ($isLocalDynamicGroupAuthorized == true) )
#set( $ctx.result.items = $items )

Yes. It's really that bad. Of course, Amazon's sample resolvers (not that you write VTL directly with AWS Amplify) don't include this—they have a single line saying, "oh, you'll just pass all the data back to the client".

What Else Is Wrong?

The above criticism, the run-time filtering of user data, is by far the most egregious failure of AWS Amplify. There's a number of other poor areas, too.

Firstly, GraphQL subscriptions are basically useless. There's a great blog post here which covers this far better than I can.

And Amplify's DataStore fails fundamentally because it relies on GraphQL subscriptions and Amplify's view on that. It doesn't support dynamic ACLs (which is technically listed in the docs but not at all clear), so you can only listen to public items, which is bizzare. And it falls into the traps above. There are so many issues about how it's unusable and how the people filing didn't realize until they were already knee-deep in Amplify's grift.

Additionally, the JS used by AWS Amplify allows for race conditions in listening to changes. What do you do when you want a list of "live" objects? You'd imagine something like:

And that's what Amplify does, … but here's the kicker, in a diagram:

Diagram of traditional subscription model
The red line is 'danger'—what happened there?

So you're going to risk losing changes. And for most users—sure, that's fine. What are the odds of something happening in that red area? But it's poor design.

I know there's a slightly tweaked approach that has revision numbers and so on, but it's not commonly understood.

I cannot stress how much Google's Firestore just solves the 'real-time' problem. Instead, for days and days, I've had to work around Amplify's limitations.

What Should AWS Do?

So, you say: Sam, you've complained a lot. But Amplify has some redeeming characteristics. How could you make it better?

If I was AWS, I would…

What Should You Do?

If you're already commited to AWS Amplify, then I'm sorry. The auth and non-database world—that's fine. I even enjoy or think that the JWT-based approach to auth is actually fairly good, and you can write alternative backends which use a user's claims to enforce ACLs.

I think the thing I'd do is… if you're somewhat "successfully" using Amplify right now, I believe that your model and data won't actually be that large—because it actually breaks down at big numbers. Therefore it is the right time to rethink.

Look at your queries which are particularly awkward—what does your app or company have the most of? Can that be refactored into its own system, potentially wrapping up a database that's more fit for purpose?

Remember too that most applications will have a notion of login, user, settings—fairly boring stuff. If that's in Amplify, fine. A lot of it is probably ID-based, which Dynamo actually does excel at—you're not listing every user to get to your own. So that can remain.

Thanks For Reading

This space isn't perfect, but I honestly believe Amplify has too many potholes to justify its use, and those are wrapped up in poor documentation that hides a lot of nuances that aren't obvious when you start and only hit you when you're actually trying to scale.

Best of luck. 😬

Hit me up on the hellbird site, or with the elephants. I'm also available for consulting.

An Addendum

Update, April 2023: This post ended up on the front page of HN. I'd like to call out some themes in the comments: