Skip to content
rmnr
DocsTourSlicesBest PracticeAgentsInstall

Command Palette

Search for a command to run...

New
  • Best Practice
  • Audit Chain

Standards

Best Practice

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.

Technology profile

Pick one frontend; Convex is additive. Docs + prompt update together.

Official docs reviewed 2026-09-09
Next.js docs ↗Next.js 16.3 ↗Convex docs ↗Convex + Svelte ↗Convex Svelte reactivity ↗Convex Svelte auth ↗

Rule tiers & conflict resolution

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.

P0P0 — security & data integrity

NEVER violate, no exceptions, no TODO escape hatch. If a P0 rule blocks the task, stop and report instead of working around it.

P1P1 — architecture & structure

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.

P2P2 — style & modularity

Enforced by lint/audit tooling. If the tooling passes, you pass.

Stack baselineP1

The active profile and its reviewed versions are shown above these docs. Version facts live in best-practice-techs.ts; rules never duplicate them.

P1Next.js profile

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.

P1Tailwind v4

Use Tailwind v4 and theme tokens. Do not copy a legacy v3 setup unless the task is explicitly a migration.

P1Convex profile

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.

P1Next + Convex auth

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.

Vertical slice structureP1

Every feature is a vertical slice that owns its full stack. No deep cross-slice imports — the barrel is the contract.

P1Slice layout

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

P1Convex backend location follows the frontend

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.

P1Barrel-only cross-slice imports

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";

P1Metadata PAIR (not trio)

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.

P1Props-driven portability

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 }) { … }

P1rr backend is admin-only

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.

Dynamic pages & route compositionP1

Routes are adapters, not feature homes. Repeated pages derive from one registry/data source instead of cloned route files.

P1Root vertical slices stay canonical

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.

P1One dynamic route for one page family

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

P1Navigation derives from the same registry

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.

P2Thin route boundaries

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.

Convex rules

P0Validators on every registered function

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) => { /* … */ },
});

P0Server-side authz inside every handler

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 });
}

P1No bare .collect(); index every filtered/ordered query

`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);

P1Generated Convex types are source-owned build artifacts

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.

P0Schema evolution is widen → backfill → tighten

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.

Integration & plugin authenticationP0

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.

P0OAuth first, least privilege

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.

P0Manual API key fallback must teach the user

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.

P0Do not fake OAuth readiness

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.

P1MCP prefers OAuth discovery

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.

Next.js rules

P0NEXT_PUBLIC_ only for non-sensitive values

Any value prefixed `NEXT_PUBLIC_` ships in the client bundle. Never put secrets, API keys, or admin emails there.

P0Server Actions verify the caller

Every `'use server'` export authenticates AND authorizes before mutating. Treat them as public API endpoints.

P1proxy.ts not middleware.ts

Next 16 renamed middleware to proxy. Put logic in `proxy.ts` at the project root.

P1next/link + next/image only

Never use `<a href="/internal">` or `<img src=…>`. Use `<Link>` / `<Image>` so Next can prefetch + optimise.

P1Cache Components for static reads

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.

P1runtime fs reads need outputFileTracingIncludes

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.

Data fetchingP1

Server data should flow through the framework/backend integration, not lifecycle-effect synchronization.

P1Next + Convex dynamic first paint

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.

P1Next + Convex reactive client data

Reactive client reads/writes use Convex React hooks through slice-local data adapters/hooks. Do not mirror a Convex subscription into useState via useEffect.

P1No lifecycle fetch synchronization

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.

Error handling & loggingP1

P1Convex → typed ConvexError

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" });

P1Client → map code to copy, surface via toast

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.

P1Route boundaries

Every relevant Next route group ships `error.tsx` (and `not-found.tsx` where relevant). Errors render inside the shell chrome, not a white page.

P1Logging

Server-side `console.error("[<slice>:<fn>]", err)` with a context prefix. No PII in logs. No `console.log` left in shipped client code.

TestingP1

A slice's tests travel with it when copied — co-locate them.

P1Co-located per slice

Unit/component tests live inside the slice — `slices/<slug>/__tests__/` or `<file>.test.ts(x)` next to the source.

P1Mandatory slice contract tests

Test the barrel's exported API and the critical user-visible state transitions the consumer relies on. Query by accessible behavior, not snapshots alone.

P1Mandatory Convex handler tests

Test every security-sensitive Convex query/mutation, including unauthenticated and wrong-owner denial paths. Use convex-test where it fits the deployed function shape.

P1App-level e2e stays global

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.

File modularityP2

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.

P2≤200 lines per source file

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/>

P2Single cohesive responsibility per file

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.

P2Extract on the SECOND occurrence

Repeated UI pattern → `components/` or `shared/`. Util needed by two slices → `shared/<name>/utils/`. Not the third copy — the second.

P2Dynamic over hardcoded

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/>;

P2Compose, don't accumulate

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.

NamingP2

P2File + export casing

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.

P2Types + constants placement

Per-slice types in `types.ts`; per-slice constants in `config/`. `index.ts` exists ONLY as a barrel — never put implementation in it.

P2Convex naming

Table names plural camel (`posts`, `auditLogs`); indexes `by_<field>`.

UI rulesP1

P1shadcn-family primitives first

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.

P1Theme tokens, not hex

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.

P1Mobile-first responsive

Single column base, layer `md:` / `lg:` upward.

P1No marketing chrome on workspace surfaces

Workspace templates render full-bleed (`h-dvh`) — the workspace IS the product.

P1Shell hierarchy: exactly one outer chrome

dashboard-shell owns admin/workspace chrome; admin-panel / admin / platform-admin mount INSIDE it. Never nest two chromes.

Delivery rulesP1

P1Solo-dev = push direct to main

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.

P1Conventional commits

`feat(scope): subject` / `fix` / `chore`. Body explains WHY and lists any P1 deviations (`TODO(rr)` markers added this commit).

P1Co-author the AI

End AI-assisted commits with `Co-Authored-By: Claude <[email protected]>` so authorship is honest.

P0Publish guardrail

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.

P1No GitHub Actions cloud minutes

Local CI via the pre-push hook or `/sc-git ci`; Dokploy builds on push.

Copy-first & Source MapP1

P1Never greenfield what a proven source solved

Check the Source Map in CLAUDE.md first; `cp -r` → adjust import aliases → strip business-specific bits.

P1Missing source path → STOP and ask

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 distribution kinds (TEMPLATE vs SLICE)P1

rr publishes TWO installable kinds. They install to different paths and answer different needs — confusing them is the #1 support issue.

P1TEMPLATE = full-app scaffold

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.

P1SLICE = drop-in vertical feature

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.

P1Trust the CLI banner

CLI prints `[TEMPLATE]` or `[SLICE]`. Wrong banner = wrong slug.

P1Lift = sanitize first (slice path only)

Strip consumer URLs, env names, role enums, table coupling → replace with props / env-configured allowlists. `npx rr lift` is operator-manual.

P1MCP connectors via create-your-mcp

Add ChatGPT / Claude / Cursor connector support via `npx rr add create-your-mcp` — never hand-roll OAuth/PKCE.

Enforcement map

What tooling guards each rule. If a rule has no tooling row, the prompt is its only guard — treat it as P1.

≤200 LOC

`audit:file-size` + eslint `max-lines`

Barrel-only imports

eslint `no-restricted-imports` / boundaries + `audit:slices`

No raw `<a>` / `<img>` / `<button>`

eslint `no-restricted-syntax`

Validators + authz on Convex fns

`audit-bp` (P0 gate)

Metadata pair version match

`audit:slices`

Catalog scalars = generated

`gen:catalog:check` (pre-commit)

Profile versions + docs freshness

`best-practice-techs.ts` SSOT + profile tests

Skills JSON sync

`sync-skills.mjs --check` (prepublishOnly)

Types

framework checker (`tsc` for Next; `svelte-check` for Svelte)