The deploy experience of a hosting platform, on an AWS account you own.
Deploy static sites, single-page apps and server-rendered apps to AWS with one
CLI. No CloudFormation, Terraform or CDK.
Get started →
·
Documentation
·
Examples
Are you an agent? Start here: blogwright.iamstan.dev/llms.txt lists every docs page as plain Markdown.
Any app that builds with pnpm build deploys one of three ways:
| App type | Config | Served by |
|---|---|---|
| Static site | nothing | private S3 bucket through CloudFront |
| Single-page app | "spa": true |
the same, with index.html for unknown paths |
| Server-rendered app | an ssr block |
an immutable Lambda Web Function revision behind the same distribution |
Server rendering covers Astro SSR, SvelteKit adapter-node, Nuxt, TanStack Start and
Next standalone - see Server-rendered sites.
Buildable starting points for each framework and mode are in examples/,
documented at blogwright.iamstan.dev/examples.
Optional plugins add traffic
analytics and standard.site publishing.
- You own all of it. The site, its logs and its data live in your AWS account. MIT-licensed, no fees on top of what AWS charges.
- No AWS expertise needed.
bootstrapcreates the bucket, CDN, HTTPS certificate, build environment and deploy roles;statusshows drift. - Deploys that only do what changed. Only changed files upload and only their CloudFront paths are invalidated. An identical redeploy touches nothing.
- Production habits by default. A private origin behind CloudFront, PR previews, keyless CI with GitHub OIDC, and one-command rollback.
- One tool for static, SPA and SSR. The mode is one config key.
Under the hood, the CLI talks to AWS by SigV4-signing raw HTTP requests, models the infrastructure as a reconcilable dependency graph, and retries throttled AWS requests with jittered exponential backoff.
Requires Node ≥ 22 and AWS credentials in the ambient provider chain.
pnpm add -D blogwright
pnpm exec blogwright init # answer a few questions - writes config/production.jsonc
pnpm exec blogwright bootstrap # create the bucket, CDN and build environment
pnpm exec blogwright deploy # build in a MicroVM and publish (`bw` works too)(No TTY? Write the config by hand - only region and siteName are required:
echo '{ "region": "us-east-1", "siteName": "example" }' > config/production.jsonc.)
The quickstart walks every step, including custom domains and checking on the result.
blogwright init # first-run wizard: config/production.jsonc
blogwright bootstrap --domain example.com # create infra; prints ACM validation CNAMEs
blogwright deploy # zip + build in a MicroVM + publish
blogwright status staging # planned graph vs. live state
blogwright whoami # AWS account, identity, profile and pin in use
blogwright history # deployment history
blogwright logs <hash> # CloudWatch build logs for a hash
blogwright rollback <hash> # re-deploy an earlier build
blogwright delete # empty site/ only
blogwright destroy --yes # tear down site infrastructure (plugin state blocks)
blogwright preview … # per-PR preview stack
blogwright pds … # standard.site publishing (after `plugin add pds`)
blogwright plugin add <name> # install a plugin, pinned to this version
blogwright plugin list # list installed plugins and their versions
blogwright plugin remove <name> # uninstall a plugin, teardown asked first
blogwright <plugin> <action> # run an installed plugin's own commandEnvironment defaults to production; pass another as the positional [env] or
--env. Choose AWS credentials with --profile <name> (or aws.profile in the
config), and pin an environment to one account with aws.accountId. Full flags, positional layouts, and output contracts are in the
CLI reference.
An installed plugin claims its own namespace: blogwright <plugin> <action> runs
one of its commands. For a plugin with an init contributor, blogwright <plugin> init
splices its config block into the environment's config file; PDS instead declares
publication setup as its own init command. The verbs blogwright <plugin> bootstrap|status|destroy
reconcile that plugin's own resources, recorded in its own state object rather
than the site's. blogwright plugin add analytics installs the package
blogwright-analytics at this CLI's own version and pins it exactly, so every
checkout of the repo gets the same pair. The pin is taken at install time, not
maintained: upgrading the CLI on its own leaves the plugin at the version it was
pinned to, and nothing declares or checks an interface version, so nothing
reports the gap - re-running blogwright plugin add will not close it either,
since a package the manifest already declares is left untouched.
The plugin interface itself is internal and unversioned: internal implementation docs record how it works, but it can change in any release without a major version and is not a supported third-party contract. Publishing a versioned API remains a separate decision.
Plugin resources use state/<env>.<plugin>.json in the site bucket. Site bootstrap
warns to re-run each recorded plugin's bootstrap for that environment; site destroy
and preview teardown refuse until plugin teardown removes those keys. For GitHub
OIDC publishing, run blogwright pds bootstrap <env> once per eligible stack to
install PDS's own secret-access policy. PDS is not installed with the CLI: a site
that publishes to standard.site runs blogwright plugin add pds once, and its
post-deploy record sync then runs through the plugin's afterDeploy hook. plugin remove --yes uninstalls while
keeping resources; run <plugin> destroy <env> --yes first when teardown is wanted.
The optional analytics plugin provides a fourteen-node
us-east-1 pipeline and a local dashboard. Its page_views table excludes raw viewer
IP, while failed Firehose objects can retain original payloads and the site's
CloudWatch copy remains separate.
Everything lives at blogwright.iamstan.dev:
- Introduction and Quickstart - three commands from an empty AWS account to a live site.
- Guides: Deploying, Environments, Configuration files, Custom domains, CI deploys with GitHub OIDC, PR previews, Single-page apps, Server-rendered sites, Monorepos, Day-2 operations and Troubleshooting.
- Examples - one complete project per framework and mode, each deployed and checked on AWS.
- Plugins - Analytics (page views from CloudFront logs, no tracking script) and standard.site.
- Reference: CLI, Configuration and Architecture.
Every page is also available as plain Markdown - append .md to any URL, or
start from blogwright.iamstan.dev/llms.txt.
| Package | What it is |
|---|---|
blogwright |
The CLI (blogwright / bw bins): graph engine, resource nodes, commands, dispatch - plus the blogwright/rkey subpath export (a re-export of blogwright-core/rkey) |
blogwright-core |
SigV4 transport + per-service HTTP clients, config, S3 state store, shared ports, URL-derived rkeys |
blogwright-pds |
Optional plugin: standard.site (AT Protocol) publishing - OAuth client, secret store, PDS record sync |
blogwright-analytics |
Optional plugin: CloudFront/Firehose to S3 Tables Iceberg, transform Lambda and local DuckDB dashboard |
blogwright-build-agent |
HTTP build server baked into the builder MicroVM image (not published - its bundle ships inside the CLI package) |
pnpm install
pnpm build # all workspace packages, dashboard/transform and docs
pnpm typecheck
TZ=America/New_York pnpm test # transport mocks; no cloud needed
pnpm lint
pnpm exec oxfmt --check .
pnpm knip
# Integration tests against the floci emulator:
docker run -d --name floci -p 4566:4566 -v /var/run/docker.sock:/var/run/docker.sock floci/floci:latest
FLOCI=1 AWS_ENDPOINT_URL=http://localhost:4566 pnpm testThe lambda-microvms control plane is not emulated by floci, so the MicroVM
client and deploy orchestration are covered by transport-level mocks rather than
integration tests.
Contributor guidelines - coding style, error handling, version control (jj), and
the definition of done - live in DEVELOPMENT.md. The docs site
sources live in docs/; PRs that touch them get a preview at
pr-<n>.preview.blogwright.iamstan.dev.
Versioning is driven by changesets and releases by tags, with staged npm
publishing - no npm token anywhere (see .github/workflows/version-pr.yml
and release.yml):
- Every user-facing change ships with a changeset:
pnpm changeset, pick the impact, write a one-liner. The four publishable packages are version-fixed, so any bump moves them together. - CI maintains a "Release: version packages" PR that folds pending changesets into a version bump + per-package CHANGELOGs. Merge it when ready to release.
- Tag the merge:
git tag v<x>.<y>.<z> && git push origin v<x>.<y>.<z>(tags are the one place plain git is used - jj does not author tags; the version is previewed in the PR). - CI validates versions match the tag, builds, runs the full gate set plus
publintandarethetypeswrong, then stages all four packages to npm via OIDC trusted publishing with provenance, and cuts a GitHub Release from the changesets CHANGELOG entry. - Nothing is live yet: approve the staged packages (
npm stage approve, or the staged-packages UI on npmjs.com). Re-running a tag is idempotent - already-published packages are skipped.
One-time setup on npmjs.com: each package (blogwright-core, blogwright-pds,
blogwright-analytics, blogwright) needs a Trusted Publisher pointing at this repository, the
release.yml workflow, and the publish environment. The repository needs a
publish GitHub environment (add a required reviewer there for an extra
approval gate if wanted).