[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6D1QKP093T2RWF6MS4DVA1T":3,"skill-same:01M6D1QKP093T2RWF6MS4DVA1T":72},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":23,"publishedAt":44,"linkOnly":45,"markdownContent":46,"markdownBody":47,"frontMatter":48,"refName":49,"commitSha":50,"skillDirectory":51,"forksCount":52,"repositoryHtmlUrl":53,"repositoryHomepageUrl":54,"ownerHtmlUrl":55,"ownerBlogUrl":54,"ownerDescription":56,"updatedAt":57,"install":58,"resources":71},"01M6D1QKP093T2RWF6MS4DVA1T","backend-stack-rails",null,"A Ruby on Rails backend: conventions, strong parameters, ActiveRecord validations, associations and includes against N+1, transactions and locking, safe migrations with strong_migrations, jobs with Solid Queue or Sidekiq, authentication with the Rails 8 generator or Devise, authorisation with Pundit, serialisation, errors with rescue_from, credentials, caching, and tests with Minitest or RSpec. Read when the project uses Rails.","enowdev\u002Fenowxcli","enowdev","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F179420349?v=4","01M6D1MVX2KC0X3Z9NESE7P22K","Enow Dev.","01M6D1MW1FMZGBDKWAJY4GQX81",67,"Apache-2.0","https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli\u002Fblob\u002Fmain\u002Fcrates\u002Fenowx-core\u002Fskills\u002Fbackend-stack-rails\u002FSKILL.md",[18],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000010","后端与接口","SCENARIO","使用场景",[24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43],"backend","backend stack rails","stack","rails","transactions","ruby","safe migrations","credentials","authentication","read","solid","queue","migrations","jobs","rescue","conventions","caching","project","tests","safe","2026-11-10 10:12:30",false,"---\nname: backend-stack-rails\ndescription: \"A Ruby on Rails backend: conventions, strong parameters, ActiveRecord validations, associations and includes against N+1, transactions and locking, safe migrations with strong_migrations, jobs with Solid Queue or Sidekiq, authentication with the Rails 8 generator or Devise, authorisation with Pundit, serialisation, errors with rescue_from, credentials, caching, and tests with Minitest or RSpec. Read when the project uses Rails.\"\n---\n\n# Ruby on Rails\n\nThe generated Rails app puts the rules in fat controllers or in callbacks\nthat fire on every save, permits whatever arrives, checks uniqueness only in\nRuby, loads `order.customer` inside a loop, adds an index that locks a busy\ntable at deploy, sends email inside the transaction, and answers\n`render json: @order` with every column. The backend rules are in the\n`backend` skill; these are Rails's own. Keep the project's versions and\ngems; the notes assume Rails 7.2 or 8.\n\n## 1. Conventions\n\n- The Ruby and Rails versions pinned (`.ruby-version`, `Gemfile.lock`). A\n  new app: current Rails 8 with `--database=postgresql`, and `--api` for an\n  API alone.\n- Follow the conventions rather than fight them: RESTful controllers,\n  Zeitwerk naming (`bin\u002Frails zeitwerk:check`), routes as `resources` inside\n  `namespace :api` and `namespace :v1`, with `only:` listing the actions and\n  extra actions as member routes (`post :cancel, on: :member`).\n- Rules live in models (validations, scopes, small methods) and in plain\n  Ruby objects for workflows that span models (`Order::Placement`, or\n  `app\u002Fservices\u002F` if the project has it). Callbacks only for the record's\n  own state (normalising a field), never for sending mail, charging cards\n  or changing other records.\n\n## 2. Controllers and strong parameters\n\n```ruby\nclass Api::V1::OrdersController \u003C Api::BaseController\n  def show\n    render json: OrderResource.new(Current.user.orders.find(params[:id])).serialize\n  end\n\n  def create\n    order = Order::Placement.new(customer: Current.user, params: order_params).call\n    render json: OrderResource.new(order).serialize, status: :created,\n           location: api_v1_order_url(order)\n  end\n\n  private\n\n  def order_params\n    params.expect(order: [:note, lines: [[:product_id, :quantity]]])\n  end\nend\n```\n\n- `params.expect` (Rails 8) returns only the listed keys and answers `400`\n  when the shape is wrong; before 8, `params.require(:order).permit(...)`.\n  Never `permit!`, never `params` straight into `create`: `role`, `price`\n  and `user_id` are set in code.\n- Every lookup goes through the caller (`Current.user.orders.find(id)`), so\n  another user's id raises `RecordNotFound`, a `404`.\n- API controllers inherit from `ActionController::API`; full-stack ones\n  keep CSRF protection (on by default in `ActionController::Base`).\n\n## 3. Models: validations and constraints both\n\n- Each validation that protects data has a database constraint behind it:\n  `uniqueness` plus a unique index (the validation alone races), `presence`\n  plus `null: false`, `inclusion` plus a check constraint, `belongs_to`\n  (required by default) plus a foreign key.\n- Duplicates caught where they happen: `rescue ActiveRecord::RecordNotUnique`\n  becomes a `409` or a field error; `create_or_find_by` relies on the\n  unique index.\n- `normalizes :email, with: ->(email) { email.strip.downcase }` (7.1+), so\n  lookups and the unique index agree.\n- Enums with explicit values\n  (`enum :status, { pending: 0, paid: 1, cancelled: 2 }, validate: true`),\n  or string values with a check constraint so the data reads well in SQL.\n- Money as integer cents plus a currency (or the money-rails gem); times\n  stored in UTC, `config.time_zone` only for display.\n\n## 4. Associations and N+1\n\n- `preload` (separate queries, the usual choice), `eager_load` (one\n  `LEFT JOIN`, when filtering on the association) or `includes` (Rails\n  picks); counts with `counter_cache` or one grouped query, never `.count`\n  per row.\n- N+1 fails in development and tests: `strict_loading` (per relation, per\n  model with `self.strict_loading_by_default = true`, or app-wide with\n  `config.active_record.strict_loading_by_default`), set to raise, and the\n  Bullet gem for what slips past.\n- Work over many rows with `find_each` (batches of 1,000) or `in_batches`,\n  never `Model.all.each`; `pluck` and `select` for the columns a report\n  needs; `exists?` to test for presence.\n\n## 5. Transactions and locking\n\n```ruby\nOrder.transaction do\n  order = Order.create!(customer: user, status: :pending)\n  lines.each do |line|\n    reserved = Product.where(id: line[:product_id])\n                      .where(\"stock >= ?\", line[:quantity])\n                      .update_all([\"stock = stock - ?\", line[:quantity]])\n    raise OutOfStock, line[:product_id] if reserved.zero?\n    order.items.create!(product_id: line[:product_id], quantity: line[:quantity])\n  end\n  order\nend\n```\n\n- Bang methods inside transactions (`create!`, `update!`, `save!`): a plain\n  `save` that returns `false` lets the rest commit.\n- Counters and stock with conditional `update_all` or `update_counters`;\n  `with_lock` (a transaction and `SELECT ... FOR UPDATE`) when you must read\n  then decide; optimistic locking with a `lock_version` column\n  (`ActiveRecord::StaleObjectError` answers `409`).\n- Side effects after commit: `after_commit`, or enqueue after the\n  `transaction` block. Rails 7.2 added `enqueue_after_transaction_commit`\n  and its default has shifted since: check the project's setting instead of\n  assuming. No HTTP calls or mail inside a transaction.\n\n## 6. Migrations with strong_migrations\n\nThe strong_migrations gem stops unsafe migrations in development and prints\nthe safe recipe. On Postgres:\n\n| Change | Safe way |\n|---|---|\n| Add an index | `algorithm: :concurrently` with `disable_ddl_transaction!` |\n| Add a foreign key | `validate: false`, then `validate_foreign_key` in a second migration |\n| `NOT NULL` on a filled column | a check constraint with `validate: false`, validate it, `change_column_null`, drop the check |\n| Remove a column | `self.ignored_columns += [\"name\"]` deployed first, then the migration |\n| Rename a column or table | a new one, write both, backfill, switch reads, drop the old |\n| Change a column's type | a new column and a backfill (most type changes rewrite the table) |\n| Backfill data | a job or a separate migration in batches, not inside the schema change |\n\n- `StrongMigrations.lock_timeout` (a few seconds) and `statement_timeout`\n  set, so a migration waiting on a lock fails fast instead of queueing every\n  request behind it. Adding a column with a constant default is instant on\n  Postgres 11+.\n- `db\u002Fschema.rb` (or `structure.sql` when the database has views, triggers\n  or functions) committed with the migration; a migration that ran is never\n  edited.\n\n## 7. Jobs\n\n- Active Job on Solid Queue (Rails 8's default, backed by the database:\n  `bin\u002Fjobs` runs the workers, `config\u002Fqueue.yml` sets queues and threads,\n  `config\u002Frecurring.yml` holds scheduled work), or Sidekiq with Redis where\n  the project has it or volume demands it.\n- Jobs take records or ids (GlobalID passes records as ids and loads them\n  when the job runs) and are idempotent (`backend-jobs`). `retry_on` what\n  can succeed later (`wait: :polynomially_longer, attempts: 10`),\n  `discard_on` what never will (`ActiveJob::DeserializationError` when the\n  record is gone).\n- Solid Queue's `limits_concurrency` for jobs that must not run twice at\n  once for the same record; Mission Control Jobs to see and retry failures.\n- Mail with `deliver_later`, never `deliver_now` in a request.\n\n## 8. Authentication and authorisation\n\n- Rails 8's `bin\u002Frails generate authentication`: database sessions,\n  `has_secure_password` (bcrypt), password reset, and a rate limit on\n  sign-in; sign-up is yours to add. Devise where the project has it or needs\n  confirmation, locking and OAuth together. API tokens are random, shown\n  once and stored as a digest; `has_secure_token` keeps them in plain text,\n  so not for credentials (`backend-auth`).\n- `rate_limit to: 10, within: 3.minutes, only: :create` (7.2+) on sign-in,\n  sign-up, reset and anything that sends mail. It counts in the cache\n  store, which must be shared across instances.\n- Pundit (or Action Policy): a policy per model, `authorize @order` in each\n  action, `policy_scope(Order)` for lists, and\n  `after_action :verify_authorized` (plus `verify_policy_scoped` on `index`)\n  so a forgotten check fails in tests.\n\n## 9. JSON and errors\n\n- Serializers decide the fields: Alba (fast, explicit) or Jbuilder views\n  where the project uses them. Never `render json: @record` or `to_json` of\n  a model: every column goes out, future ones included.\n- Lists paginated in the query (pagy, or keyset on `(created_at, id)`) with\n  the associations the serializer shows preloaded.\n\n```ruby\nclass Api::BaseController \u003C ActionController::API\n  rescue_from ActiveRecord::RecordNotFound do\n    render_problem 404, \"not_found\"\n  end\n\n  rescue_from ActiveRecord::RecordInvalid do |e|\n    render_problem 422, \"invalid\",\n                   errors: e.record.errors.map { |err| { field: err.attribute, code: err.type } }\n  end\n\n  private\n\n  def render_problem(status, code, **extra)\n    render json: { status: status, code: code, **extra }, status: status,\n           content_type: \"application\u002Fproblem+json\"\n  end\nend\n```\n\n- `StaleObjectError` and domain errors (`OutOfStock`) get their own\n  `rescue_from` with `409` and a code; anything else reaches Rails's handler,\n  a bare `500` in production. Status `422` is `:unprocessable_content` in\n  Rack 3.1+ (`:unprocessable_entity` before).\n- Messages users read come from I18n (`errors.add(:quantity, :too_many)`\n  with keys in `config\u002Flocales`, the `i18n` skill).\n\n## 10. Configuration and logs\n\n- Secrets in encrypted credentials, edited with\n  `bin\u002Frails credentials:edit --environment production` and read with\n  `Rails.application.credentials.dig(:stripe, :secret_key)`, the key in\n  `RAILS_MASTER_KEY` on the server; or in `ENV` where the platform provides\n  them. `config\u002Fmaster.key` and `config\u002Fcredentials\u002F*.key` never committed.\n- Required values read so a missing one fails at boot:\n  `ENV.fetch(\"STRIPE_SECRET_KEY\")` in an initializer, not `ENV[\"...\"]` deep\n  in a request.\n- `config.filter_parameters` covers passwords, tokens, keys and card\n  numbers (the generated list plus yours); `config.log_tags = [:request_id]`;\n  JSON logs through lograge or the project's logger.\n\n## 11. Caching, real-time and files\n\n- `Rails.cache.fetch(key, expires_in: 10.minutes, race_condition_ttl: 10.seconds)`\n  on Solid Cache (Rails 8's default) or Redis; keys from\n  `cache_key_with_version`, fragment caching with `cache @product`, and\n  Russian-doll nesting with `touch: true` on `belongs_to` (`backend-caching`).\n- Turbo Streams broadcasts (`broadcasts_refreshes`) or Action Cable channels\n  on Solid Cable or Redis, authorised in the channel's `subscribed`\n  (`backend-realtime`).\n- Active Storage with direct uploads to S3 or R2 and variants through\n  libvips (`backend-files`).\n\n## 12. Tests\n\n```ruby\nclass OrdersApiTest \u003C ActionDispatch::IntegrationTest\n  test \"refuses an order for more than the stock\" do\n    product = products(:kopi) # a fixture with stock: 5\n    post api_v1_orders_url, as: :json, headers: auth_headers(users(:ana)),\n         params: { order: { lines: [{ product_id: product.id, quantity: 999 }] } }\n    assert_response :conflict\n    assert_equal \"out_of_stock\", response.parsed_body[\"code\"]\n    assert_equal 5, product.reload.stock\n  end\nend\n```\n\n- Minitest (the default) or RSpec, as the project has; fixtures or\n  factory_bot; request tests through the whole stack against Postgres, as\n  in production (`bin\u002Frails db:test:prepare` in CI).\n- `ActiveJob::TestHelper` (`assert_enqueued_with`, `perform_enqueued_jobs`),\n  `travel_to` for time, WebMock so no test reaches a real service,\n  `parallelize(workers: :number_of_processors)`.\n- System tests (Capybara) only for the few flows that need a browser.\n\n## Check it\n\n`bin\u002Frails test` (or `bundle exec rspec`), `bin\u002Frubocop`\n(rubocop-rails-omakase), `bin\u002Fbrakeman`; `bin\u002Frails db:migrate` then\n`bin\u002Frails db:rollback` on a copy for reversible migrations;\n`bin\u002Frails routes -g orders` for the routes. Then `bin\u002Frails server` and\n`curl` the endpoints: an invalid body gives every field with its code,\nanother user's id a `404`. The log of a list request shows one query per\nassociation, not one per row, and the tests raise no strict loading error.\n\n## Avoid\n\n`permit!` or raw `params` into `create`; uniqueness checked only in Ruby;\ncallbacks that send mail, charge or call APIs; `deliver_now` in a request;\n`render json: @record`; `Model.all.each`; N+1 hidden by a small development\ndatabase; plain `save` inside a transaction; migrations that lock big tables\n(plain indexes, validated foreign keys, type changes); editing a migration\nthat ran; the master key committed; `has_secure_token` values used as API\nkeys; no `verify_authorized`; tests that call real services.\n","# Ruby on Rails\n\nThe generated Rails app puts the rules in fat controllers or in callbacks\nthat fire on every save, permits whatever arrives, checks uniqueness only in\nRuby, loads `order.customer` inside a loop, adds an index that locks a busy\ntable at deploy, sends email inside the transaction, and answers\n`render json: @order` with every column. The backend rules are in the\n`backend` skill; these are Rails's own. Keep the project's versions and\ngems; the notes assume Rails 7.2 or 8.\n\n## 1. Conventions\n\n- The Ruby and Rails versions pinned (`.ruby-version`, `Gemfile.lock`). A\n  new app: current Rails 8 with `--database=postgresql`, and `--api` for an\n  API alone.\n- Follow the conventions rather than fight them: RESTful controllers,\n  Zeitwerk naming (`bin\u002Frails zeitwerk:check`), routes as `resources` inside\n  `namespace :api` and `namespace :v1`, with `only:` listing the actions and\n  extra actions as member routes (`post :cancel, on: :member`).\n- Rules live in models (validations, scopes, small methods) and in plain\n  Ruby objects for workflows that span models (`Order::Placement`, or\n  `app\u002Fservices\u002F` if the project has it). Callbacks only for the record's\n  own state (normalising a field), never for sending mail, charging cards\n  or changing other records.\n\n## 2. Controllers and strong parameters\n\n```ruby\nclass Api::V1::OrdersController \u003C Api::BaseController\n  def show\n    render json: OrderResource.new(Current.user.orders.find(params[:id])).serialize\n  end\n\n  def create\n    order = Order::Placement.new(customer: Current.user, params: order_params).call\n    render json: OrderResource.new(order).serialize, status: :created,\n           location: api_v1_order_url(order)\n  end\n\n  private\n\n  def order_params\n    params.expect(order: [:note, lines: [[:product_id, :quantity]]])\n  end\nend\n```\n\n- `params.expect` (Rails 8) returns only the listed keys and answers `400`\n  when the shape is wrong; before 8, `params.require(:order).permit(...)`.\n  Never `permit!`, never `params` straight into `create`: `role`, `price`\n  and `user_id` are set in code.\n- Every lookup goes through the caller (`Current.user.orders.find(id)`), so\n  another user's id raises `RecordNotFound`, a `404`.\n- API controllers inherit from `ActionController::API`; full-stack ones\n  keep CSRF protection (on by default in `ActionController::Base`).\n\n## 3. Models: validations and constraints both\n\n- Each validation that protects data has a database constraint behind it:\n  `uniqueness` plus a unique index (the validation alone races), `presence`\n  plus `null: false`, `inclusion` plus a check constraint, `belongs_to`\n  (required by default) plus a foreign key.\n- Duplicates caught where they happen: `rescue ActiveRecord::RecordNotUnique`\n  becomes a `409` or a field error; `create_or_find_by` relies on the\n  unique index.\n- `normalizes :email, with: ->(email) { email.strip.downcase }` (7.1+), so\n  lookups and the unique index agree.\n- Enums with explicit values\n  (`enum :status, { pending: 0, paid: 1, cancelled: 2 }, validate: true`),\n  or string values with a check constraint so the data reads well in SQL.\n- Money as integer cents plus a currency (or the money-rails gem); times\n  stored in UTC, `config.time_zone` only for display.\n\n## 4. Associations and N+1\n\n- `preload` (separate queries, the usual choice), `eager_load` (one\n  `LEFT JOIN`, when filtering on the association) or `includes` (Rails\n  picks); counts with `counter_cache` or one grouped query, never `.count`\n  per row.\n- N+1 fails in development and tests: `strict_loading` (per relation, per\n  model with `self.strict_loading_by_default = true`, or app-wide with\n  `config.active_record.strict_loading_by_default`), set to raise, and the\n  Bullet gem for what slips past.\n- Work over many rows with `find_each` (batches of 1,000) or `in_batches`,\n  never `Model.all.each`; `pluck` and `select` for the columns a report\n  needs; `exists?` to test for presence.\n\n## 5. Transactions and locking\n\n```ruby\nOrder.transaction do\n  order = Order.create!(customer: user, status: :pending)\n  lines.each do |line|\n    reserved = Product.where(id: line[:product_id])\n                      .where(\"stock >= ?\", line[:quantity])\n                      .update_all([\"stock = stock - ?\", line[:quantity]])\n    raise OutOfStock, line[:product_id] if reserved.zero?\n    order.items.create!(product_id: line[:product_id], quantity: line[:quantity])\n  end\n  order\nend\n```\n\n- Bang methods inside transactions (`create!`, `update!`, `save!`): a plain\n  `save` that returns `false` lets the rest commit.\n- Counters and stock with conditional `update_all` or `update_counters`;\n  `with_lock` (a transaction and `SELECT ... FOR UPDATE`) when you must read\n  then decide; optimistic locking with a `lock_version` column\n  (`ActiveRecord::StaleObjectError` answers `409`).\n- Side effects after commit: `after_commit`, or enqueue after the\n  `transaction` block. Rails 7.2 added `enqueue_after_transaction_commit`\n  and its default has shifted since: check the project's setting instead of\n  assuming. No HTTP calls or mail inside a transaction.\n\n## 6. Migrations with strong_migrations\n\nThe strong_migrations gem stops unsafe migrations in development and prints\nthe safe recipe. On Postgres:\n\n| Change | Safe way |\n|---|---|\n| Add an index | `algorithm: :concurrently` with `disable_ddl_transaction!` |\n| Add a foreign key | `validate: false`, then `validate_foreign_key` in a second migration |\n| `NOT NULL` on a filled column | a check constraint with `validate: false`, validate it, `change_column_null`, drop the check |\n| Remove a column | `self.ignored_columns += [\"name\"]` deployed first, then the migration |\n| Rename a column or table | a new one, write both, backfill, switch reads, drop the old |\n| Change a column's type | a new column and a backfill (most type changes rewrite the table) |\n| Backfill data | a job or a separate migration in batches, not inside the schema change |\n\n- `StrongMigrations.lock_timeout` (a few seconds) and `statement_timeout`\n  set, so a migration waiting on a lock fails fast instead of queueing every\n  request behind it. Adding a column with a constant default is instant on\n  Postgres 11+.\n- `db\u002Fschema.rb` (or `structure.sql` when the database has views, triggers\n  or functions) committed with the migration; a migration that ran is never\n  edited.\n\n## 7. Jobs\n\n- Active Job on Solid Queue (Rails 8's default, backed by the database:\n  `bin\u002Fjobs` runs the workers, `config\u002Fqueue.yml` sets queues and threads,\n  `config\u002Frecurring.yml` holds scheduled work), or Sidekiq with Redis where\n  the project has it or volume demands it.\n- Jobs take records or ids (GlobalID passes records as ids and loads them\n  when the job runs) and are idempotent (`backend-jobs`). `retry_on` what\n  can succeed later (`wait: :polynomially_longer, attempts: 10`),\n  `discard_on` what never will (`ActiveJob::DeserializationError` when the\n  record is gone).\n- Solid Queue's `limits_concurrency` for jobs that must not run twice at\n  once for the same record; Mission Control Jobs to see and retry failures.\n- Mail with `deliver_later`, never `deliver_now` in a request.\n\n## 8. Authentication and authorisation\n\n- Rails 8's `bin\u002Frails generate authentication`: database sessions,\n  `has_secure_password` (bcrypt), password reset, and a rate limit on\n  sign-in; sign-up is yours to add. Devise where the project has it or needs\n  confirmation, locking and OAuth together. API tokens are random, shown\n  once and stored as a digest; `has_secure_token` keeps them in plain text,\n  so not for credentials (`backend-auth`).\n- `rate_limit to: 10, within: 3.minutes, only: :create` (7.2+) on sign-in,\n  sign-up, reset and anything that sends mail. It counts in the cache\n  store, which must be shared across instances.\n- Pundit (or Action Policy): a policy per model, `authorize @order` in each\n  action, `policy_scope(Order)` for lists, and\n  `after_action :verify_authorized` (plus `verify_policy_scoped` on `index`)\n  so a forgotten check fails in tests.\n\n## 9. JSON and errors\n\n- Serializers decide the fields: Alba (fast, explicit) or Jbuilder views\n  where the project uses them. Never `render json: @record` or `to_json` of\n  a model: every column goes out, future ones included.\n- Lists paginated in the query (pagy, or keyset on `(created_at, id)`) with\n  the associations the serializer shows preloaded.\n\n```ruby\nclass Api::BaseController \u003C ActionController::API\n  rescue_from ActiveRecord::RecordNotFound do\n    render_problem 404, \"not_found\"\n  end\n\n  rescue_from ActiveRecord::RecordInvalid do |e|\n    render_problem 422, \"invalid\",\n                   errors: e.record.errors.map { |err| { field: err.attribute, code: err.type } }\n  end\n\n  private\n\n  def render_problem(status, code, **extra)\n    render json: { status: status, code: code, **extra }, status: status,\n           content_type: \"application\u002Fproblem+json\"\n  end\nend\n```\n\n- `StaleObjectError` and domain errors (`OutOfStock`) get their own\n  `rescue_from` with `409` and a code; anything else reaches Rails's handler,\n  a bare `500` in production. Status `422` is `:unprocessable_content` in\n  Rack 3.1+ (`:unprocessable_entity` before).\n- Messages users read come from I18n (`errors.add(:quantity, :too_many)`\n  with keys in `config\u002Flocales`, the `i18n` skill).\n\n## 10. Configuration and logs\n\n- Secrets in encrypted credentials, edited with\n  `bin\u002Frails credentials:edit --environment production` and read with\n  `Rails.application.credentials.dig(:stripe, :secret_key)`, the key in\n  `RAILS_MASTER_KEY` on the server; or in `ENV` where the platform provides\n  them. `config\u002Fmaster.key` and `config\u002Fcredentials\u002F*.key` never committed.\n- Required values read so a missing one fails at boot:\n  `ENV.fetch(\"STRIPE_SECRET_KEY\")` in an initializer, not `ENV[\"...\"]` deep\n  in a request.\n- `config.filter_parameters` covers passwords, tokens, keys and card\n  numbers (the generated list plus yours); `config.log_tags = [:request_id]`;\n  JSON logs through lograge or the project's logger.\n\n## 11. Caching, real-time and files\n\n- `Rails.cache.fetch(key, expires_in: 10.minutes, race_condition_ttl: 10.seconds)`\n  on Solid Cache (Rails 8's default) or Redis; keys from\n  `cache_key_with_version`, fragment caching with `cache @product`, and\n  Russian-doll nesting with `touch: true` on `belongs_to` (`backend-caching`).\n- Turbo Streams broadcasts (`broadcasts_refreshes`) or Action Cable channels\n  on Solid Cable or Redis, authorised in the channel's `subscribed`\n  (`backend-realtime`).\n- Active Storage with direct uploads to S3 or R2 and variants through\n  libvips (`backend-files`).\n\n## 12. Tests\n\n```ruby\nclass OrdersApiTest \u003C ActionDispatch::IntegrationTest\n  test \"refuses an order for more than the stock\" do\n    product = products(:kopi) # a fixture with stock: 5\n    post api_v1_orders_url, as: :json, headers: auth_headers(users(:ana)),\n         params: { order: { lines: [{ product_id: product.id, quantity: 999 }] } }\n    assert_response :conflict\n    assert_equal \"out_of_stock\", response.parsed_body[\"code\"]\n    assert_equal 5, product.reload.stock\n  end\nend\n```\n\n- Minitest (the default) or RSpec, as the project has; fixtures or\n  factory_bot; request tests through the whole stack against Postgres, as\n  in production (`bin\u002Frails db:test:prepare` in CI).\n- `ActiveJob::TestHelper` (`assert_enqueued_with`, `perform_enqueued_jobs`),\n  `travel_to` for time, WebMock so no test reaches a real service,\n  `parallelize(workers: :number_of_processors)`.\n- System tests (Capybara) only for the few flows that need a browser.\n\n## Check it\n\n`bin\u002Frails test` (or `bundle exec rspec`), `bin\u002Frubocop`\n(rubocop-rails-omakase), `bin\u002Fbrakeman`; `bin\u002Frails db:migrate` then\n`bin\u002Frails db:rollback` on a copy for reversible migrations;\n`bin\u002Frails routes -g orders` for the routes. Then `bin\u002Frails server` and\n`curl` the endpoints: an invalid body gives every field with its code,\nanother user's id a `404`. The log of a list request shows one query per\nassociation, not one per row, and the tests raise no strict loading error.\n\n## Avoid\n\n`permit!` or raw `params` into `create`; uniqueness checked only in Ruby;\ncallbacks that send mail, charge or call APIs; `deliver_now` in a request;\n`render json: @record`; `Model.all.each`; N+1 hidden by a small development\ndatabase; plain `save` inside a transaction; migrations that lock big tables\n(plain indexes, validated foreign keys, type changes); editing a migration\nthat ran; the master key committed; `has_secure_token` values used as API\nkeys; no `verify_authorized`; tests that call real services.\n",{"name":5,"description":7},"main","38293fe1b096e6bf3646c7344c38786ed688f464","crates\u002Fenowx-core\u002Fskills\u002Fbackend-stack-rails",21,"https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli","https:\u002F\u002Fenowx.ai","https:\u002F\u002Fgithub.com\u002Fenowdev","Pretending to be a Developer","2026-10-06 02:05:35",{"source":8,"skillName":5,"commands":59,"prompt":69,"zipAvailable":70},[60,63,66],{"tool":61,"command":62},"npx","npx skills add enowdev\u002Fenowxcli --skill backend-stack-rails",{"tool":64,"command":65},"bunx","bunx skills add enowdev\u002Fenowxcli --skill backend-stack-rails",{"tool":67,"command":68},"pnpm","pnpm dlx skills add enowdev\u002Fenowxcli --skill backend-stack-rails","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli\u002Fblob\u002Fmain\u002Fcrates\u002Fenowx-core\u002Fskills\u002Fbackend-stack-rails\u002FSKILL.md",true,[],[73,85,110,126,153,175],{"skillNo":74,"name":75,"nameZh":6,"description":76,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":77,"categories":78,"keywords":82,"publishedAt":44},"01M6D2HQYSQD9N7MTNDT4A7CBM","demo-skill","Project-scope demo skill for tests.","https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli\u002Fblob\u002Fmain\u002Fworkspace\u002F.agents\u002Fskills\u002Fdemo-skill\u002FSKILL.md",[79],{"categoryNo":80,"categoryName":81,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000023","Skill与智能体开发",[83,84,42,41,33],"demo skill","demo",{"skillNo":86,"name":87,"nameZh":6,"description":88,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":89,"categories":90,"keywords":99,"publishedAt":44},"01M6D2HM0GCPM0TM2HPG9HHR2R","writing","Writing words people read: the specific thing instead of claims, words to drop, sentences, interface copy (buttons, labels, errors, empty states, confirmations), page copy, notifications and emails, alt text and link text, numbers and dates, words that translate well, documentation and voice. Read before writing a page's copy, an interface's text or a document.","https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli\u002Fblob\u002Fmain\u002Fcrates\u002Fenowx-core\u002Fskills\u002Fwriting\u002FSKILL.md",[91,94],{"categoryNo":92,"categoryName":93,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000018","写作与文案",{"categoryNo":95,"categoryName":96,"taxonomyType":97,"taxonomyTypeDesc":98},"01JBSEEDCAT000000000000036","内容创作者","PROFESSION","适用职业",[87,100,101,102,103,104,33,105,106,107,108,109],"text","emails","empty","drop","document","page","voice","notifications","documentation","translate",{"skillNo":111,"name":112,"nameZh":6,"description":113,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":114,"categories":115,"keywords":119,"publishedAt":44},"01M6D2HG07YS49NM196Q0R3AZ8","ui","Designing or restyling an interface: direction, layout, type, colour, icons, components, states, responsive behaviour. Read before building a page, a screen or a set of components.","https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli\u002Fblob\u002Fmain\u002Fcrates\u002Fenowx-core\u002Fskills\u002Fui\u002FSKILL.md",[116],{"categoryNo":117,"categoryName":118,"taxonomyType":97,"taxonomyTypeDesc":98},"01JBSEEDCAT000000000000032","设计师",[112,120,121,122,123,105,124,33,125],"icons","components","SET","responsive","layout","before building",{"skillNo":127,"name":128,"nameZh":6,"description":129,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":130,"categories":131,"keywords":138,"publishedAt":44},"01M6D2HB59HZHWG1RYZDV4SJ91","ui-themes","Light and dark themes, and colour that holds in both: tokens with a value per theme, and what each kind of content needs to stay visible (icons, SVG drawings and line art, logos, photos, screenshots, charts, code, maps, borders, shadows, focus, form controls), switching without a flash, and checking both. Read before building a second theme or a theme toggle, and before any drawing, logo or chart on a page that has two themes.","https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli\u002Fblob\u002Fmain\u002Fcrates\u002Fenowx-core\u002Fskills\u002Fui-themes\u002FSKILL.md",[132,135],{"categoryNo":133,"categoryName":134,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000015","视觉设计",{"categoryNo":136,"categoryName":137,"taxonomyType":97,"taxonomyTypeDesc":98},"01JBSEEDCAT000000000000030","开发工程师",[112,139,140,141,142,143,144,145,146,33,125,147,105,148,149,150,120,151,152],"ui themes","switching","photos","tokens","drawing","focus","maps","Art","flash","code","Charts","content","screenshots","logo",{"skillNo":154,"name":155,"nameZh":6,"description":156,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":157,"categories":158,"keywords":162,"publishedAt":44},"01M6D2H5R5KRAE524C8QX0T7Q0","ui-structure","Structure before styling: the blueprint written before any interface code (the screen's job, the regions its kind of page needs, their order and why, the layout skeleton in numbers, the components and their states), the anatomy and order of a homepage, a dashboard, a list, a detail page, settings, a form, sign-in, pricing, docs, an article and a checkout, and how to turn the blueprint into code (tokens, layout primitives, components, sections, page). Read first, before building or rebuilding any page or screen.","https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli\u002Fblob\u002Fmain\u002Fcrates\u002Fenowx-core\u002Fskills\u002Fui-structure\u002FSKILL.md",[159,160,161],{"categoryNo":92,"categoryName":93,"taxonomyType":21,"taxonomyTypeDesc":22},{"categoryNo":136,"categoryName":137,"taxonomyType":97,"taxonomyTypeDesc":98},{"categoryNo":95,"categoryName":96,"taxonomyType":97,"taxonomyTypeDesc":98},[163,112,164,121,165,166,148,167,168,169,124,142,170,171,172,33,125,105,173,174],"structure","ui structure","pricing","dashboard","Blueprint","article","anatomy","styling","primitives","list","why","how",{"skillNo":176,"name":177,"nameZh":6,"description":178,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":179,"categories":180,"keywords":188,"publishedAt":44},"01M6D2H0ZZBJG77MBGPZHA6MWD","ui-stack-vue","Building interfaces in Vue 3 and Nuxt: project layout, script setup with typed defineProps, defineEmits and defineModel, attribute fallthrough, slots and scoped slots, template refs, ref, reactive and computed, watch versus watchEffect, composables and VueUse, provide and inject, Pinia, keyed v-for, transitions, Nuxt 4 (app directory, pages, layouts, useFetch and useAsyncData keys, $fetch in handlers, server routes, runtime config, useSeoMeta, NuxtImg, middleware), VeeValidate forms, Reka UI and Nuxt UI, testing, vue-tsc, and the traps. Read when the project uses Vue or Nuxt.","https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli\u002Fblob\u002Fmain\u002Fcrates\u002Fenowx-core\u002Fskills\u002Fui-stack-vue\u002FSKILL.md",[181,184,187],{"categoryNo":182,"categoryName":183,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000008","测试与质量",{"categoryNo":185,"categoryName":186,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000009","前端开发",{"categoryNo":117,"categoryName":118,"taxonomyType":97,"taxonomyTypeDesc":98},[112,189,26,190,191,192,193,194,124,41,195,196,33,197,198,199,200,201,202,203],"vue","ui stack vue","forms","pages","watch","runtime","transitions","fetch","pinia","nuxt","template","server","config","directory","testing"]