Audience: support engineers, admins Difficulty: admin only
What this covers
The most common login and workspace-access dead-ends, plus the SSO handoff between the parent app and Solabooks. Each row gives a concrete check or fix.
The login chain (parent app)
/login → POST /login → Authenticated session → SetClientContext →
EnsureWorkspaceOnboardingCompleted → /portal/orgs (or /admin)
A user can break out of this chain at five points. Diagnose by where the redirect lands.
| Lands on | Meaning | Fix |
|---|---|---|
/login (loops) |
Session not persisting (cookie blocked) | Different browser; check SESSION_DOMAIN if subdomain involved |
/email/verify |
Email not verified | Resend verification from /admin/admins or have user click the link |
/onboarding/verify |
Workspace onboarding not done | User completes onboarding (or admin marks it done) |
/portal/orgs empty list |
Authenticated but no user_organizations row |
Add membership in /admin/clients > [client] > Members |
/admin 403 |
User does not have admin role | Confirm Spatie role on user; assign admin if needed |
Spatie roles vs. user_organizations.role
Two parallel role systems live on the parent app — understand both.
Spatie role. Stored in model_has_roles. Examples:
super-admin, admin, client_owner, client_manager, client_member.
Checked by Blade @hasrole, route role: middleware,
AdminPolicy::before().
user_organizations.role column. A free-form string column. Used
by SuperAdminProvisioningSeeder (writes 'client_owner') and by some
controllers when scoping per-org behaviour. Not the same as the
Spatie role.
For full access a user needs both:
- The Spatie role that matches their level (
client_ownerfor owners). - A
user_organizationsrow with the correctrolestring.
Common mismatch: the user is granted Spatie client_owner but
their user_organizations.role is still client_member. They see the
correct UI affordances but per-org operations fail.
Fix. From /admin/clients > [client] > Members, click the user
and re-pick the role. The UI writes both.
Solabooks app role chain
The finance app uses three levels at once:
- SSO entry. The handoff token (issued by parent
sso.finance.redirect) authenticates the user into the finance session. No role is checked here. - Org membership.
EnsureOrgMembershipconfirms the user is a member of the active organization (or they are switched to a valid one). This reads the financeorganizations,users,organization_usertables — separate from the parent'suser_organizations. - Permission gate. Each route applies
perm:<key>orrole:<role>middleware. Theperm:middleware readsFinanceRolePermissionSetkeyed off the user'sfinance_role_keycolumn. Therole:middleware reads Spatieroles.
Common mismatch: the user is in user_organizations on the parent
but the finance tenant DB has no organization_user row. They reach
finance and get "Organization not found".
Fix. Run the finance superadmin:setup --client-id=<id>, or
re-trigger the parent's tenancy projects sync
(/admin/clients > Sync Projects).
SSO handoff failures
The flow:
- Parent: user clicks Launch Solabooks →
GET /sso/finance/redirect. - Parent issues a short-lived signed token via
SsoTokenService. - Browser redirects to
https://solavel.com/finance/sso/callback?token=…. - Solabooks:
SsoCallbackControllercalls back to parentPOST /api/sso/validate(open route, throttle 60/min) to verify the token. - On success, finance opens a session for the user and redirects to
/finance/.
Failure modes:
| Symptom | Cause | Fix |
|---|---|---|
| "SSO token expired" | Token TTL elapsed (network slow, redirect cached) | Ask user to retry. If repeats, increase TTL in config/sso.php |
| Solabooks redirects back to parent login | auth:sanctum rejected the validate call |
Check app.key matches between apps; check parent API base URL in finance's config/parent.php |
| Solabooks shows "Tenant not found" after callback | Tenant DB not yet provisioned | Run superadmin:setup --client-id=<id> |
| Validate returns 419 | CSRF token / session mismatch | Clear cookies for both domains |
| Solabooks signs in as the wrong user | Token reused or replayed | Engineering — security incident |
Where to look in logs:
- Parent:
storage/logs/laravel.logforApi/SsoValidationController. - Solabooks:
storage/logs/laravel.logforSsoCallbackController. - Apache:
/var/log/apache2/access.logfor the redirect chain (the/sso/finance/redirect→/finance/sso/callback?token=…pair should both return 302 then 200).
"I can't see my organization in the picker."
The finance app shows organizations the user is a member of in the top-bar org switcher. If an org doesn't appear:
- Confirm the user has a row in finance's
organization_usertable for that org. - Confirm the org is
is_active = true. - Confirm the user's
users.organization_id(default org) hasn't been set to a deleted org — that wedges the resolver. - Confirm the parent's
user_organizationsrow is in sync.
Fix. From parent /admin/clients > [client] > Sync Users. Then
have the user sign out and back in.
Customer portal access dead-ends
Distinct system. See customer-portal/secure-document-links and customer-portal/overview.
| Symptom | Cause | Fix |
|---|---|---|
| Customer hits 404 on link | Token format wrong; tenant DB hint corrupt | Re-issue from document |
| "This portal link is no longer active" | Past 24-hour expiry, or revoked | Send again from document |
| "Document/customer mismatch" | Document was reassigned | Re-issue link |
| Can't sign in to customer portal | Account disabled or password wrong | Forgot password; or re-enable account |
| Customer setup link expired | Past 60 minutes | New link from /customer-portal/forgot-password |
API access dead-ends
| Symptom | Cause | Fix |
|---|---|---|
401 on api/v1/* |
Missing or wrong API key | Re-issue from /finance/admin/api-access |
403 on api/v1/* |
Key valid but missing scope | Edit key, add scope |
401 on api/workspace-control/* |
Sanctum token expired | Re-issue token from parent admin |
419 on api/sso/validate |
Throttle hit (60/min) | Wait or escalate |