This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Backend:
- Ruby 3.4.7 + Rails 8.1.1
- PostgreSQL database with UUIDv7 primary keys
- Puma web server
Frontend:
- React 19.2.0 + TypeScript 5.9.3
- Inertia.js 2.2.15 (server-driven reactive frontend)
- Vite 7.1.12 (bundler) + vite-plugin-ruby
- Tailwind CSS 4.1.16 (CSS-in-JS with native @import syntax)
- shadcn/ui component system
Authentication:
- Devise for authentication framework
- Google OAuth2 via omniauth-google-oauth2
- Environment variables via dotenv-rails (development/test only)
Development:
- Overmind/Foreman process manager
- RuboCop with rails-omakase style guide
- TypeScript strict mode enabled
This is a server-side React application using Inertia.js, not a traditional SPA:
- Rails serves pages and controls routing
- Inertia.js bridges Rails controllers to React components
- React components receive props from Rails and handle interactivity
- Vite compiles TypeScript/React; Rails serves the compiled assets
app/frontend/ # Single unified frontend directory (configured in config/vite.json)
├── entrypoints/ # Build entry points
│ ├── inertia.ts # Inertia app setup & page resolver
│ └── application.css # Tailwind imports & theme config
├── pages/ # Inertia page components (auto-loaded by glob)
│ ├── InertiaExample.tsx
│ ├── Home.tsx
│ ├── auth/
│ ├── blog/
│ ├── utils/
│ └── v1/
├── components/ # Shared component library
│ ├── ui/ # shadcn/ui components
│ ├── Layout.tsx
│ ├── Header.tsx
│ └── ...
├── lib/ # Utility functions
│ ├── utils.ts # cn() helper
│ └── i18n.ts # i18n configuration
├── contexts/ # React contexts
├── locales/ # i18n translations
└── assets/ # Static assets (SVGs, etc.)
Why a single directory?
- Simpler mental model: all frontend code in one place
- Easier imports: no confusion about directory boundaries
- Consistent @/ alias: points to the entire frontend codebase
- All files scanned by Tailwind, TypeScript, and linters
- No importmap - Everything bundled through Vite
Pages are resolved via Vite glob in app/frontend/entrypoints/inertia.ts:
const pages = import.meta.glob<ResolvedComponent>('../pages/**/*.tsx', { eager: true })
const page = pages[`../pages/${name}.tsx`]Flow:
- Rails controller:
render inertia: "InertiaExample", props: { name: "World" } - Inertia finds matching component:
app/frontend/pages/InertiaExample.tsx - React component receives props and renders
Adding a new page:
- Create
.tsxfile inapp/frontend/pages/ - Add route in
config/routes.rb - Add controller action that calls
render inertia: "PageName"
bin/dev # Runs both Vite dev server (port 3036) and Rails (port 3000)The bin/dev script uses overmind/hivemind/foreman to run Procfile.dev:
vite: bin/vite dev- Vite dev server with HMRweb: bin/rails s- Rails server
# Type checking
npm run check # Check TypeScript without emitting
# Ruby linting
rubocop # Run RuboCop style checker
# Asset compilation
bin/vite build # Build production assets
# Database
bin/rails db:migrate # Run migrations
bin/rails db:seed # Seed database
# Rails console
bin/rails c # Open Rails console- React/TypeScript changes: Auto-reload via Vite HMR
- CSS changes: Auto-reload via Tailwind JIT
- Rails controller/model changes: Require manual server restart
This application uses UUIDv7 (time-ordered UUIDs) for all primary keys instead of traditional auto-incrementing integers.
Configuration:
config/application.rbsetsprimary_key_type: :uuidfor all generators- Ruby 3.3+ provides
SecureRandom.uuid_v7for UUID generation - Rails 8.1 automatically generates UUIDs at the application level (no database default needed)
Benefits:
- Time-ordered: UUIDs are sortable by creation time (better B-tree index performance)
- Globally unique: No ID conflicts when merging databases or in distributed systems
- Security: Non-sequential IDs prevent enumeration attacks
- Privacy: User IDs are not guessable
Creating new models:
bin/rails generate model Article title:string
# Automatically creates with UUID primary keyMigration example:
create_table :articles, id: :uuid do |t|
t.uuid :author_id, null: false # UUID foreign key
t.string :title
t.timestamps
end
add_foreign_key :articles, :users, column: :author_idImportant notes:
- All new tables use UUID by default
- Foreign keys to UUID tables must also be
:uuidtype - PaperTrail's
versionstable usesstringtype foritem_idto support both integers and UUIDs
-
vite.config.ts- Vite configuration with React, Tailwind, and Ruby plugins- Sets
@/alias to./app/frontend - Configures path resolution for imports
- Security: Source maps disabled in production builds to prevent code exposure
- Production optimization: Console statements automatically removed in production
- Sets
-
tsconfig.json- Root TypeScript config with project references -
tsconfig.app.json- App TypeScript config (strict mode, includes app/frontend) -
tsconfig.node.json- Vite config TypeScript types -
config/vite.json- Vite Ruby configurationsourceCodeDir: "app/frontend"- Development port: 3036
- Test port: 3037
config/routes.rb- Application routesconfig/initializers/inertia_rails.rb- Inertia.js setup and version tracking- Note: Shared props (like
current_user) were removed. Add them back if needed viaconfig.share
- Note: Shared props (like
config/initializers/devise.rb- Devise authentication configuration- Google OAuth2 credentials loaded from ENV variables
config/application.rb- Rails app initialization
-
app/frontend/entrypoints/application.css- Main Tailwind configuration- Uses Tailwind v4 syntax:
@import "tailwindcss" @sourcedirective tells Tailwind where to scan for classes:@source "../**/*.{js,ts,jsx,tsx}"; # Scans all of app/frontend/
- Theme configuration with CSS variables for design tokens
- Custom variant for dark mode:
@custom-variant dark (&:is(.dark *))
- Uses Tailwind v4 syntax:
-
components.json- shadcn/ui configuration for component generation
Key differences from v3:
- No
tailwind.config.jsfile (configuration in CSS) - Use
@sourcedirective to specify scan paths - Use
@theme inlinefor design tokens - Use
@pluginfor plugins instead of config file - Automatic content detection via
@tailwindcss/viteplugin
Adding Tailwind classes:
- Classes are automatically scanned from files in
@sourcepaths - No need to update config when adding new classes
- JIT compilation happens automatically
Use the @/ alias for app/frontend imports:
// Good
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
// Avoid
import { Button } from '../../app/frontend/components/ui/button'Relative imports for local files:
// In app/frontend/pages/
import styles from './InertiaExample.module.css'The project uses shadcn/ui with configuration in components.json:
# Add a new component
npx shadcn-ui@latest add [component-name]Components are installed to app/frontend/components/ui/ and use the @/ alias.
Important: shadcn components require:
- Tailwind CSS properly configured (already done)
cn()utility from@/lib/utils- CSS imported in Inertia entrypoint:
import './application.css'ininertia.ts
This app uses Devise with Google OAuth2 for authentication.
Required environment variables (set in .env for development):
GOOGLE_CLIENT_ID=your_google_client_id_here
GOOGLE_CLIENT_SECRET=your_google_client_secret_here
Important: Never commit .env file. Use .env.example as a template.
The User model (app/models/user.rb) includes:
- Standard Devise modules:
:database_authenticatable, :registerable, :recoverable, :rememberable, :validatable - OmniAuth module:
:omniauthable, omniauth_providers: [:google_oauth2] - OAuth fields:
provider,uid,name,avatar_url from_omniauthclass method for OAuth callback handling
- User clicks "Sign In" → redirects to
/users/sign_in - Custom sessions controller renders Inertia page:
auth/Login - Login page uses POST request with CSRF token to
/users/auth/google_oauth2 - OmniAuth redirects to Google OAuth consent screen
- Google redirects back to
/users/auth/google_oauth2/callback Users::OmniauthCallbacksControllerhandles callback viafrom_omniauth- User is signed in and redirected to home page
app/controllers/users/sessions_controller.rb- Custom sessions controller for Inertia integrationapp/controllers/users/omniauth_callbacks_controller.rb- Handles Google OAuth2 callbacks
The app uses omniauth-rails_csrf_protection gem, which requires:
- OAuth requests must be POST (not GET)
- Must include CSRF token in the request
- Direct browser access to
/users/auth/google_oauth2will show "Authentication passthru" error
In controllers:
current_user # Devise helper
user_signed_in? # Check if user is authenticatedTo pass user data to Inertia pages, add to config/initializers/inertia_rails.rb:
config.share do |controller|
{
current_user: controller.current_user&.as_json(only: [:id, :email, :name, :avatar_url])
}
end- Create the page component in
app/javascript/pages/:
// app/javascript/pages/UserProfile.tsx
import { Head } from '@inertiajs/react'
export default function UserProfile({ user }: { user: { name: string } }) {
return (
<>
<Head title={`${user.name}'s Profile`} />
<div>
<h1>{user.name}</h1>
</div>
</>
)
}- Add route in
config/routes.rb:
get 'users/:id', to: 'users#show'- Create controller action:
class UsersController < ApplicationController
def show
user = User.find(params[:id])
render inertia: 'UserProfile', props: {
user: { name: user.name }
}
end
endImport from @/components:
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
export default function MyPage() {
return (
<div className={cn('p-4', 'bg-background')}>
<Button variant="default" size="lg">
Click me
</Button>
</div>
)
}If you see "Cannot find module '@/...'" errors:
-
Check
tsconfig.app.jsonhas correct paths incompilerOptions:"paths": { "@/*": ["./app/frontend/*"] }
-
Check
vite.config.tshas matching alias:resolve: { alias: { '@': path.resolve(__dirname, './app/frontend'), }, }
-
Restart TypeScript language server in your editor
If Tailwind classes appear in HTML but have no effect:
- Check
@sourcepaths inapp/javascript/entrypoints/application.css - Verify CSS is imported in
inertia.ts:import './application.css' - Restart Vite dev server (
bin/dev) - Check browser console for CSS loading errors
If you get "Missing Inertia page component" error:
- Verify file exists at exact path:
app/javascript/pages/${name}.tsx - Check page name matches controller render call exactly (case-sensitive)
- Restart dev server (glob patterns are cached)
If you see "Not found. Authentication passthru." when accessing OAuth URLs:
- Never access OAuth URLs directly in browser - they require POST with CSRF token
- Use the proper authentication flow (sign in button → login page → OAuth button)
- Verify environment variables are set (restart server after adding
.env) - Check Google Cloud Console redirect URI matches your app's callback URL
The main layout is app/views/layouts/application.html.erb:
<%= vite_stylesheet_tag "application" %>
<%= vite_react_refresh_tag %>
<%= vite_client_tag %>
<%= vite_typescript_tag "inertia" %>Key helper tags:
vite_client_tag- Vite dev client for HMRvite_react_refresh_tag- React Fast Refreshvite_typescript_tag "inertia"- Loads the Inertia.js entrypointvite_stylesheet_tag "application"- Loads Tailwind CSS
Ruby:
- Follow
rubocop-rails-omakasestyle guide - 2-space indentation
- Run
rubocopbefore committing
TypeScript:
- Strict mode enabled
- No unused locals or parameters
- Use
npm run checkto validate types - Prefer named exports over default exports (except for pages)
This app supports both traditional Rails ERB views and Inertia React pages:
- ERB pages: Use traditional Rails rendering (controller renders view directly)
- Inertia pages: Use React components (controller uses
render inertia: "PageName")
The layout file includes both Hotwire and Inertia assets, allowing gradual migration.
For detailed Google OAuth2 setup instructions, see GOOGLE_OAUTH_SETUP.md which includes:
- Creating a Google Cloud project
- Configuring OAuth consent screen
- Creating OAuth 2.0 credentials
- Setting up authorized redirect URIs
- Troubleshooting common OAuth errors
Quick setup:
- Get credentials from Google Cloud Console
- Copy
.env.exampleto.env - Add your
GOOGLE_CLIENT_IDandGOOGLE_CLIENT_SECRET - Restart the Rails server
- Add callback URL to Google Cloud Console:
http://localhost:3000/users/auth/google_oauth2/callback(adjust port if needed)