Files
project-work/worker-toolkit-stocks-in-the-future/repo/docs/map/objects/identity/user.md

3.3 KiB

type, cluster, universe, status, entity
type cluster universe status entity
object identity live verified app/models/user.rb

User

Every human in the app. STI base class for Student and Teacher — but admin is a boolean column on this table, not a subclass.

Verified 2026-08-16 against commit 63732df.

Why this shape

The users are middle-school students, so email cannot be the login. Devise is reconfigured to authenticate on username (config/initializers/devise.rb:49), email is optional, and its uniqueness index is partial — it applies only where email is non-null and non-empty (db/schema.rb:388), so any number of students can have no email at all. Student actively forces blank email back to nil to stay inside that index (app/models/student.rb:74-76).

Hard deletes are blocked because a user owns a financial ledger. destroy and destroy! are overridden to discard, and outside production they raise rather than silently soft-delete (app/models/user.rb:6-14,75-82). really_destroy! is the deliberate escape hatch (:16-18).

Shape

  • Table users, db/schema.rb:372-391
  • type — "User" | "Student" | "Teacher", validated at app/models/user.rb:39
  • admin — boolean, default false (db/schema.rb:373); scope at :43
  • username — null: false, unique index, the login key (db/schema.rb:385,390)
  • email — nullable, partial unique index (db/schema.rb:388); required only for teachers and admins (app/models/user.rb:61-63)
  • discarded_at — soft delete via Discard::Model (app/models/user.rb:4)
  • classroom_id — direct membership. See the roster trap in ../../CONTEXT.md

email_changed? is hard-coded to false (app/models/user.rb:65-67), which suppresses Devise's reconfirmation path. The code wins over the method name — it is not a real dirty-check.

Connected to

  • owns: portfolio (has_one, students only), order (has_many)
  • owned-by: classroom (belongs_to, optional)
  • joins: classroom-enrollment as Student, teacher_classrooms as Teacher
  • looks-like-but-is-not: admin is not an STI type — there is no Admin class. A Teacher with admin: true is one row, not two.

If you change this

  • Hits: student and teacher (same table); portfolio — Portfolio validates its user is a student (app/models/portfolio.rb:102-104); every Pundit policy, which branches on user.admin? / user.student? (app/policies/application_policy.rb:39-53); Devise sign-in if you touch username or email nullability.
  • Does not hit: portfolio-transaction. It hangs off Portfolio, not User — discarding a user leaves the ledger fully intact and still summable. That is deliberate, not an oversight.

Surfaces

Surface Role
Devise controllers reads (sign-in by username)
Admin::UsersController, Admin::StudentsController, Admin::TeachersController read/write
StudentsController (nested under classrooms, teacher-facing) read/write
ApplicationController#authenticate_user! reads every request

See

  • Source: app/models/user.rb, db/schema.rb:372-391
  • Login config: config/initializers/devise.rb:49