| name | hedgehog-bootstrap |
| description | Use once, at the start of a new Hedgehog project, to land the core workspace and scaffold whichever add-ons (Auth, Queue, Mobile) planning intake turned on (TODO.md's Add-ons block). Runs when the `bootstrap` agent runs, which `planner` invokes automatically after Confirm & Lock. Scoped to project scaffolding; per-module work runs through the `hedgehog-loop` skill, one step at a time. |
Hedgehog Bootstrap
Scaffolds a Hedgehog project's Bootstrap phase: the always-on core, plus
whichever named add-ons (Auth, Queue, Mobile) planning intake's scope
boundary (planner, running BMAD-METHOD's planning shelf then mining it
— see that agent) actually calls for. This is Phase 2 (Scaffold) of the
overall bootstrap sequence — Phase 0 (BMAD elicitation) and Phase 1
(mining into TODO.md) already closed by the time this skill runs. After
this closes, hedgehog-loop takes over per module, one step at a time.
This skill touches no domain modules — no schema, no contract, nothing
under libs/<module>/. That's Phase A, started fresh after Bootstrap
closes.
Core lands via hedgehog-bootstrap-core, run first, unconditionally.
That skill copies a pre-built, pre-verified workspace (Nx, enforcement
config, packages/db, apps/api, apps/web) rather than generating it
live — core is identical on every project, so it's built once upstream
and copied, not re-derived per project. This skill covers only what's
still genuinely project-specific: whether Auth, Queue, and Mobile are on,
and — if so — scaffolding them.
Run the nx g commands below via nrwl's nx-generate skill — it dry-runs
and verifies generator flags against the installed Nx version. Run the
nx run / nx affected commands via
nx-run-tasks the same way. The commands below are the spec; those
skills execute it correctly.
The Stack: core + add-ons
Hedgehog has one non-negotiable core — applied to every project that
uses Hedgehog at all, regardless of size — plus a small set of named
add-ons, each scaffolded only when planning intake's scope boundary
(planner) actually calls for it. The core is not "the small version of
the stack"; it's the fixed floor, landed by hedgehog-bootstrap-core.
Add-ons are not "extra polish"; each is standing infra with a real
ongoing cost (a service to run, a secret to manage, a seam to keep
idempotent) that a project without the matching need shouldn't carry.
Core (every project, no exceptions — see hedgehog-bootstrap-core)
| Layer | Choice |
|---|
| Monorepo | Nx |
| Package manager | pnpm |
| Backend framework | NestJS |
| ORM | Drizzle (+ drizzle-zod) |
| Database | PostgreSQL |
| Local infra | Docker Compose (Postgres) |
| Platform | Railway |
| API contract | ts-rest |
| Validation | Zod |
| Data fetching / hooks | TanStack Query |
| Web UI | Next.js (frontend only) + ShadCN + Tailwind |
| Logging | Pino (nestjs-pino) |
| Lint / format | ESLint (flat config) + Prettier (+ prettier-plugin-tailwindcss, scoped to apps/web) |
| Testing | Vitest (unit/integration) + Playwright (web e2e) |
| Commits | Conventional Commits + commitlint + lefthook |
| Observability | Sentry |
Constraint-contingent substitutions: Prisma for Drizzle when the team
isn't SQL-comfortable; cloud + Pulumi/SST for Railway when full
declarative IaC is a hard requirement; tRPC for ts-rest when the client is
committed TypeScript-only. A substitution here means src/golden-core
itself needs regenerating against the substitute before this project's
Bootstrap runs — not a per-project hand-edit after landing core.
Add-ons (scaffolded only when planning intake calls for them)
Each row is independent — on or off per project, decided at planning
intake's Confirm & Lock (planner) and recorded in TODO.md's
## Add-ons block. Turning one on inserts its Bootstrap step(s) into the
sequence below; turning it off means that step is skipped entirely, not
stubbed or partially wired.
| Add-on | Trigger (from planning intake scope) | Adds |
|---|
| Auth | The product has accounts, logins, or per-user data | Better Auth (+ @thallesp/nestjs-better-auth, Drizzle adapter), packages/auth, a global auth guard on apps/api, BETTER_AUTH_SECRET in the env schema |
| Queue | At least one operation is genuinely long-running, retried, or fanned out | BullMQ + Redis, apps/worker, a Queue port/adapter seam, REDIS_URL in the env schema, Redis in docker-compose.yml |
| Mobile | Mobile is explicitly in scope | Expo + React Native Reusables + NativeWind, apps/mobile |
A project with none of these on is still a full Hedgehog project — Nx,
NestJS, Postgres, Docker, ts-rest, the phase discipline, and every gate
still apply (all landed by hedgehog-bootstrap-core). What's cut is
infra with no consumer, not the discipline itself.
If a project's whole description has no persistent domain data and no
real lifecycle to model at all (a static marketing page, a one-off
script, a slide deck) — not "small," but literally no state to carry
across a schema/contract/service — Hedgehog doesn't apply. planner
checks for this before running BMAD's planning shelf (see that agent's
opening check) and says so rather than forcing the discipline onto
something with no domain module in it.
Monorepo layout
apps/
web (Next.js — UI only) core, landed by hedgehog-bootstrap-core
mobile (Expo — only if the Mobile add-on is on)
api (NestJS — owns all domain logic + DB access) core, landed by hedgehog-bootstrap-core
worker (BullMQ consumers — only if the Queue add-on is on)
packages/
db (Drizzle schema + client) core, landed by hedgehog-bootstrap-core
contracts (ts-rest + Zod contracts)
hooks (TanStack Query — shared web + mobile)
jobs (typed job registry / queue definitions — only if Queue is on)
auth (Better Auth config — only if Auth is on)
config (locked ESLint/Prettier/tsconfig/env schema) core, landed by hedgehog-bootstrap-core
shared (cross-cutting types + utils)
docs/
design (<module>.md per module — `ux-planner` agent output)
packages/auth and packages/jobs are infra, built once, here (when
their add-on is on) — not touched again per module. docs/design fills in
per module during Phase B; nothing to scaffold here beyond the empty
directory.
Queue add-on: seam in, usage deferred
When the Queue add-on is on, it goes in as a day-one standing default:
Redis provisioned on Railway, a worker app in the monorepo, a Queue
port with a BullMQ adapter (same pattern as repositories) — but usage
stays last-responsible-moment even then: an operation goes async only
when it genuinely needs to (long-running work, retries, fan-out).
Services don't know how their results are returned — the enqueue-vs-await
decision lives at the application/controller layer. Workers are
idempotent (at-least-once delivery). A project where nothing meets that
bar doesn't get the seam at all — see the Add-ons table above.
Before running
Confirm planning intake already happened — a scope boundary and domain
vocabulary should exist (planner produces these from BMAD's planning
shelf), and TODO.md should carry an explicit ## Add-ons block
recording which add-ons (Auth, Queue, Mobile) are on for this project. No
scope boundary yet, or a TODO.md with no ## Add-ons block: stop and
point to planner rather than guessing which add-ons apply.
Run hedgehog-bootstrap-core first, unconditionally, if it hasn't
already landed core (check TODO.md's Bootstrap section, or nx.json
at the repo root). That skill has its own re-run guard and Docker check
— don't duplicate those here.
Steps (run in sequence, one commit per step that actually runs)
1. packages/auth — Better Auth config (Auth add-on only)
Skip this step entirely if Auth isn't on for this project (check
TODO.md's ## Add-ons block) — don't scaffold a credential store with
no login anywhere in scope. If skipped, check its
TODO.md line off as skipped-and-confirmed (per the bootstrap agent's
handling of conditional steps), same treatment as an out-of-scope
apps/mobile.
npx nx g @nx/js:lib packages/auth --bundler=none --unitTestRunner=vitest
pnpm add better-auth @thallesp/nestjs-better-auth
Configure the Drizzle adapter against packages/db. Add
BETTER_AUTH_SECRET: z.string().min(32) to packages/config/env.schema.ts
now (it doesn't exist in the core schema hedgehog-bootstrap-core
landed), and add a matching BETTER_AUTH_SECRET= line with a generated
value to the root .env.example — a schema entry with no .env.example
line reproduces the exact loadEnv() crash-on-boot that
hedgehog-bootstrap-core's DATABASE_URL entry exists to prevent, just
for this var instead. Tag: scope:auth, type:adapter.
Also wire the global auth guard on apps/api: pnpm add @thallesp/nestjs-better-auth there too and register the guard
(secure-by-default) against packages/auth. apps/api's
depConstraints entry needs scope:auth added to its allowed
dependencies now — the one deliberate exception to "api reaches things
only through ports," since auth is cross-cutting infra, not a domain
module.
Commit: feat(auth): better auth config + global guard
2. apps/worker — BullMQ seam (Redis, no consumers yet) (Queue add-on only)
Skip this step entirely if Queue isn't on for this project (check
TODO.md's ## Add-ons block) — no operation in scope is long-running,
retried, or fanned out, so there's nothing for a queue to seam in for. If
skipped, check its TODO.md line off as
skipped-and-confirmed, same treatment as an out-of-scope apps/mobile.
npx nx g @nx/node:app apps/worker
pnpm add bullmq ioredis
Add a redis service to the root docker-compose.yml
hedgehog-bootstrap-core landed (Postgres-only until now) and
REDIS_URL: z.string().url() to packages/config/env.schema.ts (it
doesn't exist in the core schema), plus a matching REDIS_URL= line in
the root .env.example pointing at that same docker-compose.yml
service (e.g. redis://localhost:6379) — same reasoning as
BETTER_AUTH_SECRET above, a schema entry without a .env.example line
just moves the loadEnv() crash-on-boot to this var instead of fixing
the class of bug:
redis:
image: redis:7-alpine
ports:
- '6379:6379'
volumes:
- redis-data:/data
(and add redis-data: under the top-level volumes: key.)
Provision the Redis connection and a Queue port shape (port + BullMQ
adapter, same pattern repositories use later) with no consumers — usage
is deferred (see Queue add-on, above). Call loadEnv() at the top of
apps/worker/src/main.ts. Tag: scope:worker.
Add a depConstraints entry for scope:worker
(onlyDependOnLibsWithTags: ['type:port', 'type:util', 'scope:shared'])
— worker only reaches domain logic through ports, same as api.
Commit: feat(worker): bullmq seam, no consumers
3. apps/mobile — Expo shell (Mobile add-on only)
Skip this step entirely if Mobile isn't on for this project (check
TODO.md's ## Add-ons block) — don't scaffold speculative infra. If
skipped, check its TODO.md line off as skipped-and-confirmed, not left
dangling for a future run to wonder about — same pattern as Auth (step 1)
and Queue (step 2) when their add-on is off.
npx nx g @nx/expo:app apps/mobile
pnpm add react-native-reusables nativewind
Configure NativeWind's theme (tailwind.config.js colors, light/dark) to
match apps/web's base theme (landed by hedgehog-bootstrap-core) — one
visual identity across platforms, set once here rather than drifting
per-screen. Tag: scope:mobile.
Add a depConstraints entry for scope:mobile
(onlyDependOnLibsWithTags: ['scope:contracts', 'scope:hooks', 'scope:shared']) — mobile never imports db or api internals, only
contracts + hooks, same as web.
Commit: feat(mobile): expo shell + base theme
Step order
A project with every add-on off runs zero steps from this file; a
project with all three on runs three, in this order (Auth before Queue
before Mobile, since Auth's guard should exist before other infra
touches apps/api, and Queue/Mobile have no ordering dependency on each
other).
Locked format/lint config
One shared config, extended everywhere — landed by
hedgehog-bootstrap-core, referenced (not re-created) by every add-on
step above:
packages/config/eslint-base.js — flat config, extended by every
app/lib.
packages/config/prettier.js — the shared base, without
prettier-plugin-tailwindcss (that's apps/web's own config, already
wired by hedgehog-bootstrap-core).
A per-app override request signals to fix the base config at the source.
After Bootstrap
Update TODO.md: check off every add-on line now built or explicitly
skipped (core's four lines are already checked by
hedgehog-bootstrap-core). Leave Phase A/B sections as-is (per-module,
filled in by planner during planning intake or when new scope enters play).
Hand off to hedgehog-loop — from here, every domain module goes
through Phase A steps 1–5(a) one at a time, gated by lefthook, each its
own commit.
Constraints
- Run
hedgehog-bootstrap-core first, unconditionally, before any step
in this file — never scaffold an add-on against a core that hasn't
landed and verified clean.
- Add-on steps (Auth, Queue, Mobile) run only if
TODO.md's ## Add-ons
block (written by planner at planning intake) turns that add-on on —
check off its TODO.md line as skipped-and-confirmed otherwise, don't
leave it dangling.
- Don't add domain schema, contracts, or any
libs/<module>/* content —
that's Phase A, started after Bootstrap, one module at a time.
- Don't deviate from the package/library choices above, for whichever
steps actually run. If a generator or package name changed upstream
since this was written, verify against current docs before running the
command — don't substitute a different library. Skipping an add-on
step whose trigger genuinely isn't in scope is not a deviation; adding
a library the stack doesn't call for, or dropping one it does, is.
- Each step that runs is its own commit, in order — same unit-of-work
discipline as every other step in the discipline, even though this is
infra rather than a domain module.
- Never substitute a natively-installed Postgres or Redis, even to match
a contributor's existing local setup — see
hedgehog-bootstrap-core's
Local infra: Docker, always.