The CWA is in heavy development
The CWA is still in alpha and not ready for production - some code and implementations are likely to change. If you would like to try out the CWA, please enjoy what we have provided and feel free to provide feedback, or get involved on GitHub.
Introduction

Installation

Scaffold a new CWA project with the interactive CLI — it picks your features, wires CI/CD, and gets Docker running in one go.

Prerequisites

  • Docker Desktop (or Docker Engine + Compose)
  • Node ≥ 18 and pnpm (for running the CLI and the Nuxt app locally)

PHP, Composer, and all API dependencies run inside Docker — nothing else to install.

1. Scaffold your project

Run the interactive CLI:

pnpm create cwa my-project

The CLI will ask four questions:

PromptOptions
Project nameThe directory to create
CI/CD pipelineGitHub Actions, GitLab CI, or none
FeaturesNavigation links, HTML content editor, image uploads, blog, nested pages, forms (multiselect)
Include fixtures?Scaffolds sample content so the site works out of the box

After answering, the CLI downloads the template, strips unused feature code from nuxt.config.ts, and generates a README.md tailored to your choices. It then offers to run docker compose up -d and pnpm install in one step — accept both.

The CLI reads a cwa-manifest.json file hosted in the template repo. Feature options and their file exclusions are driven by this manifest, so the CLI always reflects the latest template state without needing a CLI update.
The create-cwa package stays on 0.x.x versioning until CWA v1. It always scaffolds from the template repo's main branch — the branch comes from the cwa-manifest.json the CLI fetches, not from the CLI version you install.

2. Generate JWT keys

Authentication uses JWT tokens signed with an RSA key pair. The keys are gitignored, so every environment generates its own:

docker compose exec php bin/console lexik:jwt:generate-keypair

That writes api/config/jwt/private.pem and api/config/jwt/public.pem, encrypted with the JWT_PASSPHRASE from api/.env. The template ships a working passphrase so this step needs no configuration in local development.

Set your own passphrase before deploying anywhere public. Generate one with docker compose exec php php -r "print bin2hex(random_bytes(26));", add it to api/.env.local as JWT_PASSPHRASE=<your passphrase>, then regenerate the pair with bin/console lexik:jwt:generate-keypair --overwrite. The passphrase and the keys must always match — change one without the other and every login fails silently.

Prefer to do it by hand? Run these inside the container (docker compose exec php sh) instead. Both commands prompt for a passphrase — enter the JWT_PASSPHRASE value each time:

mkdir -p config/jwt
openssl genpkey -out config/jwt/private.pem -aes256 -algorithm rsa -pkeyopt rsa_keygen_bits:4096
openssl pkey -in config/jwt/private.pem -out config/jwt/public.pem -pubout
exit

3. Load fixtures

If you selected Include fixtures during setup, load them once the API container is healthy:

docker compose exec php bin/console doctrine:fixtures:load

This creates a default admin account (admin / admin) and seeds the site structure — layout, pages, and example components.

4. Open the site

There is no dev server to start. docker compose up -d already runs the Nuxt dev server inside the app container, and Caddy proxies it:

URLWhat's there
https://localhostYour Nuxt application
https://localhost/_apiAPI (JSON-LD)
https://localhost/_cwaCWA admin panel
The stack uses self-signed certificates. Accept the browser warning or trust the CA at api/frankenphp/caddy/certs/.
The containerised dev server is also published directly on http://localhost:3001 if you ever need to bypass the proxy. Don't run pnpm dev on your host at the same time — that starts a second Nuxt on port 3000 that https://localhost never reaches.

Log in at https://localhost/login with admin / admin. Click Edit to enter edit mode and see the inline CMS.

IMAGE: The freshly scaffolded site at https://localhost with the fixture content loaded — the first thing a reader should see working
IMAGE: The same page after logging in and clicking Edit, with the admin bar at the top and editable regions outlined

What Docker handles automatically

When the php container starts, the entrypoint:

  • Runs composer install if vendor/ is empty
  • Waits for PostgreSQL to be ready
  • Runs pending database migrations
  • Runs ANALYZE to refresh PostgreSQL query statistics (keeps dev pages fast — see the Docker gotchas)

You never need to run these manually.


CI/CD

If you chose GitHub Actions during setup, four workflows are pre-wired in .github/workflows/:

WorkflowTriggerWhat it does
ci.ymlEvery pushBuild + test; deploy review environment on PRs
production.ymlManualDeploy canary or production
cleanup.ymlPR closedTear down review environment
performance.ymlManualRun sitespeed tests

Images push to GHCR. The workflows use the same bin/devops/ shell scripts as the GitLab CI option. Required secrets and variables are documented inline in each workflow file — read those.

A fifth file, publish-create-cwa.yml, belongs to the upstream template repo and is copied into your project too. It only fires on create-cwa/v* tags, so it never runs for you — delete it: rm .github/workflows/publish-create-cwa.yml.

Manual setup (alternative)

If you prefer to create a GitHub repository first and clone from there:

With the GitHub CLI:

gh repo create my-website \
    --template="components-web-app/components-web-app" \
    --private --clone

Or generate from GitHub:

Generate a new repository from the template on GitHub

Then follow steps 2–4 above. You'll configure features manually rather than through the CLI.

The template repo includes a packages/ directory used for the create-cwa CLI itself. Delete it from your project after cloning — it's not needed in your app.
rm -rf packages/

What's next