| name | starter-skill |
| description | 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. |
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:
CONVEX_AGENT_MODE=anonymous bunx convex dev --once
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:
- Monorepo — Bun workspaces, root scripts, shared tsconfig,
concurrently
dev orchestration. → references/01-monorepo.md
- 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
- Auth — Convex Auth Password provider;
ConvexAuthProvider on web,
ConvexAuthProvider + expo-secure-store adapter on mobile; route gating on
both. → references/03-auth.md
- 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
- 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
- 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
- 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.
- 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.
- 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).
- 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.
- 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.
- 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).
- 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.