From Tickets to Conversations: Upgrading Customer Support with Gladly

Upgrading Customer Support with Gladly
Table of Contents

Customer support is one of those areas where the toolset shapes not just agent efficiency, but the entire quality of the customer relationship. When the CS team at one of our e-commerce clients decided they wanted to move to Gladly, they had a clear vision: stop managing tickets, start having conversations.

I was brought in to handle the entire migration on the engineering side. In this post, I’ll walk through the technical implementation, from embedding the live chat widget and connecting customer data to setting up the self-service Help Center, including some of the gotchas I ran into along the way. Whether you’re evaluating Gladly for your own stack or a developer curious about what the integration actually involves, hopefully this is useful.

A quick disclaimer before we dive in: this is not a Gladly advertisement. This is an honest account of a specific migration, in a specific context. Cost-effectiveness and feature fit can vary depending on your team size, support volume, and the nature of your business, other platforms are still perfectly viable, and may actually be a better fit depending on your situation. As always, do your own evaluation.


Why Gladly?

The stakeholders were looking for a better way to manage customer support requests, their existing tool wasn’t cutting it anymore, and they wanted an alternative. Pricing is always part of the conversation in these decisions, and it can vary quite a bit depending on which platform you go with, but that wasn’t the main driver here. What ultimately sold them on Gladly was a difference in philosophy: rather than managing tickets, Gladly is built around people and ongoing conversations. That’s a meaningful paradigm shift for a CS team, and it aligned well with how they wanted to engage with their customers.


The Live Chat Widget: Glad App

One of the most visible parts of the integration is the Glad App β€” Gladly’s embeddable live chat widget. The concept is straightforward: a small configuration script sets window.gladlyConfig with your appId, and a minified loader script fetches and bootstraps the widget from Gladly’s CDN. In plain HTML, you’d just drop both “ tags into the page and call it a day.

In a Next.js application, it’s a bit more involved β€” and there are a few pitfalls worth knowing about before you start. Ours were compounded by an extra challenge: the application was in the middle of a migration from the Pages Router to the App Router, which meant the Glad App component needed to work correctly under the App Router while existing Pages Router code was still in place.

The Script Execution Problem

The most important thing to understand when embedding third-party scripts in a Next.js application is this: **a element inserted into the DOM via innerHTML β€” or React's dangerouslySetInnerHTML β€” does not execute.** This is defined in the HTML spec. The browser only runs scripts that are either present during initial HTML parsing or explicitly created with document.createElement('script') and appended to the DOM. Everything else is inert β€” it shows up in the inspector, looks correct, and silently does nothing. No error, no stack trace, just absence.

This is where the Pages Router and App Router diverge in a non-obvious way.

Under the Pages Router, next/script has a mature, battle-tested runtime. For an afterInteractive inline script, it doesn’t rely on React painting the element into the DOM β€” Next’s own client runtime takes the script’s content and injects it as a real createElement/appendChild node after hydration. So the inline code actually executes. This is why the original GladApp.tsx worked without any fuss: Next was handling the browser’s requirements under the hood.

The App Router tells a different story. next/script is considerably weaker for inline scripts, particularly when the component lives deep in the tree β€” which a chat widget almost always does. beforeInteractive inline scripts only work when placed in the root layout; afterInteractive and lazyOnload inline scripts in the App Router frequently get rendered into the DOM but never executed. So the element appeared in the inspector, looked perfectly correct, and window.gladlyConfig was never set. The chat widget silently failed to initialize β€” no error, no stack trace.

This is why the naive approach fails:

// Renders fine in the DOM under the App Router, but never executes
<script dangerouslySetInnerHTML={{ __html: `window.gladlyConfig = { appId: '...' }` }} />

window.gladlyConfig stays undefined, the loader has nothing to initialize with, and the chat widget never appears.

The Fix: Imperative Script Injection via useEffect

Once the problem was clear, the solution followed naturally: stop relying on next/script and do explicitly what the Pages Router runtime was previously doing implicitly. Building the script elements by hand inside a useEffect gives you the browser behavior you actually need:

useEffect(() => {
  if (featureDisabled) return
  if (document.getElementById('gladly-sdk')) return // idempotency guard

  const config = document.createElement('script')
  config.textContent = `window.gladlyConfig = { appId: '${APP_ID}' };`
  document.body.appendChild(config) // executes immediately on append

  const loader = document.createElement('script')
  loader.textContent = `!function(c,n,r,t){...}(window,document,'Gladly','PROD')`
  document.body.appendChild(loader) // executes after config
}, [])

createElement + appendChild executes inline code. Appending the config script before the loader honors the initialization order Gladly’s snippet assumes. Both script elements are assigned explicit id attributes before being appended, this is what makes the getElementById guard effective: on any subsequent render or route change, the check finds the existing element and bails out early, preventing duplicate injection.

This sidesteps next/script entirely and reproduces in plain DOM APIs exactly what the vendor’s HTML snippet expects: synchronous, ordered, execute-on-append script loading. It generalizes well beyond Gladly, any third-party integration that assumes the ordered, execute-on-parse world of plain HTML is best handled this way when working with the App Router.

Dashboard-Driven Customization

Once the widget is running, the operational story is quite clean. Virtually all customizations, widget colors, button positioning, and page-level visibility — are managed through Gladly’s Dashboard, with no front-end code changes required. Want the chat to appear only on certain pages? Done in the Dashboard. Rebranding and need to update the accent color? Dashboard. This is a meaningful win for non-engineering teams, who can iterate on the chat experience independently without opening a ticket.

Our shops application is shared across multiple publishers, and not every publisher needs the chat widget enabled. We use a feature flag to control visibility per publisher at the application level, the component checks it on mount and skips the SDK injection entirely if the feature is off, keeping things clean with no unnecessary script loading.

Another built-in behavior worth highlighting: the widget automatically hides itself during off-hours, when no agents are available. The schedule is fully configurable in the Dashboard, and Gladly handles all the visibility logic — no cron jobs, no custom business logic required on our end.

Finally, for pages where the widget’s default positioning overlaps with bottom navigation on smaller screens, we inject a small scoped CSS override to nudge it up. It’s applied conditionally based on the current route and cleaned up on navigation, a tidy pattern for route-specific widget adjustments.

πŸ‘‰ Glad App documentation


Connecting Customer Data: The Lookup Adapter

The most technically interesting part of the integration was connecting Gladly to our client’s order data via the Lookup Adapter — Gladly’s mechanism for pulling external customer and transaction data directly into the agent’s view.

The Lookup Adapter is a middleware layer that Gladly calls via HTTP GET when an agent opens a customer profile. Rather than having Gladly hit our core API directly, we routed the requests through our dedicated Integrations application. The pipeline looks like this:

Lookup Adapter Data Flow

The Integrations app sits in the middle for good reason: it handles rate limiting, performance management, and security, keeping our API shielded from direct external access and giving us a clean place to manage authentication, request throttling, and response shaping.

When an agent views a customer profile in Gladly, the Lookup Adapter automatically fetches and surfaces everything the CS team needs to resolve a support request, without switching tabs or searching a separate system:

  • Customer profile data: name, email address, phone number, and shipping address
  • Order details: order number and current order status
  • Product information: names of purchased products
  • Shipment data: carrier and tracking number for physical products
  • Digital product attributes: a custom flag indicating whether a digital product code has been redeemed

That last one is a great example of Gladly’s custom attributes feature, you can extend the standard customer profile with any domain-specific data your agents need. For our client’s digital goods business, knowing at a glance whether a customer has already redeemed their code is the difference between a quick resolution and unnecessary back-and-forth.

Gladly also supports auto-linking — when a new contact comes in, it can automatically match the customer to a record in your system of record based on available identifiers, so agents often have full order context before they even say hello.

Implementation Notes

One constraint worth flagging: Gladly enforces a tight response window on Lookup Adapter calls. If the response doesn’t come back in time, the request times out and the agent sees nothing. Given that customer data spans multiple tables — users, orders, line items, shipment trackings, sale metadata, and more. It’s worth being deliberate about how the data is fetched and assembled on the API side.

These are the practices that kept things running smoothly.

1. Eager load the entire association graph upfront

The adapter needs to walk a fairly deep tree: user β†’ orders β†’ line items β†’ shipment trackings β†’ sale details. Without eager loading, every association access deeper in that traversal fires its own query per record, the classic N+1 problem. Loading all associations upfront with LEFT JOINs resolves the whole graph in a bounded set of queries rather than one per record.

In practice this looks something like:

User.left_joins(
  :profile,
  orders: [
    :status_transitions,
    { line_items: [:shipment_trackings, { product: :metadata }] }
  ]
)

The response builder touches every level of that tree when constructing the payload, shipment tracking details, product names, redemption flags, order states, so none of those accesses should be triggering additional queries.

2. Memoize values that are used more than once

Across the response-building process, several values are computed or queried in multiple places. Instance-level memoization (the ||= pattern in Ruby) ensures each is resolved only on first access and reused after that:

  • The user lookup itself is memoized: perform checks whether any users matched, then the attribute builders iterate over the same collection, all from a single resolved query.
  • Orders are cached per user, so if the same user appears in multiple lookup paths, the order list isn’t re-fetched.
  • Product image URLs are cached per product — when the same product appears across multiple line items, the URL is only computed once.

3. Use fast paths for single-record lookups

Gladly typically looks up a customer by a single identifier, an email address or an internal ID. Rather than always issuing an IN (...) query designed for multi-value lookups, the adapter special-cases these common single-record scenarios with a LIMIT 1 query (find_by in ActiveRecord). The multi-value path is still there for edge cases, but the common path is as lean as possible.

4. Group in memory rather than round-tripping to the database

When building the products and fulfillments sections of the response, the data needs to be grouped, by product and by tracking number respectively. Since the line items are already loaded in memory from the eager load step, this grouping is done in Ruby (group_by) rather than issuing additional GROUP BY queries against the database.

πŸ‘‰ Lookup Adapter documentation


Self-Service FAQs: The Help Center

The third pillar of the integration is the Help Center — Gladly’s embeddable FAQ and knowledge base widget. Like the Glad App, it relies on two scripts: an inline configuration script that sets window.gladlyHCConfig, and an external loader script fetched from Gladly’s CDN (hcl.js). The widget mounts into a specific DOM element on the page (in our case, a <div id="gladly-help-center" />), whose selector is part of the configuration itself.

The GladlyHelpCenter component lives in the Pages Router, where next/script handles inline scripts reliably, so the execution pitfalls described in the Glad App section don’t surface here. The configuration and the external loader are both declared with strategy="afterInteractive", which the Pages Router runtime processes correctly.

window.gladlyHCConfig specifies the API endpoint, organization ID, brand ID, CDN base URL, and the DOM selector to mount into. We support multiple brands on the same platform, each with its own brandId, swapping between them is just a prop.

The real beauty here, again, is the operational independence it gives to non-engineering teams. The entire Help Center content, sections, FAQs, answer text, layout columns, search placeholder copy, is managed through the Gladly Dashboard. Adding a new FAQ category, updating an answer after a policy change, or removing outdated content requires zero front-end involvement. The Help Center also inherits the host site’s CSS, so it blends naturally into the existing design without any extra styling work.

πŸ‘‰ Help Center configuration documentation


The Results: A Happier CS Team (and Customers)

The full integration came together smoothly, and the feedback from the CS team was immediately positive. A few highlights:

Email migration was painless. Gladly makes it straightforward to redirect inbound email from a previous platform, so the team didn’t lose historical context or have to manage two tools in parallel during the transition.

The CS experience became genuinely more dynamic. Instead of a static email queue, agents now have live chat through the Glad App, a self-service Help Center that reduces ticket volume for common questions, and full order context from the Lookup Adapter — all in one unified interface. Customers get faster, more informed support. Agents spend less time hunting for information.

Engineering involvement stayed minimal after launch. Because so much of Gladly’s configuration lives in the Dashboard, the CS team can adjust and iterate on their tooling without waiting for a development cycle. That’s a win for everyone.

If you’re considering a similar migration, the integration work is more approachable than you might expect. The documentation is solid, the APIs are well-structured, and as long as you’re aware of the framework-specific quirks covered above, there aren’t many surprises. Whether Gladly is the right fit depends on your team and your needs, but if the conversation-first model resonates, it’s worth a serious look.


Sources

We want to work with you. Check out our Services page!