Skip to content

Add shared workspaces for small teams - #175

Merged
YurMil merged 3 commits into
mainfrom
feat/shared-workspaces
Sep 16, 2026
Merged

YurMil merged 3 commits into
mainfrom
feat/shared-workspaces

Conversation

@YurMil

@YurMil YurMil commented Sep 14, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #122. Signed-in users can create a workspace, invite colleagues by link, and share calculation history and report defaults with them.

Acceptance criterion Where
Workspace create / invite / leave flows with RLS enforcing membership supabase/migrations/20260915000000_create_workspaces.sql, profile → Workspaces, /workspaces/join/
Shared history view alongside personal history Profile page (Workspaces section below Calculation history); "Save to" selector in every utility that supports saving
Workspace-level report metadata defaults Company name and preferred standards per workspace, sent to apps as cas:report-defaults
Audit-friendly: entries record which member created them created_by plus a server-stamped created_by_name on every shared entry

Database design

  • Access. Every read is gated on membership via is_workspace_member() / is_workspace_owner(). These are security definer, so the policies do not recurse into workspace_members' own RLS.
  • Structural changes go through RPCs only: create_workspace, create_workspace_invite, accept_workspace_invite, revoke_workspace_invites, leave_workspace, remove_workspace_member, delete_workspace. The caller always comes from auth.uid(). Members and invites are not writable directly; owners can update only name, company_name and preferred_standards (column grants).
  • Invites are links, not emails. No mail provider, no cost. The code carries 244 random bits and is shown once; only its SHA-256 is stored. Links expire after 7 days or when revoked, and joining needs an explicit click.
  • Shared entries.
    • Immutable (no UPDATE grant).
    • Insert requires created_by = auth.uid() and membership.
    • The author name is stamped by a trigger, so it cannot be forged and stays readable after the member leaves.
    • The author or the owner can delete.
  • Ownership. When the last owner leaves or deletes their account, the longest-standing member is promoted. When the last member leaves, the workspace and its history are deleted.
  • Limits: 10 owned workspaces per user, 25 members, 20 active invites, 200 shared entries (oldest evicted, like personal history).
  • Display names. Taken from auth metadata full_name, falling back to the e-mail local part. Full e-mail addresses are never shown to other members. The migration does not depend on public.profiles, which is not in the repo (see dev-plans/supabase-pending-work-2026-08.md).

Site changes

  • src/shared/workspaces — client module; RLS is the boundary, not this code.
  • src/components/Workspaces — profile section: members, invite link, report defaults (read-only for members), shared history with author and date.
  • src/pages/workspaces/join.tsx — noindex and no-referrer, so the invite code is not leaked via Referer; excluded from the sitemap. The invite travels as ?invite=: supabase-js consumes ?code= as an OAuth PKCE code, which broke session detection during testing.
  • Utility shell: a "Save to" selector (personal or workspace, remembered per browser) and cas:report-defaults to the embedded app.
  • dev-plans/utility-share-protocol.md documents the new message. Apps must treat it as optional and never overwrite user input.
  • Translated for en, de, es, et, ru and ua.

Verification

  • pnpm test:db (new; also added to CI) applies the migration to Postgres running in-process via PGlite, with a minimal Supabase scaffold, and checks 43 behaviours:
    • isolation from non-members;
    • no direct writes to members or invites;
    • invalid, revoked and expired invites are rejected;
    • idempotent join;
    • members cannot change defaults or invite;
    • saving on behalf of another member and forging the author name both fail;
    • entries are immutable;
    • who may delete an entry;
    • removal, ownership hand-over on leave and on account deletion;
    • cascades and limits;
    • internal functions are not callable by API roles.
  • pnpm typecheck passes; pnpm lint has 0 errors; DOCUSAURUS_ONBROKENLINKS=throw docusaurus build --locale en succeeds.
  • The join page renders correctly signed out, with and without an invite, and no auth exchange is attempted.
  • Not verified against a live Supabase project: the signed-in UI flows (create, invite, join, save to a workspace), because the migration is not applied anywhere yet.

Deployment notes

  • The migration must be applied to production manually. Per dev-plans/supabase-pending-work-2026-08.md, the repo and production migration histories have drifted, and migrations are not syncing automatically. Until it is applied, the Workspaces section shows its load error and the "Save to" selector stays hidden; personal history is unaffected.
  • No app consumes cas:report-defaults yet. Each utility adopts it in its own repository, as with the share protocol itself.

🤖 Generated with Claude Code

Engineering calculations are usually reviewed by a second person. Let a
signed-in user create a workspace, invite colleagues with a link, and
share calculation history and report defaults with them.

Database (20260915000000_create_workspaces.sql):
- workspaces, workspace_members, workspace_invites and
  workspace_calculations, all behind RLS keyed on membership through
  security definer helpers
- create, invite, join, leave, remove and delete go through RPCs that
  take the caller from auth.uid(); nothing structural is writable directly
- invite codes are 244 random bits, returned once; only the SHA-256 is
  stored, and links expire after 7 days or on revocation
- shared entries are immutable, and the author name is stamped by a
  trigger so it cannot be forged and survives the member leaving
- the last owner leaving or deleting their account promotes the
  longest-standing member; the last member leaving deletes the workspace
- limits: 10 owned workspaces, 25 members, 20 active invites, 200
  shared entries per workspace

Site:
- Workspaces section on the profile page: members, invite link,
  report defaults (company name, preferred standards), shared history
  with author and date
- /workspaces/join/ accepts an invite on an explicit click; the invite
  travels as ?invite= because supabase-js consumes ?code= as an OAuth code
- utility shells offer "Save to" (personal history or a workspace) and
  send the selected workspace's defaults to apps as cas:report-defaults
- the protocol doc describes the new message; six locales translated

supabase/tests/workspaces.test.mjs runs the migration on PGlite and
checks 43 behaviours (isolation, forged authors, invites, hand-over,
cascades, limits); CI runs it as pnpm test:db.

Closes #122

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
cadautoscript-com Ready Ready Preview Sep 16, 2026 6:22pm UTC

@supabase

supabase Bot commented Sep 14, 2026

Copy link
Copy Markdown

Updates to Preview Branch (feat/shared-workspaces) ↗︎

Deployments Status Updated
Database Wed, 16 Sep 2026 18:20:17 UTC
Services Wed, 16 Sep 2026 18:20:17 UTC
APIs Wed, 16 Sep 2026 18:20:17 UTC

Tasks are run on every commit but only new migration files are pushed.
Close and reopen this PR if you want to apply changes from existing seed or migration files.

Tasks Status Updated
Configurations Wed, 16 Sep 2026 18:20:23 UTC
Migrations Wed, 16 Sep 2026 18:20:24 UTC
Seeding ⏸️ Wed, 16 Sep 2026 18:19:57 UTC
Edge Functions ⏸️ Wed, 16 Sep 2026 18:19:57 UTC

❌ Branch Error • Wed, 16 Sep 2026 18:20:25 UTC

ERROR: relation "public.profiles" does not exist (SQLSTATE 42P01)
At statement: 2
drop trigger if exists trg_log_role_change on public.profiles

View logs for this Workflow Run ↗︎.
Learn more about Supabase for Git ↗︎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 04021052c2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread supabase/migrations/20260915000000_create_workspaces.sql
Comment thread src/shared/workspaces/index.ts
Comment thread src/components/Utilities/UtilityShellPage.tsx
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 14, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-14T17:05:24.973059Z 0402105 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Resolve the sitemap conflict with the embed routes from #174: keep both
exclusions, /embed/** and /workspaces/** with their locale variants.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@YurMil
YurMil merged commit 960b725 into main Sep 16, 2026
7 of 8 checks passed
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.

Shared workspaces: calculation history and settings for small teams

1 participant