Skip to content

feat(web): operations center, support tickets and two-factor sign-in (web 0.62.0) - #611

Merged
kroqdotdev merged 5 commits into
mainfrom
t3/operations-center-support-tickets
Nov 3, 2026
Merged

kroqdotdev merged 5 commits into
mainfrom
t3/operations-center-support-tickets

Conversation

@kroqdotdev

Copy link
Copy Markdown
Owner

What

An operations center for staff at /ops, and support tickets for signed-in users at /support on webhooks.cc and mailhooks.cc. Web 0.62.0, migration 00087.

Staff

  • staff table keyed by auth user id. Add rows by hand with psql, never in a migration.
  • A trigger on users keeps a staff account on Business without a Polar subscription. A revoked subscription or the period job cannot move it off Business.
  • /ops and every /api/ops route answer 404 to anyone who is not staff. They accept a browser session only, never an API key, and need two-factor sign-in (aal2).
  • The account menu shows Operations to staff, with the count of tickets waiting.

Operations center

  • Overview: what needs you (waiting tickets, held refunds), all-time totals in one sentence, and a ledger of daily metrics.
    • Columns: yesterday, 7-day average, the previous 7 days, change, and a 28-day sparkline.
    • Rows: signups, activation, active accounts, captures, refused captures, paying accounts, MRR and subscriptions (from Polar's metrics API).
    • Read from ops_daily_metrics, which the web app fills hourly. Staff and users.internal accounts are left out.
  • Tickets: inbox views (waiting on you, waiting on user, snoozed, closed, spam), replies and internal notes, saved replies with {{name}}/{{plan}}, snooze, spam, close.
    • Shortcuts: j/k, r, n, e, /, Cmd+Enter, Cmd+Shift+Enter.
  • Accounts: search by email, id or endpoint slug.
    • The inspector shows metadata only: plan and billing, endpoints with 7-day counts and failed deliveries, teams, keys, agents, recent audit activity, tickets.
    • A test account can be marked internal.
  • View as user: read-only and 30 minutes long. It opens only from a ticket whose owner gave access.
    • Every endpoint and request opened is logged to support_access_log.
    • There is no write path and no way to sign in as the user.
  • Refunds: approve or dismiss held refunds from polar_refunds (00086, feat(web): refunds for charges that bought nothing (web 0.61.0) #609), with the orders and amounts the worker prepared.

Support tickets (users)

  • The form has topic, subject, details (markdown), endpoint, a "blocking?" question and up to 5 screenshots.
    • Screenshots are PNG/JPEG/WebP, checked by their bytes, with EXIF/XMP/text chunks stripped, and served with nosniff and a sandbox CSP.
    • Subjects get Pagefind doc suggestions.
  • Status is shown as a whose-turn line.
  • Staff replies arrive by email when "Email me…" is ticked, which is the default. The email carries the full reply; answers happen in the app.
  • A ticket waiting on the user gets a reminder at 3 days and closes at 7. A user reply within 30 days reopens it. A 1–5 rating is offered when it closes.
  • "Let support view my account while this ticket is open" is off by default, lasts until 7 days after the ticket closes, and can be withdrawn. Account > Support access lists grants and every look.
  • New tickets and replies alert SUPPORT_ALERT_EMAIL (default support@webhooks.cc) with no ticket contents.
  • Links: the account menu, the mobile menu, the settings nav footer, and the dashboard's Guide menu (prefilled with the endpoint).

Two-factor sign-in for everyone

  • Account > Security enrolls a GoTrue TOTP factor.
  • Once enrolled, RequireAuth asks for the code, and every session-authenticated route refuses an aal1 session for that account.

Fixes from a review pass before opening

  • storage_deletions accepts ticket prefixes. Without this, deleting an account whose ticket had screenshots failed.
  • Two-factor is enforced in RLS: restrictive policies on users, endpoints, requests, api_keys, device_codes and Realtime topics.
  • The CLI device approval checks the second factor. OAuth consent, agent claim, CLI verify and password reset ask for the code.
  • Email subjects are fixed text, never the user's subject.
  • Reply notices are not lost to an exhausted or in-flight notice.
  • A reopened ticket does not revive access that had lapsed.
  • The WebP cleaner keeps an allow-list of chunks, and JPEG bytes after the end marker are dropped.
  • Screenshot-only replies are kept.
  • Bodies without a Content-Length are capped.

Other

  • Privacy policy: support access and ticket retention.
  • New docs page /docs/support.
  • AGENTS.md decision note.
  • PostHog drops every event on /ops.

Deploy notes

  1. Apply 00087_ops_support.sql (autocommit, lock_timeout), then NOTIFY pgrst, 'reload schema'.
  2. Insert your own auth user id into public.staff with psql.
  3. Set up two-factor under Account > Security.
  4. Then cancel the discounted Pro subscription. The trigger keeps the account on Business.
  5. Optional: SUPPORT_ALERT_EMAIL. Object storage must be configured for screenshots (it already is in production).

Tests

  • Unit: image cleaning, markdown, MFA claims, emails, metrics ledger, analytics drop.
  • Integration: tests/integration/support-ops.test.ts covers the staff plan trigger, ticket lifecycle and notices, auto-close and reopen, grants, and the rollup. The billing suites still pass.
  • Checked by hand in the browser in clean, classic and mobile.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Important

  • Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: ac3e8bfb-3e12-4b78-ae82-5b741579d8d7
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

CodeRabbit Verify

  • Run CodeRabbit Verify to test the changes in this pull request.

@kroqdotdev

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown

⚠️ Deprecation Warning: The deny-licenses option is deprecated for possible removal in the next major release. For more information, see issue 997.

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@coderabbitai

coderabbitai Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Action not completed

Review skipped: 104 files exceed the limit of 100.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

kroqdotdev and others added 5 commits November 3, 2026 15:53
…(web 0.62.0)

Staff (a staff table, rows added by hand) get Business without a
subscription and a /ops area behind a session with two-factor sign-in:
an overview of daily metrics, a ticket inbox with notes and saved
replies, an account inspector that shows metadata only, a read-only view
of an account opened from a ticket whose owner gave access (every look
logged for the user), and held refund approvals.

Signed-in users open tickets at /support on both sites, with screenshots,
email on reply or close (on by default), reminders and auto-close, and an
opt-in, revocable grant for support to view their account. Anyone can turn
on an authenticator app under Account > Security; once on, aal1 sessions
are refused by the API and by RLS.

Migration 00087.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n-in

The legacy verified_email agent flow (until its 2026-11-30 sunset) minted
an API key for an existing account from an emailed code alone, which
proves the inbox but not the authenticator. It now answers
interaction_required for an account with a verified factor. Ticket pages
also check the ticket number before it goes into an API path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nticators

GoTrue's TOTP has no recovery codes, so the app keeps its own: ten
one-time codes shown when two-factor sign-in is turned on (copy or
download), stored as SHA-256 hashes in mfa_recovery_codes, replaceable
under Account > Security. A code cannot raise a session to aal2, so using
one on the sign-in screen removes the account's authenticators and the
person sets two-factor up again. Staff can do the same from the account
page in /ops for someone who proved who they are another way. The owner
is emailed both times.

Also asks for the authenticator code on /support, which signs in on its
own: an account that still owed the code saw "Invalid session token".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ops account page hid the button for staff accounts, but the route
only refused the caller's own id, so one staff session could strip
another staff member's authenticator. It now refuses any staff account.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Staff actions (ops.*) are no longer mirrored into PostHog as product use.
- Withdrawing access on a ticket ends that ticket's open views and logs
  view_ended, instead of every view of the account and only sometimes.
- A staff answer on a closed ticket writes a "reopened" event.
- Snooze stores a normalised timestamp, so odd date strings no longer 500.
- A staff member without a name shows as "Support", never their email.
- The plan page's staff notice needs the account to actually be staff.
- The account menu asks whether you are staff when it first opens, not on
  every page load.
- infra/supabase/gotrue-email-auth.md: TOTP runs on GoTrue's defaults
  (checked on production: no GOTRUE_MFA_* overrides, v2.197.0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kroqdotdev
kroqdotdev force-pushed the t3/operations-center-support-tickets branch from c7601a4 to 642345d Compare November 3, 2026 15:53
@kroqdotdev
kroqdotdev merged commit 1c45191 into main Nov 3, 2026
28 checks passed
@kroqdotdev
kroqdotdev deleted the t3/operations-center-support-tickets branch November 3, 2026 16:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant