Skip to content

For agents: an index of these docs is at /docs/llms.txt, and this page as markdown is at /docs/create-signet-app.md.

create-signet-app

Scaffold a new app or API.

Scaffold a Vite + React 19 + TypeScript + Tailwind v4 app wired to @dapplelight/signet-tokens, @dapplelight/signet-ui and @dapplelight/signet-lib.

Terminal window
bunx create-signet-app my-app --theme cinematic
cd my-app && bun install && bun dev

--theme takes editorial or cinematic (default editorial).

Why

The audit found this exact setup rebuilt by hand 13 times across 23 products, and the five-package eslint config copied as a block into exactly 8 of 23 repos — the identical count for all five packages is the signature of copy-paste, not configuration.

What you get

  • dev / build / preview / lint / typecheck / test — the script names already used across the existing repos, so muscle memory carries over
  • TanStack Router, file-based, with / and a placeholder /login. CONVENTIONS.md does not pick a router; this follows app-a. Routes live in src/routes; the Vite plugin regenerates src/routeTree.gen.ts on every dev and build and splits each route into its own chunk. The generated file is committed so tsc passes before Vite has run, and Biome ignores it. /login renders a card and nothing else — wire it to your own auth; the template does not choose a provider
  • sonner for toasts (the CONVENTIONS.md choice): <Toaster /> in main.tsx, from src/components/toaster.tsx, which follows light/dark and draws on the theme's popover tokens. Call toast() from "sonner"
  • src/router.test.ts (bun test), which checks / and /login each resolve to their route
  • .github/workflows/ci.yml: bun install --frozen-lockfile, lint, typecheck, test, build. Commit bun.lock
  • tsconfig.json with app/node project references, strict, path alias @/*
  • Biome for lint and format (biome check .), replacing the copied eslint config (see below)
  • src/app.css importing the chosen theme
  • main.tsx with ThemeProvider, a QueryClient on Signet defaults, the router, and a single-redirect 401 handler. The redirect is a full page load to /login (built with router.buildLocation, so deleting the route fails typecheck): it drops the old session's cached queries and re-arms the one-shot handler. In production the host must fall back to index.html for unknown paths
  • index.html with a blocking script that applies the stored theme before first paint — React mounts too late, and without it there is a visible flash
  • src/lib/api.ts with a configured createApiClient

--template api: a Worker API

bun create-signet-app my-api --template api scaffolds a Hono Worker with:

  • requestId, errorHandler and parseBody from @dapplelight/signet-server, so every error is the envelope with a code and a request id
  • exact per-IP rate limiting on /api/*: the RateLimitCounter Durable Object is exported from the entry, bound as RATE_LIMIT and created by the first migration in wrangler.jsonc — nothing to set up by hand
  • src/env.ts: a zod schema through createEnv from @dapplelight/signet-server/env, validated lazily on first read (Cloudflare runs module scope with no secrets during deploy). initRuntimeEnv hands it the bindings on each request, and env.APP_ENV decides whether 500s hide their message — failing closed (hidden) if the env does not validate. Add a var to the schema, then read env.KEY
  • test/env.test.ts (bun test) and .github/workflows/ci.yml: lint, typecheck, test and wrangler deploy --dry-run

Append migration tags; never edit one that has been deployed. A Worker entry may export only handlers and classes (workerd: "Incorrect type for map entry").

Its test scaffolds outside the monorepo, installs, typechecks, lints, bundles with wrangler, then runs it under wrangler dev and checks the envelope and that exactly 60 requests pass before a 429. Removing the RateLimitCounter export fails it: wrangler refuses to bundle a Worker whose Durable Object is not exported.

Two bugs this fixes in the copied setup

import.meta.env did not typecheck. The copied tsconfig.app.json omits "types": ["vite/client"], so tsc -b fails on any use of an env var.

The copied eslint config is broken against current versions, which is one reason the scaffold ships Biome instead. The config in the existing repos uses reactHooks.configs.flat.recommended, which was removed in eslint-plugin-react-hooks 5.2. Installing fresh gives:

TypeError: Cannot read properties of undefined (reading 'recommended')

The template uses reactHooks.configs["recommended-latest"]. Existing repos only still work because their lockfiles pin older versions — copying their config into anything new produces a crash.

Test

Both templates ship _gitignore and _github, renamed to .gitignore and .github on scaffold: npm strips .gitignore from a package and may drop dot-directories.

bun run test scaffolds a real app, installs, typechecks, runs its tests, builds and lints it, checks the build emitted a login-*.js route chunk and that .github/workflows/ci.yml exists and only calls scripts that exist, then asserts the chosen theme reached the production CSS bundle (.text-sm{font-size:1rem} and .rounded-lg{border-radius:.1875rem} for cinematic, not editorial's 0.8125rem / .75rem). Both bugs above were caught by that test on its first run.

Scaffolded apps work anywhere

Dependencies are real ranges (^0.1.2) against the published packages, so a scaffolded app installs from npm and runs outside this repo. Inside the monorepo bun still links the local copies, because the range matches the workspace version — so development keeps using source with no extra step.

The packages are public, so no npm token is needed.