A modern, production-ready full-stack starter kit for building SaaS applications with Rails and React.
- Rails 8.0 with PostgreSQL
- React 19 with TypeScript
- Inertia.js for seamless SPA experience without the API complexity
- Shadcn/UI components with Tailwind CSS v4
- Vite for lightning-fast HMR and builds
- SolidQueue for background jobs
- SolidCache for caching
- SolidCable for Action Cable
- Ahoy for analytics tracking
- Paper Trail for audit logs
- Sentry for error tracking
- Resend for transactional emails
- Lograge for structured logging
- Strong Migrations for safe deployments
This project uses mise for managing development environments. mise ensures everyone on your team uses the exact same versions of Ruby, Node.js, and other tools.
# macOS
brew install mise
# Or use the installer script
curl https://mise.run | sh
# Add to your shell (if not already done)
echo 'eval "$(mise activate bash)"' >> ~/.bashrc # for bash
echo 'eval "$(mise activate zsh)"' >> ~/.zshrc # for zshYou'll also need these installed on your system:
- PostgreSQL 14+ (check with
psql --version)
# macOS
brew install postgresql@14
# Ubuntu/Debian
sudo apt-get install postgresql postgresql-client# Clone the starter kit with your app name
git clone git@github.com:growthxai/starter.git your-app-name
cd your-app-name
# Remove the original git history and start fresh
rm -rf .git
git init
git add .
git commit -m "Initial commit from Rails starter kit"The starter kit comes with a convenient script to rename your application:
# Use the rename script - replace "YourApp" with your actual app name
bin/rename-app YourApp
# Examples:
# bin/rename-app MyAwesomeApp
# bin/rename-app ProjectManager
# bin/rename-app SaaSPlatformThis script will automatically:
- Update the module name in
config/application.rb - Update database names in
config/database.yml - Update cable channel prefix in
config/cable.yml - Update package name in
package.json
After running the script, you'll need to:
- Update the page title in
app/views/layouts/application.html.erb - Drop and recreate your database (covered in Step 5)
# Install all required tool versions (Ruby, Node.js, Yarn)
mise install
# Trust the mise configuration for this project
mise trust
# Verify tools are installed
mise list
# Install Ruby dependencies
bundle install
# Install JavaScript dependencies
yarn installNote: For local environment overrides, create mise.toml.local (gitignored):
# mise.toml.local - Local overrides (not committed)
[env]
DATABASE_URL = "postgres://localhost/my_custom_db"# Create database, load schemas, and seed
bundle exec rails db:setup
# You should see: "Seed data created successfully!"# Start Rails, Vite, and SolidQueue
./bin/dev
# Your app is now running at http://localhost:3000Visit http://localhost:3000 and you should see the welcome page! π
Rails provides encrypted credentials for managing sensitive data like API keys and passwords. Never commit secrets to git!
Rails uses encrypted YAML files to store secrets:
config/credentials/development.yml.enc- Development environment secretsconfig/credentials/production.yml.enc- Production environment secretsconfig/credentials/test.yml.enc- Test environment secrets
Each environment has its own encryption key:
config/credentials/development.key- Never commit this!config/credentials/production.key- Never commit this!
# Edit development credentials
EDITOR="code --wait" rails credentials:edit --environment development
# Or use nano/vim
EDITOR="nano" rails credentials:edit --environment developmentThis opens a YAML file where you add your secrets:
# Example credentials file
stripe:
publishable_key: pk_test_51ABC...
secret_key: sk_test_51ABC...
aws:
access_key_id: AKIA...
secret_access_key: abc123...
google:
client_id: 123456789.apps.googleusercontent.com
client_secret: GOCSPX-abc123...
resend:
api_key: re_abc123...
# You can organize however makes sense
third_party:
service_a:
api_key: key_abc123
service_b:
token: token_xyz789# In Rails controllers, models, etc.
Rails.application.credentials.stripe[:secret_key]
Rails.application.credentials.aws[:access_key_id]
Rails.application.credentials.dig(:third_party, :service_a, :api_key)
# In environment variables (for gems that expect ENV vars)
# Add to config/initializers/credentials.rb:
ENV["STRIPE_SECRET_KEY"] = Rails.application.credentials.stripe[:secret_key]To share credentials with your team:
-
Share the key file securely (1Password)
-
Team member setup:
# Create the key file mkdir -p config/credentials echo "your-key-here" > config/credentials/development.key # Verify it works rails credentials:show --environment development
- Sign up for a free account at Resend
- Get your API key from the dashboard
- Add to your
.envfile:RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxx
- Update
app/mailers/application_mailer.rbwith your "from" email - Test email sending in Rails console:
UserMailer.welcome_email(User.first).deliver_now
The starter kit comes with Shadcn/UI pre-configured. To add new components:
# See all available components
npx shadcn@latest add
# Add a specific component (e.g., card)
npx shadcn@latest add card
# Add multiple components
npx shadcn@latest add card badge alertComponents are added to app/frontend/components/ui/ and can be imported anywhere:
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';your-app/
βββ app/
β βββ controllers/
β β βββ concerns/
β β βββ breadcrumbable.rb # Breadcrumb DSL
β β βββ react_layout.rb # Layout selection
β β βββ inertia_configuration.rb
β βββ models/
β βββ views/
β β βββ shared/
β β βββ _pagination.json.jbuilder # Pagy pagination partial
β βββ frontend/
β β βββ pages/ # Inertia page components
β β βββ components/
β β β βββ ui/ # Shadcn/UI components
β β β βββ pagination.tsx # Pagination wrapper
β β β βββ workspace-sidebar.tsx
β β βββ layouts/
β β β βββ application.tsx # Default layout
β β β βββ workspace.tsx # Sidebar + header
β β β βββ fullscreen.tsx # Minimal layout
β β βββ types/
β β β βββ pagination.ts # PaginationData interface
β β βββ lib/
β β β βββ layout-resolver.ts # Dynamic layout resolution
β β βββ hooks/
β βββ jobs/
βββ config/
β βββ routes.rb
β βββ database.yml
β βββ initializers/
βββ db/
β βββ migrate/
β βββ seeds.rb
βββ test/
The starter includes three layouts:
- ApplicationLayout - Default, minimal layout for public pages
- WorkspaceLayout - Sidebar + header with breadcrumbs for authenticated pages
- FullscreenLayout - No chrome, for focused tasks (onboarding, auth)
- Create a Rails controller with layout and breadcrumbs:
# app/controllers/dashboard_controller.rb
class DashboardController < ApplicationController
react_layout "workspace"
def show
breadcrumb "Dashboard"
render inertia: "dashboard/show", props: { stats: { users: User.count } }
end
end- Create the React component:
// app/frontend/pages/dashboard/show.tsx
interface Props {
stats: { users: number };
}
export default function Show({ stats }: Props) {
return <div>Total users: {stats.users}</div>;
}- Add the route:
# config/routes.rb
resource :dashboardBreadcrumbs are server-driven and automatically displayed in the WorkspaceLayout header. Call breadcrumb(text, href) in your controller actions β the last item (no href) renders as plain text, others as links:
class ProjectsController < ApplicationController
react_layout "workspace"
def show
@project = Project.find(params[:id])
breadcrumb "Projects", projects_path
breadcrumb @project.name
end
endFor nested resources, use before_action to build a shared prefix:
class Projects::TasksController < ApplicationController
react_layout "workspace"
before_action :set_project
before_action :set_base_breadcrumbs
def index
breadcrumb "Tasks"
end
def show
@task = @project.tasks.find(params[:id])
breadcrumb "Tasks", project_tasks_path(@project)
breadcrumb @task.title
end
private
def set_project
@project = Project.find(params[:project_id])
end
def set_base_breadcrumbs
breadcrumb "Projects", projects_path
breadcrumb @project.name, project_path(@project)
end
endThe starter includes a pagination component that works with Pagy and js-routes:
- In your controller:
def index
@pagy, @projects = pagy(Project.all)
render inertia: "projects/index"
end- In your jbuilder view (
app/views/projects/index.json.jbuilder):
json.projects @projects, partial: "projects/project", as: :project
json.partial! "shared/pagination", pagy: @pagy- In your React component:
import Pagination from '@/components/pagination';
import { projects_path } from '@/rails/routes';
import type { PaginationData } from '@/types/pagination';
interface Props {
projects: Project[];
pagination: PaginationData;
}
export default function Index({ projects, pagination }: Props) {
return (
<div>
{/* Your project list */}
<Pagination pagination={pagination} pathBuilder={projects_path} />
</div>
);
}If you're improving the starter kit itself (not building an app from it), you don't need to clone or rename anything. Just work directly in the repo.
# Install tool versions
mise install && mise trust
# Install dependencies
bundle install && yarn install
# Set up the database
bundle exec rails db:setup
# Start the dev server
bin/devThe app runs at http://localhost:3000.
Always run the quality checks:
bin/checkThis runs Prettier formatting, the test suite, and Rubocop. All three must pass.
- Follow the conventions in
CLAUDE.mdβ it's the source of truth for patterns and architecture decisions - Use jbuilder views for Inertia props, not inline
props:in controllers - Keep controllers to the 7 standard RESTful actions
- Use
kebab-casefor all frontend filenames - Run
bin/checkbefore every commit