This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
bin/dev # Start Rails + Vite dev servers (via Procfile.dev)
bin/rails db:setup # Create and seed databasebin/rails test # Run unit/integration tests
bin/rails test:system # Run Capybara system tests (requires Chrome)
bin/rails test test/path/to/test_file.rb # Run a single test file
bin/rails test test/path/to/test_file.rb:42 # Run a single test at linebin/rubocop # Lint Ruby (Rails Omakase style)
bin/rubocop -a # Auto-fix Ruby offenses
npm run lint # Lint TypeScript with Biome
npm run lint:fix # Auto-fix TypeScript
npm run format # Format frontend code
npm run check # TypeScript type checkingbin/brakeman # Rails security scan
bin/bundler-audit # Gem vulnerability auditbin/rails types_from_serializers:generate # Regenerate TypeScript types from serializersThis app uses Inertia.js to bridge Rails and React — no traditional API. Controllers render Inertia responses (not JSON or HTML views), passing serialized data as props directly to React page components.
- Rails controller calls
render inertia: 'ComponentName', props: { data: serializer.new(record) } InertiaController(base controller) sharesflashand authenticateduserprops on every request- Serializers (in
app/serializers/) useoj_serializersand transform keys to camelCase for React types_from_serializersgem generates TypeScript types from serializers — run the rake task after modifying serializers- React page components receive typed props and render without additional API calls
React pages live in app/frontend/pages/ and are resolved by name in app/frontend/entrypoints/inertia.tsx. Each page file name matches the component name passed to render inertia:.
Routes are split into modular files under config/routes/:
public.rb— unauthenticated pagesdevise.rb— auth routesdemo.rb— demo/example routesitems.rb— CRUD resource routes
JS route helpers are generated by the js-routes gem and available in app/frontend/lib/.
- Devise handles authentication (email/password + Google OAuth2)
- ActionPolicy handles authorization via policy classes in
app/policies/ - Controllers call
authorize!before actions;authorized_scopefor collections ItemPolicyrestricts all CRUD to the record owner viaowner?predicate
Soft deletes: Item uses the Discard gem. A default_scope filters to kept records — use Item.with_discarded to include soft-deleted records.
Audit trail: Item has has_paper_trail — all changes are stored in the versions table with whodunnit (user ID).
Serialization: Extend BaseSerializer which auto-converts keys to camelCase. Add new serializers here when exposing new models to the frontend. The BaseSerializer is in app/serializers/base_serializer.rb.
Pagination: Use Pagy for collections. Pass @pagy metadata via PagySerializer to the frontend.
Frontend imports: Use @/ or ~/ path aliases (both resolve to app/frontend/).
| Layer | Technology |
|---|---|
| Backend | Rails 8.1, Ruby 3.3 |
| Frontend | React 19, TypeScript 5, Vite 7 |
| Styling | Tailwind CSS v4 |
| UI components | Radix UI + shadcn/ui pattern |
| DB (dev/test) | SQLite (separate DBs for cache/queue/cable) |
| Auth | Devise + OmniAuth Google |
| Authorization | ActionPolicy |
| Background jobs | Solid Queue + Mission Control Jobs |
| Linting (Ruby) | RuboCop (Rails Omakase) |
| Linting (JS) | Biome (single quotes, no semicolons, 100 char width) |
| Git hooks | Lefthook (auto-format on commit, test on push) |
See .env.example for all required variables. Key ones for local development: APP_HOST, APP_PORT, and Google OAuth credentials (GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET).