Skip to content

Support Tickets

The starter includes a user-facing contact support form that submits tickets to a configurable backend. Out of the box, it ships with two gateway drivers - Email and TeamDynamix - and an automatic email fallback that ensures user requests are never silently lost.

The feature is disabled by default and can be enabled with a single environment variable.

The system follows a layered architecture:

  • ContractTicketSystemGateway interface decouples the orchestration from gateway implementations
  • Enum-driven resolutionTicketSystemEnum::gatewayClass() maps each driver to its concrete class
  • Immutable value objectCreationResult carries the outcome of every gateway call
  • Repository — Keeps persistence concerns out of the action
  • ActionCreateSupportTicket orchestrates the full workflow in one place

.env
SUPPORT_ENABLED=true
SUPPORT_DRIVER=mail
SUPPORT_MAIL_TO=your-team@northwestern.edu

When enabled, the contact form routes are registered and the submission log appears in the Filament admin panel. When disabled, no routes are registered and the feature is completely inert.


  1. User visits the contact form

    Authenticated users access /support/contact and fill in a subject and details.

  2. Ticket is persisted

    The SupportTicketRepository saves the submission to the support_tickets table, recording the user’s email at the time of submission for audit purposes.

  3. Primary gateway submits the ticket

    The CreateSupportTicket action dispatches the ticket to the configured gateway (Email or TeamDynamix). The gateway returns an immutable CreationResult indicating success or failure.

  4. Result is recorded

    The repository updates the ticket with the gateway’s response — ticket number, system type, and any error message.

  5. Fallback fires if needed

    If the primary gateway is not the email driver and it fails, the system automatically falls back to the MailGateway so the user’s request is still delivered. The fallback_sent_at timestamp is only set if the fallback actually succeeds.

  6. User receives feedback

    The controller returns a contextual message:

    • Success — includes the ticket reference number
    • Fallback sent — confirms submission without exposing internal details
    • Both failed — generic confirmation that the team will follow up

Email

Default driver

Sends ticket details to the support team mailbox and a confirmation to the requester.

  • Generates SUP-{id} style references
  • Queued via Laravel’s mail system
  • Also serves as the automatic fallback

TeamDynamix

External ticketing system

Creates tickets directly in TeamDynamix via the REST API.

  • 3-attempt retry with 250ms backoff
  • Caches metadata IDs for one week
  • Returns native TDX ticket references

The email gateway dispatches two queued emails per submission:

  1. Team notification (SupportTicketMessage) — Contains the full request body, submitter metadata, and a fallback warning when operating in fallback mode.
  2. User confirmation (SupportTicketConfirmation) — A clean, user-facing email with the reference number and next-step expectations. No internal details are exposed.

The reference prefix is configurable and only applies to the email gateway. External gateways like TeamDynamix return their own native ticket references.

.env
SUPPORT_MAIL_TO=your-team@northwestern.edu
SUPPORT_REFERENCE_PREFIX=SUP

When the email gateway is used as a fallback after a primary gateway failure, the team notification includes a warning banner instructing the team to check Sentry for the corresponding error.

The TeamDynamix (TDX) gateway submits tickets to the TDX REST API using the northwestern-sysdev/tdx-php-sdk package. The SDK handles authentication and token management internally, and covers a broad surface beyond ticket creation — including people/group lookups, service catalog queries, and metadata management. Configuration is published to config/team-dynamix.php.

The gateway resolves metadata IDs (ticket type, form, status, priority, service) through the TeamDynamixCacheRepository, which caches name-to-ID lookups for one week to avoid repeated API round-trips:

.env
SUPPORT_DRIVER=team-dynamix
# TDX SDK credentials
TDX_API_BASE_URL=https://solutions.teamdynamix.com/TDWebApi/
TDX_USERNAME=your-service-account
TDX_PASSWORD=your-password
TDX_TICKET_APP_NAME=your-app-name
TDX_CLIENT_APP_NAME=your-client-app
# TDX ticket defaults
TDX_ASSIGNEE_ID=12345
TDX_TICKET_TYPE=Default
TDX_FORM_TYPE="NU Base Service Request"
TDX_TICKET_STATUS=New
TDX_TICKET_PRIORITY="Low (P4)"
TDX_SERVICE="My Application"

When the primary gateway is not the email driver and it fails, the system automatically dispatches the ticket via the MailGateway in fallback mode. This is a safety net — it cannot be disabled — ensuring that user requests are never silently lost.

The fallback only fires when:

  1. The primary gateway returned an error (creationError: true)
  2. The primary gateway is not the email driver itself (to avoid infinite loops)

The fallback_sent_at timestamp on the ticket is only set when the fallback actually succeeds. If the fallback also fails (e.g. mail server is down), the timestamp remains null, giving administrators a clear signal that both delivery paths failed.


The SupportTicketResource provides a read-only view of all submitted tickets in the Filament admin panel. It requires the VIEW_SUPPORT_TICKETS permission and is only visible when SUPPORT_ENABLED=true.

The resource displays a red badge on the navigation item showing the count of tickets where post_error = true, giving administrators immediate visibility into failed submissions.

Tickets are globally searchable by subject, ticket number, requester email, and submitter name/username.


The contact form is available at /support/contact for authenticated users. It collects a subject and details.

In non-production environments, a warning banner is displayed indicating that support is limited and the form may behave differently. This is controlled by:

.env
# Defaults to true for all non-production environments
SUPPORT_LIMITED_WARNING=false

Adding a new gateway requires three steps: add an enum case, implement the interface, and add configuration.

  1. Add a case to TicketSystemEnum

    app/Domains/Support/Enums/TicketSystemEnum.php
    enum TicketSystemEnum: string implements HasLabel
    {
    case TEAM_DYNAMIX = 'team-dynamix';
    case MAIL = 'mail';
    case JIRA = 'jira'; // New case
    public function getLabel(): string
    {
    return match ($this) {
    self::TEAM_DYNAMIX => 'TeamDynamix',
    self::MAIL => 'Email',
    self::JIRA => 'Jira',
    };
    }
    public function gatewayClass(): string
    {
    return match ($this) {
    self::TEAM_DYNAMIX => TeamDynamixGateway::class,
    self::MAIL => MailGateway::class,
    self::JIRA => JiraGateway::class,
    };
    }
    }

    The enum value (e.g. 'jira') is what users set in SUPPORT_DRIVER and what gets stored in the ticketing_system database column.

  2. Create the gateway class

    Implement the TicketSystemGateway interface. Your gateway must never throw — capture errors and return them via CreationResult.

    app/Domains/Support/Gateways/Jira/JiraGateway.php
    namespace App\Domains\Support\Gateways\Jira;
    use App\Domains\Support\Contracts\TicketSystemGateway;
    use App\Domains\Support\Enums\TicketSystemEnum;
    use App\Domains\Support\Gateway\CreationResult;
    use App\Domains\Support\Models\SupportTicket;
    use Exception;
    class JiraGateway implements TicketSystemGateway
    {
    public function create(SupportTicket $ticket): CreationResult
    {
    try {
    // Submit to Jira API...
    $issueKey = $this->submitToJira($ticket);
    return new CreationResult(
    ticketSystemType: TicketSystemEnum::JIRA,
    creationError: false,
    ticketNumber: $issueKey,
    errorMessage: null,
    );
    } catch (Exception $e) {
    if (app()->bound('sentry')) {
    resolve('sentry')->captureException($e);
    }
    return new CreationResult(
    ticketSystemType: TicketSystemEnum::JIRA,
    creationError: true,
    ticketNumber: null,
    errorMessage: $e->getMessage(),
    );
    }
    }
    }
  3. Add configuration

    Add a section for your driver in config/support.php and update .env.example:

    config/support.php
    'jira' => [
    'base_url' => env('JIRA_BASE_URL'),
    'project_key' => env('JIRA_PROJECT_KEY'),
    'issue_type' => env('JIRA_ISSUE_TYPE', 'Task'),
    ],
    .env
    SUPPORT_DRIVER=jira
    JIRA_BASE_URL=https://your-org.atlassian.net
    JIRA_PROJECT_KEY=SUP

That’s it. The factory resolves gateways through the enum, so no additional wiring is needed. The automatic email fallback, admin panel, and submission recording all work with your new driver immediately.

If you need a completely custom gateway that doesn’t fit the enum-driven pattern, you can bypass the factory entirely by rebinding the TicketSystemGateway interface in your AppServiceProvider:

app/Providers/AppServiceProvider.php
use App\Domains\Support\Contracts\TicketSystemGateway;
$this->app->bind(TicketSystemGateway::class, function () {
return new MyCustomGateway();
});

This overrides the binding registered by SupportServiceProvider and gives you full control over gateway resolution.


VariableDefaultDescription
SUPPORT_ENABLEDfalseEnable the contact form and submission routes
SUPPORT_LIMITED_WARNINGtrue (non-production)Show limited support warning banner
SUPPORT_DRIVERmailGateway driver (mail, team-dynamix)
SUPPORT_MAIL_TOyour-team@northwestern.eduSupport team email address
SUPPORT_REFERENCE_PREFIXSUPReference prefix for mail gateway (e.g. SUP-47)
TDX_API_BASE_URLTDX REST API base URL
TDX_USERNAMETDX service account username
TDX_PASSWORDTDX service account password
TDX_TICKET_APP_NAMETDX application name
TDX_CLIENT_APP_NAMETDX client application name
TDX_ASSIGNEE_IDTDX group ID for ticket assignment (required for TDX driver)
TDX_TICKET_TYPEDefaultTDX ticket type name
TDX_FORM_TYPENU Base Service RequestTDX form type name
TDX_TICKET_STATUSNewTDX ticket status name
TDX_TICKET_PRIORITYLow (P4)TDX ticket priority name
TDX_SERVICEAPP_NAMETDX service name (defaults to application name)