OpsDesk is a single Next.js 16 / React 19 application that gives a support team one place to manage tickets, orders, customers, and incidents — with an SLA/escalation engine, a trigger-based automation engine, org-scoped RBAC, executive reporting, and a separate customer-facing portal, all on top of a single Supabase project.

The feature set is broad by design — tickets, orders, customers, incidents, SLAs, automation, RBAC, reporting, and a separate customer portal — built as one project to work through problems a real B2B dashboard has to solve: multi-tenant data isolation, role-based permissions, and rule-based automation, all on a single Supabase project.
Two things about the architecture are easy to miss on a first pass: Postgres Row Level Security is enabled on only 3 of roughly 44 application tables (the three purely user-scoped auth tables — passkeys, passkey challenges, email-MFA challenges); every ticket/order/customer/incident/RBAC/audit table instead relies entirely on application code remembering to filter by organization_id. And three separate identity mechanisms coexist by design: a NextAuth JWT session for staff (bridged from Supabase Auth), the underlying Supabase Auth session itself, and a fully independent, hand-rolled magic-link session system for the customer portal.
Built the application end-to-end as the sole contributor — one git author across all 25 commits, same identity under two name/email-casing variants — domain model, UI, and every Route Handler API under app/api/**.
Designed the multi-tenant data model (organization-scoped tickets, orders, customers, incidents, and roughly 44 tables total) and its application-layer tenant-isolation pattern.
Built the SLA/escalation engine and the trigger → condition → action automation engine, spanning tickets, orders, customers, incidents, and portal events.
Implemented the full auth stack: NextAuth v5 bridged to Supabase Auth, Google OAuth, passwordless magic links, WebAuthn passkeys, and an email-code MFA step-up.
Built org-scoped RBAC (four system roles plus custom roles with permission overrides, optional approval-request workflow), the executive-reporting/analytics views, and a separate passwordless customer portal.
Integrated Stripe payment processing — Payment Intents and webhook-driven event handling.
The trade-off was giving up a single source of truth for "is this user logged in" — a NextAuth JWT session and an independent Supabase Auth session now both exist per user, and the code has to remember which one to tear down on failure — worth it because it gets Supabase's maintained password/OAuth/magic-link/admin-user machinery for free while every route still gets NextAuth's App-Router-native auth()/useSession() ergonomics, with the org-membership-suspension check centralized in exactly one place (assertHasActiveMembership) across all three credential providers instead of duplicated per sign-in method.
The trade-off was that tenant isolation became a property of code discipline rather than the database — a single missing .eq("organizationid", ...) filter in any future query is a cross-tenant data leak with no second line of defense, and RLS is enabled on only 3 of the ~44 tables (the auth-only ones, keyed by userid, not organization_id) — worth it as the cheapest model to build against: ordinary SQL joins across tickets/orders/customers work without per-tenant connection routing, and it's consistent with how the app already resolves the active organization everywhere else (a cookie plus a live membership lookup).
The trade-off was inconsistency — a reader can't assume "global state = source of truth" project-wide since orders, customers, incidents, automation, and reports pages all fetch with local component state instead — worth it because the three slices that did get the global-store treatment need explicit, hand-choreographed cross-slice behavior (wiping the ticket cache specifically on an org switch, optimistic patches to cached ticket details without a refetch) that a query-key-based cache library would need bespoke glue to express.
The trade-off was efficiency — the server re-queries Supabase on a fixed 5-second cadence per open browser tab whether or not anything changed, which is strictly less efficient than the real Supabase Realtime WebSocket subscription the same codebase already uses for membership-suspension detection — worth it (as the codebase evidences it) because a long-lived EventSource with hash-diffing and built-in auto-reconnect was simpler to stand up for this feature than wiring a second client-side Realtime subscription, at the cost of now having three different "live update" approaches in the app with no shared rationale between them.
The trade-off was that page protection only ever runs after the page's JS has already been requested and started rendering, so the real security boundary has to be each API route's own auth() check rather than a single edge-level chokepoint — worth it because the entire page-level gate is readable in one client file (a route Set plus a handful of prefix checks) driven by the same useSession() hook the rest of the UI already depends on, instead of maintaining a second, edge-runtime-safe way of reading the session.
No CI/CD pipeline exists yet, so every test script runs locally, on demand, with nothing blocking a push on failure. Re-run this session: 61/61 unit and component tests pass — the two previously-documented failures (a stale UI-copy assertion, a router-mock gap) are now fixed.
61
unit tests
12
E2E scenarios
No axe-core/pa11y test suite and no project-authored eslint-plugin-jsx-a11y config exist — both appear in package-lock.json only as transitive dependencies of eslint-config-next. eslint.config.mjs extends eslint-config-next's core-web-vitals preset, which resolves to enabling six jsx-a11y rules (alt-text, aria-props, aria-proptypes, aria-unsupported-elements, role-has-required-aria-props, role-supports-aria-props) — confirmed by grepping the installed eslint-config-next/dist/index.js. A repo-wide search for real JSX aria-xxx={...} attributes (not Tailwind aria- variant selectors like aria-invalid:) found 8 files under app/.
OpsDesk is live at ops-desk.ziadhatem.dev, so real Lighthouse numbers against the actual deployed production build exist: 100 (desktop) and 96 (mobile) performance, 100 accessibility, 100 best practices, and 92 SEO (the one point of friction being a missing rel=canonical tag on /login) — a large, expected jump over the 95/68 measured earlier the same week against a local, unminified next dev server. There is no visibility into real end-user traffic, uptime, or error rates beyond this one-time audit. What's otherwise verifiable from the repository: a solo build covering roughly 44 database tables across nine feature areas (tickets, orders, customers, incidents, SLA, automation, RBAC, reporting, and a separate customer portal), Stripe payment processing (Payment Intents + webhooks), and a documented, present-tense-honest test state — 61/61 unit and component tests passing via Vitest, plus 12 Playwright e2e specs, after fixing the two previously-failing component tests.
Visit the live site ↗ (opens external site)The most honest thing to say about OpsDesk's current state is that its own documentation caught real problems while being written, and both have since been fixed: 7 of 22 component tests were failing, for two distinct reasons — one assertion on UI copy that no longer existed after Google OAuth replaced a disabled placeholder button, and six more because a later change added a useSearchParams() read that the tests' router mock never accounted for. The full suite (61/61 unit and component tests, plus 12 e2e specs) now passes. What's still genuinely unresolved: Row Level Security covers only 3 of roughly 44 tables (everything else depends on every query remembering an organization_id filter), and there's still no CI to catch a regression like the one just fixed automatically next time. Those are the two things I'd still want fixed before calling this fully production-ready: get a CI pipeline actually running on push, and either extend RLS as a real second line of defense or write down app-layer-only isolation as a deliberate, accepted trade-off instead of an implicit one.