# Starter Skill

> Scaffolds a production-shaped Convex full-stack starter — a Bun-workspace monorepo with a shared Convex backend, a React 19 + Vite + TanStack Router + TanStack Query web app, and an Expo SDK 56 native app (expo-router NativeTabs + @expo/ui), wired together with Convex Auth on BOTH web and mobile. Use this skill whenever the user wants to start a new app, bootstrap a project, "spin up a starter / boilerplate", build a web + mobile app on Convex, set up a Bun monorepo with Convex, add TanStack Router/Query to Convex, create an Expo + Convex app, or asks for "web, backend and mobile" together — even if they don't name every technology. Prefer this over ad-hoc scaffolding any time Convex is the backend and the goal is a fresh, well-structured project.

- Skill: `dbjpanda/starter-skill` (Agent Skill, multi-file: 34 files)
- Install (CLI): `npx skillmds@latest add dbjpanda/starter-skill`
- Raw SKILL.md: https://api.skillmd.com/api/skills/dbjpanda/starter-skill/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: dbjpanda (https://skillmd.com/u/dbjpanda)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/dbjpanda/starter-skill

---


# starter-skill — Convex + TanStack (web) + Expo (native) monorepo

This skill scaffolds a **single Bun-workspace monorepo** with three packages that
share one Convex backend:

```
<project>/
├── packages/
│   ├── backend/   @app/backend  — Convex functions, schema, auth, components
│   ├── web/       @app/web      — React 19 + Vite + TanStack Router + Query + shadcn/base-ui
│   └── mobile/    @app/mobile   — Expo SDK 56 + expo-router NativeTabs + @expo/ui + NativeWind
└── package.json   (workspaces: ["packages/*"])
```

The web and mobile apps both import the **same generated API** from
`@app/backend/_generated/api` and authenticate against the **same Convex Auth
Password provider** — one deployment, two clients. That single-backend shape is
the whole point of the starter; keep it.

## Core philosophy — drive everything through CLIs, not hand-written boilerplate

The biggest failure mode when scaffolding is hand-typing files that a CLI would
generate correctly (and keep version-compatible). **Always reach for the tool
first.** The CLI pins compatible versions, writes the config the framework
actually expects, and won't drift from upstream defaults the way memorized
snippets do. This skill tells you *which* CLI for each step:

| Job | Use the CLI | Never |
| --- | --- | --- |
| Init workspace | `bun init` | hand-edit `package.json` deps |
| Add deps to a package | `cd packages/<x> && bun add <pkg>` (see note below) | `bun add … --filter <ws>` (misroutes to root in bun 1.3.x) |
| Create Convex backend | `bunx convex dev --once` | hand-create `_generated/` |
| Provision Convex Auth keys | `bunx @convex-dev/auth` | hand-write JWT keys |
| Create the Expo app | `bunx create-expo-app@latest` | hand-assemble RN config |
| Add native modules | `bunx expo install <pkg>` | `bun add` a raw RN version |
| Add web UI primitives | `bunx shadcn@latest add …` (⚠️ see web ref caveat) | retype components from memory |
| Add RN UI primitives | `bunx @react-native-reusables/cli@latest add …` | retype RN components |

When a step genuinely has no CLI (e.g. the `useAppQuery`/`useAppMutation`
wrappers, the SecureStore auth adapter, the NativeTabs layout), this skill ships
the exact, tested code in its reference files — copy from there.

## Start local: use a Convex local deployment first

Tell the user up front: **we start on a local, anonymous Convex deployment** —
no login, no team/project picker, instant feedback. Provision it with:

```bash
CONVEX_AGENT_MODE=anonymous bunx convex dev --once   # from packages/backend
```

This provisions a local backend bound to `127.0.0.1` under `~/.convex/`, writes
`CONVEX_DEPLOYMENT` + `CONVEX_URL` into `packages/backend/.env.local`, generates
`_generated/`, pushes the code, and **typechecks + validates the schema** — that
output is your feedback loop. Only move to a cloud deployment (`bunx convex dev`
interactively, then `bunx convex deploy`) when the user is ready to share or ship.
Explain this trade-off so the user understands why local-first is faster.

## Step 0 — Pull the supporting skills from the marketplace FIRST

This umbrella skill orchestrates; the deep per-tool knowledge lives in dedicated
skills that are almost certainly already installed in this environment (or one
install away). **Before scaffolding, check for them and use them** — they carry
upstream-current detail this skill deliberately doesn't duplicate:

- **Convex** — `convex-quickstart` (project creation, `convex dev --once`, env),
  `convex-setup-auth` (Convex Auth wiring), `convex-create-component`,
  `convex-migration-helper`, `convex-performance-audit`.
- **Expo** — `expo:building-native-ui` (NativeTabs, navigation, styling — the
  centerpiece for mobile), `expo:expo-ui-swift-ui`, `expo:expo-ui-jetpack-compose`
  (the `@expo/ui` native components), `expo:native-data-fetching`,
  `expo:expo-tailwind-setup` (NativeWind), `expo:expo-dev-client`,
  `expo:expo-deployment`, `expo:upgrading-expo`.
- **shadcn (web UI)** — `vercel:shadcn` or the shadcn CLI directly.

How to get them: run `/plugin` to browse/install from a marketplace, or invoke
the **`find-skills`** skill to discover and install anything missing (e.g. a Bun
or TanStack skill). If a skill above is listed as available, **invoke it via the
Skill tool at the relevant phase** rather than reproducing its content. Tell the
user which skills you're pulling and why — this transparency is part of the flow.

## Decide scope, then follow the build order

Ask the user what they want (most pick all three): **backend + web + mobile**,
**backend + web**, or **backend + mobile**. The backend is always first because
both clients depend on its generated types. Then follow this order — each phase
has a reference file with exact commands and tested code:

1. **Monorepo** — Bun workspaces, root scripts, shared tsconfig, `concurrently`
   dev orchestration. → `references/01-monorepo.md`
2. **Backend** — `convex dev --once` local, schema conventions, object-form
   functions, `getAuthUserId` + per-user `by_user` index, Convex components via
   `convex.config.ts`. → `references/02-backend-convex.md`
3. **Auth** — Convex Auth Password provider; `ConvexAuthProvider` on web,
   `ConvexAuthProvider` + `expo-secure-store` adapter on mobile; route gating on
   both. → `references/03-auth.md`
4. **Web app** — Vite + TanStack Router (file-based, codegen route tree) +
   TanStack Query bridged to Convex via `@convex-dev/react-query`; feature-based
   `src/features/<feature>/` layout. → `references/04-web-tanstack.md`
5. **Web UI** — shadcn components ported to **`@base-ui/react`** (the `render`
   prop, NOT Radix `asChild`), Tailwind v4 (`@theme inline`, no JS config),
   sidebar layout. → `references/05-web-components-baseui.md`
6. **Mobile app** — Expo SDK 56, expo-router with `NativeTabs`
   (`expo-router/unstable-native-tabs`), `@expo/ui` for native surfaces,
   NativeWind, MMKV-persisted Convex+TanStack. → `references/06-mobile-expo.md`
7. **CLI cheatsheet & golden rules** — every command in one place, plus the
   gotchas that bite. → `references/07-cli-and-skills.md`

Read each reference at the start of its phase. Don't pre-load them all — pull the
one you're working in.

## Copy the canonical files from `examples/`

For the load-bearing files that have no CLI and are easy to get subtly wrong,
**copy from `examples/` and adjust names** rather than retyping from the
reference prose — it's faster and avoids drift. `examples/` is a **literal mirror
of the target repo** (top-level files → repo root; everything under `packages/`
→ a workspace), so the move is "copy the file to the matching path, rename
`my-app`/`@app`/`tasks`." The highest-value files to copy verbatim:

- `examples/packages/web/src/lib/convex.tsx` and
  `examples/packages/mobile/src/lib/convex.tsx` — the TanStack↔Convex bridge +
  `useApp*` wrappers (web: IndexedDB; mobile: MMKV + SecureStore auth adapter).
- `examples/packages/web/src/components/ui/button.tsx` — the Base UI `render`
  pattern exemplar.
- `examples/packages/mobile/src/routes/(app)/(tabs)/_layout.tsx` — the
  `NativeTabs` layout.
- `examples/packages/mobile/src/features/theme/theme-toggle*.tsx` — the `@expo/ui`
  platform split (`.ios.native.tsx` / `.android.native.tsx` + dispatcher).
- `examples/packages/backend/src/*` — `auth.ts`, `auth.config.ts`, `http.ts`,
  `schema.ts`, `convex.config.ts`, and the `features/tasks/` query/mutation
  conventions (plus `examples/packages/backend/convex.json`).

These files were **type-checked end-to-end** (Convex local `dev --once` for the
backend; `tsc` against real Expo SDK 56 + Base UI + TanStack packages for web and
mobile). They're still a *slice*, not a whole app — scaffold each package with its
CLI first, then drop the files in. See `examples/README.md` for the full map.

## Non-negotiable conventions (the "house style" of this stack)

These come from the production reference apps this starter is modeled on. They
matter enough to state once, up front; the references explain each in context.

1. **One shared backend.** Both apps import `import { api } from "@app/backend/_generated/api"`. The backend package exposes `_generated/*` through its
   `exports` map; `_generated/` itself is **gitignored** (it's codegen). Never
   edit `_generated/` by hand.
2. **Installing deps:** run `bun install` (no args) from the repo ROOT — never
   `cd packages/web && bun install`, which creates a stray local `node_modules`.
   But to **add** a dep to a specific workspace, `cd packages/<x> && bun add <pkg>`:
   in bun 1.3.x, `bun add … --filter <ws>` either 404s or silently installs into
   the root `package.json` instead of the target (verified). For the Expo package,
   prefer `bunx expo install <pkg>` so native versions stay SDK-compatible. Also
   quote workspace specs in zsh: `bun add '@app/backend@workspace:*'` (the `*`
   globs otherwise).
3. **Every public Convex function is object-form and calls `getAuthUserId(ctx)`**
   first. Queries return empty (`[]`/`null`) when unauthenticated; mutations
   throw. Every user-owned table has a `userId: v.id("users")` field and a
   `by_user` index, and you **query through indexes** (`.withIndex(...)`), never
   `.filter()` on large tables.
4. **Data access goes through typed wrappers**, not raw hooks: `useAppQuery` /
   `useAppMutation` / `useAppAction` (defined identically on web and mobile in
   `lib/convex.tsx`). This keeps the `"skip"` pattern, caching, and the
   TanStack↔Convex bridge consistent.
5. **Web UI uses `@base-ui/react`, composed with `render={<X/>}`** — not Radix's
   `asChild`. Do NOT blindly `shadcn add` into the web app; the default registry
   emits Radix components that break every `render={...}` call site (see the web
   UI reference for the safe workflow).
6. **Mobile tabs are real native tabs** (`NativeTabs`), with a `Platform.OS === "web"`
   fallback to JS `<Tabs>`. Icons use dual props: `sf="house.fill"` (iOS SF
   Symbol) + `md="home"` (Android Material). This is what gives the app its
   native feel — don't replace it with a JS tab bar on native.

## Verify as you go

After each phase, run the relevant check so a broken step surfaces immediately:

- Backend: `CONVEX_AGENT_MODE=anonymous bunx convex dev --once` (re-run after
  schema/function edits — it re-typechecks and re-validates).
- Web: `bun run --filter @app/web typecheck` and `bun run dev:web`.
- Mobile: `bun run --filter @app/mobile typecheck`, then `bunx expo start`.
- Whole repo: `bun run typecheck` from root.

Finish by confirming a real auth round-trip on each client (sign up → see an
authenticated query return data → sign out), because a project can build cleanly
while auth is still misconfigured.

