Zero: my project factory Developer Zander Martineau created 'zero', a project factory using a single GitHub Actions workflow that scaffolds new private repos from his starter templates, provisions Neon databases, sets secrets, and optionally kicks off Claude to build the project. The workflow, triggered via workflow_dispatch with inputs for name, idea, and type, supports 'app' (React + Neon) and 'astro' (Astro) templates, and includes steps to create the repo, wait for readiness, and create a Neon project for app-type projects. In my zero-to-one stack post https://zander.wtf/blog/zero-to-one-stack I ended with an admission: the one part of shipping side projects I hadn't cracked was the boring ceremony between "I've just had an idea" and "something is actually being built". Create a repo from a template, create a Neon project, copy secrets around, find the repo in Claude Code and explain what I want. None of it hard, all of it friction, and ideas die in that gap. I've now cracked it. The answer is a repo called zero : a project factory built from a single GitHub Actions workflow. I trigger it from my phone, usually , give it a name, an idea, and a project type, and a few seconds later there's a new private repo scaffolded from one of my starter templates, with infrastructure provisioned, secrets set, and optionally Claude already working on the brief. TL;DR workflow dispatch workflow with three inputs: name , idea , type type: app scaffolds from type: astro scaffolds from @claude build this mention, so the first commit happens without meThe factory sits on top of the two starter templates I've written about before. zed-stack-starter https://github.com/mrmartineau/zed-stack-starter is for interactive React applications: TanStack Router and Query, Hono on Cloudflare Workers, Postgres on Neon with Drizzle and better-auth. zed-astro-starter https://github.com/mrmartineau/zed-astro-starter is for content-driven, mostly-static sites. Both use ZUI https://zui.zander.wtf , my CSS-first UI library, but otherwise they're quite different beasts — one needs a database, auth secrets, and an API; the other just needs to exist and deploy. That difference is exactly what the workflow encodes. The type input decides which template to generate from and how much infrastructure to bother with: app DATABASE URL and a freshly generated BETTER AUTH SECRET as repo secrets, then add the Cloudflare deployment secrets. astro Here's the whole thing. It lives in the zero repo as .github/workflows/create-project.yml : name: 🏭 New project on: workflow dispatch: inputs: name: description: Repo / project name required: true type: string idea: description: What should it do? required: true type: string type: description: Project type required: true type: choice default: app options: - app React + Neon zed-stack-starter - astro Astro zed-astro-starter kickoff claude: description: Kick off Claude to build the project required: false type: boolean default: false env: GH TOKEN: ${{ secrets.FACTORY GH PAT }} OWNER: mrmartineau NAME: ${{ inputs.name }} IDEA: ${{ inputs.idea }} jobs: create: runs-on: ubuntu-latest steps: - name: Pick template id: template run: | if "${{ inputs.type }}" = "astro" ; then echo "repo=zed-astro-starter" "$GITHUB OUTPUT" else echo "repo=zed-stack-starter" "$GITHUB OUTPUT" fi - name: Create repo from template run: | gh api "/repos/$OWNER/${{ steps.template.outputs.repo }}/generate" \ -f name="$NAME" \ -f owner="$OWNER" \ -F private=true \ -f description="$IDEA" - name: Wait for repo to be ready run: | template generation is async; poll until contents exist for i in $ seq 1 12 ; do if gh api "/repos/$OWNER/$NAME/contents/README.md" /dev/null 2 &1; then exit 0 fi sleep 5 done echo "Repo never became ready" &2 exit 1 - name: Create Neon project if: inputs.type == 'app' id: neon run: | RESPONSE=$ curl -sf -X POST https://console.neon.tech/api/v2/projects \ -H "Authorization: Bearer ${{ secrets.NEON API KEY }}" \ -H "Content-Type: application/json" \ -d "{\"project\": {\"name\": \"$NAME\"}}" URI=$ echo "$RESPONSE" | jq -r '.connection uris 0 .connection uri' echo "::add-mask::$URI" echo "uri=$URI" "$GITHUB OUTPUT" - name: Set app secrets if: inputs.type == 'app' run: | gh secret set DATABASE URL -R "$OWNER/$NAME" -b "${{ steps.neon.outputs.uri }}" gh secret set BETTER AUTH SECRET -R "$OWNER/$NAME" -b "$ openssl rand -base64 32 " - name: Set common secrets run: | gh secret set CLOUDFLARE API TOKEN -R "$OWNER/$NAME" -b "${{ secrets.CLOUDFLARE API TOKEN }}" gh secret set CLOUDFLARE ACCOUNT ID -R "$OWNER/$NAME" -b "${{ secrets.CLOUDFLARE ACCOUNT ID }}" - name: Kick off Claude if: inputs.kickoff claude run: | gh issue create -R "$OWNER/$NAME" \ --title "Build: $NAME" \ --body "$IDEA @claude build this. Update the readme, the Cloudflare project name, the package.json name field, and any other relevant files. Make sure the project is ready to run locally and deploy to Cloudflare Pages." \ --label "claude" - name: Summary run: | { echo " 🎉 $NAME created" echo "- Repo: https://github.com/$OWNER/$NAME" echo "- Type: ${{ inputs.type }}" } "$GITHUB STEP SUMMARY" A few details worth calling out: /generate endpoint ::add-mask:: stops the database URI from ever appearing in the workflow logs before it's passed between steps. plant-tracker-3 was supposed to be.The last optional input is my favourite bit. If kickoff claude is ticked, the workflow opens an issue in the freshly created repo titled Build: