headroom-cms
v0.1.7
Published
Headroom CMS — Serverless headless CMS for AWS, deployed with SST
Maintainers
Readme
Headroom CMS
Serverless headless CMS for AWS, deployed with SST.
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.
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 three URLs:
- API — the Lambda function URL for the content API
- CDN — the CloudFront distribution (cached, edge-authenticated)
- 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 v3 (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 CDN / API endpoint. |
| 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 |
| cdn | CloudFront distribution URL (cached, edge-authenticated) |
| admin | Admin UI URL |
| userPoolId | Cognito User Pool ID |
| userPoolClientId | Cognito User Pool Client ID |
Custom Domains
To use custom domains for the 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 adminDomain if you want a custom domain for the admin UI.
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:
- 11 DynamoDB tables (sites, content, collections, block types, media, API keys, tags, audit, relationships, webhooks, webhook deliveries)
- 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)
- 4 Lambda functions (API, webhook worker, image transform, custom message)
- 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: Forking
If you need to modify Go API handlers, add custom admin UI pages, change DynamoDB schemas, or make other changes that the configuration options don't cover, you can eject to a full source checkout.
This is a one-way migration: once ejected, updates come from merging upstream changes rather than pnpm update.
When to Eject
The packaged component covers most use cases. Eject only when you need to:
- Add custom API endpoints or middleware in the Go Lambda
- Add custom admin UI pages or block editor plugins
- Change DynamoDB table schemas
- Wire additional Lambda functions into the same infrastructure
- Replace a subsystem entirely (e.g., swap Cognito for a different auth provider)
Ejection Steps
Clone at your current version:
git clone --branch v0.1.0 https://github.com/cykod/headroom.git my-cms-custom cd my-cms-custom pnpm installCopy your SST state so SST recognizes existing AWS resources:
cp -r ../my-cms/.sst ./Update
sst.config.tsto use local source with thedevoption:/// <reference path="./.sst/platform/config.d.ts" /> import { HeadroomCMS } from "./packages/headroom-cms/src/index.js"; export default $config({ app(input) { return { name: "my-cms", // must match your previous app name removal: input?.stage === "production" ? "retain" : "remove", protect: input?.stage === "production", home: "aws", }; }, async run() { const cms = new HeadroomCMS("CMS", { senderEmail: "[email protected]", dev: { apiHandler: "packages/api", webhookWorkerHandler: "packages/webhook-worker", customMessageHandler: "packages/functions/custom-message.handler", imageLambdaHandler: "packages/image-lambda/index.handler", adminPath: "packages/admin", }, }); return cms.outputs; }, });Deploy to verify the state transfer (should be a no-op if config matches):
npx sst deploy --stage productionStart customizing. Key directories:
| Directory | What it is | |-----------|-----------| |
packages/api/| Go Lambda API — add routes inmain.go, handlers ininternal/handler/| |packages/admin/| React admin UI — add pages insrc/pages/, components insrc/components/| |packages/headroom-cms/src/| SST infrastructure component — modify DynamoDB schemas, add resources | |packages/image-lambda/| Sharp image transform Lambda | |packages/webhook-worker/| Webhook delivery worker |
License
PolyForm Noncommercial 1.0.0
