diff --git a/examples/aws-cognito/sst.config.ts b/examples/aws-cognito/sst.config.ts
index f6bc1318ed..1ac54cc3b5 100644
--- a/examples/aws-cognito/sst.config.ts
+++ b/examples/aws-cognito/sst.config.ts
@@ -1,5 +1,11 @@
///
+/**
+ * ## AWS Cognito User Pool
+ *
+ * Create a Cognito User Pool with a hosted UI domain, client, and identity pool.
+ *
+ */
export default $config({
app(input) {
return {
@@ -10,6 +16,9 @@ export default $config({
},
async run() {
const userPool = new sst.aws.CognitoUserPool("MyUserPool", {
+ domain: {
+ prefix: `my-app-${$app.stage}`,
+ },
triggers: {
preSignUp: {
handler: "index.handler",
@@ -30,6 +39,7 @@ export default $config({
UserPool: userPool.id,
Client: client.id,
IdentityPool: identityPool.id,
+ DomainUrl: userPool.domainUrl,
};
},
});
diff --git a/platform/src/components/aws/cognito-user-pool-domain.ts b/platform/src/components/aws/cognito-user-pool-domain.ts
new file mode 100644
index 0000000000..513d004a1a
--- /dev/null
+++ b/platform/src/components/aws/cognito-user-pool-domain.ts
@@ -0,0 +1,211 @@
+import { ComponentResourceOptions, Output, output } from "@pulumi/pulumi";
+import { Component, Transform, transform } from "../component";
+import { Input } from "../input";
+import { Dns } from "../dns";
+import { cognito } from "@pulumi/aws";
+import { dns as awsDns } from "./dns.js";
+import { DnsValidatedCertificate } from "./dns-validated-certificate.js";
+import { useProvider } from "./helpers/provider.js";
+import { VisibleError } from "../error";
+
+export interface Args {
+ /**
+ * The Cognito user pool ID.
+ */
+ userPool: Input;
+ /**
+ * The domain configuration - either a string for custom domain or an object.
+ */
+ domain: Input<
+ | string
+ | {
+ prefix?: Input;
+ name?: Input;
+ dns?: Input;
+ cert?: Input;
+ }
+ >;
+ /**
+ * Transform the Cognito User Pool domain resource.
+ */
+ transform?: Transform;
+}
+
+/**
+ * The `CognitoUserPoolDomain` component is internally used by the `CognitoUserPool`
+ * component to add a domain to your [Amazon Cognito user pool](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools.html).
+ *
+ * :::note
+ * This component is not intended to be created directly.
+ * :::
+ *
+ * Use the `domain` prop on `CognitoUserPool` instead.
+ *
+ * @todo Add `managedLoginVersion` prop when SST upgrades to Pulumi AWS v7.
+ * This will allow users to choose between classic hosted UI (1) and managed login (2).
+ * See: https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-managed-login.html
+ */
+export class CognitoUserPoolDomain extends Component {
+ private _domain: Output;
+ private _domainUrl: Output;
+
+ constructor(name: string, args: Args, opts?: ComponentResourceOptions) {
+ super(__pulumiType, name, args, opts);
+
+ const parent = this;
+
+ const normalized = normalizeDomain();
+ const domain = createDomainWithSsl();
+
+ this._domain = domain;
+ this._domainUrl = normalized.apply((n) =>
+ n.prefix
+ ? `https://${n.prefix}.auth.${process.env.AWS_REGION || "us-east-1"}.amazoncognito.com`
+ : `https://${n.name}`,
+ );
+
+ function normalizeDomain() {
+ return output(args.domain).apply((domain) => {
+ const norm = typeof domain === "string" ? { name: domain } : domain;
+
+ // Validate
+ if (norm.prefix && norm.name) {
+ throw new VisibleError(
+ `Cannot specify both "prefix" and "name". Use "prefix" for a Cognito-hosted domain or "name" for a custom domain.`,
+ );
+ }
+ if (!norm.prefix && !norm.name) {
+ throw new VisibleError(
+ `Must specify either "prefix" or "name" in domain configuration.`,
+ );
+ }
+
+ // For custom domains, validate DNS/cert requirements
+ if (norm.name && norm.dns === false && !norm.cert) {
+ throw new VisibleError(
+ `Need to provide a validated certificate via "cert" when DNS is disabled.`,
+ );
+ }
+
+ return {
+ prefix: norm.prefix,
+ name: norm.name,
+ dns: norm.dns === false ? undefined : norm.dns ?? (norm.name ? awsDns() : undefined),
+ cert: norm.cert,
+ };
+ });
+ }
+
+ function createDomainWithSsl() {
+ return normalized.apply((norm) => {
+ // For prefix domains, no SSL needed
+ if (norm.prefix) {
+ const [resourceName, domainArgs, resourceOpts] = transform(
+ args.transform,
+ `${name}Domain`,
+ {
+ userPoolId: args.userPool,
+ domain: norm.prefix,
+ },
+ { parent, deleteBeforeReplace: true },
+ );
+
+ const domain = new cognito.UserPoolDomain(
+ resourceName,
+ domainArgs,
+ resourceOpts,
+ );
+
+ return domain;
+ }
+
+ // For custom domains, create SSL cert if needed
+ let certificateArn: Input;
+ if (norm.cert) {
+ certificateArn = norm.cert;
+ } else {
+ const cert = new DnsValidatedCertificate(
+ `${name}Ssl`,
+ {
+ domainName: norm.name!,
+ dns: output(norm.dns!),
+ },
+ { parent, provider: useProvider("us-east-1") },
+ );
+ certificateArn = cert.arn;
+ }
+
+ const [resourceName, domainArgs, resourceOpts] = transform(
+ args.transform,
+ `${name}Domain`,
+ {
+ userPoolId: args.userPool,
+ domain: norm.name!,
+ certificateArn,
+ },
+ { parent, deleteBeforeReplace: true },
+ );
+
+ const domain = new cognito.UserPoolDomain(
+ resourceName,
+ domainArgs,
+ resourceOpts,
+ );
+
+ // Create DNS records for custom domain
+ if (norm.dns) {
+ norm.dns.createAlias(
+ name,
+ {
+ name: norm.name!,
+ aliasName: domain.cloudfrontDistribution,
+ aliasZone: domain.cloudfrontDistributionZoneId,
+ },
+ { parent },
+ );
+ }
+
+ return domain;
+ });
+ }
+ }
+
+ /**
+ * The Cognito User Pool domain string. For prefix domains, this is the prefix.
+ * For custom domains, this is the full domain name.
+ */
+ public get domainName() {
+ return this._domain.apply((d) => d.domain);
+ }
+
+ /**
+ * The full URL of the hosted UI domain.
+ */
+ public get domainUrl() {
+ return this._domainUrl;
+ }
+
+ /**
+ * The CloudFront distribution domain name. Useful for custom domain DNS setup
+ * when managing DNS outside of SST.
+ */
+ public get cloudfrontDistribution() {
+ return this._domain.apply((d) => d.cloudfrontDistribution);
+ }
+
+ /**
+ * The underlying [resources](/docs/components/#nodes) this component creates.
+ */
+ public get nodes() {
+ return {
+ /**
+ * The Cognito User Pool domain.
+ */
+ domain: this._domain,
+ };
+ }
+}
+
+const __pulumiType = "sst:aws:CognitoUserPoolDomain";
+// @ts-expect-error
+CognitoUserPoolDomain.__pulumiType = __pulumiType;
diff --git a/platform/src/components/aws/cognito-user-pool.ts b/platform/src/components/aws/cognito-user-pool.ts
index 5ef2be38d0..75d0ca0d57 100644
--- a/platform/src/components/aws/cognito-user-pool.ts
+++ b/platform/src/components/aws/cognito-user-pool.ts
@@ -2,8 +2,10 @@ import { ComponentResourceOptions, Output, all, output } from "@pulumi/pulumi";
import { Component, Prettify, Transform, transform } from "../component";
import { Input } from "../input";
import { Link } from "../link";
+import { Dns } from "../dns";
import { CognitoIdentityProvider } from "./cognito-identity-provider";
import { CognitoUserPoolClient } from "./cognito-user-pool-client";
+import { CognitoUserPoolDomain } from "./cognito-user-pool-domain";
import { Function, FunctionArgs, FunctionArn } from "./function.js";
import { VisibleError } from "../error";
import { cognito, lambda } from "@pulumi/aws";
@@ -319,6 +321,73 @@ export interface CognitoUserPoolArgs {
* ```
*/
triggers?: Input>;
+ /**
+ * Configure a domain for the User Pool's hosted UI.
+ *
+ * You can use either a Cognito-provided prefix domain or your own custom domain.
+ *
+ * @example
+ *
+ * Add a Cognito prefix domain.
+ *
+ * ```ts
+ * {
+ * domain: {
+ * prefix: "my-app-dev"
+ * }
+ * }
+ * ```
+ *
+ * This creates a domain at `my-app-dev.auth.{region}.amazoncognito.com`.
+ *
+ * Add a custom domain. By default, creates an ACM certificate and configures
+ * DNS records using Route 53.
+ *
+ * ```ts
+ * {
+ * domain: "auth.example.com"
+ * }
+ * ```
+ *
+ * Use a domain hosted on Cloudflare.
+ *
+ * ```ts
+ * {
+ * domain: {
+ * name: "auth.example.com",
+ * dns: sst.cloudflare.dns()
+ * }
+ * }
+ * ```
+ */
+ domain?: Input<
+ | string
+ | {
+ /**
+ * Use an Amazon Cognito prefix domain. Creates a domain at
+ * `{prefix}.auth.{region}.amazoncognito.com`.
+ *
+ * Cannot contain "aws", "amazon", or "cognito".
+ */
+ prefix?: Input;
+ /**
+ * The custom domain name. Must be a subdomain (e.g., `auth.example.com`).
+ */
+ name?: Input;
+ /**
+ * The DNS provider for automatic certificate validation and record creation.
+ * Set to `false` for manual DNS setup.
+ *
+ * @default `sst.aws.dns`
+ */
+ dns?: Input;
+ /**
+ * ARN of an existing ACM certificate in `us-east-1`. By default, a certificate
+ * is created and validated automatically.
+ */
+ cert?: Input;
+ }
+ >;
/**
* [Transform](/docs/components#transform) how this component creates its underlying
* resources.
@@ -328,6 +397,10 @@ export interface CognitoUserPoolArgs {
* Transform the Cognito User Pool resource.
*/
userPool?: Transform;
+ /**
+ * Transform the Cognito User Pool domain resource.
+ */
+ domain?: Transform;
};
}
@@ -439,6 +512,26 @@ interface CognitoUserPoolRef {
* });
* ```
*
+ * #### Add a hosted UI domain
+ *
+ * Use a Cognito prefix domain for the hosted UI.
+ *
+ * ```ts title="sst.config.ts"
+ * new sst.aws.CognitoUserPool("MyUserPool", {
+ * domain: {
+ * prefix: "my-app-dev"
+ * }
+ * });
+ * ```
+ *
+ * Or use your own custom domain.
+ *
+ * ```ts title="sst.config.ts"
+ * new sst.aws.CognitoUserPool("MyUserPool", {
+ * domain: "auth.example.com"
+ * });
+ * ```
+ *
* #### Configure triggers
*
* ```ts title="sst.config.ts"
@@ -480,6 +573,7 @@ interface CognitoUserPoolRef {
export class CognitoUserPool extends Component implements Link.Linkable {
private constructorOpts: ComponentResourceOptions;
private userPool: Output;
+ private _domainUrl?: Output;
constructor(
name: string,
@@ -501,9 +595,11 @@ export class CognitoUserPool extends Component implements Link.Linkable {
const triggers = normalizeTriggers();
const verify = normalizeVerify();
const userPool = createUserPool();
+ const domain = createDomain();
this.constructorOpts = opts;
this.userPool = userPool;
+ this._domainUrl = domain?.domainUrl;
function normalizeAliasesAndUsernames() {
all([args.aliases, args.usernames]).apply(([aliases, usernames]) => {
@@ -690,6 +786,20 @@ export class CognitoUserPool extends Component implements Link.Linkable {
),
);
}
+
+ function createDomain() {
+ if (!args.domain) return;
+
+ return new CognitoUserPoolDomain(
+ `${name}Domain`,
+ {
+ userPool: userPool.id,
+ domain: args.domain,
+ transform: args.transform?.domain,
+ },
+ { parent, provider: opts.provider },
+ );
+ }
}
/**
@@ -706,6 +816,13 @@ export class CognitoUserPool extends Component implements Link.Linkable {
return this.userPool.arn;
}
+ /**
+ * If a `domain` is configured, this is the full URL of the hosted UI.
+ */
+ public get domainUrl() {
+ return this._domainUrl;
+ }
+
/**
* The underlying [resources](/docs/components/#nodes) this component creates.
*/
diff --git a/platform/src/components/component.ts b/platform/src/components/component.ts
index 24bedb419b..9cfea53c46 100644
--- a/platform/src/components/component.ts
+++ b/platform/src/components/component.ts
@@ -211,6 +211,11 @@ export class Component extends ComponentResource {
"aws:cloudfront/keyValueStore:KeyValueStore": ["name", 64],
"aws:cognito/identityPool:IdentityPool": ["identityPoolName", 128],
"aws:cognito/userPool:UserPool": ["name", 128],
+ "aws:cognito/userPoolDomain:UserPoolDomain": [
+ "domain",
+ 63,
+ { lower: true },
+ ],
"aws:dynamodb/table:Table": ["name", 255],
"aws:ec2/keyPair:KeyPair": ["keyName", 255],
"aws:ec2/eip:Eip": ["tags", 255],