* Add workspace webhooks and drop the unused automation webhook node.
Give workspaces HMAC-signed outgoing webhooks for the post lifecycle, with retry, auto-pause, replay, and live logs, and keep HTTP Request as the only outbound automation node.
* Tighten webhook controller and validation after review.
Drop the redundant workspace redirects, prune logs without counting, and validate events/status with Rule::enum.
* Move leftover webhook UI copy behind i18n.
HTTP status phrases, delete-cancel, and validation attribute names were still English literals.
* Build the webhook-paused email through Maizzle.
The hand-written Blade skipped the shared layout, header, and footer used by the other mail templates.
* Cover real webhook dispatch paths and restyle the webhook pages.
* Ask for the shared delete keyword when confirming a webhook delete.
The endpoint URL is a poor confirm string; posts and assets already use the common "delete" keyword.
* Fix webhook review blockers so CI can go green.
Drop leftover French automation keys, stop mutating Inertia log props, and show delivered_at instead of created_at.
* Close the remaining webhook review gaps.
Keep Echo log updates across infinite scroll, align the channel with the policy, persist log ids across retries, and fail unknown automation nodes without throwing.
* Stop webhook delivery after disable and record last sent only on success.
Queued jobs now skip paused or disabled endpoints unless the user replays, and changing the URL re-pings it first.
* Limit webhooks to owners and admins, and encrypt signing secrets.
Members can no longer create or inspect outgoing integrations, and secrets stay encrypted at rest.
* Cover webhook secret hiding, skip-ping, and failed-delivery edges.
* Send the full post on webhooks after labels and platforms are saved.
* Fix webhook payloads for integer media ids and type webhook status.
* Split the webhook show page into focused components.
* Reset live webhook logs when switching endpoints.
* Keep the newest webhook logs at the top after live merges.
* Cast media item ids to string without the extra scalar check.
* Add post.unscheduled webhooks and put the log id on the envelope.
Unscheduling is now a first-class event, and receivers can send the delivery id back so we can find the matching log.
* Translate webhook event names in the UI.
* Make the webhook show page full-width and stop stacking flash toasts.
* Translate remaining webhook UI copy in every locale.
* Sign webhook pings and drop author email from the payload.
* Send signed webhook tests after create instead of pinging on save.
Create and update only block private URLs so the receiver can copy the secret first. The show page then sends a signed webhook.test with an object data envelope.
* Polish webhook test UX and always mint the dispatch log id in the job.
Keep send-test in the actions menu (its own group) and drop the leftover constructor param so retries reuse the serialized id instead of a caller-supplied one.
Adds a private workspace channel (WorkspaceChannel, authorised by
membership) so list views can subscribe once and receive events for
every post in the workspace, instead of opening N per-post channels.
New events:
- PostCreated (broadcast on workspace.{id})
- PostDeleted (broadcast on workspace.{id}; carries primitive ids so
it fires after \$post->delete())
- PostPlatformStatusUpdated now broadcasts on both post.{id} and
workspace.{id} so focused views and lists share the same trigger.
All broadcast events migrated to the namespaced 'entity.action'
convention from Laravel's broadcasting docs and switched from
ShouldBroadcastNow to ShouldBroadcast on a dedicated 'broadcasts'
queue (added to the supervisor-1 list in config/horizon.php) to keep
HTTP responses fast:
PostCreated -> post.created
PostDeleted -> post.deleted
PostPlatformStatusUpdated -> post.platform.status.updated
PostCommentCreated -> post.comment.created
NotificationCreated -> notification.created
PostCreationReady -> ai.creation.completed
Drops the SubscriptionCreated event — it was broadcast on
users.{owner_id} but had no listeners (frontend or backend) and the
billing/PostHog flow already handles plan-change tracking elsewhere.
PostPlatformStatusUpdated payload trimmed to {post_id} since every
consumer (Show, Edit, Index) does router.reload({ only: [...] }) and
ignored the rich shape.
- Create Account model as Cashier Billable entity (stripe, plan, subscription)
- Account owns workspaces and has an owner_id (User)
- User belongs to one Account via account_id
- Workspace belongs to Account via account_id, no longer has billing fields
- Remove Brand model entirely (workspaces serve as grouping)
- Rename brand_limit to workspace_limit in plans
- Workspace roles simplified: admin/member/viewer (owner via Account)
- Invites now belong to Account with workspaces JSON array
- Pennant features scope changed from Workspace to Account
- EnsureSubscribed middleware checks Account subscription
- All controllers updated: BillingController, OnboardingController,
WorkspaceInviteController, SocialController, StripeEventListener
- Frontend: extract GoogleAuthButton component, create WorkspaceRole
enum for type-safe role checks, fix all views for new architecture
- All 1101 tests passing
- Refactor WorkspacePolicy to use pivot role instead of workspace.user_id
- Add manageBilling policy (owner only) to BillingController
- Fix ApiKeyController authorization (view → manageTeam for store/destroy)
- Fix WorkspaceInviteController using workspace.user_id for owner checks
- Fix WorkspaceController settings is_owner using workspace.user_id
- Create PostAction enum for UpdatePost/PostController action strings
- Create ApiToken\Status enum
- Add User::SUBSCRIPTION_NAME constant, replace all hardcoded 'default'
- Convert wantsEmailFor to accept NotificationType enum
- Convert all $data[] to data_get() across publishers, controllers, jobs
- Fix SocialLoginController callback missing try/catch
- Fix SocialController::toggleActive missing workspace null check
- Fix UpdatePost NPE on meta merge when postPlatform not found
- Remove HTML5 required attributes from form inputs
- Convert function declarations to arrow functions in Vue components
- Replace hardcoded URLs with Wayfinder route helpers
- Replace new Date() with dayjs
- Add 16 new test files covering policies, authorization, publishing
Settings pages:
- Redesign layout to match Sendkit (max-w-4xl, space-y-12, Separator sections)
- Merge Members page into Workspace settings with Table, invite Dialog, ConfirmDeleteModal
- Add workspace logo upload/delete routes and controller methods
- Translate all hardcoded strings in Workspace.vue modals
Language system:
- Drop languages table, replace language_id FK with locale string column on users
- Create config/languages.php for available languages and default locale
- Add Spanish (es) translations (13 files)
- Simplify HandleInertiaRequests, ProfileController, RegisteredUserController
Code quality:
- Add declare(strict_types=1) to all PHP files
- Fix MastodonPublisher using wrong attribute (filename -> original_filename)
- Fix HasMediaTest for new has_photo/photo_url accessors
- Fix PublishToSocialPlatformTest type error revealed by strict_types
- Remove orphaned Language model from AppServiceProvider morph map
- Update User TypeScript interface (has_photo, photo_url, locale)
- Eager load media relation on workspaces to prevent N+1
- Add 8 new tests for workspace logo upload/delete
- Update workspace settings test to assert members/invitations props
All 710 tests passing.