Files
turbovault-app/docs/DEVELOPMENT_GUIDE.md
2026-03-29 02:37:49 -04:00

13 KiB

TurboVault - Development Guide

Quick Start

# Start PostgreSQL
task docker:up

# Setup database (first time only)
task db:setup

# Start Rails server
task server

# Or use bin/dev for Tailwind watch mode
bin/dev

Visit http://localhost:3000 and create an account!

Development Workflow

Daily Development

# Start all services
task docker:up  # PostgreSQL
bin/dev         # Rails server + Tailwind watcher

Database Operations

# Create a new migration
rails generate migration AddFieldToModel field:type

# Run migrations
task db:migrate

# Rollback last migration
task db:rollback

# Reset database (drops, creates, migrates, seeds)
task db:reset

# Open Rails console
task console

# Check pending migrations
rails db:migrate:status

Generating Code

# Generate a model
rails generate model ModelName field:type

# Generate a controller
rails generate controller ControllerName action1 action2

# Generate a scaffold (model + controller + views)
rails generate scaffold ModelName field:type

# Destroy generated code
rails destroy model ModelName

Running Tests

# Run all tests
task test

# Run specific test file
rails test test/models/game_test.rb

# Run specific test
rails test test/models/game_test.rb:10

# Run system tests
task test:system

Code Quality

# Run RuboCop linter
task lint

# Auto-fix RuboCop issues
task lint:fix

# Security checks
task security

Type Checking with Sorbet

TurboVault uses Sorbet for gradual static type checking.

# First time setup (after bundle install)
task tapioca:init
task tapioca:all

# Run type checker
task typecheck

# Watch mode (re-checks on file changes)
task typecheck:watch

# Update type definitions after gem/model changes
task tapioca:gems  # After bundle install
task tapioca:dsl   # After model changes

See SORBET.md for complete type checking guide.

Project Structure

app/
├── controllers/
│   ├── concerns/         # Shared controller modules
│   ├── api/v1/          # API controllers
│   └── *.rb             # Web controllers
├── models/
│   ├── concerns/         # Shared model modules
│   └── *.rb             # ActiveRecord models
├── views/
│   ├── layouts/          # Application layouts
│   └── */               # Controller-specific views
├── helpers/              # View helpers
├── mailers/              # Email mailers
└── jobs/                 # Background jobs

config/
├── routes.rb             # URL routing
├── database.yml          # Database configuration
└── environments/         # Environment-specific config

db/
├── migrate/              # Database migrations
├── seeds.rb              # Seed data
└── schema.rb             # Current database schema

test/
├── models/               # Model tests
├── controllers/          # Controller tests
├── system/               # End-to-end tests
└── fixtures/             # Test data

Common Tasks

Adding a New Model

  1. Generate the model:
rails generate model Post user:references title:string body:text published:boolean
  1. Edit the migration (add indexes, constraints, RLS if user-scoped):
class CreatePosts < ActiveRecord::Migration[8.1]
  def change
    create_table :posts do |t|
      t.references :user, null: false, foreign_key: true, index: true
      t.string :title, null: false
      t.text :body
      t.boolean :published, default: false, null: false
      t.timestamps
    end
    
    add_index :posts, :title
    
    # Enable RLS if user-scoped
    execute <<-SQL
      ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
      CREATE POLICY posts_isolation_policy ON posts
        USING (user_id = current_setting('app.current_user_id', true)::bigint);
    SQL
  end
end
  1. Run the migration:
rails db:migrate
  1. Add associations and validations to the model:
class Post < ApplicationRecord
  belongs_to :user
  
  validates :title, presence: true
  validates :body, presence: true
  
  scope :published, -> { where(published: true) }
end
  1. Add association to User model:
class User < ApplicationRecord
  has_many :posts, dependent: :destroy
end

Adding a New Controller

  1. Generate the controller:
rails generate controller Posts index show new create edit update destroy
  1. Implement controller actions:
class PostsController < ApplicationController
  before_action :require_authentication
  before_action :set_post, only: [:show, :edit, :update, :destroy]
  
  def index
    @posts = current_user.posts.order(created_at: :desc)
  end
  
  def show
  end
  
  def new
    @post = current_user.posts.build
  end
  
  def create
    @post = current_user.posts.build(post_params)
    
    if @post.save
      redirect_to @post, notice: "Post created successfully."
    else
      render :new, status: :unprocessable_entity
    end
  end
  
  def edit
  end
  
  def update
    if @post.update(post_params)
      redirect_to @post, notice: "Post updated successfully."
    else
      render :edit, status: :unprocessable_entity
    end
  end
  
  def destroy
    @post.destroy
    redirect_to posts_path, notice: "Post deleted successfully."
  end
  
  private
  
  def set_post
    @post = current_user.posts.find(params[:id])
  end
  
  def post_params
    params.require(:post).permit(:title, :body, :published)
  end
end
  1. Add routes:
resources :posts
  1. Create views in app/views/posts/

Adding an API Endpoint

  1. Create API controller:
mkdir -p app/controllers/api/v1
  1. Create controller file:
# app/controllers/api/v1/posts_controller.rb
module Api
  module V1
    class PostsController < BaseController
      def index
        @posts = current_user.posts.order(created_at: :desc)
        render json: @posts
      end
      
      def show
        @post = current_user.posts.find(params[:id])
        render json: @post
      end
      
      def create
        @post = current_user.posts.build(post_params)
        
        if @post.save
          render json: @post, status: :created
        else
          render json: { errors: @post.errors.full_messages }, 
                 status: :unprocessable_entity
        end
      end
      
      private
      
      def post_params
        params.require(:post).permit(:title, :body, :published)
      end
    end
  end
end
  1. Add API routes:
namespace :api do
  namespace :v1 do
    resources :posts, only: [:index, :show, :create]
  end
end

Adding a Background Job

  1. Generate the job:
rails generate job ProcessData
  1. Implement the job:
class ProcessDataJob < ApplicationJob
  queue_as :default
  
  def perform(user_id)
    user = User.find(user_id)
    # Do some processing
  end
end
  1. Enqueue the job:
ProcessDataJob.perform_later(user.id)

Adding Email Functionality

  1. Generate a mailer:
rails generate mailer UserMailer welcome
  1. Implement the mailer:
class UserMailer < ApplicationMailer
  def welcome(user)
    @user = user
    mail(to: @user.email, subject: "Welcome to TurboVault!")
  end
end
  1. Create email templates in app/views/user_mailer/

  2. Configure SMTP in config/environments/:

config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
  address: ENV['SMTP_ADDRESS'],
  port: ENV['SMTP_PORT'],
  user_name: ENV['SMTP_USERNAME'],
  password: ENV['SMTP_PASSWORD'],
  authentication: 'plain',
  enable_starttls_auto: true
}
  1. Send the email:
UserMailer.welcome(user).deliver_later

Testing Guide

Writing Model Tests

# test/models/game_test.rb
require "test_helper"

class GameTest < ActiveSupport::TestCase
  test "should not save game without title" do
    game = Game.new
    assert_not game.save, "Saved game without title"
  end
  
  test "should save valid game" do
    game = games(:one)  # Uses fixture
    assert game.save, "Failed to save valid game"
  end
  
  test "should belong to user" do
    game = games(:one)
    assert_respond_to game, :user
  end
end

Writing Controller Tests

# test/controllers/games_controller_test.rb
require "test_helper"

class GamesControllerTest < ActionDispatch::IntegrationTest
  setup do
    @user = users(:one)
    sign_in_as @user  # Helper method to sign in
    @game = games(:one)
  end
  
  test "should get index" do
    get games_url
    assert_response :success
  end
  
  test "should create game" do
    assert_difference('Game.count') do
      post games_url, params: {
        game: {
          title: "New Game",
          platform_id: platforms(:one).id,
          format: "physical"
        }
      }
    end
    
    assert_redirected_to game_path(Game.last)
  end
end

Writing System Tests

# test/system/games_test.rb
require "application_system_test_case"

class GamesTest < ApplicationSystemTestCase
  setup do
    @user = users(:one)
    sign_in_as @user
  end
  
  test "visiting the index" do
    visit games_url
    assert_selector "h1", text: "My Games"
  end
  
  test "creating a game" do
    visit games_url
    click_on "Add Game"
    
    fill_in "Title", with: "Test Game"
    select "Nintendo 64", from: "Platform"
    select "Physical", from: "Format"
    
    click_on "Create Game"
    
    assert_text "Game was successfully created"
  end
end

Debugging

Rails Console

rails console

# Test queries
User.first
Game.where(format: :physical).count
current_user.games.includes(:platform)

# Test RLS
ActiveRecord::Base.connection.execute(
  "SET LOCAL app.current_user_id = 1"
)

Logs

# Tail development log
tail -f log/development.log

# View specific log
cat log/test.log

Debug with Byebug

Add to your code:

require 'debug'
debugger  # Execution will pause here

Then interact in the terminal:

n     # Next line
c     # Continue
p var # Print variable
exit  # Exit debugger

Performance Tips

Avoid N+1 Queries

Bad:

@games = current_user.games
# In view: @games.each { |g| g.platform.name }  # N+1!

Good:

@games = current_user.games.includes(:platform)

Use Database Indexes

add_index :games, :title
add_index :games, [:user_id, :platform_id]

Use Counter Caches

class Collection < ApplicationRecord
  has_many :games, counter_cache: true
end

Pagination

@games = current_user.games.page(params[:page]).per(25)

Environment Variables

Create .env for development (never commit!):

DATABASE_HOST=localhost
DATABASE_USERNAME=postgres
DATABASE_PASSWORD=postgres
SMTP_ADDRESS=smtp.example.com
SMTP_USERNAME=user@example.com
SMTP_PASSWORD=secret

Load with:

# config/application.rb
config.before_configuration do
  env_file = Rails.root.join('.env')
  if File.exist?(env_file)
    File.readlines(env_file).each do |line|
      key, value = line.split('=', 2)
      ENV[key.strip] = value.strip if key && value
    end
  end
end

Deployment

Already configured! Just:

# First time setup
kamal setup

# Deploy
kamal deploy

# Check status
kamal app exec --interactive --reuse "bin/rails console"

Railway/Render

  1. Push to Git
  2. Connect repository
  3. Set environment variables
  4. Add build command: bundle install && rails db:migrate
  5. Add start command: rails server -b 0.0.0.0

Troubleshooting

Database Connection Errors

# Check if PostgreSQL is running
docker compose ps

# Start PostgreSQL
task docker:up

# Check database configuration
cat config/database.yml

Asset Issues

# Rebuild assets
rails assets:precompile

# Rebuild Tailwind
rails tailwindcss:build

Migration Issues

# Check migration status
rails db:migrate:status

# Rollback and retry
rails db:rollback
rails db:migrate

Resources

Getting Help

  1. Check the logs: tail -f log/development.log
  2. Use Rails console: rails console
  3. Check database: rails dbconsole
  4. Read the error message carefully
  5. Search Stack Overflow
  6. Check Rails Guides

Best Practices

  1. Always filter by current_user in controllers
  2. Use strong parameters for mass assignment
  3. Add validations to models
  4. Write tests for new features
  5. Keep controllers thin - move logic to models
  6. Use concerns for shared code
  7. Keep views simple - use helpers for complex logic
  8. Add indexes for frequently queried columns
  9. Use scopes for common queries
  10. Document your API endpoints

Happy coding! 🚀