Rails Markup · Product plan · Rev 1.1

Multi-user access & ticket tracking

Move Rails Markup from a single hardcoded admin gate to a host-defined capability model for internal staff: grant view, create, and/or manage so people can file bugs/change requests and track their own tickets — without becoming superadmins.

status: decisions locked · rev 1.1 · 2026-08-10 · audience: internal staff · notifications: separate track

Locked for this revision. Internal staff only. Non-manage users see only their own annotations. After create, only manage may edit, delete, reply, or change status. One shared base_controller_class for dashboard + toolbar API.

Locked decisions

QuestionDecision
Audience Internal staff only — not end-customers / multi-tenant public reporters in this track
Seeing others’ tickets No, unless the user has manage (then see all)
Edit / delete / reply / status after create Manage only — reporters cannot mutate their own tickets post-create
Auth parent One base_controller_class for dashboard + toolbar API

Why now

Today the product story is “admins annotate, agents consume.” Hosts hard-code something like current_user.admin? in both the layout gate and authorize_rails_markup!. That blocks letting broader internal staff file feedback and follow their tickets, while only a smaller set triages the whole queue.

Current state (as of codebase)

SurfaceGate todayImplication
Layout / toolbar chrome Install injects current_user.admin? Non-admins never see FAB / pins
Dashboard + toolbar API Generated authorize_rails_markup! → admin? Binary allow/deny; read = write
Page annotation pull Annotation.for_page(url) — global among authorized users No ownership scoping
fab_visible = false UI-only “read-only-ish” Not enforced server-side
Attribution user_id + metadata.author on create Identity exists; unused for ACL
Notifications on_create_callback only No status-change / per-user notify
MCP / CLI / external API Shared bearer token Out of scope for human roles
Reuse, don’t rebuild. Ticket lifecycle already exists: intents fix|change|question|approve, statuses pending → acknowledged → resolved|dismissed, toolbar panel, dashboard list/board, threads. Gap is authorization + scoping.

Personas (internal staff)

PersonaNeedsCapabilities
Reporter
internal staff
File bugs/CRs; track status of own tickets only; no post-create edits view create
Viewer
optional
Track own tickets only; no filing view
Triager / Admin
today’s admin path
See everyone’s queue; edit/status/reply/delete/export; usually also create view create manage
Agent
MCP / CLI token
Unchanged service principal separate auth track

Capability model (locked)

Keep Rails Markup policy-thin: the gem names capabilities; the host decides who gets them.

rails_markup.view rails_markup.create rails_markup.manage
CapabilityAllowsDoes not allow
view Toolbar chrome; GET pull of own annotations; “My tickets” dashboard Seeing others’ pins/tickets; any mutation
create FAB + create new annotations only (initial upsert/create) Subsequent content edit, delete, reply, status change — even on own tickets
manage See all annotations; edit content; status transitions; reply; delete; board/bulk/export —
Visibility is derived from manage. No separate visibility Proc needed for r1: manage → scope :all; otherwise scope :own (user_id == current_user.id). manage implies view. Host still maps roles onto the three capabilities.

Mutation matrix

Actionviewcreatemanage
Pull / list own✓✓✓
Pull / list others——✓
Create new ticket—✓✓ (if also granted create, or manage includes create in host mapping)
Edit content / upsert after create——✓
Acknowledge / resolve / dismiss——✓
Reply / delete / bulk / export——✓
Host mapping tip. Default generator should map today’s admin? → [:view, :create, :manage] for backwards compatibility. Reporters get [:view, :create]. Recommend treating manage as also granting create in the resolver so triagers keep the FAB.

Host integration contract

One auth parent; extend hooks rather than replacing them.

1. Capability resolver

RailsMarkup.configure do |config|
  config.base_controller_class = "RailsMarkupAuthController"

  # Returns capabilities for the current user.
  config.authorize = ->(user, _request) {
    return [] unless user
    return [:view, :create, :manage] if user.admin?
    return [:view, :create] if user.staff_reporter?
    []
  }
  # Visibility: derived — manage? :all : :own
end

2. Single generated auth controller

3. Layout gate

4. Attribution

UX surfaces

A. Toolbar

B. Dashboard (progress tracking)

C. Notifications (separate track)

Phased delivery

01
Capability contract + server enforcement

Authorize resolver; action checks; scope pulls/lists by own-vs-all from manage; reject non-manage mutations after create; toolbar boot flags. Default admin? → full caps.

02
Reporter / viewer UX

Layout gate on :view; FAB on :create; read-only chrome without :manage; My tickets dashboard mode.

03
Docs + install notes

README examples for staff role helpers; dual-gate sync (layout ↔ API); migration from binary admin?.

04
Notifications (parallel, optional)

Transition/reply callbacks; host-owned delivery.

Extension points in the gem

PiecePathChange
Auth template lib/generators/.../auth_controller.rb.erb Capability-aware entry gate
Install layout gate install_generator.rb Gate on :view
Configuration configuration.rb authorize proc; derive visibility from manage
Annotations API annotations_controller.rb Create vs manage checks; scoped for_page
Dashboard dashboard_controller.rb Own vs all default scope; hide write UI
Toolbar boot _toolbar.html.erb + toolbar.js Pass capabilities; enforce read-only client UX
Annotation model annotation.rb scope :for_user

Still open

  1. Assignees: out of scope for r1, or minimal assignee_id later?
  2. Host ACL docs: document-only integration with Permissify/Pundit, or a first-class adapter later?
  3. Manage ⇒ create: auto-imply in the gem, or leave entirely to the host resolver?

Non-goals

Suggested next steps

  1. Decide the three remaining open items (especially manage⇒create implication).
  2. Sketch concrete config + controller concern API on an r1.2 note if needed.
  3. Spike server enforcement + toolbar flags against the locked matrix.