Skip to main content
INS // Insights

Custom AWS AppSync GraphQL API Build We Deliver

Updated September 2026 · 8 min read
Custom AWS AppSync GraphQL API Build We Deliver

A custom AWS AppSync GraphQL API build is one of the shapes Rutagon delivers when a mid-market SaaS needs a real API, not a slide about "headless." This article is delivery proof: the decisions we made on a production system, the trade-offs we accepted, and what we would not repeat. It is not a competitor cookbook for standing up a look-alike product, and it is not a generic custom-software-for-any-vertical pitch.

AWS AppSync is a managed GraphQL service with resolvers that can talk to Lambda, DynamoDB, and other AWS data sources, plus built-in authorization modes (AWS AppSync). We used it as the mobile-and-web façade in front of a production SaaS platform: 24 Lambda functions, Aurora Serverless v2 for relational work, DynamoDB where the access pattern was key-shaped, OIDC CI/CD, no long-lived AWS keys.

What We Were Asked to Deliver

The buyer needed one API contract for iOS, Android, and web. They needed per-tenant isolation, Cognito-or-OIDC authenticated callers, and an audit trail of privileged mutations. They did not need a second ORM and they did not need us to invent a new database product.

We chose AppSync because the clients were already GraphQL-shaped and because field-level resolvers let us keep high-sensitivity fields off the default query without maintaining three REST gateways. We did not choose it because GraphQL is fashionable.

Custom AWS AppSync GraphQL API Build: Decisions That Stuck

Authorization. Cognito user pools for human clients, IAM for internal workers. Resolver-level checks still enforced tenant id from the identity claims, never from a raw argument. That is the same bind-from-context rule we use on MCP tool gateways.

Compute. Pipeline resolvers for simple DynamoDB gets. Lambda for anything with more than one downstream write or a policy decision. Twenty-plus Lambdas sounds large; each was small and named after a mutation. We did not run a single "god resolver."

Data. DynamoDB for session-shaped and fan-out keys. Aurora Serverless v2 PostgreSQL for relational reporting and anything that would have become a DynamoDB join festival. That split is a cost and a correctness choice — see Aurora Serverless v2 cost optimization.

Delivery. Terraform for AppSync, functions, tables, and IAM. GitHub Actions assumed a deploy role via OIDC. No access keys in secrets. Same workload identity pattern as the rest of the estate.

# Shape we actually ship — names and fields omitted on purpose
resource "aws_appsync_graphql_api" "saas" {
  name                = "saas-api"
  authentication_type = "AMAZON_COGNITO_USER_POOLS"
  user_pool_config {
    user_pool_id   = var.user_pool_id
    aws_region     = var.region
    default_action = "DENY"
  }
  additional_authentication_provider {
    authentication_type = "AWS_IAM"
  }
  log_config {
    cloudwatch_logs_role_arn = aws_iam_role.appsync_logs.arn
    field_log_level          = "ERROR"
  }
}

Default DENY on the user pool, IAM only for workers we enumerate, field logs at ERROR so we do not store customer payloads in CloudWatch as a hobby. Capability: full-stack development and AWS cloud infrastructure.

Trade-offs We Accepted

AppSync schema changes are production changes. We versioned the schema in git and refused drive-by field adds from the console. N+1 query risk is real; we batched the hot paths in Lambda instead of pretending VTL would stay readable. Subscriptions were limited to the events we could authorize per tenant — a wide subscribe is a data leak with better UX.

We would not use AppSync as a universal integration bus for every vendor. When the workflow was a long-running state machine, we kept that out of GraphQL and used the async path the product already had.

What This Article Will Not Do

It will not give you a full schema, resolver map, or tenant-isolation algorithm you can copy into a competing SaaS. It will not name the consumer product. It will not claim the build is FedRAMP authorized or that Rutagon is SOC 2 certified. It will not teach government teaming.

If you need this shape — AppSync + Lambda + DynamoDB/Aurora, identity-bound resolvers, Terraform, OIDC — that is a scoped build, not a blog series.

Observability was treated as part of the API, not an add-on. Each Lambda resolver emitted a request id that the mobile clients already had, so a failed mutation was a trace, not a 1:1 with "the app is broken." We kept field-level AppSync logs at ERROR on purpose. Debug-level field logs on a tenant API are how customer identifiers land in CloudWatch longer than the product log policy allows.

We also refused to let AppSync become the authorization system of record. Cognito groups were a coarse gate. Fine-grained tenant checks lived in code we could test. When a new client team asked for a "just this one" field that skipped the tenant bind, the answer was no. That refusal is the delivery, as much as the schema.

Caching was a product decision, not a default. AppSync's server-side cache can hide a tenant-bound response if the cache key omits the identity claim. We cached only the public, non-tenant fields and we kept the rest on the resolver path. A fast leak is still a leak. The same bind-from-context rule that applies to mutations applies to cache keys.

Rate limiting and WAF sat in front of the API, not as an afterthought. A public GraphQL endpoint with introspection left on in production is a map of every mutation. We disabled introspection outside nonprod, attached WAF rules for known abuse patterns, and kept a per-client throttle on the noisy mobile path. That is the same discipline as any other internet-facing API — GraphQL does not get a pass because the query is pretty.

On-call runbooks named the AppSync API, the Lambda resolver that owned each high-risk mutation, and the DynamoDB table or Aurora cluster behind it. When latency spiked, we did not "restart GraphQL." We looked at the resolver trace and the downstream. That operational honesty is part of what we deliver: a system someone else can run after the build.

Discuss a custom AppSync API build →

907-841-8407 · contact@rutagon.com

Frequently Asked Questions

When is AppSync the wrong façade?

When the clients want simple REST, when you have one consumer, or when every resolver would just proxy a single existing API. We have shipped REST on API Gateway for that case. AppSync earned its keep here because of multiple clients and field-level auth.

Did you put all data in DynamoDB?

No. DynamoDB for key-shaped access. Aurora for relational reporting. Forcing one store is how these builds fail.

How do you evidence privileged mutations for SOC 2?

Resolver logs plus an application audit table for high-risk mutations, retained for the observation period. That is client evidence. Rutagon does not hold the attestation.

Can this sit behind a GRC-minded AWS org?

Yes. The API lives in the prod account, logs go to the log account, deploy is OIDC. See SOC 2 ready AWS architecture for startups.

Will you publish the full resolver code?

No. This is proof we deliver the shape. The next one is built in your account, to your schema.