headroom-cms
v0.3.1
Published
Headroom CMS — Serverless headless CMS for AWS, deployed with SST
Downloads
280
Maintainers
Readme
Headroom CMS
Serverless headless CMS for AWS, deployed with SST v4.
Headroom provisions a complete CMS stack — DynamoDB, S3, Lambda, CloudFront, Cognito, and an admin UI — from a single SST component. No Go toolchain or build step required; pre-compiled binaries and a pre-built admin UI are included in the package.
Beta: Headroom is in active development and currently in beta. Expect breaking changes to the configuration interface, infrastructure, and APIs between minor versions. Not yet recommended for production workloads without pinning to an exact version.
Quick Start
npm create headroom my-cms
cd my-cms
npx sst deploy --stage production
./scripts/create-admin.sh [email protected] 'YourPassword123!'After deploying, the CLI prints four URLs:
- API — the Lambda function URL for the content API
- API CDN — the CloudFront distribution for
/v1/*and/health(cached, edge-authenticated) - Media CDN — the CloudFront distribution for
/media/*and/img/*(public assets) - Admin — the admin UI
Open the Admin URL and sign in with the credentials you just created.
Prerequisites
- Node.js 22+ and pnpm (or npm)
- AWS credentials configured (
aws configureor environment variables) - SST v4 (installed automatically as a dev dependency)
- AWS CLI (for the
create-admin.shandget-token.shhelper scripts) - jq (used by helper scripts to read SST outputs)
Configuration Reference
Your sst.config.ts imports HeadroomCMS and passes a configuration object:
/// <reference path="./.sst/platform/config.d.ts" />
import { HeadroomCMS } from "headroom-cms";
export default $config({
app(input) {
return {
name: "my-cms",
removal: input?.stage === "production" ? "retain" : "remove",
protect: input?.stage === "production",
home: "aws",
};
},
async run() {
const cms = new HeadroomCMS("CMS", {
senderEmail: "[email protected]",
// All options below are optional
domain: {
name: "api.mycompany.com",
certificateArn: "arn:aws:acm:us-east-1:...:certificate/...",
},
adminDomain: {
name: "cms.mycompany.com",
certificateArn: "arn:aws:acm:us-east-1:...:certificate/...",
},
priceClass: "PriceClass_100",
apiCacheTtl: 3600,
passwordPolicy: {
minimumLength: 12,
requireLowercase: true,
requireUppercase: true,
requireNumbers: true,
requireSymbols: true,
},
});
return cms.outputs;
},
});Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| senderEmail | string | (required) | Email for admin invitations and password resets. Must be verified in SES (see Troubleshooting). |
| domain | { name, certificateArn } | CloudFront default | Custom domain for the API CDN (/v1/*, /health). |
| mediaDomain | { name, certificateArn } | CloudFront default | Custom domain for the media CDN (/media/*, /img/*). |
| adminDomain | { name, certificateArn } | CloudFront default | Custom domain for the admin UI. |
| priceClass | "PriceClass_100" | "PriceClass_200" | "PriceClass_All" | "PriceClass_100" | CloudFront price class. 100 = US/Canada/Europe, 200 adds Asia/Africa, All = global. |
| apiCacheTtl | number | 3600 | API response cache TTL in seconds at the CloudFront edge. |
| passwordPolicy | object | See below | Cognito password policy overrides. |
Password Policy Defaults
| Field | Default |
|-------|---------|
| minimumLength | 12 |
| requireLowercase | true |
| requireUppercase | true |
| requireNumbers | true |
| requireSymbols | true |
Outputs
The component returns these outputs (available in .sst/outputs.json after deploy):
| Output | Description |
|--------|-------------|
| api | Lambda function URL for the Go API |
| apiCdn | API CloudFront distribution URL (/v1/*, /health — cached, edge-authenticated) |
| mediaCdn | Media CloudFront distribution URL (/media/*, /img/* — public assets). Use this in public HTML so the API host stays out of <img src> and og:image tags. |
| admin | Admin UI URL |
| userPoolId | Cognito User Pool ID |
| userPoolClientId | Cognito User Pool Client ID |
The API and media CDNs are deliberately split. apiCdn is the host your server-to-server code calls for /v1/* reads and writes; mediaCdn is the host that should appear in any public HTML, RSS feed, or og:image meta tag your site emits. Keeping the API host out of casually-scraped public content makes scraping, key probing, and accidental reverse-engineering of the admin endpoints noticeably harder.
Custom Domains
To use custom domains for the API CDN, media CDN, or admin UI:
Request an ACM certificate in us-east-1 (required for CloudFront):
aws acm request-certificate \ --domain-name api.mycompany.com \ --validation-method DNS \ --region us-east-1Validate the certificate by adding the DNS records ACM provides (check the AWS Console or use
aws acm describe-certificate).Add the domain config to your
sst.config.ts:const cms = new HeadroomCMS("CMS", { senderEmail: "[email protected]", domain: { name: "api.mycompany.com", certificateArn: "arn:aws:acm:us-east-1:123456789:certificate/abc-123", }, });Deploy, then create a CNAME (or alias) DNS record pointing your domain to the CloudFront distribution domain shown in the deploy output.
Repeat for mediaDomain (media CDN, serves /media/* and /img/*) and adminDomain (admin UI) if you want custom domains for those. Each certificate must be in us-east-1.
Updating
pnpm update headroom-cms
npx sst deploy --stage productionSST detects changes in the component and updates Lambda code, admin UI assets, and any infrastructure changes automatically.
Version Policy
Headroom follows semantic versioning:
- Patch (0.1.x) — Bug fixes, no infrastructure changes
- Minor (0.x.0) — New features, backward-compatible infrastructure additions
- Major (x.0.0) — Breaking changes to config interface or infrastructure that may require manual steps
Troubleshooting
SES Email Verification
Headroom creates an SES email identity for your senderEmail automatically, but AWS requires you to verify it. Check the inbox for that address and click the verification link. Until verified, admin invitation emails won't send.
If your AWS account is still in the SES sandbox, you can only send to verified email addresses. Request production access in the AWS Console under SES > Account dashboard.
IAM Permissions
The deploying user/role needs broad permissions to create DynamoDB tables, S3 buckets, Lambda functions, CloudFront distributions, Cognito user pools, SQS queues, and IAM roles. If you get AccessDenied errors during deploy, ensure your credentials have AdministratorAccess or an equivalent policy.
Deploy Errors
- "Resource already exists" — You may have resources from a previous deployment in the same AWS account/region. Use a different SST stage name or clean up the old resources.
- "CREATE_FAILED ... custom-message Lambda" — This usually means the Node.js runtime version isn't available in your region. Ensure you're deploying to a standard region.
- Timeout during deploy — CloudFront distribution creation can take 5-15 minutes on first deploy. This is normal.
Helper Scripts
The scaffolded project includes two helper scripts in scripts/:
create-admin.sh <email> <password>— Creates a Cognito admin user. Password must be 12+ characters with uppercase, lowercase, numbers, and symbols.get-token.sh <email> <password>— Returns a JWT access token for testing admin API endpoints.
Both scripts read from .sst/outputs.json, so you must deploy before running them.
What Gets Deployed
A single HeadroomCMS component creates:
- 13 DynamoDB tables (sites, content, draft content, blocks, media, collections, block types, admin audit, relationships, site users, webhooks, webhook deliveries, scheduler lock)
- 1 S3 bucket (media files and content bodies)
- 1 CloudFront KVS (edge auth cache and site version tracking)
- 1 Cognito User Pool + Client (admin authentication)
- 1 SES Email Identity (admin invitations)
- 5 Lambda functions (API, webhook worker, image transform, custom message, scheduler)
- 1 EventBridge schedule (drives the scheduler Lambda)
- 1 CloudFront Distribution with 2 CloudFront Functions (edge auth, media URL rewrite)
- 1 Admin UI (static site on CloudFront)
- 2 SQS queues (webhook delivery + dead letter queue)
Advanced: Ejecting
Ejection to a full source checkout — for customizing Go API handlers, adding admin UI pages, changing DynamoDB schemas, or swapping out subsystems — will be supported in a future release. For now, the packaged component is the only supported entry point; track the changelog for updates.
License
PolyForm Noncommercial 1.0.0
