Shared Spaces foundation: clean-break architecture for the first paid add-on#14
Shared Spaces foundation: clean-break architecture for the first paid add-on#14heyderekj wants to merge 31 commits into
Conversation
…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>
|
This pull request has been ignored for the connected project Preview Branches by Supabase. |
✅ Deploy Preview for harvouscom ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
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>
|
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>
|
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>
|
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>
|
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>
|
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>
|
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>
|
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>
|
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>
|
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>
|
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>
|
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. |
…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>
|
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>
|
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>
|
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. |
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.mdfor the full design rationale.Spaces.type(personal|shared|public) + nullableorgId(future church hosting); newSpaceMemberships(owner is a real role, not justSpaces.userId) andSpaceInvites(expiring links, validated from day one) supersede the frozenMembers/SpaceInvitationstables.UserMetadata.sharedSpacesAddOn); joining is always free. The oldunlimitedtier is fully retired — zero subscribers, no grandfathering.POST /api/notes/createpreviously accepted anyspaceIdwith no validation — now gated byrequireSpaceAccess+canAuthorInSpace.Members/SpaceInvitationsfrozen (hygiene deletes only); one migration script promotes the Harvous system user's featured spaces onto the new rails.SHARED_SPACES_DEV_NOTES.mdrewritten,SPACE_MODES_PRODUCT.md/MONETIZATION_AND_PRICING.mdupdated, newfuture/MENTION_PILLS.mdlocking 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_spacesfeature must be created per-instance (Development done; Production is a separate pass before launch) — seedocs/SHARED_SPACES_DEV_NOTES.md→ "Clerk dashboard (manual setup)".Test plan
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/createwith aspaceIdyou don't belong to → expect 403 (previously silently accepted).tsc --noEmit— no new errors vs.mainbaseline.npx vitest run— same 15 pre-existing (Classic-era) failures, zero new failures.npm run db:push— applies cleanly post-merge withmain, no data-loss prompts.npm run build:spa— production build succeeds.🤖 Generated with Claude Code