strapi-launchpad
The Next.js front end of LaunchPad
(the template Vercel lists as LaunchPad: Next.js × Strapi Starter) — a
marketing site whose home page is built from Strapi's dynamic zones, with
pricing, FAQ and contact pages, a blog and a product catalogue, in English
and French, every Strapi read a "use cache" entry, and draft mode for
Strapi's preview — running on Cloudflare Workers + Static Assets through
@opennextjs/cloudflare, with the
Next.js data cache in an R2 bucket provisioned by Terraform through
Orun and the tag cache and revalidation
queue in Durable Objects the Worker itself exports, and packaged as an
Orun baseline: one command takes a new site from nothing to live on stage
and prod, in its own repository, under its own name — in ONE landing.
The Strapi server is not part of it: running Strapi needs a long-lived Node
server (a container), which this baseline does not carry. A fresh build needs
no Strapi project: until its owner connects one — a Strapi Cloud project
running LaunchPad's strapi/ app — the site renders the template's own seed
as sample content, under a bar that says how to connect one, never a 500. The
owner connects a project by setting its URL as an Orun secret and
redeploying. See specs/epics/strapi-launchpad
for the design, the milestones and the as-built status.
Instantiating a product
git clone --depth 1 --branch <baseline-tag> https://github.com/sourceplane/strapi-launchpad
cd strapi-launchpad
orun new --blueprint repo-blueprint.yaml --out ~/sites/acme-launch --run-hooks --resume \
--set reponame=acme-launch --set productname="Acme Launch" \
--set githuborg=acme-inc --set subdomain=<workers-dev-subdomain>
Or, once the platform's registry lists it, from the Orun console (Baselines →
strapi-launchpad → Build). Two phases: 01-product creates the
repository and lands the whole product as ONE pull request — intent, CI,
tooling, the R2 root, the site and its tests — and one convergence applies the
R2 bucket on stage and prod and then deploys the site bound to it;
02-docs records the live deployment from probed facts.
BOOTSTRAP.md is the operator guide;
docs/phases/ has a page per phase.
The new repository receives the product only — the app, its R2 root, its tests, its CI, its tooling, a README of its own and its operating contract — renamed throughout to the product's name, repository, Cloudflare Worker and bucket names. None of this repository's machinery ships. Its owner then connects a Strapi project with one Orun secret and a redeploy (the product's README, "Connecting Strapi"); the card asks for nothing of Strapi's, because the platform does not broker it.
| What makes it a baseline | Where |
|---|---|
| The console card (inputs, secrets, programme) | blueprint.yaml |
| The build document (modules, phases, hooks) | repo-blueprint.yaml |
| This tree's identity, and the bootstrap's task contracts | .baseline/ |
| The rename engine | tooling/rebrand/rebrand.mjs |
| The bootstrap's helpers (deferrals, redeploy marker, work-tree strip) | tooling/bootstrap/ |
| The deployment-record renderer | hooks/render-deployment-docs.sh |
| The product's README and context pack | .baseline/product/README.md, ai/context/ |
| The baseline's tests: contract, wiring, leak, offline bootstrap, end to end | testing/, .github/workflows/baseline.yml |
Cutting a baseline-vN tag | .github/workflows/tag.yml |
What is here (the product)
| Surface | Where |
|---|---|
The pages: /[locale], /[locale]/[slug], /[locale]/blog/…, /[locale]/products/…, sign-up; /api/preview, /api/exit-preview, /api/health | apps/web/app |
| The site's name and source link | apps/web/lib/site.ts (SITE_NAME, SOURCE_URL) |
| Strapi's cached reads; whether a project is connected | apps/web/lib/strapi/client.ts |
| The sample content — the template's seed, as a Strapi that imported it returns it — and its images | apps/web/sample/strapi, apps/web/public/uploads, apps/web/lib/strapi/sample.ts |
| Workers build, cache bindings and deploy contract | apps/web/next.config.ts, open-next.config.ts, wrangler.template.jsonc + wiring.fixture.json (→ the committed wrangler.jsonc), component.yaml |
| The R2 bucket (stage, prod) | infra/terraform/cloudflare-r2 |
| The wrangler renderer | tooling/wire |
| Tests | tests/web (the port's seams) |
Getting started
pnpm install
pnpm run wire:fixture # render apps/web/wrangler.jsonc from the offline fixture
pnpm typecheck
pnpm lint
pnpm test
pnpm build # next build + opennextjs-cloudflare build → apps/web/.open-next
pnpm --filter @app/web dev # http://localhost:3000, Next's dev server
pnpm --filter @app/web preview # wrangler dev over the built Worker, a local R2 bucket and local Durable Objects
With no STRAPI_URL both serve the sample content. To run against a project,
put its URL in apps/web/.env.local (from apps/web/.env.example). Offline,
the e2e's stand-in Strapi answers the site's reads from the same seed:
python3 testing/lib/fake-strapi.py --sample apps/web/sample/strapi --uploads apps/web/public/uploads --port 4010.
How it maps to Vercel
| Vercel | Here |
|---|---|
| Next.js hosting (SSR, route handlers, the proxy, draft mode) | One Cloudflare Worker (.open-next/worker.js) + Static Assets (ASSETS) |
Cache Components with "use cache", cacheLife and cacheTag | "use cache", its profile and its tags kept through experimental.useCache; every page renders per request from those cached reads |
| Data Cache entries | The R2 bucket bound as NEXT_INC_CACHE_R2_BUCKET |
revalidateTag | A sharded Durable Object tag cache (NEXT_TAG_CACHE_DO_SHARDED) consulted on every cache read |
| Stale-while-revalidate | A Durable Object queue (NEXT_CACHE_DO_QUEUE) that re-renders stale entries through the Worker's WORKER_SELF_REFERENCE binding |
Image Optimization (sharp) | Off (images.unoptimized): Strapi's media are served as uploaded |
NEXT_PUBLIC_API_URL | The Orun secret STRAPI_URL (optional build env) — and, unset, the template's own seed as sample content instead of a failed build |
PREVIEW_SECRET | A Worker secret |
The Strapi server (strapi/) | Not carried: Strapi Cloud (or the owner's own Strapi), connected after the build |
| Deploy on push | Orun applies the bucket and deploys <repo>-web-{stage,prod} on merge to main (apps/web/component.yaml) |
| "Deploy with Vercel" (clone the template) | orun new --blueprint repo-blueprint.yaml — a new repository, branded, deployed |
Cache Components are off: @opennextjs/cloudflare 1.20 cannot run Next 16's
Cache Components on workerd yet (a Cache Components page answers 500 and then
hangs — opennextjs-cloudflare#1450), so the site keeps upstream's "use cache"
reads through experimental.useCache and renders every page at request time.
The epic's IMPLEMENTATION-STATUS
has the evidence.
How the bucket reaches the Worker
infra/terraform/cloudflare-r2 ──apply (r2-data token)──► orun secret WIRING_CLOUDFLARE_R2 (stage, prod)
│ (each deploy resolves it as it starts;
│ a missing one fails the lane)
▼
apps/web dependsOn cloudflare-r2 (enforced on main) · secretEnv WIRING_CLOUDFLARE_R2_<ENV>
build: tooling/wire/live.mjs → wrangler.jsonc with the environment's bucket name
(dev, verify-only, renders the fixture) → next build → opennextjs-cloudflare
deploy: workers-deploy token → <repo>-web-<env>, its Durable Objects migrated on first deploy
wrangler.jsonc is committed as the fixture render of
wrangler.template.jsonc, because the deploy lane's verify-app-structure
step checks the config exists before it builds; testing/e2e.test.sh fails
if the two disagree.
CI
| Workflow | What it runs | Ships to a product |
|---|---|---|
ci.yml | the Orun pipeline, and the only workflow a product carries: orun plan --changed, then one job per lane. A pull request runs the verify lanes (the R2 root planned for stage and prod, the site built and dry-deployed per environment; they neither lint nor run the tests, which baseline.yml's e2e tier runs before every tag), except the bootstrap's own landings (orun/BASE-*), where plan alone runs and nothing waits for it (the landing checked its commit in the box); a merge applies the R2 bucket, then deploys stage → prod. It plans nothing while intent.yaml names the placeholder workspace ws_CHANGE_ME, as this repository does. A manual run (Orunbase's Deploy button, or Run workflow on main) plans the environment and components it names under the push-to-main trigger, so they deploy as a merge deploys them | yes |
baseline.yml | the baseline's own tiers: contract tests, the wiring path, the blueprint read by the pinned orun, the leak gate, the offline bootstrap with the product's own plan, the product built in the job tree, dry-deployed and served as born (on its sample content), then built again with a stand-in Strapi connected and browsed — its cache reused, draft mode past it | no |
tag.yml | cuts a baseline-vN tag (manual) | no |
Build from the console
Press Build: LaunchPad goes into the workspace you choose (or a new one), you connect Cloudflare, and an agent builds it.
Build from the CLI
# check providers, then build
orun baseline check strapi-launchpad
orun baseline new strapi-launchpad --via-platform --repo acme/strapi-launchpad