Web hosting and the React admin console
Give the SaaS provider a web UI instead of curl: a React admin console where system admins sign in to the control plane, onboard and offboard tenants, and manage other system admins. It’s hosted on S3 + CloudFront, and the same hosting construct is reused for the tenant-facing app in step 11.
Pieces
Section titled “Pieces”client/├── admin/ React + Vite + TypeScript app (this step)└── cdk/ separate CDK app: stack "serverless-saas-client" └── lib/static-site.ts private S3 bucket + CloudFront (OAC) + deploy dist/ + config.jsonThe client CDK app is separate from the server one on purpose, as in the original. The server app is also synthesized inside the provisioning CodeBuild job (step 09), which doesn’t have the web apps’ build output.
Runtime config instead of build-time config
Section titled “Runtime config instead of build-time config”The original Angular build bakes the API URL and Cognito ids into the bundle by writing environment.ts inside a Docker build. Here the app is built once, and CDK writes a config.json next to it:
Source.jsonData('config.json', { controlPlaneApiUrl, userPoolId, appClientId })At startup, main.tsx fetches /config.json, configures Amplify Auth and the API client, and then renders. The same dist/ works in any account, and no Docker is needed for the front end.
Hosting: StaticSite
Section titled “Hosting: StaticSite”- Private bucket (block all public access, SSL only), auto-deleted with the stack.
- CloudFront
DistributionwithS3BucketOrigin.withOriginAccessControl, so only this distribution can read the bucket. 403/404→/index.htmlwith status 200, so deep links like/tenants/newreach the React router.BucketDeploymentuploadsdist/plusconfig.jsonand invalidates CloudFront’s cache.
The admin app
Section titled “The admin app”| Screen | Calls (control plane API, ID token from the control plane pool) |
|---|---|
Sign in (Amplify signIn, SRP). Handles the “set a new password” challenge for invited admins. |
Cognito |
| Tenants: list, tier badge, status, auto-refresh while provisioning, Offboard | GET /tenants, DELETE /tenant-registrations/{id} |
| Onboard tenant: name, admin email, tier | POST /tenant-registrations {tenantData:{tenantName,email,tier}, tenantRegistrationData:{registrationStatus}} |
| System admins: list, add | GET /users, POST /users {userName,email,userRole:"SystemAdmin"} |
Files:
- client/admin/src/main.tsx: load config, then configure auth and the API base URL.
- client/admin/src/auth.tsx: Amplify v6 auth context (
signIn,confirmSignInfor the new-password step,fetchAuthSessionfor the ID token). - client/admin/src/api.ts:
fetchwrapper that addsAuthorization: Bearer <idToken>and unwraps SBT’s{"data": ...}. - client/admin/src/pages/:
Login,Tenants,CreateTenant,Users. - client/cdk/lib/static-site.ts, client/cdk/lib/client-stack.ts, client/cdk/bin/client.ts
- scripts/deploy-client.sh: reads the control plane outputs, builds the app and deploys
serverless-saas-client.deploy.shnow calls it at the end.
Deploy
Section titled “Deploy”./scripts/deploy.sh you@example.com # everything./scripts/deploy-client.sh # just the web appsOutput: serverless-saas-client.AdminSiteUrl = https://<id>.cloudfront.net
Run locally
Section titled “Run locally”cd client/admincp public/config.example.json public/config.json # fill in the ControlPlaneStack outputsnpm install && npm run dev # http://localhost:5173The control plane API’s CORS config already allows http://localhost:5173 (set in step 01).
Verify
Section titled “Verify”Done on the test deployment (https://dthjv4e2gzxl4.cloudfront.net):
- Opening the deep link
/tenants/newserves the app (SPA fallback) and shows the sign-in page, soconfig.jsonloaded and Amplify was configured. config.json={"controlPlaneApiUrl":"https://w2g4jn8evg.../","userPoolId":"us-east-1_NJ8yJK7e9","appClientId":"2b4l..."}.- CORS from the CloudFront origin: preflight
OPTIONS /tenant-registrationsreturns204withaccess-control-allow-origin: https://dthjv4e2gzxl4.cloudfront.net, andGET /tenantswith an admin token returns200with the same header. - The API calls behind each screen were verified with curl in steps 01 and 08 (
GET /tenants,POSTandDELETE /tenant-registrations,GET /users).
Try it yourself: open the Admin site URL, sign in as admin with the password you set (./scripts/admin-token.sh '<password>' --set-password), onboard a tenant, and watch its status change from “In progress” to “Complete”.
Step 11 adds the tenant-facing SaaS app: users type their tenant name, the app looks up that tenant’s user pool and API, and they manage products, orders and users.