create-software
Create and deploy a public web app on Cloudflare with TanStack Start, shadcn/ui, Convex, Better Auth, Lumail and optional Stripe, with an admin area, API and CLI so AI controls it. Use when asked to b
SKILL.mdskills/create-software/SKILL.md
GitHubBuild a working web app on the user's own accounts and put it live on their Cloudflare domain. Do every step yourself with the CLIs and only ask for what needs a human (a browser login, a device code, a payment, a secret key). Talk to the user in their language, short messages, no jargon. For a private tool only the user and their agent use, switch to create-internal-app; for a page with no sign-in, switch to create-landing.
If you got this skill from a link instead of a local folder, install nothing else from the repo: download a file it links only when a step needs it, with curl -fsSL https://raw.githubusercontent.com/Melvynx/ai-maxxing-skills/main/skills/create-software/<path> -o <scratch>/<file> (for example references/fast-app.md), and read another skill it names only when you reach it, from https://raw.githubusercontent.com/Melvynx/ai-maxxing-skills/main/skills/<skill-name>/SKILL.md.
Keep this stack unless the user asks to swap a layer:
- App, routing, server functions: TanStack Start (React, TypeScript, Vite).
- UI: shadcn/ui on Base UI + Tailwind CSS v4,
lucide-reacticons,sonnertoasts. - Database, backend, files, crons: Convex for everything, no other database or API server.
- Auth: Better Auth through
@convex-dev/better-auth, email OTP by default. - Marketing and login emails: Lumail through
@lumail/convex. - Payments, only if the app sells: Stripe Checkout with the
stripe-checkoutskill. - Video: Tella embeds from the URL the user pastes.
- Hosting, domain, DNS: Cloudflare Workers with
@cloudflare/vite-plugin, deployed withcf, then automatically by a GitHub Action on every push tomain. - Code: Git and one private GitHub repo.
- Control by AI: an admin area at
/admin, an API at/api/v1with API keys, and a CLI named after the app with its<app>-cliskill.
Every app is built so the user's AI can run it, not only the user's mouse. Always build, and always explain to the user in one plain sentence, the admin part: an /admin area for the owner, a clean API reached with API keys, and a CLI plus skill so their agent controls the whole app from the terminal, like Lumail (admin pages, /api/admin/*, admin keys, the lumail CLI). Anything the owner can do in /admin, the API and the CLI can do too. This is never optional and never cut from v1. Details in references/admin-api-cli.md.
Reach for the TanStack library before any other package for client jobs: @tanstack/react-query + @convex-dev/react-query (convexQuery(api.x.y, args)) for fetching and cache, @tanstack/react-table for lists, sorting, filters and pagination, @tanstack/react-virtual for long lists, @tanstack/react-form + zod for forms, @tanstack/markdown to render markdown, @tanstack/react-hotkeys for shortcuts (cmd+k, j/k, esc), @tanstack/react-pacer to debounce search and throttle saves, @tanstack/ai-react for AI chat or generation. Check each package's current API (npm view <pkg> version, then its README or find-docs) instead of writing it from memory.
Check access and fix what fails with the matching setup skill (
setup-github,setup-cloudflare,setup-convex,setup-lumail,setup-nodewhennpxis missing) before writing code. When a login needs the browser, give the exact link or code and wait for "done".git --version && gh auth status # GitHub cf --version && cf auth whoami # Cloudflare (npm i -g cf, then cf auth login) npx convex --version # Convex (npx convex login when building) npx lumail@latest auth test # Lumail (npx lumail@latest setup --authenticate)Decide first, with the user: does this app need organizations (multi-tenant)? Team members, shared data, roles, invitations, billing per company or API keys per workspace mean yes; a single-user tool means no. Yes changes every table, query and URL from day 1, so follow references/organizations.md. Then run the
grill-meskill and build only once the user confirms the brief; skip it only if a brief was already confirmed in this conversation. Defaults: landing at/, app at/app(/:org/...with organizations), admin at/admin, English UI, OTP sign-in, the user is the only admin. When you show the brief, tell the user the app comes with an admin area, an API and a CLI so their AI can manage everything, and ask what they will want their AI to do in it (for example "list today's sales", "refund an order", "ban a user").Always write the confirmed brief to
SPEC.mdat the app root, before any code, so every later conversation starts from it:- The brief: goal, who uses it, the user story screen by screen, every screen, the data objects and their fields, what is out of v1, and what "done" means for the user.
- An
## Admin, API, CLIsection: every admin page, every API route (GET /api/v1/admin/users...) and the CLI command that matches it, from what the user wants their AI to do. - A
## Taskschecklist, very precise: one small change per box, named by its route, screen, field or Convex function, each with aCheck:line saying how to prove it works on the real app. Write- [ ] /app/lessons lists lessons in a table, newest first, 20 per page. Check: with 25 lessons, page 2 shows 5, never- [ ] lessons page. - Go through references/product-rules.md (URLs, access, search, composer, tags, agents, webhooks, email rendering, deploy) and add a task for every rule that applies.
- Add an
AGENTS.mdat the app root that says: readSPEC.mdbefore any change; for a new feature or fix, add its tasks toSPEC.mdfirst, then build; after building or changing any screen, runbetter; never stop until every task matches.
Scaffold TanStack Start + Tailwind +
npx shadcn@latest init(Base UI flavor) and add components withnpx shadcn@latest add.git init, thengh repo create <name> --private --source . --push.Run
npx convex dev --onceto create the project and.env.local. Write oneschema.tswith an index for every query. Every public function checks the signed-in user and ownership server-side, anything only the server calls isinternal*, and reads are bounded with.take(n)or.paginate(). With organizations, every table hasorgIdand every function checks membership and role. See theconvexskill.Wire Better Auth: register both components in
convex/convex.config.ts(app.use(betterAuth),app.use(lumail)),createAuthwithauthComponent.adapter(ctx), theemailOTPplugin and theconvex({ authConfig })plugin, routes viaauthComponent.registerRoutes(http, createAuth),BETTER_AUTH_SECRETandSITE_URLas Convex env vars. Protect/appin the routebeforeLoadand again in every Convex function.Wire Lumail in one
convex/email.tsthat holds every call. CreateLUMAIL_API_KEYyourself withlumail tokens --help; it never reaches the browser.Login codes:
sendVerificationOTPschedules an internal mutation that callslumail.sendEmail(ctx, { ..., transactional: true }). Without a key, log the code to the Convex console so local dev still works.Signups: in Better Auth
databaseHooks.user.create.after, upsert the person withlumail.upsertSubscriber(ctx, { email, tags: ["<app-slug>-user"], triggerWorkflows: true }).Skip double opt-in for anyone who signs in or buys, since the OTP or the payment proves they own the email: they go straight to "subscribed" and tag workflows fire right away. The component cannot pass that flag, so also call the API from an internal action:
await fetch("https://lumail.io/api/v1/subscribers", { method: "POST", headers: { Authorization: `Bearer ${process.env.LUMAIL_API_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ email, skipDoubleOptIn: true }), });Keep double opt-in only for a cold form (newsletter or waitlist where the email is not verified).
Create the sending domain with
lumail domain --helpand add the DNS records it returns in Cloudflare yourself (unproxied). More in thelumailskill.
Put data on screen with
useSuspenseQuery(convexQuery(...))so the UI updates live, lists in TanStack Table, forms in TanStack Form calling Convex mutations, toasts on success and error. Build every screen, admin included, to references/design-system-rules.md. After building any screen, run thebetterskill on it, in light and dark, and finish its screenshot loop before moving on.Build the admin area, API keys, API and CLI with references/admin-api-cli.md: each action is one internal Convex function called by the admin page and by the API, keys stored hashed and shown once,
/api/v1on the app domain,cli/<app>.mjsinstalled withnpm install -g ./cli, then the<app>-cliskill in~/.agents/skills. Every new feature later gets its admin view, route and CLI command in the same change.Make it fast on every page with references/fast-app.md:
defaultPreload: "intent", a 30 s query cache, a loader on detail pages, a custompendingComponentskeleton on every route that matches the real layout, a loading state on every async button, indexed and bounded Convex reads. An app without these is not done.If the app sells, start with a fake checkout until the user gives Stripe test keys, then real Checkout and a signed webhook in
convex/http.tsthat grants access. Test card4242 4242 4242 4242, live keys only once the test flow works. See thestripe-checkoutskill.Add a clean error page, a 404 page, a favicon and page titles.
Deploy on Cloudflare.
wrangler.jsoncgetsmain: "@tanstack/react-start/server-entry",compatibility_flags: ["nodejs_compat"],SITE_URLinvarsand the custom domain inrouteswithcustom_domain: true. Runnpx convex deploy, set prod env vars withnpx convex env set --prod, and commit the prodVITE_CONVEX_URL/VITE_CONVEX_SITE_URLin.env.production. Build and deploy once withcf(cf build,cf deploy, orcf cli search "deploy worker"), never by hand in the dashboard; see thecloudflareskill. If the domain is not in the user's Cloudflare account, tell them to move the name servers or buy it there. Then always set up auto deploy, by default, without asking:.github/workflows/deploy.ymldeploys Convex then the Worker on every push tomain, with theCLOUDFLARE_API_TOKENandCONVEX_DEPLOY_KEYGitHub secrets. Follow references/auto-deploy.md. From then on every redeploy is a push, watched withgh run watch.Open the live URL and walk the real flows: landing, sign in with a real OTP email, the main action, a list with real rows, a test purchase if the app sells, a keyboard shortcut, sign out, the 404 page. Then the admin: create an admin key in
/admin/api-keys,<app> login,<app> whoami, list a resource with--jsonand compare it with the admin page, make one write through the CLI and see it in Activity, and check that no key or a revoked key gets refused. Check the person shows up in Lumail as subscribed with the right tag, and that a push tomainturns the GitHub Action green and the change shows on the live URL. Fix every error you see, then runaudit-safetyon auth, data access, API keys, admin routes and payments.Re-read
SPEC.mdand run every task'sCheck:on the live URL. Tick a box only once its check passes there, never because the code is written. Do not stop and do not reply while one box is unticked or the live app differs from the spec: fix it, push, wait for the Action, check again, and repeat until everything matches. A task blocked only by a human step (a login, a key, a payment) stays unticked with the reason written next to it; finish all the others first.
Secrets go only in .env.local, npx convex env set, Cloudflare secrets or GitHub secrets (gh secret set), never in a committed file, a log or the chat. Reply with what is live, the URL, the admin URL, that every push to main now goes live on its own, the CLI command and the /<app>-cli skill their agent can now use (with one sentence to try, like "ask your AI: how many people signed up this week?"), the SPEC.md tasks all ticked, and only the human steps left to do.