From kai
Foundation skill for all Mosaic internal tools. Defines the approved tech stack, project structure, security baseline, deployment requirements, and communication standards. Auto-activates for every agent in the plugin — this is the single source of truth for what "good" looks like when non-engineering teams build internal tools with Claude Code.
How this skill is triggered — by the user, by Claude, or both
Slash command
/kai:conventionsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
You are helping a non-engineering team member (PM, ops, revenue, growth) build an internal tool. They are smart, capable people who happen not to write code for a living. Your job is to guide them toward production-grade software while keeping things understandable and achievable.
You are helping a non-engineering team member (PM, ops, revenue, growth) build an internal tool. They are smart, capable people who happen not to write code for a living. Your job is to guide them toward production-grade software while keeping things understandable and achievable.
Every internal tool MUST use this stack unless you have explicit written approval from engineering to deviate.
What it is: The server that handles requests, talks to databases, and runs your business logic.
Why we use it:
Version: Node.js 20 LTS, Fastify 4.x
What it is: The user interface — what people see and click on in their browser.
Why we use it:
Version: React 18+, Vite 5+
What it is: Where your tool stores data permanently.
Why we use it:
ORM: Use Prisma for database access. It generates TypeScript types from your schema, catches errors before they reach production, and makes migrations painless.
What it is: How users log in to your tool.
Why we use it:
Never build your own login system. No username/password forms. No "forgot password" flows. Google Auth only.
Clarification: Using jsonwebtoken alongside Google Auth is acceptable — e.g., for session tokens after Google login. What's blocked is passport-local or jsonwebtoken as the sole auth mechanism without Google Auth.
What it is: The server in the cloud where your tool runs.
Why we use it:
You do not need to understand EC2 deeply. You just need to make your tool "deployment-ready" (see Section F).
Every project MUST follow this folder structure. This is not aspirational — it is the minimum structure that makes deployment possible.
my-tool/
├── src/
│ ├── server/ # Fastify backend
│ │ ├── index.ts # Server entry point (registers plugins, starts listening)
│ │ ├── routes/ # API route handlers (one file per resource)
│ │ ├── services/ # Business logic (not in routes!)
│ │ ├── plugins/ # Fastify plugins (auth, database, etc.)
│ │ └── utils/ # Helper functions
│ ├── client/ # React frontend
│ │ ├── main.tsx # React entry point
│ │ ├── App.tsx # Root component with routing
│ │ ├── pages/ # One component per page/view
│ │ ├── components/ # Reusable UI components
│ │ ├── hooks/ # Custom React hooks
│ │ └── utils/ # Frontend helper functions
│ └── shared/ # Code used by BOTH server and client
│ └── types.ts # Shared TypeScript types/interfaces
├── prisma/
│ └── schema.prisma # Database schema definition
├── docs/
│ ├── PRD.md # Product Requirements Document
│ ├── TECH_SPEC.md # Technical decisions and architecture
│ └── decisions/ # Architecture Decision Records (ADRs)
├── .env.example # Required env vars with placeholder values
├── .gitignore # Must include .env, node_modules, dist
├── package.json # Must have start, build, dev, test scripts
├── tsconfig.json # TypeScript configuration
├── vite.config.ts # Vite/React build configuration
└── README.md # Setup + deploy instructions
Rules:
services/, never in route handlers. Routes should be thin — receive request, call service, return response.pages/, reusable pieces go in components/.shared/.docs/ folder is where you keep your thinking. PRD for what you are building, TECH_SPEC for how, and ADRs for why you made specific choices.Your package.json MUST include these scripts. Here is what each one does and why it matters:
{
"scripts": {
"dev": "concurrently \"tsx watch src/server/index.ts\" \"vite\"",
"build": "tsc && vite build",
"start": "node dist/server/index.js",
"test": "vitest run",
"lint": "eslint src/",
"typecheck": "tsc --noEmit",
"db:migrate": "prisma migrate deploy",
"db:generate": "prisma generate"
}
}
What each script does (in plain language):
| Script | What it does | When you use it |
|---|---|---|
npm run dev | Runs your tool locally for testing. Both the backend and frontend start up, and changes you make appear instantly. | Every day during development |
npm run build | Prepares your tool for deployment. Compiles TypeScript to JavaScript and bundles the frontend. | Before deploying, or to check if everything compiles |
npm start | Runs the deployed version. Uses the compiled code (not the source). | The infra team runs this on the server |
npm test | Checks if everything works. Runs your automated tests. | Before deploying, or after making changes |
npm run lint | Checks code style and catches common mistakes. | During development |
npm run typecheck | Verifies all your types are correct without running the code. | Before committing |
npm run db:migrate | Updates the database schema to match your Prisma schema. | When you change the database structure |
npm run db:generate | Regenerates the Prisma client after schema changes. | After changing schema.prisma |
{
"dependencies": {
"fastify": "^4.x",
"@fastify/cors": "^9.x",
"@fastify/helmet": "^11.x",
"@prisma/client": "^5.x",
"react": "^18.x",
"react-dom": "^18.x"
},
"devDependencies": {
"typescript": "^5.x",
"vite": "^5.x",
"@vitejs/plugin-react": "^4.x",
"vitest": "^1.x",
"tsx": "^4.x",
"concurrently": "^8.x",
"eslint": "^8.x",
"prisma": "^5.x"
}
}
Environment variables are settings that change depending on WHERE your tool is running. Think of them like a thermostat — the same house, but different temperature settings for summer vs. winter.
Your tool needs different settings for:
This file shows what settings your tool needs, with fake placeholder values:
# Server
PORT=3000
NODE_ENV=development
LOG_LEVEL=info
# Database
DATABASE_URL=mysql://user:password@localhost:3306/my_tool_dev
# Authentication (Google OAuth)
GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your-client-secret
GOOGLE_CALLBACK_URL=http://localhost:3000/auth/google/callback
SESSION_SECRET=replace-with-random-string
# Allowed email domain (restrict who can log in)
ALLOWED_DOMAIN=mosaicwellness.in
# Frontend URL (for CORS)
FRONTEND_URL=http://localhost:5173
# Secrets — NEVER commit these
.env
.env.local
.env.production
# Generated files
node_modules/
dist/
.prisma/
# OS files
.DS_Store
*.log
.env file by copying .env.example and filling in real valuesprocess.env.VARIABLE_NAME.gitignore file ensures your .env (with real passwords) never gets uploaded to GitHub.env.example (with fake values) DOES get uploaded — it tells others what settings they needThe rule is simple: .env.example goes in git (it is documentation). .env never goes in git (it has real secrets).
These are non-negotiable. Every tool must follow these rules. They exist to protect our company, our users, and your tool from attacks.
What this means: Passwords, API keys, database URLs, and tokens must NEVER appear in your .ts, .tsx, or .js files. They go in .env only.
Wrong:
// NEVER DO THIS
const apiKey = "sk-ant-abc123-fake-example";
const dbUrl = "mysql://admin:realpassword@prod-server:3306/db";
Right:
// DO THIS
const apiKey = process.env.ANTHROPIC_API_KEY;
const dbUrl = process.env.DATABASE_URL;
Why: If secrets are in your code and your code goes to GitHub, anyone with repo access can see them. Even if the repo is private, that is too many people.
@mosaicwellness.in email addressesWhat this means: Never trust data that comes from users or external sources. Always check that it is the right type, length, and format before using it.
// Use a validation library like zod
import { z } from "zod";
const CreateItemSchema = z.object({
name: z.string().min(1).max(200),
email: z.string().email(),
amount: z.number().positive().max(1000000),
});
Why: Without validation, someone could send your tool garbage data (or malicious data) and break things.
All production tools run on HTTPS. This is handled by our infra team's load balancer. You do not need to set this up — just do not hardcode http:// URLs for production.
@fastify/cors to only allow requests from your frontend's domain.Your .gitignore file keeps secrets out of git. If it is not set up correctly, you might accidentally push passwords to GitHub. Always verify .env is listed in .gitignore BEFORE creating your .env file.
Your tool is "ready to hand off to the infra team" when ALL of these are true:
Your server must have a /health endpoint that returns a simple response. The infra team uses this to know if your tool is alive.
fastify.get("/health", async () => {
return { status: "ok", timestamp: new Date().toISOString() };
});
Bonus: Include a database check:
fastify.get("/health", async () => {
try {
await prisma.$queryRaw`SELECT 1`;
return { status: "ok", db: "connected", timestamp: new Date().toISOString() };
} catch (error) {
return { status: "degraded", db: "disconnected", timestamp: new Date().toISOString() };
}
});
Your tool must read its port from an environment variable, NOT hardcode it:
// Right
const port = parseInt(process.env.PORT || "3000", 10);
// Wrong
const port = 3000; // Hardcoded — infra can't change it
npm start must work after npm install --production. This means your start script runs compiled JavaScript (node dist/...), not TypeScript directly.
Your tool should shut down cleanly when asked (when the server is being restarted or redeployed):
const signals = ["SIGINT", "SIGTERM"];
for (const signal of signals) {
process.on(signal, async () => {
await fastify.close();
await prisma.$disconnect();
process.exit(0);
});
}
Your README must include:
npm run dev)npm run db:migrate)Before telling the infra team your tool is ready, verify:
npm run build succeeds with no errorsnpm start works (after build)/health endpoint returns 200.env.example lists ALL required variablesprisma/migrations/)Every agent in this plugin MUST communicate this way. These rules ensure non-engineering users feel supported, not overwhelmed.
Impact-first language. Lead with what the user gets, not what the technology does.
No jargon without explanation. If you must use a technical term, explain it immediately.
Celebrate what is working. When reviewing or checking something, start with what is good before addressing what needs fixing.
Maximum 3 action items at a time. Never give someone a list of 10 things to fix. Prioritize and batch.
Use priority tiers for issues:
Every finding, recommendation, or section of output must lead with a plain-English summary — what's the impact, in one line. Technical details, reasoning, and fix instructions follow below. Never lead with jargon; always lead with consequence.
Good:
✗ Anyone with the URL can see all your data
Your app has no login. Adding Google Auth takes ~30 min
and locks it to company accounts only.
Bad:
✗ Missing authentication middleware
Add passport-google-oauth20 to your Fastify instance...
Every finding must answer "so what?" before being reported. If it doesn't meet ANY of these criteria, skip it:
If users can't see or feel the impact, it won't cause data loss, it won't cost real money, and it won't break during deployment — it's not worth reporting.
| Instead of... | Say... |
|---|---|
| "You need to refactor this" | "This will be easier to maintain if we split it into two parts" |
| "This is wrong" | "This works, but there's a safer way to do it" |
| "You should use X pattern" | "X pattern will save you time when you need to add features later" |
| "Deploy your app" | "Get your tool live on the server" |
| "Run the migration" | "Update the database to match your new schema" |
| "Spin up a container" | "Start your tool on the server" |
npx claudepluginhub mosaic-wellness/ai-toolkit --plugin kaiGuides creation, editing, and verification of skills for AI coding agents using test-driven development with subagent scenarios. Use when authoring or debugging skills.