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 # 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/uinative components),expo:native-data-fetching,expo:expo-tailwind-setup(NativeWind),expo:expo-dev-client,expo:expo-deployment,expo:upgrading-expo. - shadcn (web UI) —
vercel:shadcnor 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,
concurrentlydev orchestration. →references/01-monorepo.md - Backend —
convex dev --oncelocal, schema conventions, object-form functions,getAuthUserId+ per-userby_userindex, Convex components viaconvex.config.ts. →references/02-backend-convex.md - Auth — Convex Auth Password provider;
ConvexAuthProvideron web,ConvexAuthProvider+expo-secure-storeadapter 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-basedsrc/features/<feature>/layout. →references/04-web-tanstack.md - Web UI — shadcn components ported to
@base-ui/react(therenderprop, NOT RadixasChild), 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/uifor 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.tsxandexamples/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 UIrenderpattern exemplar.examples/packages/mobile/src/routes/(app)/(tabs)/_layout.tsx— theNativeTabslayout.examples/packages/mobile/src/features/theme/theme-toggle*.tsx— the@expo/uiplatform 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 thefeatures/tasks/query/mutation conventions (plusexamples/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 itsexportsmap;_generated/itself is gitignored (it's codegen). Never edit_generated/by hand. - Installing deps: run
bun install(no args) from the repo ROOT — nevercd packages/web && bun install, which creates a stray localnode_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 rootpackage.jsoninstead of the target (verified). For the Expo package, preferbunx 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 auserId: v.id("users")field and aby_userindex, 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 inlib/convex.tsx). This keeps the"skip"pattern, caching, and the TanStack↔Convex bridge consistent. - Web UI uses
@base-ui/react, composed withrender={<X/>}— not Radix'sasChild. Do NOT blindlyshadcn addinto the web app; the default registry emits Radix components that break everyrender={...}call site (see the web UI reference for the safe workflow). - Mobile tabs are real native tabs (
NativeTabs), with aPlatform.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 typecheckandbun run dev:web. - Mobile:
bun run --filter @app/mobile typecheck, thenbunx expo start. - Whole repo:
bun run typecheckfrom 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.