A production-ready Rails API template with OAuth2 authentication, multi-tenant architecture, and admin interface. This template provides a solid foundation for building SaaS applications, multi-tenant systems, or any API-first Rails application.
- Rails 8.0.2 API-only application
- PostgreSQL database with single migration setup
- Devise + Doorkeeper authentication (email/password with OAuth2)
- Multi-tenant architecture with Account model
- Administrate admin dashboard with HTTP Basic Auth
- JSONAPI serialization standard
- RSpec testing framework with 40 passing tests
- Role-based user system (employee, admin, owner)
- Phone number validation and normalization
- CORS configured for API access
- Code quality tools (RuboCop, Brakeman)
- Bruno API collection for testing
- Background jobs with Solid Queue
- AWS S3 ready for file uploads
- Docker & Kamal deployment ready
- Ruby 3.2+
- PostgreSQL 14+
- Rails 8.0.2
git clone <repository-url>
cd rails-api-template
# Install dependencies
bundle install
# Copy environment variables template
cp .env.example .env
# Edit .env file with your values
# - Set admin credentials
# - Configure database if using non-default settingsUpdate config/database.yml with your PostgreSQL credentials:
development:
<<: *default
database: rails_api_template_development
username: postgres
password: password
host: localhost
port: 5432Then create and setup the database:
rails db:create
rails db:migrate
rails db:seedThe seed file will create:
- A default OAuth application (check console output for Client ID/Secret)
- Test users: admin@example.com and user@example.com (password: password123)
rails serverThe API will be available at http://localhost:3000
This template uses OAuth2 password flow via Doorkeeper with Devise for user authentication.
curl -X POST http://localhost:3000/oauth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "password",
"username": "user@example.com",
"password": "password123",
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET"
}'Response:
{
"access_token": "YOUR_ACCESS_TOKEN",
"token_type": "Bearer",
"expires_in": 86400,
"refresh_token": "YOUR_REFRESH_TOKEN",
"created_at": 1719902400
}curl http://localhost:3000/api/users/me \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"curl -X POST http://localhost:3000/oauth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "refresh_token",
"refresh_token": "YOUR_REFRESH_TOKEN",
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET"
}'POST /oauth/token- Get access token (password or refresh_token grant)POST /oauth/revoke- Revoke access tokenGET /oauth/token/info- Token introspection
POST /api/users- Create new user (no auth required)GET /api/users/me- Get current user profile
GET /api/accounts- List accounts (returns user's account)
POST /api/presigned_urls- Get presigned URL for file uploadGET /api/images/:key- Get presigned URL for file download
Password reset is handled via Devise's default controllers (HTML forms, not API endpoints)
Access the admin dashboard at /admin with HTTP Basic Authentication:
- Username: Set via
ADMIN_USERNAMEenv variable (default: admin) - Password: Set via
ADMIN_PASSWORDenv variable
Create a .env file with:
ADMIN_USERNAME=admin
ADMIN_PASSWORD=your_secure_password
email- User email (unique, used for authentication)encrypted_password- User password (Devise/bcrypt)name- User full namephone_number- User phone number (validated and normalized)role- User role (enum: employee, admin, owner)account_id- Associated account (optional)- Devise fields:
reset_password_token,reset_password_sent_at,remember_created_at
name- Account/Organization nameusers- Associated users (has many)
oauth_applications- OAuth2 client applicationsoauth_access_tokens- Active access tokensoauth_access_grants- Authorization grants
Run the test suite:
# Run all tests
bundle exec rspec
# Run specific test types
bundle exec rspec spec/models
bundle exec rspec spec/requests
# Run with coverage report
COVERAGE=true bundle exec rspecCurrent test status: 40 examples, 0 failures
The project includes a Bruno API collection for testing endpoints.
- Download Bruno
- Open Bruno and select "Open Collection"
- Navigate to the
brunodirectory in this project - Set environment variables:
baseUrl:http://localhost:3000clientId: Your OAuth Client IDclientSecret: Your OAuth Client Secret
bruno/
βββ auth/ # OAuth token operations
βββ users/ # User management
βββ accounts/ # Account endpoints
βββ images/ # Image download endpoints
βββ presigned_urls/ # File upload URLs
βββ docs/ # API documentation
- Generate model:
rails generate model ModelName field:type
rails db:migrate- Create admin dashboard:
rails generate administrate:dashboard ModelName- Add to routes:
namespace :admin do
resources :model_names
end
namespace :api do
resources :model_names
end- Create API controller:
# Create app/controllers/api/model_names_controller.rb
# Inherit from ApplicationController
# Add Doorkeeper authentication- Create serializer:
# Create app/serializers/model_name_serializer.rb
# Use JSONAPI::Serializable::Resource- Create tests:
# Create spec/models/model_name_spec.rb
# Create spec/requests/api/model_names_spec.rb
# Create spec/factories/model_names.rb# Run RuboCop (Ruby linter)
rubocop # Check all files
rubocop -a # Auto-fix issues
rubocop -A # Auto-fix more aggressively
rubocop --parallel # Run in parallel
# Run Brakeman (security scanner)
brakeman # Basic scan
brakeman -A # Run all checks
brakeman -o report.html # Generate HTML report
# Run both
rubocop && brakemanCurrent Status:
- RuboCop: 1 offense (missing unique index on users.email)
- Brakeman: Clean (no security warnings)
rails-api-template/
βββ app/
β βββ controllers/
β β βββ admin/ # Admin controllers
β β βββ api/ # API controllers
β βββ dashboards/ # Administrate configs
β βββ models/ # User, Account
β βββ serializers/ # JSONAPI serializers
βββ bruno/ # API collection
β βββ auth/ # OAuth endpoints
β βββ users/ # User endpoints
β βββ accounts/ # Account endpoints
βββ config/
β βββ initializers/
β β βββ cors.rb # CORS config
β β βββ devise.rb # Devise config
β β βββ doorkeeper.rb # OAuth2 config
β βββ routes.rb # API routes
βββ db/
β βββ migrate/ # Single migration file
βββ lib/
β βββ custom_failure_app.rb # API error responses
βββ spec/ # RSpec tests
βββ factories/ # FactoryBot
βββ models/ # Model specs
βββ requests/ # API specs
All API responses follow the JSONAPI specification:
{
"data": {
"id": "1",
"type": "users",
"attributes": {
"email": "user@example.com",
"name": "John Doe",
"phone_number": "+12125551234",
"role": "employee"
},
"relationships": {
"account": {
"data": {
"id": "1",
"type": "accounts"
}
}
}
}
}This template includes configurations for Docker and Fly.io deployment.
The Dockerfile is optimized for production Rails API deployments with:
- Multi-stage build for smaller final image
- PostgreSQL client and dependencies
- Non-root user for security
- Health check endpoint at
/up - Thruster for efficient request handling
# Build the production image
docker build -t rails-api-template .
# Run locally with environment variables
docker run -d \
-p 80:80 \
-e RAILS_MASTER_KEY=$(cat config/master.key) \
-e DATABASE_URL="postgresql://username:password@host:5432/dbname" \
-e ADMIN_USERNAME="admin" \
-e ADMIN_PASSWORD="secure_password" \
--name rails-api-template \
rails-api-templateThis template includes a fly.toml configuration for easy deployment to Fly.io.
- Install Fly CLI:
# macOS
brew install flyctl
# Linux
curl -L https://fly.io/install.sh | sh- Sign up/Login to Fly.io:
fly auth login- Create a new Fly app:
fly launch --no-deploy- Create a PostgreSQL database:
fly postgres create --name rails-api-template-db
fly postgres attach rails-api-template-db- Set secrets:
# Set Rails master key
fly secrets set RAILS_MASTER_KEY=$(cat config/master.key)
# Set admin credentials
fly secrets set ADMIN_USERNAME=admin
fly secrets set ADMIN_PASSWORD=your_secure_password
# Set AWS credentials if using S3
fly secrets set AWS_ACCESS_KEY_ID=your_key
fly secrets set AWS_SECRET_ACCESS_KEY=your_secret- Deploy:
fly deploy# Deploy latest changes
fly deploy
# Deploy with remote Docker builder (if local builds fail)
fly deploy --remote-only
# Check deployment status
fly status
# View logs
fly logs
# SSH into the app
fly ssh console
# Run Rails console
fly ssh console -C "/rails/bin/rails console"
# Run database migrations manually
fly ssh console -C "/rails/bin/rails db:migrate"# Scale horizontally
fly scale count 2
# Scale vertically
fly scale vm dedicated-cpu-1x --memory 1024
# Auto-scaling configuration
fly autoscale set min=1 max=5Required environment variables for production:
RAILS_MASTER_KEY- Rails master key for credentialsDATABASE_URL- PostgreSQL connection stringADMIN_USERNAME- Admin dashboard usernameADMIN_PASSWORD- Admin dashboard password
Optional for AWS S3:
AWS_ACCESS_KEY_ID- AWS access keyAWS_SECRET_ACCESS_KEY- AWS secret keyAWS_REGION- AWS region (default: us-east-1)AWS_BUCKET_NAME- S3 bucket name
The application includes a health check endpoint at /up that verifies:
- Application is running
- Database connection is active
- Migrations are up to date
Solid Queue is configured for background job processing. The Fly.io configuration includes a separate worker process that runs automatically.
If you see connection errors, ensure PostgreSQL is running:
# macOS
brew services start postgresql
# Linux
sudo systemctl start postgresql
# Docker
docker run -p 5432:5432 -e POSTGRES_PASSWORD=password postgresIf seeds fail, manually create OAuth application:
rails console
app = Doorkeeper::Application.create!(
name: 'Default App',
redirect_uri: 'urn:ietf:wg:oauth:2.0:oob'
)
puts "Client ID: #{app.uid}"
puts "Client Secret: #{app.secret}"For file upload/download functionality, configure AWS credentials:
rails credentials:editAdd the following structure:
aws:
region: us-east-1
access_key_id: your_access_key_id
secret_access_key: your_secret_access_key
bucket_name: your-bucket-name
endpoint_url_s3: https://s3.amazonaws.com # Optional, for S3-compatible services- Client requests presigned URL:
POST /api/presigned_urls - Server returns presigned URL and object key
- Client uploads file directly to S3 using the presigned URL
- Client stores the object key for later retrieval
- Client requests download URL:
GET /api/images/:key - Server returns presigned URL for the S3 object
- Client downloads file directly from S3
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Run tests (
bundle exec rspec) - Run linters (
rubocop && brakeman) - Commit your changes
- Push to the branch
- Open a Pull Request
This template follows security best practices:
- All sensitive configuration uses environment variables
- Database passwords are never hardcoded
- Rails credentials used for production secrets
- OAuth tokens expire after 24 hours
- Admin dashboard uses HTTP Basic Auth
- All passwords are encrypted with bcrypt
- CORS is configured (update for production use)
- SSL is enforced in production
- Never commit
.envfiles - Only.env.exampleis tracked - Keep
master.keysecure - Required for production deployments - Rotate credentials regularly - OAuth secrets, API keys, etc.
- Use strong passwords - The seed file passwords are for development only
- Configure CORS properly - Current setting allows all origins (development only)
MIT License