Skip to content

Shared Spaces foundation: clean-break architecture for the first paid add-on#14

Open
heyderekj wants to merge 31 commits into
mainfrom
feat/shared-spaces-foundation
Open

Shared Spaces foundation: clean-break architecture for the first paid add-on#14
heyderekj wants to merge 31 commits into
mainfrom
feat/shared-spaces-foundation

Conversation

@heyderekj

Copy link
Copy Markdown
Collaborator

Summary

Foundation for Shared Spaces, Harvous's first paid feature ("PAID ADD-ON / COMING SOON — Study together, everyone contributes to the same threads"). This is a clean-break rebuild, not a migration of the Feb 2026 v1 sharing model — see docs/SHARED_SPACES_DEV_NOTES.md for the full design rationale.

  • Schema: Spaces.type (personal|shared|public) + nullable orgId (future church hosting); new SpaceMemberships (owner is a real role, not just Spaces.userId) and SpaceInvites (expiring links, validated from day one) supersede the frozen Members/SpaceInvitations tables.
  • Entitlement: owner-pays "Shared Spaces" add-on (UserMetadata.sharedSpacesAddOn); joining is always free. The old unlimited tier is fully retired — zero subscribers, no grandfathering.
  • Content model: native authoring (any member) + copy-in (independent new note row, source untouched); locked notes never appear in shared contexts for any viewer.
  • Security fix: POST /api/notes/create previously accepted any spaceId with no validation — now gated by requireSpaceAccess + canAuthorInSpace.
  • Client: space-switcher popover (My Home + owned/joined shared spaces + paid-gated "New shared space"), a dedicated shared-space sidebar view (author attribution, people/invite management, 45s poll until Realtime ships), copy-to-space note action, and the upgrade page generalized off the retired Unlimited plan.
  • Legacy retirement: all v1 shareToken routes return 410; Members/SpaceInvitations frozen (hygiene deletes only); one migration script promotes the Harvous system user's featured spaces onto the new rails.
  • Docs: SHARED_SPACES_DEV_NOTES.md rewritten, SPACE_MODES_PRODUCT.md/MONETIZATION_AND_PRICING.md updated, new future/MENTION_PILLS.md locking in cross-space visibility rules for the next editor branch.

Explicit non-goals (documented fast-follow list): Supabase Realtime/presence, email invites, leader-role activation, unioned study-threads/scripture index, billing webhook, public-spaces UI, church org, full sidebar generalization (folders/threads/scripture index inside a shared space).

Manual setup required (not in this PR)

Clerk Billing plan + shared_spaces feature must be created per-instance (Development done; Production is a separate pass before launch) — see docs/SHARED_SPACES_DEV_NOTES.md → "Clerk dashboard (manual setup)".

Test plan

  • Two-user flow on staging (new.harvous.com, dev Clerk + prod DB): create shared space (blocked without add-on → upgrade), grant add-on, create invite, second user joins, composes, first user sees it with author attribution, owner removes an item, revoke add-on and confirm existing access is unaffected.
  • curl -X POST /api/notes/create with a spaceId you don't belong to → expect 403 (previously silently accepted).
  • tsc --noEmit — no new errors vs. main baseline.
  • npx vitest run — same 15 pre-existing (Classic-era) failures, zero new failures.
  • npm run db:push — applies cleanly post-merge with main, no data-loss prompts.
  • npm run build:spa — production build succeeds.

🤖 Generated with Claude Code

heyderekj and others added 10 commits July 2, 2026 08:27
…ips, SpaceInvites, sharedSpacesAddOn

Clean-break rails for the Shared Spaces paid add-on. Legacy Members/
SpaceInvitations and Spaces.isPublic/shareToken are frozen (deprecation
annotations); all existing Spaces rows default to type='personal'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…by space access

New server/utils/space-access.ts replaces space-permissions.ts: roles come
from SpaceMemberships (owner/leader/member, owner holds a row, heal-on-read
for partial creates) and canAuthorInSpace expresses the per-type contribution
rule (personal=owner, shared=any member, public=owner/leader broadcast).

Security fix: POST /api/notes/create previously accepted any spaceId without
validation, letting a user inject notes into arbitrary spaces. It now requires
space access + authoring capability, and rejects locked notes into non-personal
spaces.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… tier

- tier-limits.ts: hasSharedSpacesAddOn (UserMetadata.sharedSpacesAddOn DB-first,
  Clerk JWT shared_spaces feature fallback) is the only grant for owning shared
  spaces; free = 0 owned. Owned count = Spaces.type='shared'. Join limits
  removed (joining is free); 150 people cap now counts SpaceMemberships and is
  enforced at member-add time only.
- Deleted canOwnerAddOneMoreSharedSpace/canJoinSpace/byOwnerId variants and the
  shareToken-or-members heuristics; legacy tier plumbing kept as deprecated
  no-ops for the backfill script and the inert hasUnlimited status field.
- billing.ts: checkout accepts only CLERK_SHARED_SPACES_PLAN_ID and passes the
  requested plan through; /api/subscription/status adds hasSharedSpaces.
- Call sites in spaces/user/shared/admin routes updated to the new gates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ing routes

- POST /api/spaces/create-shared: paid-gated transactional create (Spaces
  type='shared' + owner SpaceMemberships row). Personal create/update no longer
  accept isPublic.
- Invite CRUD: POST/GET /api/spaces/:spaceId/invites (owner-only; invite
  creation is the paid gate; default 30-day expiry, optional maxUses),
  DELETE .../invites/:inviteId (revoke).
- Public GET /api/spaces/invite-preview/:token (validates revoke/expiry/uses
  from day one; rich preview shape ported from the old join-preview) and
  POST /api/spaces/invites/:token/redeem (150-person cap, double-join guard,
  useCount increment).
- Members list/remove rewired to SpaceMemberships; space delete cleans up the
  new tables (legacy deletes kept as hygiene).
- Legacy shareToken join/join-preview, members/invite, and /api/invitations/*
  now return 410 GONE.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lder registry to members

- getNotesForSharedSpace (dashboard-data): all members' notes by spaceId,
  locked notes excluded for every viewer, pagination/sort/total parity with
  the personal query, and author attribution (authorUserId/authorDisplayName/
  authorColor via one UserMetadata batch).
- /notes, /items, /bootstrap branch on Spaces.type — owners of shared spaces
  now see member contributions too; personal (My Home) paths untouched. The
  /notes route also gains the previously-missing access check.
- Folder registry: read + create-label for any member; space-wide
  folders/remove stays owner-only in shared spaces. Prefetch member path reads
  SpaceMemberships.
- POST /api/spaces/:spaceId/copy-notes: copies notes as new independent rows
  (author = caller, target space set, metadata carried, no connections);
  sources must be readable (own notes, or notes in spaces you can access).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…new rails

- Navigation: getSpacesWithCounts includes Spaces.type; getMemberOfSpaces reads
  SpaceMemberships and returns type/role so the client can partition owned vs.
  joined shared spaces.
- Profile: /api/profile/my-shared-spaces reads type='shared' + SpaceMemberships;
  shareUrl comes from the space's active SpaceInvites link.
- Featured items: space auto-dismiss membership check reads SpaceMemberships.
- Admin: system-space add-member endpoint inserts SpaceMemberships and rejects
  personal spaces; admin content catalog member counts read SpaceMemberships.
- Threads: dropped a dead legacy-Members fallback in the thread-notes member
  path (requireSpaceAccess is now the single source of truth).
- Account deletion/clear-data, dev reset-to-new, and dev/live merge now
  reassign/delete SpaceMemberships + SpaceInvites alongside the legacy tables
  (hygiene deletes on the frozen tables kept until they're dropped).
- New server/scripts/migrate-harvous-system-spaces.ts: promotes the Harvous
  system user's spaces to type='shared', creates the owner membership row, and
  copies existing legacy Members rows onto SpaceMemberships (idempotent). All
  other v1 shared spaces retire naturally.
- Removed a dead unused getSpaceMemberCount duplicate in dashboard-data.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ade UI

- proto-shell-context / proto-sidebar-nav-store: activeSpaceId state,
  persisted across refresh, with a setter that clears drill-downs and lands
  on the space layer (mirrors setSidebarListMode's reset discipline).
- New useActiveSpace() hook: resolves the space in scope (shared/public
  selection, or My Home), silently falls back to My Home when the persisted
  id no longer resolves (left/removed/deleted).
- SpaceSwitcherMenu: the Home-toggle orb becomes a popover — My Home, owned +
  joined shared spaces, and a paid-gated "New shared space" action (routes
  to /upgrade without the add-on). Portaled/anchored like ListViewMenu.
- New PrototypeSidebarSharedSpaceView (rendered in place of the personal
  sidebar when a shared space is active) + PrototypeSpacePeopleSheet
  (members list + invite-link create/copy/revoke, owner-only). Both
  prototype-native — no Classic panel reuse. Notes list shows author
  attribution and polls every 45s (no realtime yet).
- Compose: PrototypeNotePage's new-note space fallback now uses the active
  space instead of always My Home; create calls into a non-personal space set
  allowOffline:false so shared-space composes fail loudly instead of queueing
  (useCreateSimpleNote gained the allowOffline option).
- Copy-in: useCopyNotesToSpace + a "Copy to shared space…" submenu in
  PrototypeNoteMoreMenu.
- Join flow: PublicJoinSpacePage/useJoinSpace re-pointed at
  invite-preview/redeem; successful join persists the space as active before
  landing in the shell.
- Upgrade UI: UpgradeCheckoutButton and UpgradePageContent generalized from
  the hardcoded Unlimited plan to any plan/copy (props), then wired to the
  Shared Spaces add-on; UpgradePage reads hasSharedSpaces.
- server/routes/spaces.ts: /prefetch now returns type/isOwner/real
  memberCount so the client can tell personal from shared spaces.

Verification note: the preview MCP tool in this session binds dev servers to
the main checkout's cwd rather than this worktree (confirmed via process
inspection), so interactive browser verification wasn't possible here.
Verified instead via: tsc (no new errors vs. baseline), a full production
vite build (spa) succeeding, and a worktree-rooted dev server run directly
via Bash confirming the correct source is served. Needs a real click-through
in a properly-rooted dev environment before merge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- SHARED_SPACES_DEV_NOTES.md: full rewrite around Spaces.type, SpaceMemberships
  (owner-as-role), SpaceInvites, the add-on entitlement, native+copy-in content
  model, join flow, reserved public-space shape, forward hooks (org, challenges,
  mentions), fast-follow list, and a pre-merge note about unrelated schema drift
  on main (SupportTicketNotes) that this branch must pick up before its next
  db:push.
- SPACE_MODES_PRODUCT.md: glossary and limits matrix updated to the add-on model
  (free = 0 owned, no grandfathering); public-space entry aligned with the
  broadcast model.
- MONETIZATION_AND_PRICING.md: Group Sharing / unlimited tier marked retired,
  superseded by the Shared Spaces add-on (same price point).
- DATABASE.md: new tables/columns (SpaceMemberships, SpaceInvites, Spaces.type/
  orgId, UserMetadata.sharedSpacesAddOn); Members/SpaceInvitations marked
  deprecated.
- New future/MENTION_PILLS.md: design doc for the follow-up @ mention editor
  branch, including the cross-space visibility rules this branch locked in.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…undation

# Conflicts:
#	Changelog/1.217.211.md
#	Changelog/1.217.212.md
#	Changelog/1.217.213.md
#	README.md
#	package.json
#	public/sw.js
#	release-notes/v1.217-july-2026.md
Mirrors the existing VITE_CLERK_UNLIMITED_PLAN_ID entry — the plan id is a
public identifier inlined into the SPA bundle by Vite, not a secret.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@supabase

supabase Bot commented Jul 2, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project mhriprqpyvhjgdssjlfl because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@netlify

netlify Bot commented Jul 2, 2026

Copy link
Copy Markdown

Deploy Preview for harvouscom ready!

Name Link
🔨 Latest commit 579eba7
🔍 Latest deploy log https://app.netlify.com/projects/harvouscom/deploys/6a4ef09c0137760007c2af36
😎 Deploy Preview https://deploy-preview-14--harvouscom.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

heyderekj and others added 7 commits July 3, 2026 08:53
On dedicated prototype hosts (localhost/app/new) the upgrade page now uses
the same hero-image + letter-stack layout as the new sign-in/sign-up pages
('Study together.' headline, footer with back link + Secured by Clerk),
instead of the Classic mesh-gradient card. Non-dedicated hosts keep the
classic fallback, mirroring SignInPage's conditional.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dd-ons, billing webhook

- /upgrade is now a single-purpose Shared Spaces page on the public-page
  shell (fixed group-study hero, paper-stack letter card, feature list,
  price toggle); signed-out visitors get a sign-in CTA, subscribers get
  Manage-or-remove via Clerk's SubscriptionDetailsButton.
- New Settings > Add-ons page lists Shared Spaces (Active badge when owned)
  plus Review and Challenges as coming later; replaces the old multi-add-on
  list on the upgrade card.
- Clerk billing webhook: subscriptionItem.active grants
  UserMetadata.sharedSpacesAddOn, canceled/ended/expired revoke it (filtered
  to CLERK_SHARED_SPACES_PLAN_ID); includes a fix deduplicating svix-*
  headers that Netlify's proxy occasionally doubles, which broke signature
  verification.
- Dev test endpoint POST /api/test/set-shared-spaces-addon toggles the
  entitlement locally.
- hasUnlimited fully swapped for hasSharedSpaces in ManageBillingPanel and
  panel prefetch; useSubscriptionStatus invalidates on subscriptionUpgraded.
- Switcher polish: real check icons, Add-on badge on the gated create row.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…heet, upgrade copy

- Join page shows the owner + up to 2 anonymized members, each avatar using
  that person's Settings > Appearance accent (light/dark); invite-preview API
  resolves per-user accents and exposes member previews (initial + colors only).
- Read-only foreign notes show a compact "View only" eye chip instead of the
  authored-by lock banner.
- People sheet is now a "Manage space" hub with People / Invite links / Space
  settings sub-views; each sub-view uses its own section name as the header
  title (no redundant all-caps eyebrow). Adds ProtoSpaceMenuIcon.
- Upgrade / Add-ons: "Sign in to add", "Back to my Harvous", and "Manage
  subscription" rendered as the standard secondary settings button; addon icon
  gets the join-letter accent treatment.
- Mirror every state in the shared-spaces design gallery fixture.

Co-authored-by: Cursor <cursoragent@cursor.com>
…age space sheet

- 21-step two-user Playwright tour (e2e/shared-spaces-ui.spec.ts) covering the
  /addon + Settings add-on states, space switcher, shared-space sidebar, the
  Manage space hub and its People / Invite links / Space settings sub-views, the
  join flow, author chips, the view-only foreign-note chip, copy-to-shared-space,
  and the revoke / leave / remove / delete confirm dialogs.
- Helpers (e2e/shared-spaces-helpers.ts): API setup (grant add-on, create space /
  invite / notes, remove member, delete space), space-switcher navigation, an
  openManageSubView() drill-in for the hub -> sub-views model, and ordered
  screenshot capture into test-results/shared-spaces-screenshots/.
- Selectors match the current UI: hub sub-views (no "Danger zone"), "Revoke link",
  member "Remove"/"Leave space", "New note" compose. Member-authored note is
  pinned via API so the author-chip / editable-note checks don't depend on the
  compose-targeting nav-readiness race.

Co-authored-by: Cursor <cursoragent@cursor.com>
Draft compose was landing in My Home because useForeignSharedNote(null)
defaulted effectiveSpaceId to the personal home space, which took
precedence over the persisted space switcher selection. Draft paths now
use composeTargetSpaceId from the shell selection; the hook returns an
empty space id when there is no note. E2E test 13 asserts UI compose
without the API pin workaround.

Co-authored-by: Cursor <cursoragent@cursor.com>
Trim the shared-space sidebar and add an about sheet with join letter and member roster; fix people count, owner settings gating, and SPA-origin invite URLs in dev; wire entitlement bridge, visit tracking, and design gallery fixtures.

Co-authored-by: Cursor <cursoragent@cursor.com>
Align add-on limits across API and client copy, show owners dynamic
remaining capacity on /addon and in the People sheet, and add invite
expiry controls, member avatars, and shared-dashboard layout fixes.

Co-authored-by: Cursor <cursoragent@cursor.com>
@cursor

cursor Bot commented Jul 7, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

…ists.

Move shared-spaces foundation off the 1.217.x line onto 2.0.x to match
main, and hide the note-details orb until a saved note is loaded.

Co-authored-by: Cursor <cursoragent@cursor.com>
@cursor

cursor Bot commented Jul 7, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

Keep icon-only menu tile styling and inactive orb chrome so users can
switch spaces without expanding the sidebar first.

Co-authored-by: Cursor <cursoragent@cursor.com>
@cursor

cursor Bot commented Jul 7, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

Co-authored-by: Cursor <cursoragent@cursor.com>
@cursor

cursor Bot commented Jul 7, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

Bring SHARED_SPACES_LAUNCH_STRATEGY.md and README index entry so
implementation on this branch matches the locked UX spec on main.

Co-authored-by: Cursor <cursoragent@cursor.com>
@cursor

cursor Bot commented Jul 8, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

Co-authored-by: Cursor <cursoragent@cursor.com>
@cursor

cursor Bot commented Jul 8, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

Members attach notes to owner-created threads; leader role may grant
create/pin later.

Co-authored-by: Cursor <cursoragent@cursor.com>
@cursor

cursor Bot commented Jul 8, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

Co-authored-by: Cursor <cursoragent@cursor.com>
@cursor

cursor Bot commented Jul 8, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

Show all members' highlights in shared/public spaces, attribute rows with author chips, load unioned studyThreads on foreign note details for dock deep-links, and fix view-only flicker by seeding ownership from list cache.

Co-authored-by: Cursor <cursoragent@cursor.com>
@cursor

cursor Bot commented Jul 8, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

…e UX.

Add This space | My Home list scope, restrict folder/thread creation to owners on shared lists (none on My Home overlay), and show owner author chips in member empty states with tighter attribution layout.

Co-authored-by: Cursor <cursoragent@cursor.com>
@cursor

cursor Bot commented Jul 8, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

…spector.

Replace circle-user icons with SharedSpaceMemberAvatar (photo or accent initial), tighten chip padding, and align inspector Added by with the same treatment.

Co-authored-by: Cursor <cursoragent@cursor.com>
@cursor

cursor Bot commented Jul 8, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

heyderekj and others added 2 commits July 8, 2026 16:57
…vigation.

Separate overlay vs in-body highlight styling, clear stale dock open requests when switching notes, and tighten the foreign-note read-only banner layout.

Co-authored-by: Cursor <cursoragent@cursor.com>
@cursor

cursor Bot commented Jul 8, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

Capture follow-up product directions for foreign-note highlight layering so the broken member UX is tracked as a known gap, not an overlay bug.

Co-authored-by: Cursor <cursoragent@cursor.com>
@cursor

cursor Bot commented Jul 9, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

Co-authored-by: Cursor <cursoragent@cursor.com>
@cursor

cursor Bot commented Jul 9, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

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.

1 participant