Customer Portal — Improvement & Growth Plan
Package: capell-app/customer-portal · Kind: package · Tier: premium · Product group: Capell Content · Bundle: content-product · Status: Draft
1. Snapshot
Section titled “1. Snapshot”Customer Portal is the authenticated self-service foundation for Capell sites. It exposes two surfaces: a frontend dashboard (3 routes under /portal behind web,auth — ShowCustomerPortalController, UpdatePortalPreferencesController, StorePortalSupportRequestController) and an admin Filament triage resource (PortalSupportRequestResource). Domain logic lives in 6 Actions (FindOrCreatePortalAccountAction, ResolveAuthenticatedPortalAccountAction, ResolvePortalDashboardItemsAction, ResolvePortalSelfServiceItemsAction, SubmitSupportRequestAction, UpdatePortalPreferencesAction, UpdateSupportRequestStatusAction). It owns two tables (portal_accounts, portal_support_requests) via models PortalAccount and PortalSupportRequest, with encrypted PII columns and HMAC email hashing. Extensibility runs through four provider registries (dashboard items, self-service items, profile, preferences); deps are capell-app/core + capell-app/admin, with supports for access-gate, document-lifecycle, events, newsletter, payments (each of which registers self-service/dashboard providers). 1804 LOC across 41 PHP files. Current marketplace summary: “A unified, logged-in customer hub for Capell sites — payments, documents, event registrations, gated content and support requests in one secure dashboard.” Screenshots: 4 promoted light-mode Capell runner PNGs plus committed dark variants cover the frontend dashboard, preferences/support form, admin triage list, and admin triage detail.
2. Improvements (existing functionality)
Section titled “2. Improvements (existing functionality)”- Shipped 2026-06-03: health check body.
CustomerPortalHealthChecknow probes both required tables and verifiesPortalAccount/PortalSupportRequestmodel resolution, with coverage inCustomerPortalHealthCheckTest. — S - Shipped 2026-06-03: admin triage site scoping.
PortalSupportRequestResource::getEloquentQuery()appliesSiteScope::applyForCurrentActor()so non-global admins only see assigned-site support requests, with multi-site coverage inPortalSupportRequestResourceScopeTest. — S - Shipped 2026-06-04: preference schema drives rendering and validation.
ResolvePortalPreferenceOptionsActionreadscapell-customer-portal.preferences, the dashboard loops over the resolved options, andUpdatePortalPreferencesControllernormalizes only configured keys. - Done/Shipped: cap dashboard provider fan-out.
ResolvePortalDashboardItemsActionandResolvePortalSelfServiceItemsActionnow cap each provider before merging and apply a global cap after sorting, usingdashboard_items_per_provider_limit,dashboard_items_limit,self_service_items_per_provider_limit, andself_service_items_limit. This keeps the dashboard bounded when payments, documents, events, newsletter, access-gate, and support all contribute items; dedicated “view all” routes remain part of the later support/billing/download depth rather than dead links. —src/Actions/ResolvePortalSelfServiceItemsAction.php,src/Actions/ResolvePortalDashboardItemsAction.php,config/capell-customer-portal.php— M - Validate support requests in one place —
StorePortalSupportRequestControllervalidatessubject/message, thenSubmitSupportRequestActionre-trims and re-throwsValidationExceptionfor empty values. The Action’s checks are unreachable from the HTTP path (alreadyrequired) and duplicate intent. Consolidate on a FormRequest or keep the guard only in the Action and thin the controller. —src/Actions/SubmitSupportRequestAction.php,src/Http/Controllers/StorePortalSupportRequestController.php— S - Make the
noStore()header order intentional — the controller setsCache-Control: private, no-storebut the test assertsno-store, private; it passes only because Symfony’sResponseHeaderBagre-orders directives. This is fragile coupling to framework internals for asensitiveOutput: truepackage. Assert the normalized value deliberately (or set directives so order is explicit) and add a regression note. —src/Http/Controllers/ShowCustomerPortalController.php,tests/Feature/Frontend/CustomerPortalFrontendTest.php— S - Reuse the priority/status option helpers —
ShowCustomerPortalController::priorityOptions()andPortalSupportRequestResource::priorityOptions()/statusOptions()independently re-map the same enums. Extract a single helper on the enum or a Data object. —src/Http/Controllers/ShowCustomerPortalController.php,src/Filament/Resources/PortalSupportRequests/PortalSupportRequestResource.php— S
3. Missing Features (gaps)
Section titled “3. Missing Features (gaps)”Tied to capabilities[] and customer-portal table stakes:
portal-profilecapability is unreachable (table stakes, broken) —capabilitiesadvertisesportal-profileandportal-preferences, andPortalProfileProviderRegistry/PortalPreferencesProviderRegistryare bound as singletons inCustomerPortalServiceProvider. But there is noResolvePortalProfileAction/ResolvePortalPreferencesAction, the dashboard reads raw$portalAccount->profile/preferencescolumns directly, and a monorepo-wide grep finds zero implementers ofPortalProfileProvider/PortalPreferencesProvider. Account-profile self-service (name, contact, addresses) is the most basic customer-portal feature and is currently absent end-to-end. Wire a resolve Action + render a profile section, or drop the dead registries. — table stakes.- Done/Positioned: authentication is a BYO host-app boundary. The package intentionally mounts behind the configured authenticated middleware stack and resolves the current Laravel user into a site-scoped
PortalAccount; it does not install login, registration, password reset, magic-link, or SSO screens. README/foundation docs now state this boundary and point buyers to host auth, Fortify, Socialite, Access Gate, or another auth package for the identity journey. — table stakes boundary. - Shipped 2026-06-04: support workflow events and requester notifications.
SubmitSupportRequestActionnow emitsPortalSupportRequestSubmitted, andUpdateSupportRequestStatusActionemitsPortalSupportRequestStatusChangedonly for real status transitions. Both workflows queue requester mail notifications when an email address is available. - Done/Shipped: two-way support threading.
portal_support_request_repliesstores encrypted reply bodies withsender_type, optional morph author, submitted timestamp, and encrypted attachment metadata references. Customers can reply from recent support history, replies move tickets toWaitingOnTeam, staff can reply from the admin triage table, and team replies move tickets toWaitingOnCustomer. Customer Portal intentionally stores attachment references rather than owning file storage/downloads. - Done/Positioned: billing, downloads, and entitlements are owning-package surfaces.
portal-payments-feed,portal-document-feed, andportal-gated-resource-feedare intentionally provider-owned: Payments issues invoice, payment-method, checkout, and subscription URLs; Document Lifecycle and gated-resource packages issue signed download or entitlement URLs. Customer Portal aggregates and renders those customer-facing self-service items without importing billing, document, or entitlement internals. — suite boundary. - Shipped 2026-06-04: account status enforcement.
ResolveAuthenticatedPortalAccountActionnow blocks suspended and archived accounts before dashboard, preference, or support workflows run, with frontend coverage for both statuses. - No factories or seeders — neither model ships a factory (
database/factories/does not exist) though bothuse HasFactory;PortalAccount::factory()would throw. Blocks downstream demo/testing and contradicts Capell’s “create useful factories” convention. — table stakes for a sellable package.
4. Issues / Risks
Section titled “4. Issues / Risks”- Shipped 2026-06-03: admin cross-tenant support-request leak fixed. The support-request resource is site-scoped for non-global admins.
- Shipped 2026-06-03: critical health check is real. The package health check now fails when required storage is missing.
- Dead capability
portal-profile/ unused registries (tech debt + manifest mismatch). Profile & preferences provider registries have no resolver, no consumer, and no implementer anywhere. —src/Support/PortalProfileProviderRegistry.php,src/Support/PortalPreferencesProviderRegistry.php,src/Providers/CustomerPortalServiceProvider.php. - Shipped 2026-06-04: frontend render/query budgets.
capell.jsonnow declaresfrontendRenderBudgetMs: 200andfrontendQueryBudget: 20, with manifest tests covering both values. - Shipped 2026-06-04: frontend account isolation coverage.
CustomerPortalFrontendTestnow proves one account cannot see another account’s support requests on the same site. - No negative-path / authz tests. Missing: unauthenticated request → 403 (the
abort_unlessinResolvesPortalAccount), throttle onsupport.store(throttle:12,1), and broader admin resource gating. Suspended/archived accounts and cross-site admin scope are now covered. —tests/. - PII handling is good but partial.
subject/message/requester_email/contextareencryptedand emails are HMAC-hashed for lookup (PortalAccount::emailHash), buthash_secretdefaults tonullin config and falls through to an app secret — document the key-rotation story, and note encrypted columns are unsearchable in admin (the resource has no search, consistent with this, but worth stating). —config/capell-customer-portal.php,src/Models/PortalSupportRequest.php. ResourceEnumis unreferenced (minor dead code).src/Enums/ResourceEnum.phpmaps one case to the resource class with no consumer found. —src/Enums/ResourceEnum.php.- Public output safety: OK, with one note. The dashboard passes hydrated arrays (no DB queries in Blade), sends
noindex+no-store, and the test asserts no package name /portal_account_id/signed/Filamentleaks — aligns with Capell public-output rules. Risk: provider-suppliedurl/description/labelstrings are rendered with{{ }}(escaped) but their provenance is third-party packages; keep them text-only and never{!! !!}. —resources/views/dashboard.blade.php. - i18n: strong. All user-facing strings use
capell-customer-portal::translations and enum labels resolve via translation keys. No hardcoded English in templates found. Maintain parity if keys are added for new preferences. - No README (docs debt). The package now has
docs/foundation.mdandCHANGELOG.md, but still lacks a rootREADME.md. — package root.
5. Marketplace & Selling
Section titled “5. Marketplace & Selling”Current summary: “Customer Portal gives Capell sites a safe authenticated self-service foundation.” — accurate but generic and inward-facing (“foundation” signals unfinished); leads with “safe” (table stakes) rather than buyer value. Composer description: “Authenticated customer self-service foundation for Capell CMS.” — nearly identical, also says “foundation”. Both undersell the real, working differentiator: a pluggable dashboard that aggregates payments, documents, events, gated content and support into one logged-in hub without those packages knowing about each other.
Improved 1-sentence summary: “A unified, logged-in customer hub for Capell sites — payments, documents, event registrations, gated content and support requests in one secure dashboard.”
Improved 3–4 sentence description: “Customer Portal turns a Capell site into an authenticated self-service experience. A single dashboard aggregates billing, documents, event registrations, gated resources and newsletter preferences contributed by your other Capell packages — no custom glue code, just install and they appear. Customers raise and track support requests; staff triage them from the admin panel with full status workflow and audit-safe, encrypted records. Built on Capell’s Action and provider-registry architecture, it stays cache-safe and never leaks admin internals into public output.”
Screenshot / media status: Done/Shipped. docs/screenshots.json now defines four route-backed Capell runner captures: frontend dashboard, preferences + support form, admin triage list, and admin triage edit/detail. capell.json promotes the four light PNGs into marketplace media, and dark variants are committed for documentation use.
Pricing / tier / bundle positioning: premium in the content-product bundle is defensible if the dead portal-profile capability and notifications land; today the standalone value is thin (it’s a shell over host auth + other packages’ feeds). Position as the anchor of an Extension Suite: it is most valuable when sold with payments (billing self-service), document-lifecycle/downloads (entitlements), and events (registrations). Cross-sell is already wired via the provider registries — lead with “buy the suite, get a portal that lights up.”
Differentiators / value props / target buyer: Differentiator = zero-glue aggregation via typed provider contracts + encrypted PII + Capell cache-safe rendering. Target buyer = agencies/site owners already running Capell payments/events/documents who want a customer login area without bespoke development. Value props: one hub, install-and-appear integrations, GDPR-friendly encrypted support records, multi-site aware (once admin scoping is fixed).
Keywords / tags (8–12): customer portal, self-service, account dashboard, support tickets, member area, authenticated frontend, billing self-service, document downloads, event registrations, preference center, multi-site, CRM. (Current categories: admin, frontend, crm, self-service — extend with the above.)
6. Prioritized Roadmap
Section titled “6. Prioritized Roadmap”| Item | Bucket | Effort | Impact | Section ref |
|---|---|---|---|---|
Scope PortalSupportRequestResource per site (getEloquentQuery) | Shipped | S | High | §2, §4 |
| Add tenant-isolation test (account A cannot see B’s data) | Shipped | S | High | §4 |
| Implement real health-check probe (tables + models) | Shipped | S | High | §2, §4 |
Resolve portal-profile: add resolve Action + render, or remove dead registries | Shipped | M | High | §3, §4 |
| Add factories (+ optional seeder) for both models | Shipped | S | Med | §3 |
| Add unauthenticated/throttle/suspended negative-path tests | Shipped | S | Med | §4 |
Enforce PortalAccountStatus (block suspended/archived) | Shipped | S | Med | §3 |
| Emit events + notifications on submit / status change | Shipped | M | High | §3 |
Set real frontendRenderBudgetMs + frontend query budget | Shipped | S | Med | §4 |
| Done/Shipped: Capture marketplace screenshots (4 surfaces) + rewrite summary/description | Shipped | S | High | §5 |
| Schema-drive preferences (single source for view + validation) | Shipped | M | Med | §2, §3 |
| Done/Shipped: Cap / paginate dashboard + self-service provider fan-out | Shipped | M | Med | §2 |
| Add README + CHANGELOG | Shipped | S | Med | §4 |
| Two-way support threading (replies + attachments) | Shipped | L | High | §3 — 2026-06-06: added support replies table/model/action, customer reply route/form, admin reply action, encrypted reply bodies, and encrypted attachment-reference metadata. |
| Done/Positioned: First-party billing & downloads/entitlement surfaces (suite cross-sell). Evidence: README/foundation docs state Payments, Document Lifecycle, and gated-resource packages own their domain operations and contribute self-service URLs through Customer Portal providers. | Shipped | L | High | §3, §5 |
| Done/Positioned: Provide auth/SSO surface (or formally position as BYO-auth). Evidence: README/foundation docs state Customer Portal is a BYO-auth package that uses the configured authenticated middleware stack and delegates login/registration/password/SSO to the host auth layer. | Shipped | L | High | §3 |