CLI, not a PaaS

Deploy Next.js into your AWS account.

From repo to live site in one push. Short-lived OIDC instead of stored keys. Site bytes stay in their account — Coresecure never holds a copy.

Pin @coresecure/deploy@0.4.0. Actions never runs npx @latest.

Architecture

From repo to live site, in one push.

csd is the layer between your Next.js repo and your AWS account. It stands up the GitHub side, hands GitHub Actions a short-lived OIDC identity instead of a key, and lets SST and OpenNext provision the actual AWS services your site runs on. You never touch the AWS console.

Next.js projectYour repo, your codecsdinit · oidcGitHub ActionsShort-lived OIDC tokenAssumeRoleno keysYOUR AWS ACCOUNTCloudFrontGlobal edge CDNLambdaSSR via OpenNextS3Static assets + stateRoute53 + ACMYour domain and cert -- already yours, never created or deletedsst deployLive sitewww.acme.com

Simplify. csd turns two logins and three commands into a working GitHub-to-AWS pipeline. No console clicking, no hand-written IAM.

Own it. Every AWS service in the diagram is provisioned inside your account under your name. Coresecure never holds a copy of your site.

No keys. GitHub Actions authenticates with a short-lived OIDC token, not a stored AWS access key.

Stages

Pick how many gates stand before the public site.

csd init --preset takes two, three, or four. Each stage gets its own branch, its own hostname, and its own GitHub environment holding its own secrets. Non-prod defaults to basicAuth. --auth-mode can set every non-prod stage to none, basicAuth, or signedCookie. Production authMode is always none, and that part is enforced.

  • --preset twoOne gate before the public site.
    basicAuth

    staging

    staging-www.apex

    staging

    public

    production

    www.apex

    main

  • --preset threeA developer stage ahead of staging.
    basicAuth

    dev

    dev-www.apex

    dev

    basicAuth

    staging

    staging-www.apex

    staging

    public

    production

    www.apex

    main

  • --preset fourA dedicated QA stage between dev and staging.
    basicAuth

    dev

    dev-www.apex

    dev

    signedCookie

    test

    test-www.apex

    test

    either mode, set per stage

    basicAuth

    staging

    staging-www.apex

    staging

    public

    production

    www.apex

    main

basicAuth
A shared password for the whole stage. Rotate it to change who has access.
signedCookie
A signed link that expires. Access is granted per link, not per person.
public
authMode none. Required on production. Also a valid choice on non-prod, via --auth-mode none.

A lock is a choice, per stage. basicAuth puts one shared password in front of the whole stage, checked at the CloudFront edge before the request reaches any origin, and csd rotate replaces it. signedCookie instead mints a link that expires: csd link signs one URL with a TTL, so you hand out access to a page for an afternoon rather than a password for the quarter. Neither is simply stronger — they answer different questions about who you are sharing with.

Production is not configurable. Its authMode is always none, and the config throws if you set it to anything else. All stages live in the same AWS account and are separated by resource naming and IAM scoping, not by account. csd does not configure branch protection or required reviewers — if you want a human gate on the promotion PR, that stays yours to set in GitHub.

csd statusmain
acme/www apex acme.com
staging staging-www.acme.com live a1b2c3d head a1b2c3d
production www.acme.com live 9f8e7d6 head 9f8e7d6 behind staging by 0
next: gh pr create --base main --head staging
watch: gh run watch --repo acme/www

Install

Pin it in the tenant lockfile.

Pin @coresecure/deploy@0.4.0. The package is on GitHub Packages (npm.pkg.github.com), not the public npm registry. This host pins that version in the lockfile so Actions installs it from the registry. Actions runs pnpm i --frozen-lockfile, then pnpm exec csd-deploy. The workflow never runs npx @latest.

devDependency
pnpm add -D @coresecure/deploy@0.4.0
onboard a tenant
gh auth login -s read:packages
aws sso login --profile <client>
gh repo clone <owner>/<repo> && cd <repo>
csd init --preset two --apex <apex>
csd oidc --profile <client>
git push -u origin staging
  1. 1. gh auth login -s read:packages. csd uses your GitHub user. No GitHub App.
  2. 2. aws sso login --profile <client>. ACM cert in us-east-1 and a Route53 zone already exist.
  3. 3. Clone the tenant repo, then csd init from the top of the clone. Init does not create the repo. When it does not exist yet, gh repo create --private --add-readme --clone.
  4. 4. csd oidc --profile <client>. It checks the cert covers every stage hostname. --allow-uncovered-cert uses an uncovered cert anyway. oidc writes CSD_DEPLOY_ROLE_ARN, CSD_ZONE_ID, and CSD_CERT_ARN. In this repo run pnpm exec csd. csd refuses to run when CI=true.
  5. 5. git push. That is the first deploy. The hint uses the clone's remote name.

Use cases

What you actually type.

csd init is GitHub. csd oidc is AWS SSO, once. csd-deploy is CI only (deploy, redeploy, remove, link). Humans never run csd-deploy.

01 / csd init

Stand up a client tenant

From the top of a clone of their Next.js repo. Log in with gh auth login -s read:packages first. Init does not create the repo. Clone with gh repo clone, or use gh repo create --private --add-readme --clone when the repo does not exist yet. With --repo it uses the matching remote. Without --repo it uses origin, or the only GitHub remote. If several remotes exist and none is named origin, it stops and lists them. SSH host aliases are fine. Vendors one Actions job per stage, GitHub Environments, and secrets. Does not set CSD_DEPLOY_ROLE_ARN. Does not commit or push. Does not call AWS. The printed git push hint uses the remote's name.

csd init
gh repo clone <owner>/<repo> && cd <repo>
unset CI
csd init --apex acme.com --preset two

02 / csd init --kind service

Stand up a service tenant

From the top of a clone, same as a site, with csd init --kind service. A service is a small Lambda behind a function URL. There is no apex, no CloudFront distribution, no certificate, and no hosted zone. Custom domains for services are out of scope. csd.config.json sets "kind" to "service". No kind still means a site. csd init writes project. The name must match ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$, must not be site or csd-site, and must not include a csd- prefix. SST forms the app name as csd-<project> after lowercasing and sanitizing. Any apexDomain, certArn, or zoneId key is rejected. Auth mode resolves to none, so csd rotate and csd link exit with "services have no preview auth".

csd init --kind service
gh repo clone <owner>/<repo> && cd <repo>
csd init --kind service
csd oidc --profile <client>

03 / WebhookSecret

What the service starter runs

Http is an sst.aws.Function with url: true. Do not rename Http. The function URL comes from that logical name. WebhookSecret is new sst.Secret("WebhookSecret", "unset"). While the value is missing or still unset, every request except GET and HEAD /_csd/version is 401. Bearer unset is not accepted. GET and HEAD /_csd/version answer before any check, with { sha, tree, stage }. After a real value is set and the stage is redeployed, the handler compares Authorization to Bearer <secret> with timingSafeEqual. It invokes Worker with InvocationType Event and replies 202 only when that status is 202. It does not wait for the worker to finish, and it does not log the body. A CronV2 example is commented out at the bottom of sst.config.ts. Uncomment it to run the worker on rate(1 hour). sst secret set alone does not change the running function. Set the secret, then redeploy.

WebhookSecret
pnpm exec sst secret set WebhookSecret <value> --stage <stage>

04 / csd oidc

Create the GitHub OIDC deploy role

Once per tenant repo, using the operator SSO profile. For a site, lists profiles, Route53 zones, and us-east-1 ACM certs if you omit flags, then writes CSD_DEPLOY_ROLE_ARN, CSD_ZONE_ID, and CSD_CERT_ARN. Every given, saved, or picked cert has to cover every stage hostname. csd oidc stops if a cert is uncovered, not issued, or missing. --allow-uncovered-cert uses an uncovered cert anyway. Only an access-denied error while reading the cert warns and continues. A kind service config with no cert or zone skips the check and does not write CSD_ZONE_ID or CSD_CERT_ARN. A private zone is never auto-picked. The account-level OIDC provider and shared access-log bucket are created once, on first use, and every later repo just references them. Idempotent. Does not deploy the site.

csd oidc
aws sso login --profile acme
csd oidc
git push origin staging

05 / git push origin staging

First deploy

Push to a mapped stage branch. Actions on that branch is the only deployer: OIDC into the client account, sst deploy --stage staging. cancel-in-progress is false. Non-prod defaults to basicAuth. Production authMode is always none.

git push origin staging
git push origin staging
csd status
# watch: gh run watch --repo acme/www

06 / gh pr create

Promote along the chain

No csd command in this path. Open a PR from the current stage branch into the next. Merge deploys the target. two: staging -> main. three: dev -> staging -> main. four: dev -> test -> staging -> main. The live tree hash is the proof both stages ran the same content.

gh pr create
gh pr create --base main --head staging

07 / csd status

See one tenant live

Polls GitHub (branch heads, runs) and GET /_csd/version on each hostname. Prints how far each stage is behind the one before it. Waits if a deploy.yml run is in progress, then prints the next gh pr create line.

csd status
csd status
csd status --repo acme/www

08 / csd status --all

Fleet check from GitHub

GitHub is the index. csd.projects.yaml is a laptop cache of org repos this token can see that contain csd.config.json. Missing cache runs projects sync first. gone and not_csd are warnings. no_access or a live check failure is exit 1.

csd status --all
csd projects sync
csd status --all

09 / csd doctor

Audit the vendored workflow

Checks properties, not a file hash: checkout fetch-depth 0 and the workflow_dispatch sha fallback, job ids deploy-<stage>, environment equals the stage id, the push if matches that stage branch, concurrency csd-<stage> with cancel-in-progress false, CSD_CF_PRIVATE_KEY only on the link step, and no ${{ inputs. inside a run: scalar. On 0.1.17 it also checks sst.config.ts: permissions boundary, no top-level imports, the IAM path and Lambda name transforms, and that basicAuth does not return event.request.

csd doctor
csd doctor
csd doctor --all

10 / csd init --refresh

Ship a YAML contract change

Rewrites the vendored caller and composite action. On 0.3.0 that updates the pinned setup-node SHA and the pinned actions/checkout SHA. Does not rotate keys. Does not call AWS. Does not set branch protection. Operator commits and pushes. Deploys never fetch Coresecure GitHub at runtime.

csd init --refresh
csd init --refresh
git push origin staging

11 / csd init --refresh

Upgrade a tenant to 0.4.x

On 0.4.x, an existing site copies the template pins, lib/next-cache-key.ts, and the CloudFront transform. The copy steps are in docs/upgrading-next-16.md. cloudFrontPublicKey sends the trimmed PEM plus one newline. The name hashes the trimmed PEM, so the existing public key stays in place. The 0.3.x and 0.2.x steps still apply. An existing site runs csd oidc once so the deploy role picks up the scheduler grants and the conditional zone and certificate statements. Then csd init --refresh and a normal staging deploy. Site config stays the same. The generated deploy.yml keeps its jobs. The composite action passes GITHUB_TOKEN so the deployment record is created. csd init --refresh does not rewrite project, does not patch src/http.ts, and does not replace sst.config.ts. On a service, leave csd.config.json project as written. Parsing no longer strips a leading csd-. The SST app name stays csd-<project>, so project csd-service-smoke stays the app csd-csd-service-smoke. In src/http.ts, keep forwardedHeaders deleting authorization and cookie before the worker invoke, and do not forward cookies. In sst.config.ts, drop any local projectName loop that strips a leading csd-. A service tenant deletes its local csd-project.ts and imports sstAppName and normalizeServiceProject from @coresecure/deploy/service-project. The tenant-service template uses that same import. If 0.2.0-rc.1 already rewrote project, keep the rewritten name so the SST app and function URL stay on the current state. Only restore a csd- prefix when project still has it and the deployed app name matches csd-csd-.... On 0.2.1, run csd oidc again. That run removes the SstAppName parameter and the ServiceSstStageSecretCleanup and ServiceSstStagePassphraseCleanup statements from an existing stack. It makes no other IAM change. SstBootstrapS3 and SsmProjectStage already allow both deletes. On 0.3.0, run csd oidc again. That run checks cert coverage. It stops if a cert is uncovered, not issued, or missing. --allow-uncovered-cert uses an uncovered cert anyway. Then run csd init --refresh. That updates the pinned setup-node SHA in the composite action and the pinned actions/checkout SHA in both deploy jobs.

csd init --refresh
csd oidc --profile <client>
csd init --refresh
git push origin staging

12 / csd rotate staging

Rotate preview Basic Auth

On basicAuth, writes a new PREVIEW_BASIC_AUTH secret, prints the password, then dispatches redeploy on the live /_csd/version SHA and waits. The password is printed before the redeploy: a GitHub secret cannot be read back. On signedCookie, rotate replaces CSD_CF_PRIVATE_KEY and CSD_CF_PUBLIC_KEY, then redeploys. It does not print a password. Rotate refuses authMode none. Production is always none. Rotate does not call AWS.

csd rotate staging
csd rotate staging

14 / csd destroy staging

Tear down a stage

Usage is csd destroy <stage> [--confirm <value>]. Type the apex for a site, or owner/repo for a service. When there is no terminal, pass that same text as --confirm. Then workflow_dispatch action=remove. For a site, sst remove deletes that hostname's aliases and the OpenNext stack. Zone and cert remain. Never destroy on branch delete. Never local sst remove from csd.

csd destroy staging
csd destroy staging
# site: type the apex (acme.com)
# service: type owner/repo

csd destroy staging --confirm <owner/repo>

15 / csd init --preset three

Grow two to three to four

Add dev, then test. Each new stage is another isolated OpenNext stack and another GitHub Environment named the stage id. Shrink only with typed destroy. No fifth stage. GitHub Environment is never named development.

csd init --preset three
csd init --preset three
git push origin dev

16 / this host

www.coresecure.info is a tenant

Same CLI as a client. Preset two. Apex coresecure.info. Staging is staging-www.coresecure.info (signedCookie). Production is www.coresecure.info (authMode none). The pin is @coresecure/deploy 0.4.0 from GitHub Packages. Run it with pnpm exec csd.

this host
pnpm exec csd status --repo coresecure/deploy_coresecure_com
pnpm exec csd doctor --repo coresecure/deploy_coresecure_com

Feature set

The boring parts are load-bearing.

01

Their org. Their account.

One GitHub org per client. Their AWS account can safely host several tenant repos, several domains, one company. Each site repo gets its own stack, deploy role, and state bucket, named from org/repo, never a single account-wide name. A service sets CreateStateBucket to false, so oidc does not create that bucket. One repo's CI can never reach another's resources.

02

One deployer.

GitHub Actions runs sst deploy. The workflow YAML lives in the tenant repo. No cross-org reusable workflow, no GitHub App, no webhook.

03

Two binaries, two credential paths.

Humans run csd. csd oidc is the only operator command that calls AWS (SSO, once per repo). Everything else is GitHub. CI runs csd-deploy under OIDC.

04

Promotion is a pull request.

Open a PR from staging into main. Merge deploys production. The live tree hash is the proof. There is no csd promote.

05

Preview gates, public production.

Non-prod defaults to basicAuth. csd init --auth-mode sets every non-prod stage to none, basicAuth, or signedCookie. Production is always none. /_csd/version stays public so status can drift-check without a password. A 401 there is version_path_authed, and csd status exits 1.

06

DNS and certs are inputs.

Bring an existing us-east-1 ACM cert and Route53 zone. SST upserts the stage alias. It will not create or delete the zone or the cert.

07

Every write is logged.

Each tenant's state bucket delivers S3 server access logs to one shared, account-level log bucket, prefixed by org and repo. CloudTrail shows the operator's SSO role. Nothing writes as a Coresecure identity.

08

A service is not a site.

csd init --kind service writes a small Lambda behind a function URL. csd.config.json says "kind": "service". There is no apex, CloudFront distribution, certificate, or hosted zone. Custom domains for services are out of scope.