This README would normally document whatever steps are necessary to get the application up and running.
Things you may want to cover:
-
Ruby version
-
System dependencies
-
Configuration
-
Database creation
-
Database initialization
-
How to run the test suite
-
Services (job queues, cache servers, search engines, etc.)
-
Deployment instructions
-
...
Operational and launch documentation lives in docs/.
- Documentation index:
docs/README.md - Local production mode boundaries:
docs/local-prod-mode.md - Production email setup:
docs/deploy/production-email-setup.md - Metrics model and observability pipeline:
docs/metrics-model.md - Ruby code quality metrics:
docs/rubycritic.md
This app intentionally separates API identity from internal operator identity.
Useris the app/API identity.Adminis the internal/operator identity.Adminhasbelongs_to :user, optional: true.
- API requests use token auth via
devise_token_auth. - Internal/admin-only routes and mounted tools use Devise session auth.
- An admin session does not imply API token authentication.
- API token headers do not grant access to admin-only browser routes.
/api/v1/*is JSON API surface and should use token auth withcurrent_user.- Admin/internal browser routes and mounted tools are session-protected (in non-development), including:
/pghero/blazer/good_job/flipper/solid_errors/field_test
- API docs (
/docs,/openapi.yml) are browser routes and are not part of the token-authenticated API surface.
ApplicationControllershould remain browser/session-safe and framework-level.Api::BaseControllerowns API-specific behavior:- Devise Token Auth integration
- API authentication helpers and identity (
current_user,authenticate_user!) - API error rendering
- Ahoy/Field Test API participant identity wiring
Api::V1::BaseControllerowns API v1 request concerns:- pagination
- serialization
- policy authorization
- contract handling
- API controllers: use
current_userandauthenticate_user!. - Admin/browser controllers: use
current_adminandauthenticate_admin!(or route-levelauthenticate :adminconstraints for mounted engines). - Current implementation note:
DocsControllerusesuser_signed_in? && current_user.admin?as its admin gate outside development. - Keep session helper usage out of API controllers and token helper usage out of admin browser flows.
- API tests:
- authenticate with token headers from
/auth/sign_in(for example viaauth_headers_for(user)). - verify token-authenticated requests can access API endpoints.
- authenticate with token headers from
- Admin/browser and mounted tool tests:
- authenticate with Devise session helpers (for example
sign_in adminor the applicable session scope). - verify token-only requests are rejected or redirected for admin/session-only routes.
- authenticate with Devise session helpers (for example
- Auth-boundary tests:
- explicitly verify that token auth cannot access admin-only tools.
- explicitly verify that admin session auth does not replace API token requirements.
- Frontend should own page/journey tracking.
- Backend Ahoy should capture server-confirmed business events.
- API attribution should rely on
current_user/Ahoy identity so events and experiments stay consistent. This should largely be within the API domain where field_test is hooked into Ahoy identity and current_user. It can be used elsewhere, but attribution will not be automatic. - Field Test participant identity uses Ahoy identity (
ahoy.user,ahoy.visitor_token) so experiment assignment aligns with tracked API activity.
This app uses Searchkick with OpenSearch for model search.
Current config:
- Default URL:
http://localhost:9200 - URL override:
OPENSEARCH_URL - Local and CI OpenSearch image:
opensearchproject/opensearch:2.19.1 - Gem compatibility:
searchkick ~> 6.1andopensearch-ruby ~> 3.4are compatible with OpenSearch 2.x.
OpenSearch is defined in compose.yml and can be started with:
docker compose up -d opensearchbin/dev also starts OpenSearch before starting the app processes.
rails searchkick:healthYou should see cluster status and version info if it's running and healthy.
- See
test/test_helper.rbfor Searchkick test setup and configuration. - Callbacks are disabled by default in tests, but can be enabled on a per-test basis when needed.
Configure your production env and creds as needed
Local development uses fixed ports to stay consistent with workspace orchestration.
- API server default:
http://localhost:50001 - Web template dev server:
http://localhost:3000
Details:
bin/devlaunches Rails on50001viaProcfile.dev.- If you run Rails directly (without
bin/dev),config/puma.rbstill usesPORTwith default5001.
This template enables CORS with rack-cors.
- Set
CORS_ALLOWED_ORIGINSto a comma-separated list of allowed origins. - CORS origin parsing is configured in environment files (
config/environments/*.rb). - Production setup guide:
docs/deploy/production-cors-setup.md.
This template uses rack-attack to throttle sensitive and write-heavy endpoints.
Configured throttles live in config/initializers/rack_attack.rb.
Current throttled routes (by request IP):
POST /auth/sign_inPOST /authPOST|PUT|PATCH|DELETE /api/v1/users*
Default limits:
- Auth sign in:
10requests /60seconds - Auth sign up:
10requests /60seconds - Users write endpoints:
15requests /60seconds
Environment variables:
THROTTLE_AUTH_SIGN_IN_LIMITTHROTTLE_AUTH_SIGN_IN_PERIODTHROTTLE_AUTH_SIGN_UP_LIMITTHROTTLE_AUTH_SIGN_UP_PERIODTHROTTLE_USERS_WRITE_LIMITTHROTTLE_USERS_WRITE_PERIOD
When a request is throttled, Rack::Attack returns 429 Too Many Requests using its default response behavior.
This template includes the bullet gem in the development, test group.
For current environment configuration choices see:
config/environments/development.rb:config/environments/test.rb:
Summary:
- Bullet is active in both development and test.
- In development, output is logger-based only (no browser alert/console/footer).
- In test, Bullet raises on detected N+1/unused eager loading issues.
API endpoints should be added under /api/v1.
The /api/ namespace defaults to JSON responses.
- Canonical authenticated user endpoint:
GET /api/v1/users/me
This template includes a lightweight ReDoc UI backed by the OpenAPI document. Docs endpoints are always mounted, and access is enforced in the controller.
The source OpenAPI file lives at docs/openapi.yml.
Access behavior:
development: docs are available without authentication.- non-development (
test/production): only authenticated admin Devise session users can access docs; all other requests receive404 not found.
PgHero is mounted for basic query/index visibility at:
GET /pghero
Access behavior:
development: route is available without auth for local debugging.- non-development (
test/production): route is mounted only for authenticated admin users via Devise session auth.
To access in non-development, sign in through session auth first:
GET /admins/sign_in
bin/setup now runs Blazer default query and dashboard installation automatically for local bootstrap.
Use these tasks to install the bundled Blazer content in a local or production database.
Run in this order:
bin/rails blazer:default_queries:installbin/rails blazer:install_dashboards
What each task does:
blazer:default_queries:installinstalls versioned default queries and records installation markers so reruns are safe.blazer:install_dashboardscreates/updates the API Observability dashboard and links dashboard entries to queries.
Expected behavior:
- Safe to rerun.
- Existing installed query markers are skipped.
- Operators can still edit or delete query names/statements directly in Blazer after install.
GoodJob is mounted at:
GET /good_job
Access behavior in this app:
development: route is behind admin session auth.- non-development (
test/production): route is behind admin session auth via route constraints.
Recurring schedules are configured in config/recurring.yml using GoodJob cron format and loaded directly in config/initializers/good_job.rb.
This template includes Ahoy for server-side event tracking.
Installed pieces:
- Gem:
ahoy_matey(seeGemfile) - Initializer:
config/initializers/ahoy.rb - Database tables:
ahoy_visitsandahoy_events - Models:
app/models/ahoy/visit.rbandapp/models/ahoy/event.rb
Current Ahoy config (config/initializers/ahoy.rb):
Ahoy.api = trueAhoy.geocode = falseAhoy.server_side_visits = :when_needed
Basic controller usage:
ahoy.track "event.name", { key: "value" }Test coverage:
test/integration/ahoy_tracking_test.rbverifies that an Ahoy event is persisted.
This template includes a built-in first-party observability pipeline based on the app-owned metrics table.
Detailed documentation for the metrics model, dry-schema contracts, API request fanout behavior, and Blazer dashboards now lives in docs/metrics-model.md.
This app includes Field Test scaffolding for experiments and conversion tracking.
Configured experiment definitions live in:
config/field_test.yml
Dashboard route:
GET /field_test
Route protection:
development: mounted directly for local debugging.- non-development (
test/production): mounted behind admin session auth in routes.
Ahoy integration:
ApplicationController#field_test_participantreturns[ahoy.user, ahoy.visitor_token].- This makes Field Test participant identity reuse Ahoy's visitor token.
API status in this template:
- Field Test is installed and admin UI is mounted at
/field_test. - The template does not currently ship API endpoints to fetch or set experiment assignments.
- API contract and endpoint shape are intentionally TBD and should be designed alongside the frontend template.
Model wiring:
Userhasfield_test_membershipsassociation viaFieldTest::Membership.
This app uses a standard Flipper setup for feature flags. This is configured to be used first party, backed by the primary app DB. It does not use their cloud service.
Persistence:
- ActiveRecord-backed flag storage via
flipper_featuresandflipper_gatestables. - Tables are created by the Flipper migration in
db/migrate.
UI route:
GET /flipper
Route protection:
development: mounted directly for local use.- non-development (
test/production): mounted behind admin session auth in routes.
Setup checklist:
- Run migrations:
bin/rails db:migrate-
In production/non-development, sign in as an admin user to access
/flipper. -
Installation verified with basic smoke tests
This app supports two different authentication styles at the same time:
- Token auth for JSON API clients (mobile/SPA) via Devise Token Auth.
- Cookie session auth for browser-based admin-only routes via Devise sessions.
They intentionally use different URL paths so they do not conflict:
- Token auth endpoints:
/auth/* - Session auth endpoints:
/users/*
Route mount:
mount_devise_token_auth_for "User", at: "auth", as: "token_auth_users"
Main endpoints:
POST /auth(registration)POST /auth/sign_in(token login)DELETE /auth/sign_out(token logout)GET /auth/validate_token
Token login response headers (used on subsequent API requests):
access-tokenclientuidexpirytoken-type
Typical API client flow:
POST /auth/sign_inwith email/password.- Store response token headers on the client.
- Send those headers with each protected API call (for example
GET /api/v1/users/me). - Rotate stored token values from response headers when returned.
Route mount:
devise_for :users, only: [ :sessions ]
Main endpoints:
GET /admins/sign_inPOST /admins/sign_inDELETE /users/sign_out
Use this flow for browser-only/admin-only routes that rely on cookie sessions.
Recommended pattern for admin routes:
- Require an authenticated Devise session (
authenticate_user!). - Require admin role (
current_user.admin?). - Return
403 forbidden(or redirect for HTML pages) when non-admin users attempt access.
Minimal controller gate example:
before_action :authenticate_user!
before_action :require_admin!
private
def require_admin!
return if current_user&.admin?
head :forbidden
endFor route-level constraints, use the same logic (authenticated user + admin?) before mounting admin-only endpoints.
This template uses a service-object layer under app/services/svc.
- Controllers should stay thin and delegate business logic to service objects.
- Service objects should expose a single entrypoint via
.call. - Place shared behavior in
Svc::Base. - Group services by domain/version (for example
Svc::Api::V1::Users::List). - Service objects in this app are Rails-aware by design.
Rails-awareness:
- It is acceptable for service objects to use Rails primitives directly (for example
Rails.cache,ActiveRecord,ActiveSupport, models, etc...). - Do not pass dependency inject things like
Rails.cache, request objects, or controller instances into services unless there is a strong reason.
Usage conventions:
- Keep domain behavior, orchestration, and reusable rules in SVC objects.
- Return plain Ruby hashes/values from services for easy composition and testing.
- Raise meaningful exceptions from services and let controller-level error handling render API error envelopes.
- Unit test services directly under
test/services, and keep integration tests focused on endpoint behavior. - Svc naming and namespacing does not necessarily need to mirror controller structure and controller action names, but should be organized in a way that is easy to find and understand.
Example flow:
- Controller:
Api::V1::UsersController#index - Service object:
Svc::Api::V1::Users::List
This template uses a common gem-style JSON error envelope.
{
"success": false,
"errors": [
"param is missing or the value is empty: widget"
],
"error_type": "bad_request",
"request_id": "9a9de824-fdb2-4f57-9525-c3fd2930a34d"
}The request_id value matches Rails request logging and can be used for tracing.
When details are provided (for example validation failures), they are appended to the errors array.
Default exception mapping:
ActionController::ParameterMissing->400 bad_requestActionController::BadRequest->400 bad_requestActiveRecord::RecordNotFound->404 not_foundActiveRecord::RecordInvalid->422 unprocessable_entity(includes validation messages inerrors)ActiveRecord::RecordNotSaved->422 unprocessable_entityStandardError->500 internal_server_error
This format is aligned with authentication error responses from devise_token_auth.
API input are validated with contract objects under app/contracts.
Pattern:
- Controllers build and validate API inputes with a contract before calling a service object.
- Contracts raise
ApplicationContract::Invalidwhen invalid, which is handled by normal API error handling flow.
This template uses blueprinter as the single approach for API success-response serialization.
- Controller helper:
render_serialized(blueprint, payload, status: :ok)inApi::V1::BaseController
Conventions:
- Controllers should render API success responses through Blueprinter, not ad hoc hashes.
- Shared response metadata (for example
request_id) is injected in the base controller before serialization. - Endpoint blueprints define only fields that belong to the public API contract.
This template uses pagy for consistent collection endpoint pagination.
- See BaseController helpers
- Query params for collection endpoints:
page(default:1)per_page(default:25, max:100)
This template establishes where authorization decisions live, without predefining app-specific rules.
- Authentication answers: "Who is this?"
- Authorization answers: "Can they do this?"
Policy classes live in app/policies.
Defaults are deny-all to enforce explicit allow rules when real resources are added.
API controllers can call authorize!(record, query = nil) from Api::V1::BaseController.
- Authorization failures return a standard JSON error with
403 forbiddenthrough the normal API error handling flow.
This project includes a local CI runner using Rails 8.1's ActiveSupport::ContinuousIntegration.
Run the full local pipeline:
bin/ciThe pipeline currently runs:
- Dependency check/install
- Test database prepare
- RuboCop
- Bundler Audit
- Brakeman
- FactoryBot lint
- Test suite
- Seed validation in test
Prerequisite: PostgreSQL must be running locally and accessible with your configured test DB settings.
Cheeck config/ci.rb for the full list of steps and commands run by the pipeline. You can also run individual steps manually.
This app uses SimpleCov in test/test_helper.rb for first-party coverage reporting.
Current coverage settings:
- Line coverage is enabled.
- Branch coverage is enabled.
- Coverage output directory:
coverage/ - Minimum required coverage (enforced):
- line:
80% - branch:
80%
- line:
If either threshold is below 80%, the test run exits non-zero.
bundle exec rails test
or
bin/ciThen open:
open coverage/index.html
- Coverage filters exclude
test/,config/,vendor/, anddocs/. - In parallel test runs, coverage results are merged via SimpleCov configuration in
test/test_helper.rb.
This template is configured to run GoodJob and Solid components on the primary PostgreSQL database in production.
config/database.ymluses a singleproductionconnection viaDATABASE_URL.config/application.rbconfiguresconfig.active_job.queue_adapter = :good_job.config/cable.ymlpoints Solid Cable towriting: primary.config/cache.ymlpoints Solid Cache todatabase: primary.
DATABASE_URL(single managed Postgres database)RAILS_MASTER_KEY
Use separate process types in production:
web: Puma app serverjob:bin/jobs(GoodJob worker)
Run standard Rails database tasks against the single database:
RAILS_ENV=production bin/rails db:prepare
RAILS_ENV=production bin/rails db:migrateDo not use RAILS_ENV=production bin/rails db:seed to create admin access.
- The template's seeded admin account is development-only convenience data.
- Bootstrap production admin users via a controlled manual process (for example a one-off
rails runnercommand executed by an operator).
RAILS_ENV=production bin/rails runner "puts Rails.application.config.active_job.queue_adapter"
RAILS_ENV=production bin/rails runner "Rails.cache.write('smoke','ok'); puts Rails.cache.read('smoke')"This template is open source. Generated/default secrets must be replaced before deployment.
- Rotate Rails credentials and
secret_key_base. - Set your own
RAILS_MASTER_KEYin secret management. - Update mailer sender/SMTP credentials (
DEVISE_MAILER_SENDERand provider secrets). - Never commit production keys or credentials to version control.