Standards
Single source of truth for how rr-based projects are built. Two surfaces, one data file — the Docs tab is for humans, the AI Prompt tab is for pasting into your AI agent so it follows the same rules.
Every rule carries a tier. When two rules conflict, the higher tier wins. When you can't ask the user (autonomous run, mid-task): pick the option this doctrine recommends, add a `// TODO(rr): confirm — chose X over Y because …` marker, and continue. Never silently guess.
NEVER violate, no exceptions, no TODO escape hatch. If a P0 rule blocks the task, stop and report instead of working around it.
Violate only when genuinely necessary. Every violation needs a `// TODO(rr): <why + what the compliant version looks like>` at the site AND a note in the commit body.
Enforced by lint/audit tooling. If the tooling passes, you pass.
The pin is intentional: rr slices must compose identically across every consumer app. "Works on my machine" is a lift blocker. `check:stack-pin` compares package.json against this section in CI.
Pin Next ^16 and React ^19 in package.json. No `middleware.ts` — use `proxy.ts` at the project root.
Why: Next 16 deprecates middleware.ts and ships App Router + Cache Components as the default.
// package.json "next": "^16.0.0", "react": "^19.0.0"
Use Tailwind v4 with `@tailwindcss/postcss`. Bridge a v3 config via `@config` only during migration.
Use Convex self-hosted via Docker Compose on the same Dokploy node. Pin `convex` ^1.16 minimum.
Why: Self-hosted = zero per-user cost, full schema portability, deploy via `npx convex deploy --env-file …`.
Use `@convex-dev/auth` for sessions. NO Clerk. Custom auth slices are allowed only when @convex-dev/auth is documented as insufficient.
TS strict everywhere. If package.json versions disagree with this baseline, FLAG it — don't silently adopt either side.
Every feature is a vertical slice that owns its full stack. No deep cross-slice imports — the barrel is the contract.
Consumer projects: each feature lives at `slices/<slug>/` (+ optionally `convex/features/<slug>/`). rr internal repo only: `frontend/slices/<slug>/` (preserves Next routing). Don't mix the two conventions. Per-slice shape: `components/ lib/ utils/ hooks/ config/ api/` + `types.ts` + tests + the metadata pair.
slices/cta/ ├── components/ ├── lib/ ├── hooks/ ├── config/ ├── utils/ ├── api/ ├── types.ts ├── index.ts └── slice.json slice.manifest.json
Inside a slice, imports resolve ONLY via `@/components/ui/*`, `@/shared/*`, `@/features/<own-slug>/*`, `@convex/*`, or relative-within-slice. No `../../` reaching into another slice's internals.
Why: Deep imports lock you into another slice's internal layout. The barrel is the contract.
// DON'T — deep import into another slice
import { parseMention } from "@/features/comments/lib/mention-parser";
// DO — through the barrel
import { parseMention } from "@/features/comments";Every slice ships `slice.json` (contract folded in under the `contract` block since 2026-06-21) + `slice.manifest.json`. Version SSOT: `slice.json.version === slice.manifest.json.version`, gated by `audit:slices`. `lib/content/slices.ts` scalars are GENERATED via `gen-slice-catalog.mjs` — never hand-edit them.
Why: The old third file (`slice.contract.ts`) was folded into `slice.json.contract` — one SSOT, drift impossible.
Portable slices NEVER hardcode consumer URLs, env names, role enums, or copy. Hardcode = lift blocker.
// BAD
const SITE = "https://rahmanef.com";
// GOOD
export function HeroView({ siteUrl }: { siteUrl: string }) { … }Site demos run on the localStorage adapter, NOT Convex. `convex/features/*` in rr is copy-source for consumers. Never compose every feature into rr's own `convex/schema.ts` — that turns the library into a monolith.
Every `mutation()` / `query()` reachable from the client MUST declare `args:` with `v.*` validators.
Why: Without them, anything goes from a crafted client. audit-bp marks missing validators as P0.
// DO
export const setRole = mutation({
args: { userId: v.id("users"), role: v.union(v.literal("admin"), v.literal("member")) },
handler: async (ctx, args) => { /* … */ },
});Call `requireUser` / `requireAdmin` from `convex/_shared/auth.ts` as the FIRST line of the handler. Route-layer gates do not protect Convex HTTP endpoints.
Why: Convex HTTP queries are directly reachable — Next.js layout gates don't protect them.
// DO
handler: async (ctx, args) => {
await requireAdmin(ctx);
await ctx.db.patch(args.id, { role: args.role });
}`ctx.db.query(...).collect()` scans the table. Use `.withIndex(...).take(N)` or paginate; add the index in `defineTable(…).index(…)`. Exception: tiny bounded config tables (< ~50 rows) may `.collect()` with a `// TODO(rr): bounded table` marker.
Why: Bare collects bypass query-budget guardrails and degrade as the table grows.
// BAD
await ctx.db.query("posts").collect();
// GOOD
await ctx.db.query("posts").withIndex("by_author", q => q.eq("authorId", args.authorId)).take(50);Any value prefixed `NEXT_PUBLIC_` ships in the client bundle. Never put secrets, API keys, or admin emails there.
Every `'use server'` export authenticates AND authorizes before mutating. Treat them as public API endpoints.
Next 16 renamed middleware to proxy. Put logic in `proxy.ts` at the project root.
Never use `<a href="/internal">` or `<img src=…>`. Use `<Link>` / `<Image>` so Next can prefetch + optimise.
Marketing/SSG pages opt into `"use cache"` + `cacheLife` / `cacheTag`. Enable `experimental.cacheComponents` in next.config.mjs first.
Runtime `fs.readdir` / `readFile` on repo dirs requires that dir in `outputFileTracingIncludes` in next.config.mjs. Works locally, silently empty in the standalone Docker image otherwise.
If you're reaching for `useEffect` + `fetch`/`useState`, the answer is `useQuery` or a server component.
Server component calls `preloadQuery` (convex/nextjs) for first paint, passes the ref to a client component using `usePreloadedQuery` — reactive after hydration, no loading flash.
After first paint, reactive client state uses `useQuery` / `useMutation` from `convex/react`.
Static reads use `fetchQuery` inside a `"use cache"` component, or build-time data.
Client mutations go through slice-local hooks (`slices/<slug>/hooks/`), never inline in JSX handlers scattered across components.
Throw `ConvexError({ code, message })` with a typed code from the slice's `types.ts` (e.g. `NOT_AUTHORIZED | NOT_FOUND | RATE_LIMITED`). Never throw raw strings; never leak internal details to clients.
// DON'T
throw new Error("db failed: " + JSON.stringify(user));
// DO
throw new ConvexError({ code: "NOT_AUTHORIZED", message: "Admin role required" });Catch in the slice's mutation hook, map code → user copy, surface via the shared toast (sonner). Never swallow silently; never `alert()`.
Every route group ships `error.tsx` (and `not-found.tsx` where relevant). Errors render inside the shell chrome, not a white page.
Server-side `console.error("[<slice>:<fn>]", err)` with a context prefix. No PII in logs. No `console.log` left in shipped client code.
A slice's tests travel with it when copied — co-locate them.
Unit/component tests live inside the slice — `slices/<slug>/__tests__/` or `<file>.test.ts(x)` next to the source.
(1) the barrel's exported API — the contract consumers rely on; (2) every Convex mutation/query via `convex-test`, INCLUDING the authz-denied path (unauthenticated caller must be rejected).
Playwright smoke is global (`npm run e2e` local, `e2e:staging` against staging). Slices don't own e2e. Test files are excluded from the 200-LOC cap but still obey single-responsibility.
Files are read more than written. Keep them small, single-purpose, composable so consumers grok + reuse + replace pieces without reading the whole thing. Tooling-enforced.
Hard cap enforced by `audit:file-size` + eslint `max-lines`. Exclusions: pure data exports (`lib/content/*.ts`, `*/seed.ts`, theme presets), `_generated/`, test files, and `components/ui/*` (vendored shadcn — never edit, never count; customize by wrapping in `shared/` or slice components, or regenerate via the shadcn CLI).
Why: Large files hide concerns, resist diff review, force consumers to scroll instead of compose.
// BAD: 400-line PostEditor.tsx (toolbar + body + sidebar + status) // GOOD: PostEditor.tsx (≤200) composes <Toolbar/> + <EditorBody/> + <SidebarMeta/> + <StatusPanel/>
One default export OR one cohesive named-export cluster per file. Prefixed exports (`createX`, `parseX`, `serializeX`, `validateX`) = 4 files, not 4 exports.
Repeated UI pattern → `components/` or `shared/`. Util needed by two slices → `shared/<name>/utils/`. Not the third copy — the second.
Lookup maps over if/switch-chains; derived selectors over literal arrays; `labels` props over inline copy.
// BAD
if (kind === 'admin') return <AdminLink/>;
if (kind === 'user') return <UserLink/>;
// GOOD
const LINKS = { admin: AdminLink, user: UserLink };
const L = LINKS[kind];
return <L/>;Prefer a new file that composes with the existing one over editing the existing one bigger. Existing file stays small + tested; the new file is the one that changes.
Files: `kebab-case.ts(x)`. Component exports: `PascalCase`. Hooks: `useCamelCase`. Utils/fns: `camelCase`.
Per-slice types in `types.ts`; per-slice constants in `config/`. `index.ts` exists ONLY as a barrel — never put implementation in it.
Table names plural camel (`posts`, `auditLogs`); indexes `by_<field>`.
All UI builds on shadcn primitives. Never raw `<button>`, `<dialog>`, `<input type=date|file>` — use `Button`, `ResponsiveDialog`, `DateField`, `FileUpload`.
Use `bg-background` / `text-foreground` / `border-border`. Semantic status colors resolve through the tones SSOT (`_shared/.../ui/tones.ts`) — never invent a local green-means-success.
Single column base, layer `md:` / `lg:` upward.
Workspace templates render full-bleed (`h-dvh`) — the workspace IS the product.
dashboard-shell owns admin/workspace chrome; admin-panel / admin / platform-admin mount INSIDE it. Never nest two chromes.
Tests + typecheck + validate green → push direct to main. No PRs. Dokploy auto-deploys. Risky changes go to staging first: `git push origin main:staging` → verify `e2e:staging` → then main.
Why: PRs add ceremony without review benefit when the solo dev is also the reviewer.
`feat(scope): subject` / `fix` / `chore`. Body explains WHY and lists any P1 deviations (`TODO(rr)` markers added this commit).
End AI-assisted commits with `Co-Authored-By: Claude <noreply@anthropic.com>` so authorship is honest.
NEVER run `npm publish` yourself. When publish conditions hold (packages/ modified + version bumped above `npm view` + tsc green + pushed to main), end your response with the publish suggestion and let the user run the OTP step.
Local CI via the pre-push hook or `/sc-git ci`; Dokploy builds on push.
Check the Source Map in CLAUDE.md first; `cp -r` → adjust import aliases → strip business-specific bits.
Source Map paths are machine-local (`~/projects/...`). If a source path doesn't exist on this machine, STOP and ask — don't reconstruct from memory; that defeats copy-first.
rr publishes TWO installable kinds. They install to different paths and answer different needs — confusing them is the #1 support issue.
A TEMPLATE (catalog: `lib/content/layouts.ts`) is a whole-app starter. Install `npx rr add <template-slug>` — defaults to `--at root` (routes promoted to `app/(public)/` + `app/admin/`, `/preview/<slug>` constants auto-rewritten). `--at preview` only for sandbox demos.
Why: Templates do NOT ship slice metadata — they're monolithic scaffolds you fork.
A SLICE (catalog: `lib/content/slices.ts`) is one self-contained feature. `npx rr add <slice-slug>` copies into `slices/<slug>/` (+ optional `convex/features/<slug>/`) with the metadata pair. Variants: `add <slug> <variant>` flattens one; `add <slug>` copies all + switcher prop.
Why: The metadata pair is what makes a slice composable + auditable.
CLI prints `[TEMPLATE]` or `[SLICE]`. Wrong banner = wrong slug.
Strip consumer URLs, env names, role enums, table coupling → replace with props / env-configured allowlists. `npx rr lift` is operator-manual.
Add ChatGPT / Claude / Cursor connector support via `npx rr add create-your-mcp` — never hand-roll OAuth/PKCE.
What tooling guards each rule. If a rule has no tooling row, the prompt is its only guard — treat it as P1.
`audit:file-size` + eslint `max-lines`
eslint `no-restricted-imports` / boundaries + `audit:slices`
eslint `no-restricted-syntax`
`audit-bp` (P0 gate)
`audit:slices`
`gen:catalog:check` (pre-commit)
`check:stack-pin`
`sync-skills.mjs --check` (prepublishOnly)
`npx tsc --noEmit`