# serverless-domain-manager: custom domains for API Gateway from serverless.yaml

> serverless-domain-manager is a Serverless Framework plugin that turns a domain name in your configuration file into an API Gateway custom domain, a certificate attachment and a Route53 record. It removes a stack of manual console work, and it asks your deployment role for a wide slice of AWS permissions to do it.

**amplify-education/serverless-domain-manager** — Serverless plugin for managing custom domains with API Gateways.

- Repository: https://github.com/amplify-education/serverless-domain-manager
- Stars: 953 · Forks: 235
- Language: TypeScript
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/amplify-education-serverless-domain-manager

## What the plugin takes over from the AWS console

The plugin's one line description is that it creates custom domain names that your lambda can deploy to with serverless, allows base path mapping when deploying, and allows deletion of domain names.

That covers a task that is genuinely tedious by hand. To put a Serverless deployment behind `serverless.foo.com`, a team normally creates a custom domain in API Gateway, attaches an ACM certificate to it, creates a base path mapping onto the stage, adds a Route53 alias record pointing at the API Gateway domain, and then repeats the whole thing in reverse when the domain is retired. Each of those steps is a separate console operation, and each one is easy to get subtly wrong.

The plugin moves all of that into the deployment configuration, so the domain becomes part of the CloudFormation stack that the Serverless Framework manages. Because the domain name is a piece of configuration, it can be different per stage, and the plugin's job at deploy time is to create whatever is missing and to reconcile whatever is not.

The project comes from Amplify Education, an organisation described in the readme as building digital educational products for K-12 classrooms, and the plugin is published on npm as `serverless-domain-manager` under the MIT licence. It is written in TypeScript, with the package version at 10.3.1 and the runtime requirement of Node.js 20 or newer. The latest releases are v10.3.1 on 2026-07-21 and v10.3.0 on 2026-07-17, and the last push to the default branch was on 2026-10-19.

## The whole configuration is a block in serverless.yaml

Everything is driven from a `custom.customDomain` block. For a single domain and a single API type, the readme's example looks like this.

```yaml
custom:
  customDomain:
    domainName: serverless.foo.com
    stage: ci
    basePath: api
    certificateName: '*.foo.com'
    createRoute53Record: true
    createRoute53IPv6Record: true
    endpointType: REGIONAL
    securityPolicy: tls_1_2
    apiType: rest
    autoDomain: false
```

The keys carry most of the meaning. `domainName` is the domain to create in API Gateway and, if enabled, in Route53. `stage` names the API Gateway stage to map, `basePath` is the path segment that maps onto it, and `certificateName` names the ACM certificate to attach. `endpointType` and `securityPolicy` set the API Gateway endpoint configuration and the TLS policy, with `tls_1_2` in the example. `apiType` selects the API Gateway API type, `rest` there. `createRoute53Record` and `createRoute53IPv6Record` decide whether DNS records are created for the domain, and `autoDomain` is set to false in the example.

Two more shapes are documented. When you run several API types against different domains, the key becomes the API type, with `rest`, `http` and `websocket` shown as separate blocks in the readme. When you want several domains of the same type, the configuration moves to `customDomains` as a list, each entry keyed by API type, with domain names built from variables such as `${opt:RANDOM_STRING}` and `${env:TEST_DOMAIN}`.

The `stage` key deserves attention in a multi-stage setup, since it is what separates a `ci` deployment from a `prod` one, and it is the value that decides which base path mapping the plugin reconciles.

## Installing the plugin and wiring it into the service

The prerequisites are nodejs, npm and the serverless framework itself. The plugin is installed from npm as a development dependency, since it only needs to exist at deploy time.

```bash
npm install serverless-domain-manager --save-dev
```

Then it is registered in the service configuration by adding the plugin name to the `plugins` list.

```yaml
plugins:
  - serverless-domain-manager
```

That is the entire setup. There is no separate configuration file and no credentials of its own: the plugin runs inside the Serverless Framework deployment, so it uses the same AWS credentials and the same region as the deployment it is attached to.

The project also ships a CloudFormation template for the deployment role, referenced from the readme as `scripts/cloudformation/serverless-domain-manager-deploy-policy.yaml`, which generates a least privileged IAM managed policy for deployment. The readme is explicit that using it is an alternative to assembling the permissions yourself.

For development on the plugin itself, package.json defines the scripts. The unit test suite runs mocha with ts-node against `test/unit-tests/**/*.test.ts` under c8 coverage, and there are three separate integration suites, `integration-basic`, `integration-deploy` and `integration-osls`. Linting is eslint over `src`, and the build is a plain `tsc --project.` that emits into `dist`, with `main` pointing at `dist/src/index.js`.

## The permission list is wide, and one entry looks malformed

The readme lists the IAM permissions the role that deploys the lambda might need, and it is a long list with some broad wildcards.

```yaml
acm:ListCertificates                   *
acm:DescribeCertificate                *
apigateway:AddCertificateToDomain      /domainnames*
apigateway:RemoveCertificateFromDomain /domainnames*
apigateway:GET                         /domainnames*, /apis*, /restapis*
apigateway:DELETE                      /domainnames*, /apis*, /restapis*
apigateway:POST                        /domainnames*, /apis*, /restapis*
apigateway:PATCH                       /domainnames*, /apis*, /restapis*
cloudformation:GET                     *
cloudformation:ListStacks              *
cloudformation:DescribeStacks          *
cloudfront:UpdateDistribution          *
route53:ListHostedZones                *
route53:ChangeResourceRecordSets       hostedzone/{HostedZoneId}
route53:GetHostedZone                  *
route53:ListResourceRecordSets         *
iam:CreateServiceLinkedRole            arn:aws:iam::${AWS::AccountId}: role/aws-service-role/ops.apigateway.amazonaws.com/AWSServiceRoleForAPIGateway
s3:ListBucket                          *
s3:GetObject                           *
```

Two things stand out. The Route53 write permission is one of only two entries with a resource scope, `hostedzone/{HostedZoneId}`, which is the pattern to use if you want to constrain it further. The `iam:CreateServiceLinkedRole` line has a space between the colon and `role/aws-service-role`, which reads as a typo in the ARN and is worth correcting if you copy the list directly into a policy.

The `cloudfront:UpdateDistribution` and `s3` entries are broader than the core domain work would suggest, which is another reason to prefer the provided managed policy template and to review it rather than pasting the whole list into a production role.

## DNS records, IPv6 and multi-region routing

Route53 record creation is opt in through `createRoute53Record`, with a separate flag for IPv6 through `createRoute53IPv6Record`. Both are true in the readme's example, and turning them on means the plugin writes DNS records for you rather than leaving a manual step in the runbook.

For deployments that span regions there is an extra `route53Params` structure, which the readme says supports latency or weighted routing policies.

```yaml
custom:
  customDomain:
    domainName: serverless.foo.com
    stage: ci
    basePath: api
    certificateName: '*.foo.com'
    createRoute53Record: true
    createRoute53IPv6Record: true
    endpointType: REGIONAL
    securityPolicy: tls_1_2
    route53Params:
      routingPolicy: latency
```

A weighted routing policy needs the hosted zone to be identified, which is where the scoped `route53:ChangeResourceRecordSets` permission on `hostedzone/{HostedZoneId}` becomes relevant. The plugin cannot own the whole DNS delegation of your domain, so a zone that already exists elsewhere in the account still has to be pointed at the right place by whoever manages it.

The `certificateName` key deserves a second look in any multi-region setup, because an ACM certificate is regional. The readme does not describe how a certificate in another region is handled, so a multi-region deployment needs its own decision about which certificate is attached where.

## Where this plugin stops being the right tool

The clearest case is a team that does not deploy through the Serverless Framework. Everything here hangs off the `custom` block of `serverless.yaml` and runs during a `serverless deploy`. If your API Gateway is created by CloudFormation directly, by CDK, by SAM, or by a hand rolled pipeline, this plugin has no hook and no value, and the same work has to be written as your own CloudFormation resources.

The second case is the permission ceiling. The plugin needs to create and delete API Gateway domain names, attach and detach certificates, read and write Route53 records, inspect CloudFormation stacks and list S3 buckets, all under one deployment role. Some organisations run CI with a role deliberately restricted to a handful of CloudFormation actions, and for those the plugin cannot be installed at all without widening that role.

The third is the certificate lifecycle. The plugin attaches a certificate by name, which means the certificate has to exist already, in the right region, and be managed by something else. If the certificate is issued by a different pipeline, or the organisation moves to a wildcard strategy with a different naming convention, the plugin's key names will not match anything.

The fourth is the documentation itself. The readme does not document a rollback path or what happens when a deployment fails halfway through, for example after the API Gateway domain is created but before the Route53 record is written. The CHANGELOG.md at the repository root is where behaviour changes are recorded, and the package version, 10.3.1, has moved through several minor releases this year.

## How this differs from managing the domain yourself

The alternative is to model the domain in your infrastructure as code and let CloudFormation own it: an `AWS::ApiGateway::DomainName`, an `AWS::ApiGateway::BasePathMapping`, an `AWS::Route53::RecordSet` and an `AWS::CertificateManager::Certificate` in the same template as the Lambda. No plugin, no custom domain keys.

The difference in approach is ownership and reconciliation. In CloudFormation, the domain is a resource in the stack, so the stack engine decides what to create, what to update and what to delete, and the drift is corrected on the next deploy. With a plugin, the domain is reconciled by the plugin's own code, which reads the API, compares it with the configuration and issues the calls it thinks are needed. That is a more capable model for the cases the plugin was written for, such as attaching an existing domain to a new stage without recreating it, but it means the behaviour lives in the plugin's version rather than in a resource definition you can read in your own template.

The practical difference in day to day use is that the plugin adds configuration vocabulary to a service that already has a configuration file, and it requires the deployment role to be able to reach across ACM, API Gateway, Route53 and CloudFormation. The CloudFormation approach needs those permissions too, but they are visible in the template and shared with the rest of the stack.

For a team already standardised on the Serverless Framework, the plugin removes the most tedious part of putting a hostname in front of a deployed API. For a team that manages its own templates, the native resources are likely the better answer.

## Conclusion

serverless-domain-manager suits teams that deploy API Gateway behind a custom hostname through the Serverless Framework and want the domain, certificate and DNS record created and deleted with the stack, and it is the wrong tool for anyone who manages domains outside that framework or who cannot widen their deployment role permissions. Before adopting it, read the permission list in the readme against your deployment role, since the plugin needs `apigateway`, `acm`, `route53`, `cloudformation` and `s3` access, and check that the certificate you reference by `certificateName` exists in the same region as the API.

## FAQ

### What does serverless-domain-manager do?

It is a Serverless Framework plugin that creates custom domain names for API Gateway deployments, maps a base path onto the stage, attaches a certificate and optionally creates Route53 records, and it deletes the domain names again when the deployment is torn down.

### How do I install serverless-domain-manager?

Install it as a development dependency with `npm install serverless-domain-manager --save-dev`, then add `- serverless-domain-manager` to the `plugins` list in your serverless.yaml and configure the `custom.customDomain` block.

### Which API Gateway API types are supported?

The readme shows configurations for `rest`, `http` and `websocket`, either under a single `customDomain` key or under `customDomains` as a list when several domains of the same type are needed. The `apiType` key selects the type for the single domain form.

### What permissions does the deployment role need?

The readme lists ACM, API Gateway, CloudFormation, CloudFront, Route53, IAM and S3 actions, with wildcards on most of them and Route53 record changes scoped to `hostedzone/{HostedZoneId}`. A least privileged managed policy template is provided at `scripts/cloudformation/serverless-domain-manager-deploy-policy.yaml`.

### Does serverless-domain-manager work across regions?

Yes, through a `route53Params` structure with a `routingPolicy` of `latency` for latency based routing, and weighted routing is also mentioned. The certificate itself is regional, so each region needs a certificate that exists in it.

## Sources

- [amplify-education/serverless-domain-manager on GitHub](https://github.com/amplify-education/serverless-domain-manager)
- [Issues](https://github.com/amplify-education/serverless-domain-manager/issues)
- [License: MIT](https://github.com/amplify-education/serverless-domain-manager/blob/main/LICENSE)
- [README](https://github.com/amplify-education/serverless-domain-manager/blob/main/README.md)
- [Releases](https://github.com/amplify-education/serverless-domain-manager/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/amplify-education-serverless-domain-manager
