cover-letter-llm

Full-Stack Implementation Plan for cover-letter-llm

This document outlines a best-practices-based implementation roadmap using Rails 7.1+, LLM integration, modern frontend stack, and scalable architecture.


Table of Contents


1. Project Setup

Rails App

Install Ruby (Windows):

  1. Download and install Ruby for Windows:
    • Go to https://rubyinstaller.org/
    • Download the latest Ruby+Devkit installer (recommended).
    • Run the installer and follow the prompts. Make sure to check the box to add Ruby to your PATH during installation.
  2. After installation, restart your terminal and verify Ruby is installed: ```Bash ruby -v gem -v

If command is not found after restarting terminal use external terminal (outside of VS Code) for Ruby/Rails commands or close VS Code, run code . from new terminal, then navigate back to the project to refresh the PATH.


3. Install Rails:
```Bash
gem install rails
  1. Create a new Rails application and change into that directory (where CoverLetterAppis the name of the Rails Project):
    rails new CoverLetterApp --database=postgresql --css=tailwind --javascript=esbuild
    cd CoverLetterApp
    
  2. Install PostgreSQL:
    • Download and install PostgreSQL from the official site:https://www.postgresql.org/download/windows/
    • Add PostgreSQL bin directory to your PATH After installation, add the PostgreSQL bin directory (e.g., C:\Program Files\PostgreSQL\15\bin) to your system PATH environment variable. This directory contains pg_config.exe.
    • Restart your terminal Close and reopen your terminal (or VS Code) so the new PATH is recognized.
    • Verify pg_config is available:
      pg_config --version
      
  3. Install Bundler:
    bundle install
    bundler -v
    

Gems (in Gemfile)

Add these dependancies to Gemfile:

gem 'devise'
gem 'sidekiq'
gem 'redis'
gem 'rspec-rails', group: [:development, :test]
gem 'factory_bot_rails', group: [:development, :test]
gem 'faker', group: [:development, :test]
gem 'shoulda-matchers', group: [:test]
gem 'gemini-ai', '~> 4.2.0' # or latest stable version
gem 'view_component'

RSpec Install

bundle install
rails generate rspec:install

# Confirm installation
undle exec rspec --version
bundle exec rspec
# Confirm rspec is installed by searching for rspec-rails in Gemfile.lock and /spec in Rails root dir

Modern Frontend Build System & Hotwire Integration

Install Node.js and Yarn (or npm) installed, as jsbundling-rails and cssbundling-rails will use them.

What to do if they are not installed:

After installation, close and reopen your terminal, then try the version commands again to confirm.

node -v
yarn --version
npm -v

A lot of the following steps are confirmed by the file(s)/dependancies being present in the Rails App Dir

Step 1: Confirm jsbundling-rails (with esbuild) is set up.

  1. Check Gemfile: Open your Gemfile and look for gem "jsbundling-rails".

  2. Check package.json: Open your package.json (in the root of your Rails app) and look for "esbuild" in devDependencies. Also, check for a build script, e.g., "build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=/assets".

    • To generate package.json (if it does not exist)
      cd CoverLetterApp
      rails javascript:install:esbuild
      # This generates package.json and adds esbuild as dev dependency
      

Step 2: Install and configure cssbundling-rails.

  1. Add the gem:
    bundle add cssbundling-rails
    
  2. Run the installer for your chosen CSS framework. Let’s assume you want to use Tailwind CSS (as it’s very common). You can replace tailwind with bootstrap, bulma, postcss, or sass.
    rails css:install:tailwind
    

    This will typically:

    • Add necessary Node.js packages (e.g., tailwindcss) to your package.json.
    • Create configuration files (e.g., tailwind.config.js, postcss.config.js).
    • Create an entry point CSS file (e.g., app/assets/stylesheets/application.tailwind.css).
    • Add a CSS build script to package.json.
    • Add app/assets/builds to .gitignore (if not already there).
    • Add a css: line to Procfile.dev (e.g., css: yarn build:css --watch).
    • Update app/assets/config/manifest.js to include //= link application.css (if not already there) and ensure your layout file (e.g., app/views/layouts/application.html.erb) links to the compiled stylesheet: <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>.
  3. Install Node.js packages:
    yarn install # or npm install
    

Step 3: Integrate Hotwire (turbo-rails, stimulus-rails).


  1. Check Procfile.dev (or bin/dev if you don’t use foreman/overmind): Look for a line that runs the JavaScript bundler, e.g., js: yarn build --watch.
      • Confirmed with these files existing in Rails root dir
  2. Check app/assets/config/manifest.js: Ensure it includes //= link_tree ../builds (or similar, to include the output of esbuild).

  3. Check app/javascript/application.js: This file should exist. It’s the entry point for esbuild.

If jsbundling-rails with esbuild is NOT set up:


Step 2: Install and configure cssbundling-rails.

  1. Add the gem:
    bundle add cssbundling-rails
    
  2. Run the installer for your chosen CSS framework. Let’s assume you want to use Tailwind CSS (as it’s very common). You can replace tailwind with bootstrap, bulma, postcss, or sass.
    rails css:install:tailwind
    

    This will typically:

    • Add necessary Node.js packages (e.g., tailwindcss) to your package.json.
    • Create configuration files (e.g., tailwind.config.js, postcss.config.js).
    • Create an entry point CSS file (e.g., app/assets/stylesheets/application.tailwind.css).
    • Add a CSS build script to package.json.
    • Add app/assets/builds to .gitignore (if not already there).
    • Add a css: line to Procfile.dev (e.g., css: yarn build:css --watch).
    • Update app/assets/config/manifest.js to include //= link application.css (if not already there) and ensure your layout file (e.g., app/views/layouts/application.html.erb) links to the compiled stylesheet: <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>.
  3. Install Node.js packages:
    yarn install # or npm install
    

Step 3: Integrate Hotwire (turbo-rails, stimulus-rails).


Database Setup (PostgreSQL)

Okay, let’s break down the steps to set up your Ruby on Rails application with PostgreSQL, including the option for UUID primary keys.

Prerequisites:

  1. Ruby on Rails installed: You should have a working Rails environment.
  2. PostgreSQL installed and running: Make sure you have PostgreSQL server installed and accessible. You’ll need to be able to create users and databases.
    • Confirm install by running psql -U postgres

Part 1: Configure config/database.yml for PostgreSQL

  1. Install the pg gem (if not already present): Rails applications generated with PostgreSQL as the database (rails new myapp -d postgresql) will have this. If you’re converting an existing app or it’s missing, add it to your Gemfile:
    gem 'pg'
    

    Then run:

    bundle install
    
  2. Create a PostgreSQL User and Databases (if you haven’t already): It’s good practice to have a dedicated user for your application. Open the PostgreSQL console (psql):
    # Completed in WSL
    sudo -u postgres psql # Or however you access your psql admin
    

    Then, in the psql prompt:

    CREATE USER myapp_user WITH PASSWORD 'your_strong_password';
    ALTER USER myapp_user CREATEDB; -- Grants permission to create databases (useful for `rails db:create`)
    
    -- For development database
    CREATE DATABASE myapp_development OWNER myapp_user;
    -- For test database
    CREATE DATABASE myapp_test OWNER myapp_user;
    
    -- Optional: For production database (you might create this on a separate server)
    -- CREATE DATABASE myapp_production OWNER myapp_user;
    
    \q -- to quit psql
    

    Replace myapp_user, your_strong_password, myapp_development, and myapp_test with your desired names.

  3. Edit config/database.yml: Open config/database.yml and modify it to look something like this. The key is setting adapter: postgresql.

    default: &default
      adapter: postgresql
      encoding: unicode
      # For OS X and Linux:
      host: localhost
      # For Windows:
      # host: 127.0.0.1
      # For Docker:
      # host: db  # (or whatever your PostgreSQL service is named in docker-compose.yml)
      pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
      username: myapp_user      # The user you created
      password: <%= ENV['MYAPP_DATABASE_PASSWORD'] || 'your_strong_password' %> # Use ENV var for production!
    
    development:
      <<: *default
      database: myapp_development # The development database name
    
    test:
      <<: *default
      database: myapp_test         # The test database name
    
    production:
      <<: *default
      database: myapp_production   # Your production database name
      # Ensure you use environment variables for username and password in production!
      username: <%= ENV['MYAPP_PROD_DATABASE_USER'] %>
      password: <%= ENV['MYAPP_PROD_DATABASE_PASSWORD'] %>
      # host: <%= ENV['MYAPP_PROD_DATABASE_HOST'] %> # e.g., your RDS endpoint or remote server
    

    Important Notes on database.yml:

    • host: localhost is common for local development. If PostgreSQL is in Docker, it might be the service name (e.g., db).
    • username: The PostgreSQL user you created (e.g., myapp_user).
    • password:
      • For local development, you can hardcode it (like 'your_strong_password'), but it’s better practice to use environment variables even locally (e.g., ENV['MYAPP_DATABASE_PASSWORD']).
      • For production, ALWAYS use environment variables or Rails encrypted credentials for passwords. Never commit actual passwords to version control.
    • database: The specific database name for each environment.
  4. Create the Databases (via Rails): If you haven’t created the databases directly in psql (or want Rails to ensure they match the config and have the correct owner), run:
    rails db:create
    

    This will create the development and test databases specified in config/database.yml if they don’t exist, using the provided credentials.

  5. Run Migrations (if you have any):
    rails db:migrate
    
  6. Verify Connection: Try starting your Rails server or console:
    rails s
    # or
    rails c
    

    If there are no database connection errors, you’re good for this part!


Part 2: Optional - Configure for UUID Primary Keys

If you want new tables to use UUIDs as their primary keys by default:

  1. Enable the pgcrypto Extension in PostgreSQL: UUIDs are often generated using functions from the pgcrypto extension (specifically gen_random_uuid()). You need to enable this extension in your database(s). Create a new migration:
    rails g migration EnablePgcryptoExtension
    

    Open the generated migration file (e.g., db/migrate/TIMESTAMP_enable_pgcrypto_extension.rb) and add:

    class EnablePgcryptoExtension < ActiveRecord::Migration[7.0] # Adjust Rails version if needed
      def change
        enable_extension 'pgcrypto' unless extension_enabled?('pgcrypto')
      end
    end
    

    Note: For Rails 7.1+, you can use ActiveRecord::Migration[7.1].

  2. Configure Rails Generators to Use UUIDs: Create an initializer file, for example, config/initializers/generators.rb (or add to an existing one like config/application.rb within the class Application block, but a separate initializer is cleaner). Add the following code:
    # config/initializers/generators.rb
    Rails.application.config.generators do |g|
      g.orm :active_record, primary_key_type: :uuid
    end
    

    Alternatively, in config/application.rb (less common for this specific setting):

    # module MyApp
    #   class Application < Rails::Application
    #     # ...
    #     config.generators do |g|
    #       g.orm :active_record, primary_key_type: :uuid
    #     end
    #     # ...
    #   end
    # end
    
  3. Run the Migration: Execute the migration to enable pgcrypto:
    rails db:migrate
    

    This needs to be run for both your development and test databases. rails db:test:prepare can help set up the test DB after migrating development. Or, if you reset your test DB frequently:

    RAILS_ENV=test rails db:migrate
    
  4. How it Works for New Tables: Now, when you generate a new model and its migration, Rails will automatically set up the primary key as a UUID:
    rails g model Post title:string content:text
    

    The generated migration (db/migrate/TIMESTAMP_create_posts.rb) will look something like this:

    class CreatePosts < ActiveRecord::Migration[7.0] # Or your Rails version
      def change
        create_table :posts, id: :uuid do |t| # Note: id: :uuid
          t.string :title
          t.text :content
          t.timestamps
        end
      end
    end
    

    The id: :uuid tells ActiveRecord to create an id column of type uuid and use gen_random_uuid() as the default value function.

  5. Foreign Keys: When you create foreign keys referencing a UUID primary key, Rails will also correctly set them as uuid type:
    rails g model Comment body:text post:references
    

    The migration for comments will have:

    # ...
    t.references :post, null: false, foreign_key: true, type: :uuid # Note: type: :uuid
    # ...
    

Considerations for UUIDs:


Summary of Steps (Checklist):

Database Setup (PostgreSQL):

Optional: Configure for UUID primary keys:

You should now have a Rails application configured to use PostgreSQL, with the option to automatically use UUIDs for primary keys in new tables.

Step 4: Final Checks & Running the Development Server

  1. Verify app/javascript/application.js: It should now look something like this (order might vary slightly):
    // Entry point for the build script in your package.json
    import "@hotwired/turbo-rails"
    import "./controllers" // For Stimulus
    // Any other JS you import
    
  2. Verify app/views/layouts/application.html.erb: Ensure you have:
    • <%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %> (or javascript_importmap_tags if you were using importmaps, but with jsbundling, it’s javascript_include_tag "application" pointing to the build output). The defer: true is good practice.
    • <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
  3. Start your development server: Use bin/dev (which uses Procfile.dev and foreman or a similar process manager).
    ./bin/dev
    

    This should start:

    • The Rails server.
    • The JavaScript bundler in watch mode (e.g., yarn build --watch).
    • The CSS bundler in watch mode (e.g., yarn build:css --watch).
  4. Test:
    • Open your application in a browser.
    • Check the browser’s developer console for any JavaScript errors.
    • Test navigation to see if Turbo Drive is working (pages should load faster, without full reloads).
    • If you have a Stimulus controller (like the default hello_controller.js), try using it on a page to confirm Stimulus is active. For example, add <div data-controller="hello"></div> to a view and check if “Hello, Stimulus!” (or whatever its connect() method does) appears or logs to the console.

Commit your changes: Once everything is confirmed to be working, commit these changes to your version control system.

git add .
git commit -m "Set up jsbundling, cssbundling, and Hotwire (Turbo & Stimulus)"

Configure Database

Set up your database (create and migrate):

rails db:create
rails db:migrate

2. LLM Service Layer

app/services/llm_cover_letter_generator.rb

require “google/generative_ai”

class LlmCoverLetterGenerator
  def initialize(api_key: ENV["GOOGLE_API_KEY"])
    # No longer using Google Cloud
    #@client = Google::Cloud::GenerativeAI.new(api_key: api_key)
    @model = Google::GenerativeAI::Client.new(api_key: api_key).gemini_pro
  end

  def generate(job_description:, skills: [])
    # Commentted out potential Google Cloud syntax
    =begin
    prompt = "Compose a professional cover letter for: #{job_description}. Include skills: #{skills.join(', ')}."
    @client.generate_content(prompt).text
    =end
    # End of comment
    # Google Generative AI || Google Cloud AI Platform V1 Syntax
    prompt = <<~TEXT
      Write a professional cover letter for the following job description:
      #{job_description}

      Include the following skills: #{skills.join(', ')}.
      Format it in a polished, formal tone.
    TEXT

    response = @model.generate_content(prompt)
    response.text
  end
end

3. Background Jobs

app/jobs/generate_cover_letter_job.rb

class GenerateCoverLetterJob < ApplicationJob
  queue_as :default

  def perform(job_description_id)
    job = JobDescription.find(job_description_id)
    user = job.user
    generator = LlmCoverLetterGenerator.new
    letter = generator.generate(job_description: job.content, skills: user.skills.pluck(:name))
    job.update!(generated_letter: letter)
  end
end

4. Authentication

rails generate devise:install
rails generate devise User
rails db:migrate

5. Front-End Setup

Tailwind Setup

Already added via --css=tailwind. Start customizing styles in app/assets/stylesheets/application.tailwind.css

Turbo + Stimulus

Already bundled in Rails 7. Use Stimulus controllers for interactivity (e.g., async form submission).


6. Testing

Directory Structure

spec/
 ├─ system/       # Full user flow tests
 ├─ requests/     # API/controller tests
 ├─ services/     # LLM generation logic tests
 ├─ mailers/      # Email delivery tests

RSpec Example: Service Test

RSpec.describe LlmCoverLetterGenerator do
  it "generates content" do
    stub_api_response
    generator = described_class.new(api_key: "fake")
    result = generator.generate(job_description: "Test Job", skills: ["Ruby"])
    expect(result).to include("Test Job")
  end
end

7. Docker Setup

Dockerfile

FROM ruby:3.3
WORKDIR /app
COPY . .
RUN bundle install
CMD ["rails", "server", "-b", "0.0.0.0"]

docker-compose.yml

version: '3.8'
services:
  web:
    build: .
    ports:
      - "3000:3000"
    depends_on:
      - db
      - redis
  db:
    image: postgres:13
    environment:
      POSTGRES_PASSWORD: password
  redis:
    image: redis:7

8. Deployment


9. Mailer for Delivery

rails generate mailer CoverLetterMailer

<!-- app/views/cover_letter_mailer/cover_letter_email.html.erb -->
<h1>Your Cover Letter</h1>
<p><%= @cover_letter %></p>

Triggering Mailer

CoverLetterMailer.with(user: user, cover_letter: letter).cover_letter_email.deliver_later

Next Steps Checklist