Audience: end users (the company using Solavel), customer support Difficulty: new user
What this covers
The two ways a customer can interact with documents you send from Solabooks: a one-tap secure link and a fully branded customer portal account. Both live under the Solabooks app and are distinct from the parent Solavel client portal (which is for tenant owners managing their own workspace).
If you are looking for the link-based flow only, jump to secure-document-links.
The two flows
| Secure link | Customer portal account | |
|---|---|---|
| URL pattern | /i/{token} (canonical) or /portal/{token} (legacy) |
/customer-portal/login |
| Identity | None — the token is the credential | Email + password |
| Lifetime | 24 hours from creation | Persistent until disabled |
| What customer sees | One document (invoice / quote / etc.) | Dashboard with all documents |
| Login required | No | Yes |
| Backed by | customer_portal_links table |
customer_portal_accounts table |
| Auth guard | None — token middleware | Separate customer-portal guard |
The two flows can co-exist for the same customer. A secure link can deep-link a one-off document to a customer who has never created an account; if the customer wants more, they can sign up using the Setup account prompt offered alongside the document.
Portal account architecture
The customer portal is built on a separate user model
(App\Models\CustomerPortalAccount) and a separate auth guard
(customer-portal). It does not share users with the staff side. A
customer who is also an internal user must log in twice with different
credentials.
The flow is bound to a tenant by URL: each request resolves the active
tenant DB through customer-portal.tenant
(ResolveCustomerPortalTenant middleware) before any auth happens.
Sign-in happens at /customer-portal/login with a throttle bucket of
customer-portal-login to deter credential stuffing.
Sign-up
Customer portal accounts are created in two ways:
- Triggered by the company. When you mark a customer as having
portal access in Customers > [Customer] > Portal Access, the
system issues a setup token valid for 60 minutes and emails
the customer a one-time setup link. The link lands at
/customer-portal/setup?token=<...>. The customer chooses a password and is signed in. - Self-service from a secure link. When a customer opens a
/i/{token}document, the page offers a "Create an account to see all your documents" prompt. Clicking it issues a setup token for that customer and emails it.
Setup tokens are SHA-256 hashed at rest, single-use, and consumed in a
DB transaction with lockForUpdate() to prevent race conditions
(CustomerPortalTokenService::consumeSetupToken).
Sign-in
/customer-portal/login accepts email and password.
- Throttle:
customer-portal-login. Repeated failures lock the bucket for the standard window. - On success, a session cookie is set scoped to the tenant subdomain/path.
- "Forgot password" leads to
/customer-portal/forgot-password, which issues a 60-minute reset token (also hashed).
Dashboard & document list
After login the customer lands on /customer-portal/dashboard with a
side nav for:
| Section | URL | Shows |
|---|---|---|
| Dashboard | /customer-portal/dashboard |
Open balance summary, recent activity |
| Invoices | /customer-portal/invoices |
All invoices issued to this customer |
| Payments | /customer-portal/payments |
Recorded payments and refunds |
| Quotes | /customer-portal/quotes |
Quotes (open, accepted, declined) |
| Orders | /customer-portal/orders |
Sales orders if sales.use_sales_orders is on |
| Settings | /customer-portal/settings |
Profile + change password |
All views are scoped to the signed-in customer's customer_id. Data
crosses tenant boundaries only via the tenant resolution middleware —
two customers in two organizations of the same tenant DB cannot see
each other.
Disabling and revoking access
- Disable account. Set
customer_portal_accounts.is_active = false. Existing sessions are invalidated on next request. - Revoke all secure links. From the customer record, Revoke
active links. This sets
revoked_aton every active row incustomer_portal_links. - Revoke a specific document's links.
PortalLinkService::revokeForDocument($doc)— fires from the document page's Revoke link action.
Logging
Every portal access is recorded in portal_access_logs with the
customer id, document id, IP, and user agent. Admin support can search
this log via Admin > Activity Logs.