internal/planning/3357-react-on-rails-starter-2026.md
Status: Draft — design approved 2026-05-21; revised 2026-05-22 after /autoplan multi-voice review (CEO + Design + Eng + DX, each dual-voice via Claude subagent + Codex).
Tracking issue: https://github.com/shakacode/react_on_rails/issues/3357
Public repo (to be created): shakacode/react-on-rails-starter-tanstack
Two strategic reframes adopted after dual-voice review surfaced cross-phase convergent dissent on the original framing:
Demo portfolio framing. The 2026 starter is the greenfield seed at the head of an existing demo portfolio (react-on-rails-demo-marketplace-rsc, react-on-rails-demo-hacker-news-rsc, react-on-rails-demo-gumroad-rsc, react_on_rails-demo-octochangelog-on-rails-pro). The demos carry the proof points (RSC streaming, performance under traffic, Inertia head-to-head, migration story). The starter doesn't re-prove what the demos prove.
"Best TanStack on Rails" positioning. Reframed from "Inertia counter-kit" to a category claim. Two structural reasons it's defensible: (a) Inertia is a routing model — adding TanStack Router on top fights what Inertia owns; (b) RSC plus streaming is hard to ship correctly — Inertia is unlikely to bring up its own RSC-streaming plumbing, and if it ever does, it would most naturally do so on top of React on Rails Pro (a separate, intriguing discussion: "Inertia on Pro" as a bridge product, captured in #3144's neighborhood). So the moat is the combination — incremental React, full-page React, RSC streaming via Pro, AND a deep TanStack integration — not any one piece.
The review also surfaced 7+ engineering and DX gaps that survived the reframes — see Required Before Implementation below.
Findings traceability: see ~/.gstack/projects/shakacode-react_on_rails/ for the full dual-voice transcripts. Restore point for pre-review plan: see the HTML comment above this file's title.
The Evil Martians Inertia Rails React Starter Kit, the official Inertia Rails starter kits (React / Vue / Svelte), and similar polished kits such as GrowthX Starter (Rails 8 + React 19 + shadcn/ui) are the most visible front door into "Rails + React in 2026" for new teams.
create-react-on-rails-app exists and is solid, but does not currently lead with the same modern stack defaults (shadcn/ui, TanStack ecosystem, RSC, Rails 8 auth) — so a developer in a 30-minute side-by-side evaluation sees Inertia as the more "batteries-included" choice for greenfield work, even though React on Rails has a deeper feature set under the hood.
This plan ships the best TanStack-on-Rails starter kit — a public greenfield seed at the head of the React on Rails demo portfolio. The category claim is structural, not derivative: Inertia is a routing model and therefore cannot host TanStack Router as a peer; React on Rails Pro can and does. The starter operationalizes that claim end-to-end (Router + Query + Table as a coherent surface) while the existing demo repos (Hacker News, Marketplace, Gumroad, Octochangelog) carry the orthogonal proof points (perf, RSC streaming, Inertia head-to-head, migration story).
Beyond the front-door positioning, the kit serves three additional purposes the dual-voice review surfaced as material:
Two audiences with different leverage points:
Primary: Developers using AI agents to bootstrap and iterate on a Rails + React SaaS ("vibe coders"). The agent is the proximate reader; it learns this kit's patterns and extrapolates them across the user's future work. Implication: ship complete patterns the agent can clone (auth, mailer, background job, form, CRUD, nested route, system spec) — and let the code structure itself be the load-bearing teaching artifact. AGENTS.md documents the patterns but isn't the load-bearing primitive; the canonical reference files are.
Secondary but consequential: The senior Rails/React maintainer doing a 30-minute side-by-side eval against Inertia/GrowthX. Dual-voice review flagged this audience as more decision-making than the original framing acknowledged. Implication: surface polish, TTHW, error message quality, deployment readiness, and the "why TanStack" narrative all matter — the human evaluator never gets to the AI-agent value if they bounce in the first 5 minutes.
Both audiences are served by the same core decision: an opinionated TanStack-first surface with concrete, copy-pasteable reference patterns.
react-on-rails-demo-hacker-news-rsc for public-traffic perf, react-on-rails-demo-marketplace-rsc for e-commerce, react-on-rails-demo-gumroad-rsc for direct Inertia head-to-head, react_on_rails-demo-octochangelog-on-rails-pro for migration from a real Rails app. The starter doesn't re-prove what the demos prove.AGENTS.md as the index, not the strategy pillar.--flagship variant flag on create-react-on-rails-app for Tier 1. (Revisit at Tier 2 — see Required-Before-Implementation #4.)react-on-rails-demo-gumroad-rsc).Standalone public GitHub repo, shakacode/react-on-rails-starter-tanstack, public from commit 1, configured as a GitHub Template Repository (so adopters get a fresh tree via "Use this template," not a stale clone).
Considered alternatives:
create-react-on-rails-app: deferred to Tier 2 review, not rejected. Original argument ("no Inertia kit ships as a CLI") is competitor-mimicry rather than user reasoning. The real argument is CLI-matrix bloat + Pro-heavy maintenance burden, which is legitimate but not load-bearing. After Tier 1 ships, if the maintained template proves stable, fold its defaults back into create-react-on-rails-app --template tanstack (the flag clones the maintained template repo, doesn't re-generate every permutation).create-react-on-rails-flagship): rejected — duplicates the maintenance burden of both the CLI and the template repo.The kit is bootstrapped via create-react-on-rails-app --rsc --rspack and then heavily customized; downstream improvements to that CLI flow benefit both projects.
Naming decision (2026-05-22): the repo is react-on-rails-starter-tanstack. The -tanstack suffix locks the kit's identity to its strongest positioning rather than a calendar year — sharper claim, no maintenance trap. If TanStack momentum stalls in the future the name becomes a softer "the modern Rails+React starter" identity, which is a survivable downside. Either way the name doesn't expire on a fixed date.
Tier 1 is what ships in the first public release. Tier 2 is deferred (listed below).
bin/rails generate authentication plus custom additions:
POST /session (singular resource :session), logout at DELETE /session, password reset at /passwords/new and /passwords/:token/edit, PasswordsMailer, User / Session / Current models, Authentication concern auto-included in ApplicationController.RegistrationsController + view at /signup — the generator does NOT provide this), email verification (token column on User, EmailVerificationsController, EmailVerificationMailer, email_verified_at gate that blocks the dashboard until verified), profile edit at /settings/profile, password change at /settings/security.letter_opener link surfaced. (Both Design voices flagged this as the highest-friction touchpoint in the funnel; full spec lives in Required-Before-Implementation #1.)letter_opener for dev mail./dashboard — single canonical dashboard. TanStack Table drives the Projects list (column sort, status filter, pagination, persisted view). TanStack Query fans out the 4 metric cards as independent fetches with per-card loading/error states, no head-of-line blocking. TanStack Router owns navigation to /projects/:id with route-level loader prefetch. A small "rendering mode" drawer in the dashboard header tells the honest surface-split story: "This authenticated surface is TanStack-driven, classic SSR via the Pro Node renderer — chosen for type safety + interactivity. The public landing uses RSC for cold-load and SEO. The full RSC streaming story lives in the [Hacker News demo →] and [Marketplace demo →]; the Inertia head-to-head lives in the [Gumroad demo →]."/settings overview, /settings/profile, /settings/security — TanStack Router nested routes with SSR loaders, same primitive as the dashboard./projects/:id, /projects/:id/edit, /projects/new — TanStack-routed, with route-level loaders.new, show, edit, destroy. Reference form pattern: Rails server-side validation as the source of truth, inline error display per field, optimistic-pending state on submit, marked with a canonical-reference comment header (see Required-Before-Implementation #6). TanStack Form is not in Tier 1 scope — deferred to Tier 2 as a second TanStack-ecosystem milestone.ProjectArchiveJob (archives projects whose last_activity_at is older than N days). Retry policy + DLQ behavior + Sentry capture all spec'd; tests cover retry exhaustion.WelcomeMailer (sent on signup, custom), the Rails 8 generator's PasswordsMailer (reset), and a custom EmailVerificationMailer (the generator does not provide one).Gemfile, initializer commented out, single env var (SENTRY_DSN) to enable. README documents how to flip it on.aria-live on flash + form errors, focus management on dialog open/close + route changes, reduced-motion handling on transitions, touch targets ≥ 44px. Not a section in AGENTS.md — a property of every shipped surface.letter_opener (dev mail)aria- + keyboard nav assertions)User (Rails 8 generator emits email_address + password_digest; remaining columns are this kit's additions)
- email_address (string, NOT NULL, uniquely indexed) # from generator
- password_digest (string, NOT NULL) # from generator
- name (string) # custom (for signup + profile)
- email_verification_token (string, nullable, indexed) # custom
- email_verified_at (datetime, nullable) # custom
- timestamps
Session (from Rails 8 generator; database-backed sessions)
- user_id (FK)
- ip_address (string)
- user_agent (string)
- timestamps
Project
- name (string)
- description (text)
- status (integer enum: active / paused / completed / archived)
- user_id (FK)
- last_activity_at (datetime)
- timestamps
Current is a Rails ActiveSupport::CurrentAttributes class also emitted by the generator (not a database table). Holds the request-scoped Current.user and Current.session.
The auth generator's password-reset token lives on the Session row indirectly via the PasswordsController flow — no separate password_reset_token column is required.
db/seeds.rb creates:
[email protected] / password, pre-verified.last_activity_at values.config/routes.rb) handle the public landing (RSC-rendered), the auth surface (classic Rails views), and the single authenticated shell route. Everything under that shell is TanStack-Router-driven./) renders via react_on_rails_component with rsc: true — RSC + streaming. This is the surface where RSC's headline wins (TTFB, mobile perf, SEO content streaming) actually apply. Cold visitors land on a fast, search-indexable, low-JS first paint.Authentication concern (before_action :require_authentication) + before_action :require_verified_email on the shell controller. Unverified users never reach the TanStack mount point./dashboard, /dashboard/projects/:id, /projects/new, /settings, /settings/profile, /settings/security. SSR loaders run server-side via the Pro Node renderer so first paint is correct; client takes over for subsequent navigations with prefetch./dashboard renders via react_component (classic SSR through the Pro Node renderer), not RSC. Rationale: behind auth, RSC's TTFB / SEO / cold-load wins don't apply; TanStack's interactivity / type-safety / URL-state wins do. The RSC streaming story is carried by the public landing here and by the demo portfolio (Hacker News, Marketplace) externally./api/projects, /api/projects/:id/metrics). Each metric card on /dashboard is an independent query — one slow query never blocks the rest of the dashboard.getCsrfToken() reader (meta tag on the Rails shell). Documented in AGENTS.md as the one cross-cutting subtlety.public/404.html, public/500.html).Project and User models. The Project form is the reference pattern: validation errors render inline next to fields with aria-live, success redirects with a flash toast.ProjectArchiveJob (max 5, exponential backoff). Failed jobs land in DLQ + are captured by Sentry when enabled. Spec'd test: forced failure → 5 retries → DLQ entry.<Suspense> boundaries scoped per metric card + per Projects-list segment, with error boundaries that allow the rest of the dashboard to render. Spec'd Playwright test: mid-stream connection cut → other cards still appear.react_on_rails.node_renderer_down Sentry event. README documents the operational signal.bin/rails db:seed not run, SolidQueue not started, Node renderer not running, missing RAILS_MASTER_KEY) each get a problem + cause + fix message. bin/doctor checks preflight; dev-only middleware surfaces the "no demo user — run bin/rails db:seed" hint inline when Rails.env.development? && User.count == 0.letter_opener shows the message in a browser tab. Production: README documents Mailgun / Postmark / Resend swap-in with both the one-line ActionMailer config AND the relevant ENV vars.Test types AND specific acceptance targets — "at least one spec per category" is not enough for a kit whose value prop is "agents clone these patterns."
User, Project./dashboard; unverified user can't reach /dashboard; user A can't access user B's /projects/:id./dashboard → /projects/:id → back, with prefetch verification and URL-state-reflects-table-state assertions./dashboard and /settings/profile.prefers-color-scheme: dark shows no light-mode frame before paint.aria-live on flash messages and form errors, focus-trap on dialog open/close.factory_bot for User, Project. Verification-token factory traits: :unverified, :expired_token, :verified./dashboard (logged-in seeded user) with budgets defined in Required-Before-Implementation #2.bin/test runs everything, bin/test --smoke runs Playwright only, bin/test --a11y runs only the accessibility-tagged Playwright specs.AGENTS.md (Index, Not Strategy)The kit's root AGENTS.md is the index that points AI agents at the canonical reference files. It is not the load-bearing teaching artifact — the code structure (with marker comments + colocated specs and factories) is. Dual-voice review explicitly challenged the original "AGENTS.md as load-bearing" framing: AI agents extract patterns from code, not docs; the docs index those patterns, the code teaches them.
Density target: match the monorepo's existing AGENTS.md (concrete commands, exact file paths, key concept deep-dives where they matter). Every "How to add X" section ends with a literal command the agent can execute (bunx shadcn add card, bin/rails g migration AddXToProjects) plus the exact paths it will touch.
Contents (sections, each with a working code reference + canonical-marker comment in the kit):
/settings/profile as canonical.bunx shadcn add ... workflow, where the block lives, RSC-vs-client decision tree (see below).ProjectArchiveJob (SolidQueue, retry, DLQ, Sentry).WelcomeMailer (mailer, view, preview, spec, throttle pattern)..client./'use client' block. THE most valuable single decision tree the kit can ship. Includes the shadcn-on-Tailwind-v4-inside-RSC boundary placement guidance.react_on_rails_pro has a canonical alternative pattern, match it.localStorage, inline <script> in <head> to prevent flicker, also check prefers-color-scheme on first visit if no localStorage value is set. Palette = shadcn defaults for Tier 1 (full brand system deferred to Tier 2).[email protected] / password, pre-verified. db/seeds.rb is guarded by Rails.env.development? || Rails.env.test? to prevent the seed running in production (dual-voice Eng review flagged this as a takeover vector). Production seed (if any) lives in db/seeds/production.rb and is opt-in.urlsafe_base64(32) token, stored as digest, 24-hour TTL, single-use, throttled at 5/hour/IP + 3/hour/email via Rack::Attack, session rotated on verification.Listed for tracking only. Each may become its own plan doc later.
create-react-on-rails-app --template tanstack flag — reopen the CLI question after Tier 1 ships and the maintained template proves stable; folding the starter's defaults back into the CLI flow is the natural distribution play once the maintenance burden is known.Quarterly review with a named DRI (not "owned by the team" — dual-voice review flagged that as ownership-by-nobody). DRI named at kick-off, rotates yearly.
2026.Q3, 2026.Q4, …).UPGRADING.md updates per release (what changed, migration steps for adopters).Calendar reminder + release checklist owned by the DRI. If the DRI changes roles, the role transfers before any quarterly slot is missed. Two missed quarters = the kit auto-archives with a banner pointing to the next maintained alternative — better to retire honestly than rot publicly.
The detailed implementation plan is the deliverable of the next phase. High-level phases:
create-react-on-rails-app --rsc --rspack. Land first CI green build. bin/doctor and the "first-5-minutes error budget" copy ship in this phase.bin/rails generate authentication, add name column + signup + email verification (full spec from Required-Before-Implementation #1), re-skin views with shadcn/ui, add letter_opener, ship WelcomeMailer. Post-signup verification UX is part of THIS phase, not back-filled later./dashboard with TanStack Router + Query (metric cards) + Table (Projects list). /settings/* nested routes. /projects/:id and /projects/new. Rendering mode drawer + outbound links to demo portfolio.prefers-color-scheme fallback.ProjectArchiveJob, SolidQueue config + Procfile.dev, Sentry initializer (commented). Retry exhaustion + DLQ test.AGENTS.md + canonical-reference comments. Authored against the actual reference implementations in the kit. Density target: monorepo's existing AGENTS.md.Procfile, .env.example, bin/setup, bin/doctor, UPGRADING.md, deploy docs (Mailgun/Postmark/Resend swap-in + RSC bundle precompile gotcha + production seed safety).Each phase is one PR. Each PR ships green CI before merge. Phase 0 (spike) is non-optional and runs BEFORE the public template repo flips from placeholder to populated.
All originally-listed open questions are now resolved or absorbed into the Phase 0 spike. Nothing here gates implementation.
AGENTS.md vs CLAUDE.mdAGENTS.md is canonical; CLAUDE.md is a thin pointer.User migrationAGENTS.md.-2026 namingreact-on-rails-starter-tanstack (locks identity to positioning, sidesteps the calendar-suffix maintenance trap).Active follow-up (separate discussion, NOT in this plan's scope): "Inertia on Pro" as a bridge product — tracked in #3371. Inertia's hard problem is RSC streaming; Pro solves it. The natural shape if Inertia ever wants RSC is on top of Pro. Discussed separately so the 2026 starter ships on TanStack-on-Rails without entangling Inertia-bridge product strategy.
These are pre-Phase-0 deliverables — gaps the autoplan dual-voice review surfaced that cannot ride along with implementation. Block phase 0 (spike) on items 3 and 5; block phase 1 (public scaffold push) on the rest.
Full spec, not a column name:
SecureRandom.urlsafe_base64(32), generated at signup or resend request.Digest::SHA256.hexdigest(token)). Compare via ActiveSupport::SecurityUtils.secure_compare.verification_sent_at column.EmailVerificationsController#create returns identical UX whether email exists or not.reset_session + re-issue the auth cookie).Rails.logger.tagged("auth") with user_id and IP.Justin's call: defer to post-Tier-1. Strict perf budgets aren't a ship blocker; the demo portfolio carries the perf story (Hacker News demo specifically), and ship velocity matters more here than measured Lighthouse numbers.
Tier 1 commits to "no obviously broken behavior" — dark-mode flicker test (Playwright assertion: zero pixels of wrong-mode before paint) is the one perf-adjacent check that stays in Tier 1 because it's a correctness bug, not a budget. Everything else (TTFB targets, Lighthouse gates, bundle-size budgets, Node renderer warmup script) moves to Required After Tier 1 Ships:
One-day spike, one throwaway branch, validates:
Fallback decision tree (committed BEFORE the spike, so the outcome doesn't require a new decision under pressure):
/dashboard; ship classic SSR via Pro Node renderer. Demo portfolio still carries the RSC story. The kit's TanStack-on-Rails identity holds.Tier 1 ships with:
Procfile.dev defining web / Rspack-watch / SolidQueue / Node-renderer processes.Procfile for production (web, worker, node-renderer)..env.example with every required ENV var (RAILS_MASTER_KEY, DATABASE_URL, SOLID_QUEUE_IN_PUMA, SENTRY_DSN, mail-provider vars).bin/setup runs the full prerequisite check + db prep + seed (dev only).bin/doctor smoke-checks Ruby/Postgres/Node/pnpm/bun versions + dependencies.db/seeds.rb guarded by Rails.env.development?; config.force_ssl = true in production.yml; production secret hints in .env.example.config/initializers/assets.rb (the Pro RSC pipeline emits a separate bundle that assets:precompile must include).docs/ set: 01-architecture.md, 02-vs-inertia.md, 03-customizing.md, 04-deploying.md, 05-troubleshooting.md, UPGRADING.md.-2026 naming resolutionRepo named react-on-rails-starter-tanstack. Locks identity to the strongest positioning rather than a calendar year. Sidesteps the maintenance trap of a year-suffixed name without giving up the freshness signal entirely (the kit's latest 2026.Q3 tag carries the calendar information).
If TanStack momentum stalls in some future year, the name degrades gracefully to "the modern Rails+React starter" identity — still a survivable downside.
Lock now, not back-fill at Phase 7:
# REFERENCE PATTERN: <name> — see AGENTS.md §N. Every canonical example file has one; non-canonical files do not. AI agents grep for REFERENCE PATTERN: to find the source-of-truth example per pattern.REFERENCE PATTERN: marker.git clone to dashboard visible (measured, not asserted).git clone … && cd … && bin/setup && bin/dev && open http://localhost:3000./letter_opener in development + surfaced via a "Dev Tools" nav item on the dashboard (development env only) alongside the SolidQueue dashboard.Pre-implementation:
aria-live on flash + errors, focus management on dialog + route changes, reduced-motion.Both Design voices: "is this a SaaS app dashboard, a rendering-tech demo, or a pattern catalog?" Pick one. Recommend: SaaS app dashboard first, rendering-tech and pattern catalog visible as drawers/links — not the lead. A first-time user lands on something that feels like a real product, not a lab demo. The rendering-mode drawer is for the curious; the demo-portfolio links are for the seriously evaluating.
UPGRADING.md ships in Tier 1, not "later."bin/upgrade-check script diffs the cloned repo against the latest tag and prints categorized changelist (config bumps vs structural changes vs adopter code).app/) vs what they should pull from upstream (bin/, config/, package.json defaults).internal/planning/examples-catalog-and-repo-naming-plan.md — naming taxonomy this plan extendsdocs/oss/getting-started/comparison-with-alternatives.md — Inertia vs React on Rails comparison this plan operationalizesDemo portfolio (the starter links to these instead of duplicating their proof points):
react-on-rails-demo-hacker-news-rsc — public-traffic perf demo (Pro + React 19 + RSC)react-on-rails-demo-marketplace-rsc — e-commerce / marketplace surfacereact-on-rails-demo-gumroad-rsc — creator dashboard with direct Inertia head-to-headreact_on_rails-demo-octochangelog-on-rails-pro — migration of an existing Rails app to Pro + RSCDual-voice review (Claude subagent + Codex) ran CEO, Design, Eng, and DX phases on the pre-rewrite plan. Headline:
| Phase | Subagent verdict | Codex verdict |
|---|---|---|
| CEO | "Would not fund as scoped" | "Fund with changes" |
| Design | "Ships the checklist of a starter, not the aesthetic" | 2/10 overall |
| Eng | "Not implementation-ready" | "Blocked on 6 items" |
| DX | "Cheap concrete fixes turn a 6/10 starter into 8.5/10" | (not run — superseded by reframes) |
Convergent dissent on the original strategic foundation (vibe-coders-as-primary, AGENTS.md-as-load-bearing, standalone-repo-as-the-artifact, two-dashboard demo, RSC-as-moat) drove the two reframes adopted at the top of this file. Convergent engineering findings (security, performance, deployment, a11y, TTHW, naming, freshening ownership) survived the reframes and live in Required Before Implementation.
Full dual-voice transcripts: ~/.gstack/projects/shakacode-react_on_rails/ and the conversation log from 2026-05-21 → 2026-05-22.