Laravel backend¶
The backend is a Laravel 13 application on PHP 8.3+. It owns every trust decision: routing, authentication, account security, authorization, validation, persistence, sanitization, auditing, queues, mail, integration calls, import/export, and PDF rendering.
Request surfaces¶
| Surface | Route file | Middleware/auth | Purpose |
|---|---|---|---|
/, /app/*, /join/* |
routes/web.php |
web session shell | serve the React entry view |
/app-api/* |
routes/web.php |
web, then auth/account.secure by group |
same-origin browser JSON API |
/auth/oauth/*, /auth/sso/* |
routes/web.php |
web session/state | external identity redirects/callbacks |
/api/v1/* |
routes/api.php |
api.credential, throttle:api |
versioned external bearer API |
/docs/api, OpenAPI |
Scalar/routes | configurable docs access | interactive API reference/spec |
/up |
bootstrap/app.php |
framework health | basic application health |
Laravel is configured to render JSON exceptions for api/* and app-api/*. The SPA shell uses one Blade template with CSRF meta and Vite entrypoints.
The generated manual is a static /docs/ directory, while /docs/api and /docs/openapi.yaml remain Laravel routes. The root server.php file is used only by php artisan serve; it resets PHP's inferred script base before handing a non-static request to Laravel. Apache handles the same separation through its normal front-controller rewrite.
Middleware¶
AuthenticateApiCredential¶
Splits the bearer token at the first dot, looks up public ID, rejects revoked/expired credentials, hashes the presented secret with SHA-256, compares with hash_equals, quietly updates last_used_at, and attaches the credential to request attributes.
EnsureAccountSecurity¶
Evaluates persistent email/TOTP state against current deployment policy. Incomplete setup returns a structured 403 before any workspace endpoint executes.
EnsureApiDocumentationAccess¶
Allows Scalar/spec when docs are public or a Laravel user is authenticated. Static MkDocs is served by the web server and is outside this middleware.
Controllers¶
Browser controllers are domain-focused:
- authentication/security, invitations, OAuth, SSO, and user profile;
- organizations/members, projects/members, boards, labels;
- tickets, comments, attachments, revisions, analytics;
- documents, document comments, editor assets, PDF;
- audit, transfers, notifications, webhooks, API credentials;
- API documentation.
ExternalApiController consolidates the external v1 contract so credential/scope actor handling remains consistent. It intentionally repeats domain validation rather than delegating to browser controller methods that assume a user/session request.
Domain services¶
| Service | Responsibility |
|---|---|
AccountSecurityService |
email code issuance/verification, TOTP policy/setup/challenge/QR |
AuthorizationService |
organization/project/comment invariants for browser operations |
DocumentAccessService |
document query/object visibility and eligible editors |
ActivityRecorder |
insert audit event and enqueue matching webhook deliveries |
ApiCredentialService |
generate public/secret token and persist hash/scopes |
DataTransferService |
versioned graph export/import, ID/identity/asset mapping |
HtmlSanitizer |
DOM-based allowed HTML/attribute/URL boundary |
MentionService |
extract/validate/sync mention identities and send direct alerts |
NotificationService |
recipient derivation, in-app notification, preference-aware email |
RevisionService |
canonical ticket/comment snapshots and immutable versions |
TagService / DocumentTagService |
normalize/reuse/synchronize lightweight tags |
InvitationService |
one-way invitation token generation/lookup and notification |
OutboundUrlGuard |
webhook scheme/DNS/private-range checks |
DocumentPdfService |
inline authenticated local images before DOMPDF |
UserDataExportService |
privacy-oriented per-account export without secrets |
SsoManager |
resolve and validate configured driver adapters |
Keep cross-controller invariants in services. Keep presentation shaping in controllers/resources as appropriate. A new mutation that should be auditable must call ActivityRecorder with the correct organization/project/subject and actor type.
Mutation pattern¶
A typical protected browser mutation follows:
route model binding
→ load parent organization/project
→ AuthorizationService check
→ Request validation
→ relationship/business invariant checks
→ database transaction when multiple writes must agree
→ domain writes + sanitized content
→ revision/mention/notification synchronization
→ ActivityRecorder
→ response with required relations
External API mutation follows the same domain steps but uses authorize(request, scope, org) and records an API actor/source: api metadata.
Eloquent conventions¶
- Principal domain models use
HasUlids. - Membership/assignment pivots generally use numeric surrogate IDs plus composite unique constraints.
- Soft deletes preserve organizations, projects, boards, tickets, ticket comments, documents, and document comments.
- Casts handle JSON arrays, datetimes, booleans, hashed passwords, and encrypted TOTP/webhook secrets.
- Sensitive model properties are hidden from serialization.
- Relationships constrain access queries; never trust a client-supplied parent/child pair without verifying it.
Transactions and locking¶
Use a database transaction when a state transition spans related rows:
- organization creation plus first owner;
- project creation plus default phases;
- ticket number allocation and relationship setup;
- movement plus movement timeline entry;
- board deletion plus ticket migration;
- graph imports.
Ticket creation locks the project row before calculating max(number)+1. Reorder operations normalize positions transactionally. The database queue’s after_commit prevents jobs from observing uncommitted rows.
Audit and side effects¶
ActivityRecorder::record() inserts the audit row, derives a subject label, finds active endpoints in scope, stores one immutable-ish payload snapshot in each delivery, and dispatches a job.
Because webhook work is queued after the audit write, domain HTTP requests do not wait for remote services. Email notifications are also queued through Laravel Notifications. Failures should not roll back completed domain work unless the domain write itself failed.
When adding an event:
- choose a stable dot-separated name;
- record enough before/after/metadata for consumers;
- add it to the webhook selector catalog when users should select it explicitly;
- add docs/reference/OpenAPI where applicable;
- test actor, scope, persistence, and signature flow.
Rich text boundary¶
Never store client HTML without HtmlSanitizer, including imports and external API. Validate mention IDs after sanitization and before syncing notification relationships. A sanitizer change must include unit tests for dangerous wrappers, URL schemes, attributes, mentions, and table spans.
Error conventions¶
401for external credential authentication.403for authenticated but unauthorized operations.404for missing route-bound/resource-constrained relationships.409for incompatible account-security state.410for unusable invitations.419for expired challenge/session/state.422for validation and business invariants such as WIP/last owner.429for throttle.
Return actionable messages but avoid disclosing cross-tenant existence or secret validation detail.
Adding a backend feature¶
- Define tenant/authorization invariants and soft-delete/audit requirements.
- Add a forward migration with indexes and cross-driver behavior.
- Add/update model casts, hidden fields, and relationships.
- Create focused service logic where reused or security-sensitive.
- Add browser and/or external route/controller validation.
- Record event and side effects after consistent state exists.
- Add feature/unit tests, including denial and tenant-isolation cases.
- Update OpenAPI, route/reference docs, and user/admin/operator docs.
- Run Pint/Prettier/tests/UI audit/MkDocs strict build.