Architecture

Multi-tenancy

Every piece of data in OpsDash is scoped to an organization. Users can belong to multiple organizations simultaneously. Billing is anchored to a personal org, not individual workspaces.

Organization types

TypeCreated byPurpose
personalAutomatically on signupPersonal workspace — one per user, the billing anchor
teamUser action — Create OrganizationShared workspace — inherits billing from the owner's personal org

Signup trigger

When a new user signs up, the handle_new_user() PostgreSQL trigger fires automatically:

  1. Creates public.users profile
  2. Creates a personal organizations row (slug auto-generated)
  3. Creates an org_members row (role = owner)
  4. Creates a subscriptions row (plan = free)
  5. Seeds default feature_flags for the org
  6. Sets users.current_org_id to the new org
  7. Writes to audit_logs

No manual inserts are needed — everything is automatic.

Billing anchor

Billing is anchored to a user's personal org, not to individual team workspaces. This means:

  • One Stripe subscription per user, not per workspace
  • Team orgs inherit the plan from their billing_owner_user_id
  • Upgrading your personal org upgrades all team orgs you own

Row-Level Security

All data tables enforce org_id scoping via Supabase RLS policies. The core helper functions used in policies:

-- True if auth.uid() is a member of the given org
is_org_member(org_id uuid) → boolean

-- True if auth.uid() is an admin or owner of the given org
is_org_admin(org_id uuid) → boolean

Even if a server action had a bug, the database would not return another org's data — RLS is the last line of defense.

Switching organizations

The org switcher (top of the sidebar) sets users.current_org_id and reloads the app. All subsequent requests resolve to the new org via the URL slug (/org/[slug]/). Middleware reads the slug on every request — the current_org_id value is only used for the initial redirect after login.

Soft-delete

Organizations are never hard-deleted. The deleted_at column is set instead. All RLS SELECT policies include deleted_at IS NULL, so deleted orgs are invisible to all queries automatically.

Cookie preferences

We use cookies to improve your experience, analyze site traffic, and personalize content. You can choose which categories to allow.
Multi-tenancy — OpsDash Docs | OpsDash