cover-letter-llm
This document outlines a best-practices-based implementation roadmap using Rails 7.1+, LLM integration, modern frontend stack, and scalable architecture.
Install Ruby (Windows):
code .
from new terminal, then navigate back to the project to refresh the PATH.
3. Install Rails:
```Bash
gem install rails
CoverLetterApp
is the name of the Rails Project):
rails new CoverLetterApp --database=postgresql --css=tailwind --javascript=esbuild
cd CoverLetterApp
pg_config --version
bundle install
bundler -v
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'
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
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:
nvm
(Node Version Manager) which allows you to easily switch between Node.js versions.
npm install --global yarn
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.
Check Gemfile
:
Open your Gemfile
and look for gem "jsbundling-rails"
.
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"
.
cd CoverLetterApp
rails javascript:install:esbuild
# This generates package.json and adds esbuild as dev dependency
Step 2: Install and configure cssbundling-rails
.
bundle add cssbundling-rails
tailwind
with bootstrap
, bulma
, postcss
, or sass
.
rails css:install:tailwind
This will typically:
tailwindcss
) to your package.json
.tailwind.config.js
, postcss.config.js
).app/assets/stylesheets/application.tailwind.css
).package.json
.app/assets/builds
to .gitignore
(if not already there).css:
line to Procfile.dev
(e.g., css: yarn build:css --watch
).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" %>
.yarn install # or npm install
Step 3: Integrate Hotwire (turbo-rails
, stimulus-rails
).
turbo-rails
gem:
bundle add turbo-rails
rails turbo:install
This will:
@hotwired/turbo-rails
to your package.json
(if using jsbundling) or pin it in config/importmap.rb
(if using importmaps - but since you’re using jsbundling-rails
, it will likely add it to package.json
).app/javascript/application.js
:
import "@hotwired/turbo-rails"
redis
or skip it.
bundle install
and bundle show redis
package.json
was modified:
yarn install # or npm install
stimulus-rails
gem:
bundle add stimulus-rails
rails stimulus:install
This will:
@hotwired/stimulus
and stimulus-webpack-helpers
(or similar, depending on the bundler context) to your package.json
.app/javascript/controllers/application.js
.app/javascript/controllers/hello_controller.js
(an example controller).app/javascript/controllers/index.js
(which loads all controllers from the controllers
directory and registers them with Stimulus)../controllers
) in app/javascript/application.js
:
import "./controllers"
package.json
was modified:
yarn install # or npm install
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
.
Check app/assets/config/manifest.js
:
Ensure it includes //= link_tree ../builds
(or similar, to include the output of esbuild).
app/javascript/application.js
:
This file should exist. It’s the entry point for esbuild.If jsbundling-rails
with esbuild is NOT set up:
bundle add jsbundling-rails
rails javascript:install:esbuild
This will:
esbuild
to your package.json
.app/javascript/application.js
.package.json
.app/assets/builds
to .gitignore
.js:
line to Procfile.dev
.yarn install # or npm install
Step 2: Install and configure cssbundling-rails
.
bundle add cssbundling-rails
tailwind
with bootstrap
, bulma
, postcss
, or sass
.
rails css:install:tailwind
This will typically:
tailwindcss
) to your package.json
.tailwind.config.js
, postcss.config.js
).app/assets/stylesheets/application.tailwind.css
).package.json
.app/assets/builds
to .gitignore
(if not already there).css:
line to Procfile.dev
(e.g., css: yarn build:css --watch
).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" %>
.yarn install # or npm install
Step 3: Integrate Hotwire (turbo-rails
, stimulus-rails
).
turbo-rails
gem:
bundle add turbo-rails
rails turbo:install
This will:
@hotwired/turbo-rails
to your package.json
(if using jsbundling) or pin it in config/importmap.rb
(if using importmaps - but since you’re using jsbundling-rails
, it will likely add it to package.json
).app/javascript/application.js
:
import "@hotwired/turbo-rails"
redis
or skip it.package.json
was modified:
yarn install # or npm install
stimulus-rails
gem:
bundle add stimulus-rails
rails stimulus:install
This will:
@hotwired/stimulus
and stimulus-webpack-helpers
(or similar, depending on the bundler context) to your package.json
.app/javascript/controllers/application.js
.app/javascript/controllers/hello_controller.js
(an example controller).app/javascript/controllers/index.js
(which loads all controllers from the controllers
directory and registers them with Stimulus)../controllers
) in app/javascript/application.js
:
import "./controllers"
package.json
was modified:
yarn install # or npm install
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:
psql -U postgres
config/database.yml
for PostgreSQLpg
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
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.
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
:
'your_strong_password'
), but it’s better practice to use environment variables even locally (e.g., ENV['MYAPP_DATABASE_PASSWORD']
).database
: The specific database name for each environment.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.
rails db:migrate
rails s
# or
rails c
If there are no database connection errors, you’re good for this part!
If you want new tables to use UUIDs as their primary keys by default:
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]
.
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
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
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.
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):
pg
gem: Ensure gem 'pg'
is in Gemfile
and run bundle install
.myapp_user
, myapp_development
, myapp_test
).config/database.yml
:
adapter: postgresql
.host
, username
, password
, and database
for development
, test
, and production
environments.rails db:create
.rails s
or rails c
.Optional: Configure for UUID primary keys:
pgcrypto
:
rails g migration EnablePgcryptoExtension
enable_extension 'pgcrypto'
to the migration.config/initializers/generators.rb
(or config/application.rb
):
Rails.application.config.generators do |g|
g.orm :active_record, primary_key_type: :uuid
end
rails db:migrate
(and RAILS_ENV=test rails db:migrate
or rails db:test:prepare
).id: :uuid
and type: :uuid
for references.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
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
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" %>
bin/dev
(which uses Procfile.dev
and foreman
or a similar process manager).
./bin/dev
This should start:
yarn build --watch
).yarn build:css --watch
).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)"
Set up your database (create and migrate):
rails db:create
rails db:migrate
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
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
rails generate devise:install
rails generate devise User
rails db:migrate
Already added via --css=tailwind
. Start customizing styles in app/assets/stylesheets/application.tailwind.css
Already bundled in Rails 7. Use Stimulus controllers for interactivity (e.g., async form submission).
spec/
├─ system/ # Full user flow tests
├─ requests/ # API/controller tests
├─ services/ # LLM generation logic tests
├─ mailers/ # Email delivery tests
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
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
rails generate mailer CoverLetterMailer
<!-- app/views/cover_letter_mailer/cover_letter_email.html.erb -->
<h1>Your Cover Letter</h1>
<p><%= @cover_letter %></p>
CoverLetterMailer.with(user: user, cover_letter: letter).cover_letter_email.deliver_later