TickIt is a configurable ticketing and helpdesk platform — a Laravel 13 API behind a Vue 3 single-page app. Admins build their own ticket types, each with its own statuses and custom fields, without touching code; a versioned integration API lets other apps file tickets programmatically with idempotent, replay-safe intake. Client, agent, and admin roles each get their own views, with a Kanban board, threaded comments, file attachments, and an activity log.
TickIt is a place for apps to send their feedback and for teams to work it. The interesting part isn’t the ticket list — it’s that the workflow is data, not code. A ticket type carries its own statuses and its own custom fields, so an admin can stand up a new kind of ticket, with its own pipeline and its own form, without anyone writing a migration. TicketWorkflowService reads that configuration to decide which moves are legal.
The other half is the integration API. Household and Chrono don’t email their bug reports — they POST them. That endpoint is versioned and authenticated per client, and it’s idempotent: ApiClientTicketService.createOrReplay() keys each submission on an Idempotency-Key, replays the original ticket on retry, and leans on a unique database constraint to settle the race when two retries land at once. Around that sit the things you’d expect of a helpdesk — a Kanban board, Tiptap-authored comments, file attachments, an activity log, and web-push plus mail notifications when work arrives.
Around the workflow engine and the integration API sit the everyday parts of a helpdesk — projects, ticket assignment, comments, attachments and notifications — so the configurable core and the machine-to-machine intake both land in a tool people actually run day to day.
Each ticket type owns its own set of statuses and its own custom fields. Admins define the type, its workflow states, and the fields agents fill in — none of it hardcoded. TicketWorkflowService governs which status transitions are legal.
TicketTypeField stores a label, field type, a JSON options blob for select-style fields, a required flag, and a sort order — so a new field is a row, not a schema change. The admin UI reorders fields by drag.
A versioned endpoint (Integrations/V1/TicketController) lets external apps submit feedback tickets. Household and Chrono both use it — authenticated per-client by AuthenticateApiClient middleware against an ApiClient token, scoped to a project.
Tickets live on a drag-and-drop board at /tickets/board organised by the type's statuses. Each ticket carries threaded comments authored in a Tiptap editor, uploadable and downloadable file attachments, and a full activity log.
When a ticket is created, TickIt fans out a TicketCreated notification over web push (VAPID keys, push_subscriptions table) and the mail channel, so agents hear about new work without watching the board.
Client, agent, and admin each get a dedicated view tree. New projects spin up from templates via ProjectSetupService, so a fresh project arrives with sensible default types, statuses, and fields rather than a blank slate.
Different teams want different ticket shapes — distinct statuses, distinct fields — and shipping a migration per request doesn't scale.
Modelled the workflow as data. TicketType owns its TicketStatus rows and TicketTypeField rows (label, field_type, JSON options, is_required, sort_order). Admins compose types in the UI; TicketWorkflowService reads that data to validate transitions, so new workflows need no deploy.
External apps retry on network failure. A naive create-ticket endpoint would duplicate a ticket every time a client resent the same request.
createOrReplay() keys each submission on an Idempotency-Key. A repeat key returns the original ticket instead of creating a second one — making retries safe for the calling app.
Two retries can arrive close enough that both pass the initial "does this key exist?" lookup before either has committed.
A unique constraint on idempotency_key is the source of truth. The service catches the unique-constraint violation on insert, then re-reads and returns the winning ticket — so the database, not application timing, resolves the race.
Clients, agents, and admins need very different surfaces over the same data without leaking each other's capabilities.
Separate view trees (client / agent / admin) on the frontend, backed by Sanctum-authenticated, role-aware API access. Google OAuth covers human sign-in; the integration API stays on its own per-client token path entirely.