Multi-tenant organizations: scoped dashboards, roles, and org deletion#82
Merged
Conversation
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>
…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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #23.
What this adds
Organizations. Every monitor, group, and user now belongs to an organization, and each organization gets its own dashboard.
/organizations. Open self-registration is removed.organizations:purge-deleted) permanently purges anything past the window.Key design decisions
monitor:deleteis overridden with a soft-deleting version.Testing
Deploy notes
php artisan migrate(includes the data backfill; safe on existing data).DEFAULT_USER_EMAILcorrectly before migrating — that account becomes the super-admin.Design specs and implementation plans are in
plans/.🤖 Generated with Claude Code