Skip to content

Multi-tenant organizations: scoped dashboards, roles, and org deletion#82

Merged
rathorevaibhav merged 47 commits into
mainfrom
feat/issue-23-organization-dashboard
Jul 3, 2026
Merged

Multi-tenant organizations: scoped dashboards, roles, and org deletion#82
rathorevaibhav merged 47 commits into
mainfrom
feat/issue-23-organization-dashboard

Conversation

@rathorevaibhav

@rathorevaibhav rathorevaibhav commented Jun 26, 2026

Copy link
Copy Markdown
Member

Closes #23.

What this adds

Organizations. Every monitor, group, and user now belongs to an organization, and each organization gets its own dashboard.

  • Roles: org admins manage monitors, groups, and users; members are view-only. Platform super-admins sit above all orgs.
  • Org switcher in the navbar — users with multiple orgs (and super-admins) can switch; each switch lands on that org's dashboard.
  • Onboarding: super-admins create an organization together with its first admin at /organizations. Open self-registration is removed.
  • User management: org admins add/edit/remove their own members. Adding an email that already exists links the existing account instead of duplicating it.
  • Organization deletion (soft delete): super-admins can delete an org — its monitors, groups, and sole-org members are soft-deleted with it, invisible everywhere, and restorable for 60 days from a "Deleted" section. A daily job (organizations:purge-deleted) permanently purges anything past the window.

Key design decisions

  • No global tenant scope. Web requests scope explicitly (query scope + route-binding 404 + policies — three independent barriers). Background uptime/certificate/domain checks run unscoped, so monitoring keeps working exactly as before.
  • Soft delete composes with Spatie natively. The package reads monitors through our configured model, so trashed monitors automatically stop being checked and keep their full history for restore. The vendor's hard-deleting monitor:delete is overridden with a soft-deleting version.
  • Safe deletion semantics: one shared timestamp marks each deletion cascade, so restore brings back exactly what that deletion took — and purging one org can never destroy data belonging to another org's restore window.
  • Existing data is migrated into a default "ColoredCow" organization; existing users become its admins.

Testing

  • 146 PHP tests (697 assertions) and 68 JS tests, all green; production build verified.
  • Covers tenant isolation (cross-org access → 404/403), role enforcement, switcher, onboarding, deletion/restore/purge lifecycle, and a regression test proving background checks still see all orgs' monitors.

Deploy notes

  • Run php artisan migrate (includes the data backfill; safe on existing data).
  • Requires MySQL ≥ 8.0.13 for the live-only unique indexes — production (8.0.42) confirmed compatible.
  • The scheduler picks up the daily purge job automatically. Set DEFAULT_USER_EMAIL correctly before migrating — that account becomes the super-admin.

Design specs and implementation plans are in plans/.

🤖 Generated with Claude Code

rathorevaibhav and others added 30 commits June 25, 2026 23:19
Design for multi-tenant organizations: a session-scoped active org with a
membership pivot (admin/member roles), platform super-admin tier, direct
organization_id on monitors and groups, and explicit forOrganization()
scoping + scoped route-model binding + policies (no global scope, so
console/background checks stay unscoped). Includes the existing-data
backfill into a default org and a TDD test plan.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
12 TDD tasks covering schema, membership/roles, the CurrentOrganization
service, explicit forOrganization() scoping, scoped route-model binding,
policies, the org switcher, registration disable, super-admin onboarding,
org-scoped user management, and the default-org backfill.

Incorporates an adversarial review pass: the design is now middleware-order
independent (binding self-resolves via resolveFor + 404; lazy Inertia auth
prop; policy ownership re-checks), and the MonitorHistory test migration is
sequenced so every commit stays green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…mport

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Organizations management (/organizations) was only reachable by typing
the URL. Adds a nav item (desktop + mobile) gated on auth.isSuperAdmin.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The shared auth.organizations prop only contained the user's own
memberships, so a super-admin (who can switch to ANY org per the switch
endpoint and resolveFor) saw an empty or incomplete switcher. Share all
orgs for super-admins; members still see only their memberships. Adds
regression tests for both.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ns, Users tab)

Members got 403s from affordances the backend already forbids: the
Create buttons on Monitors/Groups, edit/delete icons on cards, and the
Users nav tab. Share a server-derived auth.isOrgAdmin flag (super-admin
or admin of the active org) and gate all admin-only UI on it. Server
policies/gates remain the enforcement layer; this is presentation only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rywhere

Switching from /organizations appeared to do nothing: back() returned to
a page that ignores the active org, and the switcher label always read
"Select organization" there because CurrentOrganization is only bound
inside the active.organization middleware. Switching now redirects to
the org's dashboard (also avoids a 403 when leaving /users for an org
where the user is only a member), and the shared activeOrganization prop
falls back to resolving the session on pages outside the middleware.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Approved design: SoftDeletes on Organization/Monitor/Group/User (the
package-recommended path — Spatie's repository reads through the
configured model, so trashed monitors stop being checked automatically),
conditional user cascade (sole-org, never super-admins), shared-timestamp
cascade marker for precise restore, super-admin restore UI, a dedicated
FK-ordered organizations:purge-deleted command, functional live-only
unique indexes for monitors.url and organizations.slug, and
restore-and-link semantics for users.email.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Incorporates an adversarial review pass: purge() now filters users by the
org's cascade marker (purging one org could otherwise permanently destroy
a user inside another org's restore window), the purge command gains an
orphan pass for trashed children of live orgs (individually deleted
records would otherwise be retained forever), delete()/restore() are
idempotent with state guards, and the TDD tests were hardened to
genuinely fail before implementation. Spec amended to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
rathorevaibhav and others added 9 commits July 2, 2026 21:42
…dex migration

The final whole-feature review flagged the hard-coded DROP INDEX names as
the one deploy risk: if production's index names ever drifted from
Laravel's convention, the migration would half-apply (functional index
added, plain one left blocking trashed-value reuse). Discover the plain
single-column unique index from information_schema and drop whatever it
is actually named; functional indexes never match (NULL COLUMN_NAME).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per-check credit metering per organization: live balance counter,
auditable grant ledger, daily usage rollups, query-level enforcement,
runway-based warnings, super-admin grants.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@rathorevaibhav rathorevaibhav changed the title feat: organization dashboard (multi-tenant) — issue #23 Multi-tenant organizations: scoped dashboards, roles, and org deletion Jul 2, 2026
rathorevaibhav and others added 8 commits July 2, 2026 22:35
Metering failure isolation (try/catch, never break checks) and the
buffered-flush escape hatch for future scale.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Projected runway from live monitor configuration (computed on read),
shown on the dashboard balance card, a header chip, and a monitor-form
impact preview. Warning emails now use the same projection instead of
trailing 7-day historical burn.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Code review found a critical chain: an org admin could link ANY existing
account by email (users.store) and then overwrite its global
email/password (users.update, guarded only by membership in the admin's
own org) — escalating to super-admin or hijacking another org's user.

Guards added: super-admins are never manageable via org user management
(store refuses, edit/update/destroy 404); global credentials are only
editable for an account owned SOLELY by the acting org (shared accounts
get role-only changes); a last admin can't demote/remove themselves; and
re-adding an existing member no longer silently changes their role.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The default-org backfill (2026_06_25_000200) ran Eloquent queries on
models that gain SoftDeletes later in the sequence, so on a non-empty
production database `php artisan migrate` would emit `... AND deleted_at
IS NULL` against a column that doesn't exist yet and abort mid-run.
Rewritten with the query builder only — no model scopes, safe regardless
of column state. Added a data-path regression test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
These vendor commands operate on the base Spatie model, so they bypass
organization assignment and soft deletes — monitor:sync-file
--delete-missing would hard-delete monitors across all organizations and
destroy check history, defeating the 60-day retention window. Rebind
both to refusing shims (a config comment alone protected nobody at
runtime). monitor:delete already had a soft-delete-aware override.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- #4 MonitorRequest: reject a soft-deleted group id (withoutTrashed) so a
  monitor can't be orphaned onto a trashed group and vanish.
- #5 restore flash: share session status to the Organizations page and
  render it, so the "skipped monitors" restore warning is actually shown.
- #8 fresh-install bootstrap: UserSeeder marks the default user super-admin
  so `migrate --seed` yields a usable install (self-registration is gone).
- #9 restore: resurrect every trashed member of a restored org, not just
  those matching this cascade's timestamp, so a user cascaded by another
  org's deletion isn't locked out after their other org is restored.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Moved to the feat/credit-system-spec branch. Keeps PR #82 scoped to the
multi-tenant organization work; the credit system is deferred until this
merges to main.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
DEFAULT_USER_EMAIL determines which account the tenancy backfill promotes
to platform super-admin, but it was undocumented — easy to miss on a
production deploy and end up with no super-admin. Surface both vars with
a note to set the email before migrating.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@rathorevaibhav rathorevaibhav merged commit 1ea2b74 into main Jul 3, 2026
1 check passed
@rathorevaibhav rathorevaibhav deleted the feat/issue-23-organization-dashboard branch July 3, 2026 07:32
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.

Organization dashboard

1 participant