Standards
One doctrine, selectable stack profiles. Activate Next.js or Svelte, add Convex when needed, then copy a prompt generated from the exact same SSOT rules shown in Docs.
Pick one frontend; Convex is additive. Docs + prompt update together.
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 active profile and its reviewed versions are shown above these docs. Version facts live in best-practice-techs.ts; rules never duplicate them.
Use the reviewed Next.js + React versions from the active profile, App Router, TypeScript strict, Tailwind v4, and shadcn/ui. Upgrade security/LTS releases instead of pinning an older remembered version.
Use Tailwind v4 and theme tokens. Do not copy a legacy v3 setup unless the task is explicitly a migration.
Use the reviewed Convex version from the active profile. Keep backend functions framework-neutral; use the official frontend adapter for the selected framework and keep Convex functions in the framework-supported location.
For rr's Next + Convex starter, use @convex-dev/auth unless a documented requirement needs another provider. Keep auth checks inside Convex handlers regardless of route protection.
Every feature is a vertical slice that owns its full stack. No deep cross-slice imports — the barrel is the contract.
Consumer UI features live at root `slices/<slug>/`; rr internal source stays at `frontend/slices/<slug>/`. Framework routes import the root slice barrel. Per-slice UI shape stays `components/ lib/ utils/ hooks|state/ config/ api/` + types + tests + barrel.
slices/cta/ ├── components/ ├── lib/ ├── hooks/ ├── config/ ├── utils/ ├── api/ ├── types.ts ├── index.ts └── slice.json slice.manifest.json
Keep the root slice as the feature UI contract, but put Convex functions where the framework integration supports them: rr/Next copy-source may use `convex/features/<slug>/`; SvelteKit + convex-svelte uses `src/convex/` (configured by `convex.json`). Expose typed data adapters through the root slice instead of importing generated backend internals everywhere.
Cross-slice access goes through the target slice barrel only. Use the host alias (`@/features/<slug>` in the Next rr convention or `$features/<slug>` in the Svelte starter), shared UI/backend aliases, or relative paths within the same 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.
Routes are adapters, not feature homes. Repeated pages derive from one registry/data source instead of cloned route files.
Consumer feature code lives at root `slices/<slug>/`. Route files import the slice barrel and adapt route params/data; they do not become a second feature implementation.
When multiple pages share one shape, use one dynamic route plus a registry/data SSOT. Do not create one hardcoded page file per entity.
// Next: app/apps/[slug]/page.tsx // SvelteKit: src/routes/apps/[slug]/+page.svelte // Both resolve slug -> one registry/data source -> slice component
Menus, breadcrumbs, page titles, sitemap entries, permissions, and dynamic-page lookup derive from one typed registry where possible. Never maintain parallel route/nav arrays for the same facts.
A route should parse params, load/authorize data, choose the slice, and render. Business logic, reusable UI, and mutations stay inside the vertical slice/backend boundary.
Every public or internal Convex query/mutation/action declares object-form `args` AND `returns` validators. Add semantic bounds beyond broad `v.string()`/`v.number()` where the domain requires them.
Why: Validated inputs and outputs keep the generated API trustworthy and fail closed when contracts drift.
// DO
export const setRole = mutation({
args: { userId: v.id("users"), role: v.union(v.literal("admin"), v.literal("member")) },
handler: async (ctx, args) => { /* … */ },
});Authenticate and authorize ownership/tenant membership inside every protected Convex handler before protected data access. Reuse the repository's shared auth helper; route, layout, selected-workspace and hidden-UI gates never grant backend authority.
Why: Convex functions are independently callable; client and framework UI gates are not authorization boundaries.
// 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);Use generated `api`/`internal` references plus generated `Doc`/`Id` types, regenerate with the official CLI after backend changes, commit generated files when the project contract requires clean-clone typechecking, and never edit `_generated` by hand.
For existing production data, deploy compatible schema/code first, backfill in bounded resumable batches, verify representative populated data, then tighten validation. Never clear production tables just to satisfy a schema deploy.
Connecting a provider must feel like signing in, not configuring infrastructure. OAuth/OIDC is the default when the provider supports it; manual secrets are an explicit fallback with complete acquisition guidance.
For every plugin/provider integration, prefer the provider's supported OAuth/OIDC authorization flow over asking the user to copy credentials. Keep client secrets server-side; bind state/PKCE/nonce as applicable; use an exact redirect allowlist; request the minimum provider permissions; store tokens encrypted server-side; implement expiry/refresh/revocation; and never put access or refresh tokens in URLs, browser localStorage, or public env variables.
If OAuth is unavailable or inappropriate and a token/API key is required, NEVER render a blank secret field by itself. The same integration surface must provide an official provider setup URL, 3–6 numbered steps to obtain the credential, exact minimum scopes/permissions, expiry/rotation guidance, note when the secret is shown only once, the exact field where it is pasted, masked/server-side secure storage, a verify/test action, and disconnect/revoke instructions.
Only show OAuth as actionable when the backend/operator client configuration and callback lifecycle are actually implemented and configured. If provider OAuth credentials or discovery are missing, show the OAuth path as unavailable and expose the documented fallback instead of a broken button or fabricated success.
For remote MCP, prefer standards-based OAuth discovery/authorization when the server advertises it. Bearer/manual auth is fallback only; it must follow the same official-link + numbered-steps + minimum-scope contract and must never weaken tool approval or workspace authorization.
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.
Use Cache Components when explicit caching helps: set `cacheComponents: true`, then apply `"use cache"` with `cacheLife` / `cacheTag` at the correct boundary. Do not use the old `experimental.cacheComponents` flag.
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.
Server data should flow through the framework/backend integration, not lifecycle-effect synchronization.
For authed/dynamic first paint, use the current Convex Next.js server helpers (for example preloadQuery/fetchQuery where appropriate) and hand the typed result/reference to the client boundary. Keep reactive reads on the Convex client after hydration.
Reactive client reads/writes use Convex React hooks through slice-local data adapters/hooks. Do not mirror a Convex subscription into useState via useEffect.
Do not use React `useEffect` or Svelte `$effect` as a default data-fetch mechanism. Use the framework loader/server boundary or reactive backend adapter; effects are for external side effects, not derived state.
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 at the slice's data/action adapter, map typed error codes to user copy, and surface them through the shared feedback/toast primitive. Never swallow silently; never use blocking `alert()` as product UX.
Every relevant Next 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.
Test the barrel's exported API and the critical user-visible state transitions the consumer relies on. Query by accessible behavior, not snapshots alone.
Test every security-sensitive Convex query/mutation, including unauthenticated and wrong-owner denial paths. Use convex-test where it fits the deployed function shape.
Playwright smoke is app-level, not slice-owned. Run it with the project package runner (`npm run` in rr/Next today; `bun run` in the Svelte starter). Query accessible behavior and preserve focused test 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 rr/Next `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/>
Keep each file centered on one cohesive responsibility. A cohesive named-export cluster is fine; split when exports evolve independently or the boundary becomes hard to review/test, not just to satisfy an arbitrary line count.
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 use kebab-case (`.ts`, `.tsx`, `.svelte` as appropriate). Component exports are PascalCase. React hooks use `useCamelCase`; Svelte state/helpers use descriptive camelCase names rather than fake React-hook naming. Utils/functions use 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>`.
Build shared product UI from the active framework's shadcn port (shadcn/ui for React, shadcn-svelte for Svelte) and composed wrappers. Use native semantics where they are the correct accessible primitive; do not hand-roll a second design-system primitive.
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 <[email protected]>` 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)
`best-practice-techs.ts` SSOT + profile tests
`sync-skills.mjs --check` (prepublishOnly)
framework checker (`tsc` for Next; `svelte-check` for Svelte)