The unified LinkedIn card grouped connected accounts by looking the platform up in the rendered card list, but linkedin-page is no longer a card — so an org-only connection fell through to its own network key and the card showed 'Connect' for an already-connected page, with no way to disconnect or reconnect. Expose each account's network on SocialAccountResource and group by account.network instead.
Onboarding still built its connect grid from SocialPlatform::enabled() instead of isConnectable(), so it rendered a standalone LinkedIn Page card with a missing logo whose connect button hit the deleted connect/linkedin-page route (404). Filter by isConnectable() to match the accounts page.
Add a connect step between persona selection and Stripe checkout: persona ->
connect >=1 network (server-enforced) -> checkout. Extract the network grid into
a shared NetworkConnectGrid used by both onboarding and the accounts page, which
is redesigned from a table into the same grid (one account per network, with
per-card connect / connected+disconnect / reconnect states). Drop the openDialog
auto-open flow now that networks are shown inline.
Bugs (both with regression tests):
- OnboardingController::store now guards already-subscribed accounts (mirrors
index), preventing a second Stripe Checkout / double subscription if a
subscribed user re-POSTs /onboarding.
- SocialAccountObserver: drop the `platform_user_id != …` clause from the
creating-time one-per-network check. On create there is no "self" to exclude,
so it only weakened the rule — the same account connected via two network
variants (e.g. Instagram standalone + via Facebook, same id) could slip a
second account into the network. Now any account in the network blocks.
Robustness:
- CreateWorkspace wraps create + member attach + switchWorkspace in a
transaction (cache-forget / quantity-sync run after), so a partial failure
can't leave an orphan workspace that inflates the Stripe seat count — covers
both the signup and the add-workspace paths.
Test honesty & coverage:
- Scope the ten "connect multiple <platform> accounts" tests to self-hosted
mode (config + name); they only passed because the test env defaults
SELF_HOSTED=true, and in cloud the one-per-network rule blocks them.
- network_taken popup now has controller-level tests on all six OAuth
controllers (added Threads, YouTube, LinkedInPage, and a new
InstagramFacebook test file; LinkedInPage/InstagramFacebook also exercise
variant collapse).
- Wiring tests that creating/deleting a workspace actually calls
syncWorkspaceQuantity (guards per-seat billing against silent breakage).
- Strengthen TrialLengthTest to assert the configured length reaches
trial_ends_at; add a same-id network-variant block test.
Pricing
- Bill per workspace ($12/mo or $120/yr each); Stripe quantity tracks the
workspace count and syncs on workspace create/delete.
- 2,500 AI credits per workspace, pooled at the account level; monthly reset
on the billing anniversary, annual granted upfront (no rollover).
- One social account per network per workspace; remove all count-based limits
(workspace/social/member) and the legacy plan tiers (single Workspace plan).
Onboarding (cloud only: SELF_HOSTED=false + PostHog)
- Replace the /subscribe plan picker with /onboarding persona selection
(Creator/Freelancer/Startup/Agency/Small business/Other), saved on the user
(users.persona) and mirrored to PostHog, then Stripe Checkout on the monthly
price. 8-day trial so Stripe displays 7.
Billing screen
- Remove the Change Plan dialog (dead with a single plan); add an annual-upgrade
banner for monthly subscribers (swapToYearly).
- Current-plan card shows the workspace count instead of the plan name.
System AI
- Brand analyzer / workspace autofill is always allowed and never debits credits
(system feature, not the user's usage).
Self-hosted (SELF_HOSTED=true) bypasses all billing, credit, limit, network,
and onboarding logic.