Skip to content

Custom domain and the documentation site

Serve the web apps on readable hostnames instead of *.cloudfront.net, and publish these docs as a website with screenshots of the live apps.

Site URL
SaaS app https://saas.haibui.click
Admin console https://saas-admin.haibui.click
Documentation https://saas-docs.haibui.click

DOMAIN_NAME names a Route 53 hosted zone in the same account. When it’s set, the client stack:

  1. finds the zone with HostedZone.fromLookup,
  2. creates one DNS-validated ACM certificate for saas., saas-admin. and saas-docs.<zone> (in us-east-1, as CloudFront requires),
  3. gives each CloudFront distribution its hostname and the certificate,
  4. adds A and AAAA alias records pointing at the distributions.

Without DOMAIN_NAME nothing changes. No backend change is needed: the control plane’s CORS allows any https:// origin, the tenant APIs allow *, and the apps sign in with SRP, so Cognito has no callback URLs.

client/docs is an Astro site using the Starlight docs theme, with hand-written pages for concepts, app guides, operations and reference. The walkthroughs in docs/ stay the single source: scripts/sync-steps.mjs copies them into the site on every build, turns each # NN — Title into the page title, links between steps into site links and links to code into GitHub links.

It’s hosted by the same StaticSite construct as the apps, with a new singlePageApp: false mode:

  • A CloudFront Function rewrites /guide/ and /guide to /guide/index.html, because an S3 origin behind Origin Access Control has no directory indexes.
  • Missing pages return /404.html with status 404, instead of the SPA fallback to /index.html with 200.
  • No config.json is written.

The architecture and flow diagrams are written in D2 and rendered to SVG at build time by astro-d2, with official AWS service icons. They replace the ASCII art the step docs used to have.

  • Concept pages hold the source as ```d2 blocks; the big overview is in client/docs/diagrams/architecture.d2.
  • The rendered SVGs in client/docs/public/d2/ are committed. The step docs and the README embed them as plain images, so they also render on GitHub, and sync-steps.mjs points those images at the site’s copy.
  • astro.config.mjs skips generation when the d2 binary isn’t installed (brew install d2), so the site still builds from the committed SVGs.

Clicking any screenshot or diagram opens it full size, using starlight-image-zoom.

The screenshots are captured from the deployed apps by client/docs/screenshots/capture.sh with npx playwright screenshot at 2× pixel density. Amplify keeps the Cognito session in localStorage, so login.mjs signs in to each app once and saves the browser storage, and each capture loads it with --load-storage. A short viewport plus --full-page crops each image to the page’s content.

Terminal window
DOMAIN_NAME=haibui.click ./scripts/deploy-client.sh

The first deploy with a domain waits a few minutes for certificate validation. Outputs:

serverless-saas-client.AdminSiteUrl = https://saas-admin.haibui.click
serverless-saas-client.AppSiteUrl = https://saas.haibui.click
serverless-saas-client.DocsSiteUrl = https://saas-docs.haibui.click
Check Result
GET https://saas.haibui.click/config.json 200, the control plane URL
GET https://saas-admin.haibui.click/tenants/new (SPA deep link) 200
GET https://saas-docs.haibui.click/concepts/architecture/ and without the trailing slash 200
GET https://saas-docs.haibui.click/nope/ 404, the docs’ not-found page
Signed in to both apps on the new hostnames (Playwright) dashboard, products, orders and users load data from the APIs
Every internal link and image in the built site resolves
Clicking a screenshot or diagram opens it full size in a dialog
Terminal window
cd client/docs && npx playwright install chromium
ADMIN_USER=… ADMIN_PASSWORD=… TENANT=acme TENANT_USER=… TENANT_PASSWORD=… npm run screenshots

Use dedicated accounts for this, for example a tenant user made with scripts/dev-create-tenant-user.sh.